Search Engine Optimization 가이드, 검색하면 상단에 나오는 웹 사이트 만들기

검색엔진 최적화(SEO)를 통해 검색 결과 상단에 노출되는 웹 사이트를 만드는 방법을 알아보자

|22분 읽기
SEOSearch Engine Optimization검색엔진 최적화Google Search메타태그크롤링색인웹 표준웹 접근성Next.js

📢 사용자에게 도움이 되는 웹 사이트를 빌드하고 사용자 환경을 개선하는 것을 목표로 최적화해야 한다.

검색엔진 최적화란 검색엔진이 콘텐츠를 이해하고 제공하도록 돕는 것이다.

현재 웹페이지의 상황을 파악하는 방법

메타 태그 조회하기

개발자 도구를 통해 메타 태그를 우선 확인할 수 있고, 다음 도구를 활용해 메타 태그가 제대로 설정되어 있는지 확인할 수 있다. 아무리 개발자 도구의 Elements 탭에서 메타 태그가 작성된 것을 확인하더라도, 누구에게 공유할 때 제대로 잘 나오지 않는 경우가 있는데, 이럴 때 다음 도구를 활용하면 확인할 수 있다.

Google 색인 확인하기

추가적으로, Google SEO 가이드에 따르면 Google에 site: 접두어를 추가하여 검색하면, Google의 색인에 추가되어 있는 지를 확인할 수 있다고 한다.

Google SEO 가이드에서도 site: 검색 결과에 웹 페이지가 포함되지 않는 경우의 원인에 대해 남겨놓았는데, 다음과 같다.

  • 사이트가 웹의 다른 사이트에서 잘 연결되지 않습니다.
  • 사이트가 개설된 지 얼마 되지 않아 Google이 아직 크롤링하지 못했습니다.
  • 사이트의 디자인 때문에 Google이 사이트의 콘텐츠를 효과적으로 크롤링하기 어렵습니다.
  • Google이 사이트를 크롤링하려는 중에 오류가 발생했습니다.
  • 사이트 정책으로 인해 Google의 사이트 크롤링이 차단되었습니다.

결과

Google에 따르면 실제로 Google 검색결과에 게재되는 사이트 중 대부분은 수동으로 등록된 것이 아니라, 크롤링을 통해 자동으로 발견되고 추가된 것이라고 하기 때문이다.

따라서 Google이 자동으로 크롤링을 하고, 색인을 생성할 때까지 SEO를 최적화하는 방법에 대해 알아보고, 이를 적용하여 시간을 단축시키거나 원하는 데이터를 제공해서 Google의 검색 결과에 표시되는 데 도움을 주어야 한다.

Google 기준 SEO 최적화하기

사이트맵 생성하기

사이트맵은 해당 웹 페이지에 존재하는 페이지, 동영상 등과 그 관계에 대한 정보를 제공하는 파일으로, 효율적으로 크롤링할 수 있게 도움을 주는 정보를 제공한다. 사이트맵이 존재한다고 모든 페이지가 크롤링되고, 색인이 생성되는 것은 아니지만, 대부분 도움을 줄 수 있다는 점에서 반드시 추가하여야 하는 데이터이다.

XML 사이트맵

sitemaps.org - Protocol

XML 태그로 구성된 사이트맵의 예시는 다음과 같다.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
   <url>
      <loc>http://www.example.com/</loc>
      <lastmod>2005-01-01</lastmod>
      <changefreq>monthly</changefreq>
      <priority>0.8</priority>
   </url>
   <url>
      <loc>http://www.example.com/catalog?item=12&amp;desc=vacation_hawaii</loc>
      <changefreq>weekly</changefreq>
   </url>
   <url>
      <loc>http://www.example.com/catalog?item=73&amp;desc=vacation_new_zealand</loc>
      <lastmod>2004-12-23</lastmod>
      <changefreq>weekly</changefreq>
   </url>
   <url>
      <loc>http://www.example.com/catalog?item=74&amp;desc=vacation_newfoundland</loc>
      <lastmod>2004-12-23T18:00:15+00:00</lastmod>
      <priority>0.3</priority>
   </url>
   <url>
      <loc>http://www.example.com/catalog?item=83&amp;desc=vacation_usa</loc>
      <lastmod>2004-11-23</lastmod>
   </url>
</urlset>

동적 사이트맵 추가하기(with Nextjs)

Learn | Next.js

개발자를 위한 검색 엔진 최적화(SEO) 가이드2 — next.js로 동적 사이트맵 쉽게 제작하기

고유하고 정확한 페이지 제목 추가하기

Google SEO 기본 가이드, 검색 결과에서 시선을 끄는 제목 링크와 스니펫 작성하기

Google SEO 기본 가이드, 검색 결과에서 시선을 끄는 제목 링크와 스니펫 작성하기

