한 줄: Next.js 콘텐츠 사이트는 npm run build 직전 prebuild에서 sitemap·RSS·Atom을 같이 재생성해야 검색·구독 피드가 본문과 어긋나지 않는다.
data/*.js나 posts/**/*.js를 추가·수정한 뒤 생성 스크립트를 빼먹으면 네이버 Yeti·구글·RSS 리더가 옛 URL만 본다. package.json에 "prebuild": "node scripts/generate-sitemap.js && node scripts/generate-feeds.js"를 두고, 로컬에서도 커밋 전 npm run prebuild로 public 산출물 mtime·URL 개수를 확인한다. Vercel 빌드는 훅이 돌지만, dev 서버와 수동 확인은 명시 실행이 필요하다.
정적 산출 사이트(또는 빌드 타임에 XML을 쓰는 하이브리드)에서 새 글은 보통 posts/·data/에 들어가고, 검색엔진용 목록은 public/sitemap*.xml, 구독용은 public/rss.xml·public/atom.xml에 남는다. 본문만 커밋하고 피드를 안 돌리면 배포는 성공해도 크롤러·리더는 어제 목록을 읽는다.
실무에서 자주 나는 패턴이다.
- 글 파일은 있고 사이트 맵에는 slug가 없음 → 내부 링크로만 도달, 검색 유입 지연
- sitemap은 갱신됐는데 RSS만 옛 날짜 → 구독 독자 이탈
- news sitemap(최근 30일)만 빠져 최신 글 신호가 약함
- 로컬에선 수동 생성 파일을 보고, CI는 옛 public을 그대로 올림
prebuild는 npm 라이프사이클 훅이라 npm run build 앞에 자동 실행된다. “빌드하면 피드도 따라온다”는 팀 규칙을 코드로 고정하는 장치다. Next.js 라우팅·기능 자체는 Next.js 15 새 기능 가이드 쪽을 보면 되고, 이 글은 검색용 정적 피드 동기화만 다룬다.
최소 구성은 두 스크립트를 한 줄로 잇는 것이다. news sitemap이 있으면 같은 훅에 포함한다.
// package.json (발췌)
{
"scripts": {
"generate-sitemap": "node scripts/generate-sitemap.js",
"generate-feeds": "node scripts/generate-feeds.js",
"prebuild": "npm run generate-sitemap && npm run generate-feeds",
"build": "next build"
}
}
생성기는 보통 data/posts.js(또는 posts 디렉터리 require)를 읽어 다음을 쓴다.
public/sitemap.xml 인덱스 + sitemap-posts.xml 등 엔티티별 분할
public/sitemap-news.xml (최근 30일, 있는 사이트만)
public/rss.xml (RSS 2.0), public/atom.xml (Atom 1.0)
- 각 URL에
lastmod·changefreq·priority (정책에 맞게)
canonical과 trailing slash 정책이 어긋나면 중복 색인이 난다. Next 설정에 trailingSlash: true를 쓰는 사이트는 sitemap·RSS 안의 모든 링크도 슬래시로 끝나게 맞춘다. OG 이미지 redirect 이슈까지 보면 skipTrailingSlashRedirect: true와 세트로 점검한다. App Router 이전 이슈는 App Router 마이그레이션 글의 라우팅 절과 겹친다.
콘텐츠 추가 후 표준 발행 순서를 표로 고정해 두면 실수가 줄어든다.
| 단계 |
명령·확인 |
통과 기준 |
| 1. 문법 |
node --check posts/{id}.js |
exit 0, SyntaxError 없음 |
| 2. 피드 재생성 |
npm run prebuild |
에러 없이 완료, public 파일 mtime 갱신 |
| 3. URL 포함 |
rg "my-new-slug" public/sitemap*.xml public/rss.xml |
신규 slug가 sitemap·RSS에 존재 |
| 4. 개수 sanity |
URL 개수 vs posts 메타 개수 비교 |
의도적 제외(noindex 등) 외 일치 |
| 5. 커밋 범위 |
data + posts + public/sitemap* + rss/atom |
산출물을 본문과 같은 커밋에 포함 |
| 6. 배포 빌드 |
npm run build (prebuild 자동) |
빌드 로그에 generate 스크립트 실행 흔적 |
# 콘텐츠 수정 직후 (로컬)
node --check posts/3535.js
npm run prebuild
# 산출물 확인
ls -la public/sitemap.xml public/rss.xml public/atom.xml
rg -n "nextjs-prebuild-sitemap-rss-2026" public/sitemap*.xml public/rss.xml public/atom.xml
# 배포 파이프라인에서는 build만 호출해도 prebuild가 선행
npm run build
호스팅(Vercel 등)에서 npm run build를 돌리면 prebuild 훅이 따라와 서버 산출물은 최신일 수 있다. 그래도 로컬 검증·리뷰 PR·“sitemap.xml 직접 열어보기” 시나리오에서는 명시 실행이 필요하다.
- 로컬 dev:
next dev만 켜면 prebuild가 안 돈다. 피드 확인 전에 npm run prebuild.
- git diff 리뷰: public XML diff가 없으면 생성 누락 신호다. 콘텐츠 PR에 피드 diff를 같이 요구한다.
- 자동화 포스팅 (launchd·cron·GitHub Actions): 글 파일 write 후 반드시 prebuild 단계를 포함한다. Claude 잡이 본문만 쓰고 끝내면 색인이 lagged 상태가 된다.
- 캐시: CDN이 XML을 길게 캐시하면 재생성 후에도 옛 목록이 보일 수 있다. Cache-Control·퍼지 정책을 확인한다.
팀 규칙으로 “콘텐츠 커밋 = data/posts + public 피드 동시 커밋”을 권장한다. 빌드 서버만 최신이고 저장소 public은 한 달 전인 상태는, 다른 브랜치·롤백·정적 미러에서 사고 낸다.
robots.txt에서 sitemap 위치를 가리키는지, Yeti·Daumoa Allow가 있는지도 같이 본다. 피드 링크는 _document.js 또는 root layout의 <link rel="alternate" type="application/rss+xml">로 노출한다.
현장에서 반복되는 실패 모드다.
- prebuild 스크립트 미연결: generate 파일은 있는데 package.json에 훅이 없음. build 로그에 node scripts 실행이 안 보인다.
- 한 쪽만 실행: sitemap만 돌리고 feeds를 빼먹음. RSS 구독기만 구식.
- trailing slash 불일치: 페이지 canonical은
/foo/인데 sitemap은 /foo. 검색엔진이 둘 다 수집하거나 OG 수집이 깨질 수 있다.
- draft·noindex를 피드에 포함: 미공개 slug가 외부에 노출. 생성기에 필터 플래그를 둔다.
- 날짜 필드 오류:
lastmod·RSS pubDate가 하드코딩 고정일이면 크롤러가 갱신을 무시한다. posts 메타의 date를 소스로 쓴다.
- 절대 URL 누락:
https://tech.example.com/... 대신 상대 경로만 넣으면 일부 리더가 깨진다. 사이트 기본 URL env를 한곳에서 읽는다.
생성기 디버깅 시에는 새 글 하나 slug를 골라 rg로 public 전체를 긁는 방법이 가장 빠르다. 개수가 안 맞으면 필터 조건(카테고리, isTrend, published 플래그)을 로그로 덤프한다.
Next 쪽 데이터 로딩·마이그레이션 이슈와 섞이면 원인 분기가 어려워진다. 라우터·데이터 계층 문제는 별도 글로 분리하고, 피드 생성기는 “메타 배열 → XML 문자열” 순수 변환으로 유지하는 편이 안전하다.
권장 발행 파이프라인을 한 줄로 고정한다.
# 1) 본문·메타 추가/수정
# data/posts.js + posts/{id}.js
# 2) 문법 검사
node --check posts/{id}.js
# 3) 검색·구독 피드 동기화 (필수)
npm run prebuild
# 4) 스테이징
git add data/ posts/ public/sitemap*.xml public/rss.xml public/atom.xml
# 5) 커밋 후 푸시 (배포는 팀 정책에 따름)
git commit -m "post: add {slug} + refresh feeds"
git push
자동화 에이전트(Claude Code headless, GitHub Actions)도 동일하다. 본문 생성 성공 코드 다음에 prebuild를 강제하고, prebuild 실패 시 커밋을 막는다. 인증 만료로 CLI가 죽으면 피드 단계까지 못 가므로, Claude Code 401 인증 복구와 운영을 같이 본다.
관련 자동화 스케줄은 cron 스케줄 가이드, 리밋 시 재개는 리밋·재개 전략을 참고한다. 피드 동기화는 “SEO 장식”이 아니라 배포 산출물의 일부로 취급해야 한다.
※ 프레임워크·호스팅·스크립트 경로는 프로젝트마다 다르다. 스크립트 이름이 다르면 package.json 훅만 맞춰 같은 원칙을 적용하면 된다.
프레임워크·호스팅 설정은 프로젝트마다 다릅니다. package.json 훅 이름과 스크립트 경로만 환경에 맞게 바꾸면 됩니다.