NextJSSEOnext-intlGoogleSearchConsole

Next.js 다국어 블로그에서 canonical 빠뜨리면 생기는 일

May 17, 20261 min read

Google Search Console을 열었더니 이런 메시지가 떠 있었다.

중복 페이지, Google에서 사용자와 다른 표준을 선택함

영향받은 페이지: https://backtodev.com/ko, https://backtodev.com/ko/contact

"내가 표준을 선택하려고 했는데 Google이 다르게 골랐다"는 뜻이다. 알고 보니 홈 페이지에 canonical 태그 자체가 없었다.


문제 상황

이 블로그는 next-intl로 한국어·영어를 분리 운영한다. URL 구조는 이렇다.

https://backtodev.com/ko    ← 한국어 홈
https://backtodev.com/en    ← 영어 홈

루트 /로 접근하면 next-intl이 자동으로 언어를 감지해 /ko 또는 /en으로 리다이렉트한다.

Google 입장에서는:

  • /를 크롤링 → /ko로 리다이렉트됨
  • /ko도 따로 크롤링
  • 둘 다 같은 내용인데 canonical이 없으니 "이 중에 뭐가 표준이야?" 혼란

원인 파악

canonical 태그가 있는 페이지를 찾아보니 포스트 상세, posts 목록, about, portfolio 등에는 다 있는데, 홈 페이지만 없었다.

app/
  [locale]/
    page.tsx         ← ❌ generateMetadata 없음 (홈)
    contact/
      page.tsx       ← ⚠️ canonical은 있지만 description이 영어 고정
    posts/
      page.tsx       ← ✅ canonical 있음
    about/
      page.tsx       ← ✅ canonical 있음

Next.js App Router에서 canonical 태그는 generateMetadata에서 alternates.canonical을 설정해야 자동으로 <link rel="canonical"> 태그가 붙는다. 이게 없으면 canonical 태그 자체가 HTML에 포함되지 않는다.


해결

Step 1 — 홈 페이지에 generateMetadata 추가

// app/[locale]/page.tsx
import type { Metadata } from "next";

const BASE_URL = "https://backtodev.com";

export async function generateMetadata({
  params,
}: {
  params: Promise<{ locale: string }>;
}): Promise<Metadata> {
  const { locale } = await params;
  const isKo = locale === "ko";

  return {
    title: isKo ? "backtodev — 다시 개발자로" : "backtodev — Back to Dev",
    description: isKo
      ? "40대 PM이 다시 개발자로 돌아오는 기록. 실패하고 배우며 성장하는 이야기."
      : "A 40-something PM returning to development. Notes on learning, building, and shipping.",
    alternates: {
      canonical: `${BASE_URL}/${locale}`,   // 핵심
      languages: {
        ko: `${BASE_URL}/ko`,
        en: `${BASE_URL}/en`,
      },
    },
  };
}

alternates.canonical을 설정하면 Next.js가 자동으로 아래 태그들을 <head>에 삽입한다.

<link rel="canonical" href="https://backtodev.com/ko" />
<link rel="alternate" hrefLang="ko" href="https://backtodev.com/ko" />
<link rel="alternate" hrefLang="en" href="https://backtodev.com/en" />

hrefLang alternate는 Google에게 "이 페이지의 다른 언어 버전은 여기야"라고 알려준다. 다국어 사이트에서 중복 콘텐츠 신호를 줄이는 데 도움이 된다.

Step 2 — contact 페이지 description 분리

contact 페이지는 canonical은 있었지만 description이 영어 고정이었다.

// 수정 전
description: "Get in touch with backtodev.",

// 수정 후
const isKo = locale === "ko";
description: isKo ? "backtodev에 연락하기" : "Get in touch with backtodev",

같은 URL인데 언어마다 다른 내용을 보여주는 페이지에서 description까지 같으면 Google이 중복 신호로 읽을 수 있다. 사소해 보이지만 다국어 사이트에서는 이런 디테일이 쌓인다.


배포 후 확인

빌드하고 배포하면 실제 HTML에서 확인할 수 있다.

curl -s "https://backtodev.com/ko" | grep -i "canonical\|hrefLang"
<link rel="canonical" href="https://backtodev.com/ko"/>
<link rel="alternate" hrefLang="ko" href="https://backtodev.com/ko"/>
<link rel="alternate" hrefLang="en" href="https://backtodev.com/en"/>

이렇게 나오면 정상이다.

Search Console에서는 해당 URL을 URL 검사 → 색인 생성 요청해두면 Google이 빠르게 재크롤링한다. 오류가 해소되기까지는 보통 며칠에서 몇 주 걸린다.


next-intl 다국어 사이트 canonical 체크리스트

페이지canonicalhreflangdescription 분리
홈 (/[locale])✅ 필수✅ 필수✅ 권장
목록 (/[locale]/posts)✅ 필수✅ 필수✅ 권장
상세 (/[locale]/posts/[slug])✅ 필수✅ 필수— (포스트별 자동)
정적 페이지 (about, contact)✅ 필수✅ 필수✅ 권장

빠진 페이지가 없는지 이 표로 한 번씩 점검해보면 좋다.


트러블슈팅

generateMetadata를 추가했는데 canonical 태그가 안 보인다

params에서 locale을 제대로 받아오는지 확인한다. App Router에서는 params가 Promise라 await이 필요하다.

// 틀림
export async function generateMetadata({ params }) {
  const locale = params.locale  // ❌ Promise를 await 안 함
}

// 맞음
export async function generateMetadata({ params }) {
  const { locale } = await params  // ✅
}

getLocale()로 locale을 가져오는데 generateMetadata에서 쓸 수 없다

getLocale()은 서버 컴포넌트 내부에서만 동작한다. generateMetadata에서는 반드시 params에서 locale을 받아야 한다.


정리

문제: 홈 페이지 generateMetadata 누락
        ↓
  canonical 태그 없음
        ↓
  Google이 / 와 /ko 를 중복으로 인식
        ↓
  "중복 페이지, Google에서 사용자와 다른 표준을 선택함" 오류

해결: generateMetadata 추가
  → alternates.canonical 설정
  → alternates.languages로 hreflang까지 한 번에 처리

포스트 상세 페이지만 SEO를 신경 쓰고 홈이나 목록 페이지는 빠뜨리는 경우가 많다. 다국어 사이트라면 특히 모든 라우트를 빠짐없이 점검해야 한다.

PM

backtodev

40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.

Next.js 다국어 블로그에서 canonical 빠뜨리면 생기는 일 | backtodev