HTML 문서의 head 내부에 존재하는 title 태그를 활용해 각 웹 페이지에 고유한 제목을 생성해야 한다. 다만, 아래의 경우를 지양해야 한다.

  • 여러 페이지에서 동일한 제목을 활용하는 경우
  • 불분명하거나 명확하지 않은 제목을 활용하는 경우
  • 불필요한 키워드를 추가하여 매우 긴 제목을 활용하는 경우
  • 관련성이 낮은 제목을 활용하는 경우
<title>블로그 | SEO 향상시키는 방법</title> <title>블로그 | Contact</title>

페이지의 컨텐츠를 요약하는 설명 추가하기

메타 디스크립션: CTR을 상승시키는 비결 7가지

https://www.youtube.com/watch?v=vS1Mw1Adrk0

HTML 문서의 <head> 내부에 meta 태그를 활용해 페이지의 컨텐츠를 명확하고 정확하게 요약하는 설명을 추가해야 한다. 이는 검색 결과에서 페이지에 대한 설명으로 활용되기 때문에 각 페이지마다 고유한 설명을 1 ~ 2 문장이나 짧은 단락으로 잘 구성하는 것이 중요하다.

Google SEO 기본 가이드, 메타 설명 태그 사용하기

Google SEO 기본 가이드, 메타 설명 태그 사용하기

아래와 같은 경우는 지양해야 한다.

  • 페이지 내용과 관련이 없는 메타 설명 태그를 작성하는 경우
  • '이것은 웹페이지입니다' 또는 '야구 카드에 관한 페이지'와 같은 일반적인 설명을 사용하는 경우
  • 설명을 키워드로만 채우는 경우
  • 메타 설명 태그에 문서의 전체 내용을 복사하여 붙여넣는 경우

Semantic HTML 요소 활용하기

HTML Semantic Elements

Semantics - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN

HTML and semantic tagging  |  Google developer documentation style guide  |  Google for Developers

시맨틱 태그: 쉽게 이해하는 8가지 방법

크롤러가 웹 페이지를 크롤링할 때 해당 웹 페이지를 구조적으로 보여주기 위해서는 Semantic HTML 요소를 잘 활용해야 한다. Google은 Semantic HTML 요소가 검색 엔진이 웹 페이지의 컨텐츠를 이해하는데 도움을 준다고 말하고 있다.

Google의 Senior Search Analyst John Mueller에 따르면 Semantic HTML 요소는 웹 페이지의 컨텐츠에 의미와 구조를 부여하게 되는데, 이는 검색 엔진과 브라우저가 해당 웹 페이지의 컨텐츠를 보다 쉽게 이해할 수 있도록 도와주게 된다. 다만, Semantic HTML 요소로 작성한 페이지가 검색 엔진의 랭킹에 직접적인 영향을 미치지는 않는다고 말했다.

Heading 태그(h1 ~ h6)를 구조적으로 사용하기

How To Use Header Tags: SEO Best Practices

구조화된 JSON-LD 데이터를 활용해 컨텐츠를 설명하기

Google 검색에서 지원하는 구조화된 데이터 마크업 | Google 검색 센터  |  문서  |  Google for Developers

Google 검색에서 지원하는 구조화된 데이터 마크업 | Google 검색 센터  |  문서  |  Google for Developers

의미있는 URL과 알기 쉬운 구조의 URL을 구성하기

**BAD** https://www.example.com/news/12345678
**BAD** https://www.example.com/articles/article/class/class-customer-example/123412
**GOOD** https://www.example.com/posts/SEO-향상시키기

문서에 하나의 URL만 연결하기

사용자에 따라 동일한 페이지에 URL이 변경되는 경우, URL별로 컨텐츠의 평판이 달라지기 때문에 하나의 URL을 활용하는 것이 중요하다. 따라서 어떤 이유로든 동일한 페이지가 여러 URL을 가지게 되는 경우에는 Canonical URL을 설정하여 이를 하나의 URL로 설정해주어야 한다.

Canonical URL은 다음과 같이 설정할 수 있다.

<!-- https://www.example.com/ko -->
<!-- https://www.example.com/en -->
 
<link rel="canonical" href="https://www.example.com" />

웹 페이지의 컨텐츠를 최적화하기

SEO 기본 가이드: 기본사항 | Google 검색 센터  |  문서  |  Google for Developers

웹 페이지의 이미지 최적화하기

SEO 기본 가이드: 기본사항 | Google 검색 센터  |  문서  |  Google for Developers

웹 페이지를 모바일 친화적으로 만들기

SEO 기본 가이드: 기본사항 | Google 검색 센터  |  문서  |  Google for Developers

Nextjs App Router를 활용해 SEO 개선하기

Metadata API 활용하기

Optimizing: Metadata

Nextjs App Router에 추가된 Metadata API는 두 가지 방식으로 사용할 수 있는데, 첫 번째는 데이터 기반의 Metadata 작성 방식이고, 두 번째는 파일 기반으로 Metadata를 추가할 수 있다.

