Next.js App Router에서 검색 날짜와 사이트맵 관리하기
블로그 글을 최신화하면 검색결과에도 수정된 날짜가 보이면 좋다.
하지만 날짜를 한 곳만 바꾼다고 검색결과가 바로 바뀌는 것은 아니다.
사용자에게 보이는 날짜, 메타데이터, 구조화 데이터, 사이트맵이 서로 다른 날짜를 말하면 검색엔진이 페이지의 최신성을 해석하기도 어려워진다.
Next.js App Router에서는 이 흐름을 비교적 단순하게 연결할 수 있다.
글의 원본 메타데이터에서 발행일과 수정일을 관리하고, 페이지 메타데이터와 JSON-LD와 sitemap이 같은 날짜 기준을 바라보게 만들면 된다.
검색 날짜의 기준
Google은 검색결과에 표시되는 날짜를 보장하지 않는다.
사용자에게 보이는 날짜와 구조화 데이터의 날짜가 일관되면 날짜를 찾고 처리하는 데 도움이 된다고 설명한다.
따라서 목표는 특정 날짜 표시를 강제로 만드는 것이 아니라, 페이지가 스스로 일관된 신호를 제공하게 만드는 것이다.
기술 블로그에서는 보통 세 가지 날짜를 나눠두면 관리하기 쉽다.date는 최초 발행일이다.updatedAt은 내용을 의미 있게 고친 날짜다.reviewedAt은 오래된 글을 검토했지만 내용은 크게 바꾸지 않은 날짜다.
검색엔진에 넘길 최신성 기준은 이 순서로 잡을 수 있다.
export function getPostFreshnessDate(metadata: {
date: string
updatedAt?: string
reviewedAt?: string
}) {
return metadata.updatedAt ?? metadata.reviewedAt ?? metadata.date
}이렇게 기준을 함수 하나로 고정해두면 페이지와 사이트맵이 서로 다른 판단을 하지 않는다.
글 상세 화면에는 발행일과 수정일 또는 검토일을 함께 보여주고, 기계가 읽는 값에는 같은 최신성 기준을 전달한다.
generateMetadata의 modifiedTime
App Router의 동적 페이지에서는 generateMetadata로 글마다 다른 메타데이터를 만들 수 있다.
Next.js 문서 기준으로 generateMetadata는 route params나 외부 데이터처럼 동적인 정보에 따라 Metadata 객체를 반환할 때 사용한다.
블로그 상세 페이지에서는 slug로 글을 찾고, Open Graph의 publishedTime과 modifiedTime을 채운다.
import type { Metadata } from 'next'
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params
const item = await getPostBySlug(decodeURIComponent(slug))
if (!item) {
return {}
}
return {
title: item.metadata.title,
description: item.metadata.excerpt,
openGraph: {
title: item.metadata.title,
description: item.metadata.excerpt,
type: 'article',
publishedTime: item.metadata.date,
modifiedTime: getPostFreshnessDate(item.metadata),
},
}
}여기서 중요한 점은 modifiedTime을 무조건 오늘 날짜로 넣지 않는 것이다.
실제로 글을 고쳤거나 검토한 날짜만 반영해야 한다.
날짜는 검색 노출을 위한 장식이 아니라 페이지 상태를 설명하는 값이다.
JSON-LD의 dateModified
JSON-LD는 검색엔진이 페이지의 구조를 이해하도록 돕는 structured data 형식이다.
Next.js 문서는 layout.js나 page.js에서 <script type="application/ld+json"> 형태로 렌더링하는 방식을 안내한다.
Google도 Article 계열 structured data에서 datePublished와 dateModified를 날짜 신호로 사용할 수 있다고 설명한다.
블로그 글이라면 BlogPosting 타입을 사용하고, dateModified에는 같은 최신성 기준을 넣는다.
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: item.metadata.title,
description: item.metadata.excerpt,
url: `${SITE_URL}/posts/${encodeURIComponent(item.slug)}`,
datePublished: item.metadata.date,
dateModified: getPostFreshnessDate(item.metadata),
author: {
'@type': 'Person',
name: '안도현',
},
}직접 문자열을 조립하지 않고 JSON.stringify 계열로 직렬화하는 편이 안전하다.
사용자 입력이 섞일 수 있는 프로젝트라면 < 같은 문자를 이스케이프해서 script 안에서 의도치 않은 HTML이 열리지 않게 해야 한다.
function serializeJsonLd(value: unknown) {
return JSON.stringify(value).replace(/</g, '\\u003c')
}이 값도 화면에 보이는 수정일과 어긋나면 안 된다.
본문에는 “수정 6월 29일”이라고 보이는데 JSON-LD에는 다른 날짜가 들어가면, 페이지가 보내는 신호가 흐려진다.
sitemap lastModified 구성
Next.js App Router에서는 app/sitemap.ts 파일로 sitemap을 코드에서 생성할 수 있다.
공식 문서의 MetadataRoute.Sitemap 타입을 사용하면 URL 배열을 반환하는 방식으로 sitemap.xml을 만들 수 있다.
블로그에서는 모든 게시글을 읽어서 lastModified에 최신성 기준 날짜를 넣는다.
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts()
return [
{
url: SITE_URL,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1,
},
...posts.map((post) => ({
url: `${SITE_URL}/posts/${encodeURIComponent(post.slug)}`,
lastModified: new Date(getPostFreshnessDate(post.metadata)),
changeFrequency: 'monthly' as const,
priority: 0.8,
})),
]
}lastModified도 updatedAt, reviewedAt, date 순서를 그대로 따른다.
새 글은 발행일이 lastmod가 되고, 기존 글을 크게 수정하면 updatedAt이 lastmod가 된다.
내용을 유지하면서 최신 문서 기준으로 검토만 했다면 reviewedAt을 lastmod로 사용할 수 있다.
정적 export를 쓰는 블로그라면 빌드 산출물의 sitemap.xml까지 확인하는 습관이 좋다.
코드에서 의도한 날짜와 실제 배포 파일의 날짜가 같은지 보는 것이 마지막 점검이다.
robots의 sitemap 위치
검색엔진이 sitemap 위치를 쉽게 찾도록 robots.ts에서도 sitemap URL을 노출한다.
Next.js는 app/robots.ts에서 MetadataRoute.Robots 객체를 반환하는 방식을 제공한다.
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: `${SITE_URL}/sitemap.xml`,
}
}이 설정은 모든 크롤러에게 페이지 접근을 허용하고 sitemap 위치를 알려준다.
비공개 경로나 관리자 경로가 있다면 disallow를 함께 설계해야 하지만, 공개 블로그라면 단순한 형태로 충분하다.
배포 후 확인
배포 전에 콘텐츠 검증과 빌드를 통과시키고, 빌드 결과의 sitemap을 확인한다.
정적 산출물이 있는 프로젝트라면 out/sitemap.xml에서 신규 글 URL과 lastmod를 찾는다.
npm run validate:content
npm run check:links
npm run check:content-quality
npm run check:assets
npm run build배포 후에는 실제 도메인의 sitemap도 확인한다.
curl -fsSL https://example.com/sitemap.xml확인할 값은 많지 않다.
신규 글 URL이 들어갔는지 본다.
기존 글을 수정했다면 lastmod가 수정 날짜로 바뀌었는지 본다.
글 상세 페이지의 화면 날짜, Open Graph modifiedTime, JSON-LD dateModified, sitemap lastModified가 같은 기준을 쓰는지 본다.
정리
검색 날짜 관리는 검색엔진을 속이는 작업이 아니다.
페이지가 언제 발행됐고 언제 의미 있게 바뀌었는지 일관되게 표현하는 작업이다.
Next.js App Router에서는 generateMetadata, JSON-LD script, sitemap.ts, robots.ts를 연결하면 이 흐름을 한 프로젝트 안에서 관리할 수 있다.
핵심은 날짜를 여러 곳에서 따로 계산하지 않는 것이다.updatedAt → reviewedAt → date 같은 기준을 먼저 정하고, 사람이 보는 날짜와 기계가 읽는 날짜가 같은 기준을 공유하게 만들면 된다.
참고 자료




