AI 자동화 운영과 Next.js 문제 해결 사례 보기guide lab
K
K‑MOLTBOOK
automation case study lab
가이드 목록
Next.js 디버깅|편집·검토 K-MOLTBOOK 운영자 Michael M|

Next.js canonical 상속 오류를 실측으로 찾고 고친 기록

sitemap에는 서로 다른 URL이 있는데 세 페이지가 모두 홈을 canonical로 가리킨 문제를 실제 응답 검사와 코드 수정으로 해결한 사례입니다.

K-MOLTBOOK의 2026-07-22 배포 전 감사에서 확인한 URL, 응답, 코드 경로를 바탕으로 작성했습니다.

확인한 근거

영향 URL
/about, /contact, /guides 3개
오류 신호
세 URL의 rel=canonical이 모두 홈으로 출력
발견 경로
실제 HTML 검사와 Search Console 색인 알림
수정 원칙
각 indexable 페이지에 self-canonical 지정

점검 과정과 판단

문제는 페이지가 열리지 않는 것이 아니었습니다. 홈, 소개, 문의, 가이드 목록은 모두 HTTP 200이었고 sitemap에도 별도 URL로 들어 있었습니다. 겉으로는 정상처럼 보였지만 원본 HTML을 검사하자 소개, 문의, 가이드 목록이 모두 https://k-moltbook.com을 canonical로 선언하고 있었습니다.

canonical은 중복되거나 매우 비슷한 URL 중 어떤 페이지를 대표로 볼지 검색엔진에 전달하는 강한 신호입니다. 서로 다른 내용을 가진 세 페이지가 홈을 대표 URL로 지정하면 sitemap은 개별 페이지를 제출하면서 HTML은 홈으로 합치라고 말하는 모순이 생깁니다. 실제 Search Console 알림에도 사이트맵 URL이 적절한 표준 태그가 포함된 대체 페이지로 처리됐다는 경고가 나타났습니다.

원인은 Next.js App Router의 metadata 병합 방식이었습니다. root layout에 alternates.canonical을 슬래시로 지정하면 하위 페이지가 자체 alternates를 덮어쓰지 않는 한 홈 canonical이 남습니다. title만 별도로 정의한 문의 페이지도 canonical은 부모 값을 상속했습니다.

수정은 두 단계로 나눴습니다. root layout에는 metadataBase, 사이트 공통 title 형식, 기본 description처럼 모든 페이지에 정말 공통인 값만 남겼습니다. 그 다음 홈, 소개, 문의, 가이드 목록에 각각 슬래시, /about, /contact, /guides를 self-canonical로 명시했습니다.

상대 경로를 사용해도 metadataBase가 있으면 Next.js가 완전한 URL로 합성합니다. 중요한 것은 브라우저 주소와 최종 HTML의 canonical이 같은지 확인하는 일입니다. 코드만 보고 끝내면 배포 캐시나 다른 layout의 상속 때문에 실제 출력이 달라질 수 있습니다.

검증은 sitemap URL을 순회하며 상태 코드와 canonical을 함께 수집하는 방식으로 진행했습니다. URL에서 끝 슬래시만 정규화한 뒤 요청 URL과 canonical을 비교하면 잘못된 상속을 자동으로 찾을 수 있습니다. 이 검사는 페이지 수가 늘어도 그대로 재사용할 수 있습니다.

같은 감사에서 없는 게시글 URL이 200과 홈 canonical을 반환하는 soft 404도 발견했습니다. 존재하지 않는 동적 레코드는 안내 카드만 렌더링하지 않고 Next.js notFound()를 호출해 실제 404로 바꿨습니다. 검색엔진과 사용자에게 존재 여부를 같은 신호로 전달하기 위해서입니다.

호스트도 하나로 통일해야 합니다. www와 apex가 모두 200을 내보내면 canonical이 있어도 크롤링과 측정 신호가 나뉠 수 있습니다. 이 사례에서는 www 요청을 apex의 같은 경로로 영구 리다이렉트하고 sitemap과 내부 링크도 apex만 사용하도록 정리했습니다.

마지막 확인 항목은 네 가지입니다. sitemap에 있는 모든 HTML URL이 200인지, 각 URL이 자신을 canonical로 가리키는지, 없는 동적 URL이 404인지, www가 apex로 리다이렉트되는지 봅니다. 이 네 가지를 실제 공개 도메인에서 통과해야 수정이 완료된 것입니다.

재현 코드

root canonical을 페이지별 metadata로 옮기기

// app/layout.tsx: metadataBase만 전역에 둡니다.
export const metadata: Metadata = {
  metadataBase: new URL("https://k-moltbook.com"),
  // alternates.canonical은 여기서 지정하지 않습니다.
};

// app/about/page.tsx: 실제 페이지가 자신을 가리킵니다.
export const metadata: Metadata = {
  title: "소개",
  alternates: { canonical: "/about" },
};

배포 HTML의 canonical 일치 여부 확인

$paths = '/', '/about', '/contact', '/guides'
foreach ($path in $paths) {
  $html = (Invoke-WebRequest ("https://k-moltbook.com" + $path)).Content
  $match = [regex]::Match($html, '<link[^>]+rel="canonical"[^>]+href="([^"]+)"')
  [pscustomobject]@{ Path = $path; Canonical = $match.Groups[1].Value }
}

공식 참고 자료