Metadata 객체

Functions: generateMetadata

Metadata 객체에는 여러 필드가 존재하는데, 어떤 필드가 어떤 Metadata로 활용되는 지 확인해야, 어떤 데이터를 넣을 지 결정할 수 있기 때문에, 해당 객체의 모든 필드를 확인해보자.

  • Interface Metadata

    // node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts
     
    interface Metadata extends DeprecatedMetadataFields {
      /**
       * The base path and origin for absolute urls for various metadata links such as OpenGraph images.
       */
      metadataBase?: null | URL;
      /**
       * The document title.
       * @example
       * ```tsx
       * "My Blog"
       * <title>My Blog</title>
       *
       * { default: "Dashboard", template: "%s | My Website" }
       * <title>Dashboard | My Website</title>
       *
       * { absolute: "My Blog", template: "%s | My Website" }
       * <title>My Blog</title>
       * ```
       */
      title?: null | string | TemplateString;
      /**
       * The document description, and optionally the OpenGraph and twitter descriptions.
       * @example
       * ```tsx
       * "My Blog Description"
       * <meta name="description" content="My Blog Description" />
       * ```
       */
      description?: null | string;
      /**
       * The application name.
       * @example
       * ```tsx
       * "My Blog"
       * <meta name="application-name" content="My Blog" />
       * ```
       */
      applicationName?: null | string;
      /**
       * The authors of the document.
       * @example
       * ```tsx
       * [{ name: "Next.js Team", url: "https://nextjs.org" }]
       *
       * <meta name="author" content="Next.js Team" />
       * <link rel="author" href="https://nextjs.org" />
       * ```
       */
      authors?: null | Author | Array<Author>;
      /**
       * The generator used for the document.
       * @example
       * ```tsx
       * "Next.js"
       *
       * <meta name="generator" content="Next.js" />
       * ```
       */
      generator?: null | string;
      /**
       * The keywords for the document. If an array is provided, it will be flattened into a single tag with comma separation.
       * @example
       * ```tsx
       * "nextjs, react, blog"
       * <meta name="keywords" content="nextjs, react, blog" />
       *
       * ["react", "server components"]
       * <meta name="keywords" content="react, server components" />
       * ```
       */
      keywords?: null | string | Array<string>;
      /**
       * The referrer setting for the document.
       * @example
       * ```tsx
       * "origin"
       * <meta name="referrer" content="origin" />
       * ```
       */
      referrer?: null | ReferrerEnum;
      /**
       * The theme color for the document.
       * @example
       * ```tsx
       * "#000000"
       * <meta name="theme-color" content="#000000" />
       *
       * { media: "(prefers-color-scheme: dark)", color: "#000000" }
       * <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000" />
       *
       * [
       *  { media: "(prefers-color-scheme: dark)", color: "#000000" },
       *  { media: "(prefers-color-scheme: light)", color: "#ffffff" }
       * ]
       * <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000" />
       * <meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />
       * ```
       */
      themeColor?: null | string | ThemeColorDescriptor | ThemeColorDescriptor[];
      /**
       * The color scheme for the document.
       * @example
       * ```tsx
       * "dark"
       * <meta name="color-scheme" content="dark" />
       * ```
       */
      colorScheme?: null | ColorSchemeEnum;
      /**
       * The viewport setting for the document.
       * @example
       * ```tsx
       * "width=device-width, initial-scale=1"
       * <meta name="viewport" content="width=device-width, initial-scale=1" />
       *
       * { width: "device-width", initialScale: 1 }
       * <meta name="viewport" content="width=device-width, initial-scale=1" />
       * ```
       */
      viewport?: null | string | Viewport;
      /**
       * The creator of the document.
       * @example
       * ```tsx
       * "Next.js Team"
       * <meta name="creator" content="Next.js Team" />
       * ```
       */
      creator?: null | string;
      /**
       * The publisher of the document.
       * @example
       *
       * ```tsx
       * "Vercel"
       * <meta name="publisher" content="Vercel" />
       * ```
       */
      publisher?: null | string;
      /**
       * The robots setting for the document.
       *
       * @see https://developer.mozilla.org/en-US/docs/Glossary/Robots.txt
       * @example
       * ```tsx
       * "index, follow"
       * <meta name="robots" content="index, follow" />
       *
       * { index: false, follow: false }
       * <meta name="robots" content="noindex, nofollow" />
       * ```
       */
      robots?: null | string | Robots;
      /**
       * The canonical and alternate URLs for the document.
       * @example
       * ```tsx
       * { canonical: "https://example.com" }
       * <link rel="canonical" href="https://example.com" />
       *
       * { canonical: "https://example.com", hreflang: { "en-US": "https://example.com/en-US" } }
       * <link rel="canonical" href="https://example.com" />
       * <link rel="alternate" href="https://example.com/en-US" hreflang="en-US" />
       * ```
       *
       * Multiple titles example for alternate URLs except `canonical`:
       * ```tsx
       * {
       *   canonical: "https://example.com",
       *   types: {
       *     'application/rss+xml': [
       *       { url: 'blog.rss', title: 'rss' },
       *       { url: 'blog/js.rss', title: 'js title' },
       *     ],
       *   },
       * }
       * <link rel="canonical" href="https://example.com" />
       * <link rel="alternate" href="https://example.com/blog.rss" type="application/rss+xml" title="rss" />
       * <link rel="alternate" href="https://example.com/blog/js.rss" type="application/rss+xml" title="js title" />
       * ```
       */
      alternates?: null | AlternateURLs;
      /**
       * The icons for the document. Defaults to rel="icon".
       *
       * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#attr-icon
       * @example
       * ```tsx
       * "https://example.com/icon.png"
       * <link rel="icon" href="https://example.com/icon.png" />
       *
       * { icon: "https://example.com/icon.png", apple: "https://example.com/apple-icon.png" }
       * <link rel="icon" href="https://example.com/icon.png" />
       * <link rel="apple-touch-icon" href="https://example.com/apple-icon.png" />
       *
       * [{ rel: "icon", url: "https://example.com/icon.png" }, { rel: "apple-touch-icon", url: "https://example.com/apple-icon.png" }]
       * <link rel="icon" href="https://example.com/icon.png" />
       * <link rel="apple-touch-icon" href="https://example.com/apple-icon.png" />
       * ```
       */
      icons?: null | IconURL | Array<Icon> | Icons;
      /**
       * A web application manifest, as defined in the Web Application Manifest specification.
       *
       * @see https://developer.mozilla.org/en-US/docs/Web/Manifest
       * @example
       * ```tsx
       * "https://example.com/manifest.json"
       * <link rel="manifest" href="https://example.com/manifest.json" />
       * ```
       *
       */
      manifest?: null | string | URL;
      /**
       * The Open Graph metadata for the document.
       *
       * @see https://ogp.me
       * @example
       * ```tsx
       * {
       *   type: "website",
       *   url: "https://example.com",
       *   title: "My Website",
       *   description: "My Website Description",
       *   siteName: "My Website",
       *   images: [{
       *     url: "https://example.com/og.png",
       *   }],
       * }
       *
       * <meta property="og:type" content="website" />
       * <meta property="og:url" content="https://example.com" />
       * <meta property="og:site_name" content="My Website" />
       * <meta property="og:title" content="My Website" />
       * <meta property="og:description" content="My Website Description" />
       * <meta property="og:image" content="https://example.com/og.png" />
       * ```
       */
      openGraph?: null | OpenGraph;
      /**
       * The Twitter metadata for the document.
       * @example
       * ```tsx
       * { card: "summary_large_image", site: "@site", creator: "@creator", "images": "https://example.com/og.png" }
       *
       * <meta name="twitter:card" content="summary_large_image" />
       * <meta name="twitter:site" content="@site" />
       * <meta name="twitter:creator" content="@creator" />
       * <meta name="twitter:title" content="My Website" />
       * <meta name="twitter:description" content="My Website Description" />
       * <meta name="twitter:image" content="https://example.com/og.png" />
       * ```
       *
       */
      twitter?: null | Twitter;
      /**
       * The common verification tokens for the document.
       * @example
       * ```tsx
       * { verification: { google: "1234567890", yandex: "1234567890", "me": "1234567890" } }
       * <meta name="google-site-verification" content="1234567890" />
       * <meta name="yandex-verification" content="1234567890" />
       * <meta name="me" content="@me" />
       * ```
       */
      verification?: Verification;
      /**
       * The Apple web app metadata for the document.
       *
       * @see https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
       * @example
       * ```tsx
       * { capable: true, title: "My Website", statusBarStyle: "black-translucent" }
       * <meta name="apple-mobile-web-app-capable" content="yes" />
       * <meta name="apple-mobile-web-app-title" content="My Website" />
       * <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
       * ```
       *
       */
      appleWebApp?: null | boolean | AppleWebApp;
      /**
       * Indicates if devices should try to interpret various formats and make actionable links out of them. For example it controles
       * if telephone numbers on mobile that can be clicked to dial or not.
       * @example
       * ```tsx
       * { telephone: false }
       * <meta name="format-detection" content="telephone=no" />
       * ```
       *
       */
      formatDetection?: null | FormatDetection;
      /**
       * The metadata for the iTunes App.
       * It adds the `name="apple-itunes-app"` meta tag.
       *
       * @example
       * ```tsx
       * { app: { id: "123456789", affiliateData: "123456789", appArguments: "123456789" } }
       * <meta name="apple-itunes-app" content="app-id=123456789, affiliate-data=123456789, app-arguments=123456789" />
       * ```
       */
      itunes?: null | ItunesApp;
      /**
       * A brief description of what this web-page is about. Not recommended, superseded by description.
       * It adds the `name="abstract"` meta tag.
       *
       * @see https://www.metatags.org/all-meta-tags-overview/meta-name-abstract/
       * @example
       * ```tsx
       * "My Website Description"
       * <meta name="abstract" content="My Website Description" />
       * ```
       */
      abstract?: null | string;
      /**
       * The Facebook AppLinks metadata for the document.
       * @example
       * ```tsx
       * { ios: { appStoreId: "123456789", url: "https://example.com" }, android: { packageName: "com.example", url: "https://example.com" } }
       *
       * <meta property="al:ios:app_store_id" content="123456789" />
       * <meta property="al:ios:url" content="https://example.com" />
       * <meta property="al:android:package" content="com.example" />
       * <meta property="al:android:url" content="https://example.com" />
       * ```
       */
      appLinks?: null | AppLinks;
      /**
       * The archives link rel property.
       * @example
       * ```tsx
       * { archives: "https://example.com/archives" }
       * <link rel="archives" href="https://example.com/archives" />
       * ```
       */
      archives?: null | string | Array<string>;
      /**
       * The assets link rel property.
       * @example
       * ```tsx
       * "https://example.com/assets"
       * <link rel="assets" href="https://example.com/assets" />
       * ```
       */
      assets?: null | string | Array<string>;
      /**
       * The bookmarks link rel property.
       * @example
       * ```tsx
       * "https://example.com/bookmarks"
       * <link rel="bookmarks" href="https://example.com/bookmarks" />
       * ```
       */
      bookmarks?: null | string | Array<string>;
      /**
       * The category meta name property.
       * @example
       * ```tsx
       * "My Category"
       * <meta name="category" content="My Category" />
       * ```
       */
      category?: null | string;
      /**
       * The classification meta name property.
       * @example
       * ```tsx
       * "My Classification"
       * <meta name="classification" content="My Classification" />
       * ```
       */
      classification?: null | string;
      /**
       * Arbitrary name/value pairs for the document.
       */
      other?: {
        [name: string]: string | number | Array<string | number>;
      } & DeprecatedMetadataFields;
    }

metadataBase

Metadata API에서 활용되는 URL의 접두어로 활용할 기본 URL을 설정하는 것으로, 추후 URL을 활용할 때, 상대 경로로 사용할 수 있게 된다.

export const metadata = {
  metadataBase: new URL("https://www.example.com"),
  alternates: {
    canonical: "/",
    languages: {
      "en-US": "/en-US",
      "de-DE": "/de-DE",
    },
  },
  openGraph: {
    images: "/og-image.png",
  },
};
 
// output
// <link rel="canonical" href="https://www.example.com" />
// <link rel="alternate" hreflang="en-US" href="https://www.example.com/en-US" />
// <link rel="alternate" hreflang="de-DE" href="https://www.example.com/de-DE" />
// <meta property="og:image" content="https://www.example.com/og-image.png" />

title

해당 페이지의 제목을 나타내는 필드로, <title> 태그에 추가된다. 기본적인 사용법은 다음과 같다.

export const metadata: Metadata = {
  ...,
	title: '블로그',
	...,
}
 
// output: <title>블로그</title>

다만, 단순 String이 아니라, TemplateString을 받을 수도 있는데, 이는 다음과 같이 설정된 타입이다.

export type TemplateString =
  | DefaultTemplateString
  | AbsoluteTemplateString
  | AbsoluteString;
 
export type DefaultTemplateString = {
  default: string;
  template: string;
};
 
export type AbsoluteTemplateString = {
  absolute: string;
  template: string | null;
};
 
export type AbsoluteString = {
  absolute: string;
};

즉, title 필드에 객체 데이터를 넘겨주어 기본값(fallback title), 템플릿, 절대값 등을 다음과 같이 설정할 수 있다.

// app/layout.tsx
 
export const metadata: Metadata = {
  ...,
	title: {
		default: '블로그',
		template: '%s | SEO 향상시키기',    // template 값을 넣을 땐 반드시 default 값이 존재해야 한다.
	},
	...,
}
 
// output: <title>블로그 | SEO 향상시키기</title>
// app/solutions/class/page.tsx
 
export const metadata: Metadata = {
  ...,
	title: 'SEO 향상시키기',
	...,
}
 
// output: <title>CPR class | SEO 향상시키기</title>

description

해당 페이지의 <meta> 태그 중 description을 설정하는 필드로, String 형식을 받아 설정할 수 있다.

export const metadata: Metadata = {
  ...,
	description: '설명입니다.',
	...,
}
 
// output: <meta name='description' content='설명입니다.' />

keywords

해당 페이지의 <meta> 태그 중 keywords을 설정하는 필드로, String 배열 형식을 받아 설정할 수 있다.

export const metadata: Metadata = {
  ...,
	keywords: ['SEO', 'Google Search Console', 'Nextjs'],
	...,
}
 
// output: <meta name='keywords' content='SEO, Google Search Console, Nextjs' />

author

해당 페이지의 author 필드로, 두 가지 방식으로 설정할 수 있으며, 각각 <meta> 태그의 author<link> 태그로 설정된다.

export const metadata: Metadata = {
  ...,
	authors: [
		{ name: 'HyoungMin', },
		{ name: 'HyoungMin Lee', url: 'https://github.com/hyoungqu23' }
	],
	...,
}
 
// output
// <meta name="author" content="HyoungMin" />
// <link rel="author" href="https://github.com/hyoungqu23" />
// <meta name="author" content="HyoungMin Lee" />

robots

해당 페이지의 robots 설정을 관리하는 필드로, <meta> 태그의 robots 태그로 설정된다.

export const metadata: Metadata = {
  ...,
	robots: {
    index: false,
    follow: true,
    nocache: true,
    googleBot: {
      index: true,
      follow: false,
      noimageindex: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
	...,
}
 
// output
// <meta name="robots" content="noindex, follow, nocache" />
// <meta name="googlebot" content="index, nofollow, noimageindex, max-video-preview:-1, max-image-preview:large, max-snippet:-1" />

Basic Fields

다음과 같이 추가적인 기본 필드를 설정할 수 있다.

export const metadata: Metadata = {
  generator: "Next.js",
  applicationName: "블로그",
  referrer: "origin-when-cross-origin",
  colorScheme: "light",
  creator: "HyoungMin",
  publisher: "HyoungMin",
  formatDetection: {
    email: false,
    address: false,
    telephone: false,
  },
};
 
// output
// <meta name="application-name" content="블로그" />
// <meta name="generator" content="Next.js" />
// <meta name="referrer" content="origin-when-cross-origin" />
// <meta name="color-scheme" content="light" />
// <meta name="creator" content="HyoungMin" />
// <meta name="publisher" content="HyoungMin" />
// <meta name="format-detection" content="telephone=no, address=no, email=no" />

openGraph

open graph <meta> 태그들도 다음과 같이 쉽게 추가할 수 있다.

export const metadata: Metadata = {
  openGraph: {
    title: "Next.js",
    description: "The React Framework for the Web",
    url: "https://nextjs.org",
    siteName: "Next.js",
    images: [
      {
        url: "https://nextjs.org/og.png",
        width: 800,
        height: 600,
      },
      {
        url: "https://nextjs.org/og-alt.png",
        width: 1800,
        height: 1600,
        alt: "My custom alt",
      },
    ],
    locale: "en_US",
    type: "website",
  },
};
 
// output
// <meta property="og:title" content="Next.js" />
// <meta property="og:description" content="The React Framework for the Web" />
// <meta property="og:url" content="https://nextjs.org/" />
// <meta property="og:site_name" content="Next.js" />
// <meta property="og:locale" content="en_US" />
// <meta property="og:image:url" content="https://nextjs.org/og.png" />
// <meta property="og:image:width" content="800" />
// <meta property="og:image:height" content="600" />
// <meta property="og:image:url" content="https://nextjs.org/og-alt.png" />
// <meta property="og:image:width" content="1800" />
// <meta property="og:image:height" content="1600" />
// <meta property="og:image:alt" content="My custom alt" />
// <meta property="og:type" content="website" />
export const metadata: Metadata = {
  openGraph: {
    title: "Next.js",
    description: "The React Framework for the Web",
    type: "article",
    publishedTime: "2023-01-01T00:00:00.000Z",
    authors: ["Seb", "Josh"],
  },
};
 
// output
// <meta property="og:title" content="Next.js" />
// <meta property="og:description" content="The React Framework for the Web" />
// <meta property="og:type" content="article" />
// <meta property="article:published_time" content="2023-01-01T00:00:00.000Z" />
// <meta property="article:author" content="Seb" />
// <meta property="article:author" content="Josh" />

twitter

Cards markup

twitter 태그 또한 open graph 처럼 손쉽게 추가할 수 있다.

export const metadata: Metadata = {
  twitter: {
    card: "app",
    title: "Next.js",
    description: "The React Framework for the Web",
    siteId: "1467726470533754880",
    creator: "@nextjs",
    creatorId: "1467726470533754880",
    images: {
      url: "https://nextjs.org/og.png",
      alt: "Next.js Logo",
    },
    app: {
      name: "twitter_app",
      id: {
        iphone: "twitter_app://iphone",
        ipad: "twitter_app://ipad",
        googleplay: "twitter_app://googleplay",
      },
      url: {
        iphone: "https://iphone_url",
        ipad: "https://ipad_url",
      },
    },
  },
};
 
// output
// <meta name="twitter:site:id" content="1467726470533754880" />
// <meta name="twitter:creator" content="@nextjs" />
// <meta name="twitter:creator:id" content="1467726470533754880" />
// <meta name="twitter:title" content="Next.js" />
// <meta name="twitter:description" content="The React Framework for the Web" />
// <meta name="twitter:card" content="app" />
// <meta name="twitter:image" content="https://nextjs.org/og.png" />
// <meta name="twitter:image:alt" content="Next.js Logo" />
// <meta name="twitter:app:name:iphone" content="twitter_app" />
// <meta name="twitter:app:id:iphone" content="twitter_app://iphone" />
// <meta name="twitter:app:id:ipad" content="twitter_app://ipad" />
// <meta name="twitter:app:id:googleplay" content="twitter_app://googleplay" />
// <meta name="twitter:app:url:iphone" content="https://iphone_url" />
// <meta name="twitter:app:url:ipad" content="https://ipad_url" />
// <meta name="twitter:app:name:ipad" content="twitter_app" />
// <meta name="twitter:app:name:googleplay" content="twitter_app" />
export const metadata: Metadata = {
  twitter: {
    card: "summary_large_image",
    title: "Next.js",
    description: "The React Framework for the Web",
    siteId: "1467726470533754880",
    creator: "@nextjs",
    creatorId: "1467726470533754880",
    images: ["https://nextjs.org/og.png"],
  },
};
 
// output
// <meta name="twitter:card" content="summary_large_image" />
// <meta name="twitter:site:id" content="1467726470533754880" />
// <meta name="twitter:creator" content="@nextjs" />
// <meta name="twitter:creator:id" content="1467726470533754880" />
// <meta name="twitter:title" content="Next.js" />
// <meta name="twitter:description" content="The React Framework for the Web" />
// <meta name="twitter:image" content="https://nextjs.org/og.png" />

icons

icons의 경우, Metadata 객체를 활용해서 처리할 수도 있으나, 파일 기반 Metadata 설정 방식이 보다 적합하며, 권장되는 바이다.

export const metadata: Metadata = {
  icons: {
    icon: "/icon.png",
    shortcut: "/shortcut-icon.png",
    apple: "/apple-icon.png",
    other: {
      rel: "apple-touch-icon-precomposed",
      url: "/apple-touch-icon-precomposed.png",
    },
  },
};
 
// output
// <link rel="shortcut icon" href="/shortcut-icon.png" />
// <link rel="icon" href="/icon.png" />
// <link rel="apple-touch-icon" href="/apple-icon.png" />
// <link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-precomposed.png" />
export const metadata: Metadata = {
  icons: {
    icon: [{ url: "/icon.png" }, new URL("/icon.png", "https://example.com")],
    shortcut: ["/shortcut-icon.png"],
    apple: [
      { url: "/apple-icon.png" },
      { url: "/apple-icon-x3.png", sizes: "180x180", type: "image/png" },
    ],
    other: [
      {
        rel: "apple-touch-icon-precomposed",
        url: "/apple-touch-icon-precomposed.png",
      },
    ],
  },
};
 
// output
// <link rel="shortcut icon" href="/shortcut-icon.png" />
// <link rel="icon" href="/icon.png" />
// <link rel="apple-touch-icon" href="/apple-icon.png" />
// <link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-precomposed.png" />
// <link rel="icon" href="https://example.com/icon.png" />
// <link rel="apple-touch-icon" href="/apple-icon-x3.png" sizes="180x180" type="image/png" />

alternates

페이지의 현지화된 버전 | Google 검색 센터  |  문서  |  Google for Developers

현재 상황에서 가장 설정해주어야 할 것 같은 필드는 alternates 필드이다. alternates 필드의 media를 잘 활용하면, 모바일과 데스트탑을 분리하여 m.example.comexample.com으로 분리할 수도 있고, 다국어 페이지를 각 언어에 맞는 검색 결과에 노출시킬 수 있다.

또한, 다국어 등으로 인해 각각 노출되는 페이지의 표준 페이지를 canonical으로 설정하면, 검색엔진이 중복되는 URL에 접근하는 경우 표준 페이지를 크롤링할 수 있게 해주므로, 표준 페이지가 검색 결과에 노출될 확률을 높일 수 있다.

명확하게 canonical과 alternate 태그가 어떤 역할을 하는지 이해가 되지 않아 ChatGPT에게 예시 설명을 부탁했다.

Question) 다음 기준에 맞춰 alternate, canonical 태그를 작성해줘.

기준:

  1. https://www.example.com이 현재 BASE URL입니다.
  2. https://www.example.com로 진입 시, 사용자의 브라우저 언어 설정에 맞게 https://www.example.com/ko, https://www.example.com/en으로 리다이렉트 됩니다.
  3. 각각 https://www.example.com, https://www.example.com/ko, https://www.example.com/en에 alternate, canonical 태그를 추가해야 합니다.

Answer) 기준에 따라 각 페이지의 <head> 섹션에 추가해야 할 alternatecanonical 태그는 다음과 같습니다:

<!-- [**https://www.example.com**](https://www.example.com/) -->
 
<link rel="canonical" href="<https://www.example.com>" />
<link rel="alternate" hreflang="ko" href="<https://www.example.com/ko>" />
<link rel="alternate" hreflang="en" href="<https://www.example.com/en>" />
<!-- [**https://www.example.com](https://www.example.com/)/ko** -->
 
<link rel="canonical" href="<https://www.example.com/ko>" />
<link rel="alternate" hreflang="x-default" href="<https://www.example.com/>" />
<link rel="alternate" hreflang="en" href="<https://www.example.com/en>" />
<!-- [**https://www.example.com](https://www.example.com/)/en** -->
 
<link rel="canonical" href="<https://www.example.com/en>" />
<link rel="alternate" hreflang="x-default" href="<https://www.example.com/>" />
<link rel="alternate" hreflang="ko" href=" https :// www .example .com / ko " />

위 코드에서, rel=canonical 태그는 검색 엔진이 현재 페이지를 '기본 버전'(즉, 캐노니컬)으로 인식하도록 지시합니다. 이렇게 하면 URL 파라미터나 세션 ID 등으로 인한 중복 콘텐츠 문제를 방지할 수 있습니다.

rel=alternatehreflang 속성은 다른 언어로 작성된 동일한 콘텐츠의 버전을 가리킵니다. 이는 검색 엔진이 사용자의 언어 설정과 일치하는 콘텐츠를 제공하는 데 도움이 됩니다.

마지막으로, hreflang=x-default는 사용자의 언어 설정이 사이트가 지원하는 어떤 언어와도 일치하지 않을 때 제공되는 기본 페이지를 가리킵니다. 이 경우, 메인 URL(즉, https://www.example.com)을 기본 페이지로 설정하였습니다.

추측건대, 현재 페이지의 색인이 제대로 생성되지 않는 이유는 다음 중 하나인 듯 하다.

  1. 사용자의 언어 설정에 따라 Redirect하는 로직이 클라이언트에 존재하여, 크롤러가 Redirect되지 못하고, 데이터가 없는 페이지(https://cpraddonkit.com)을 크롤링하게 되기 때문에.
  2. 언어 설정에 따라 Redirect된 페이지에 canonical, alternate 태그가 없어 제대로 표준 페이지와 언어에 따른 페이지를 구분못하기 때문에.

또한, Google 공식 문서에서 밝혔듯, 각 언어별 페이지는 각각의 canonical 태그를 가지고 있어야 할 것 같다.

한 페이지에 여러 언어 버전이 있으면 주요 콘텐츠의 언어가 같은 경우에만 중복으로 간주합니다. 즉, 머리글, 바닥글, 기타 중요하지 않은 텍스트만 번역되어 있고 본문이 동일한 페이지는 중복으로 간주합니다.

따라서 Nextjs에서 하단에 동적 Metadata 설정 방식을 따라 설정해야 한다.

정적 Metadata 추가하기

Nextjs File Convention상 웹 페이지를 보여주는 layout.tsx 혹은 page.tsx에서 Metadata 타입의 객체를 내보내면 정적 Metadata가 추가된다.

import type { Metadata } from "next";
 
export const metadata: Metadata = {
  title: "블로그",
  description: "어쩌고 저쩌고 설명",
};
 
export default function Page() {}

다만, Metadata 객체는 서버 컴포넌트에서만 지원되기 때문에 최상단에 클라이언트 컴포넌트임을 알려주는 ‘use client’는 존재하면 안 되며, 추후 설명할 동적 Metadata 추가 함수와 공존할 수 없다.

동적 Metadata 추가하기

동적으로 Metadata를 추가하기 위해서는 generateMetadata 함수를 생성해야 하는데, 단순 Metadata 객체를 반환하는 함수부터 비동기 함수, params 혹은 searchParams를 활용하는 함수, 부모의 Metadata 객체를 활용하는 함수 등이 존재한다.

단순 Metadata 객체 반환 함수

import type { Metadata } from "next";
 
export function generateMetadata(): Metadata {
  return {
    title: "Next.js",
  };
}

비동기 함수

import type { Metadata } from "next";
 
export function generateMetadata(): Metadata {
  return {
    title: "Next.js",
  };
}

params, searchParams를 활용하는 함수

import type { Metadata } from "next";
 
type Props = {
  params: { id: string };
  searchParams: { [key: string]: string | string[] | undefined };
};
 
export function generateMetadata({ params, searchParams }: Props): Metadata {
  return {
    title: "Next.js",
  };
}
 
export default function Page({ params, searchParams }: Props) {}

부모의 Metadata 객체를 활용하는 함수

import type { Metadata, ResolvingMetadata } from "next";
 
export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata,
): Promise<Metadata> {
  return {
    title: "Next.js",
  };
}