수정: Qwik가 빈 sitemap.xml을 생성
Source: Dev.to
문제
Node.js 어댑터(nodeServerAdapter)로 Qwik(또는 Qwik City) 애플리케이션을 배포할 때, sitemap.xml은 로컬에서는 정상 작동하지만 프로덕션에서는 비어 있습니다:
<!-- empty sitemap -->
- 로컬 빌드: ✅ 정상 작동
- 개발 서버: ✅ sitemap을 올바르게 제공
- 프로덕션 빌드: ❌ 빈 sitemap, 경고 없음
Node.js 어댑터는 **정적 사이트 생성(SSG)**을 빌드 단계에서 실행합니다(SSR 빌드에서도). 이 단계에서:
- 정적으로 내보낼 수 있는 페이지를 찾기 위해 라우트를 스캔
- 찾은 라우트로 sitemap을 생성
- 직접
public/sitemap.xml에 넣어 둔 파일을 덮어씀
SSG 단계에서 내보낼 수 있는 라우트를 찾지 못하면(예: 동적 라우트, SSR 전용 라우트, 혹은 Node.js 어댑터 사용 시) 빈 sitemap이 생성됩니다.
빠른 해결책 – SSG 비활성화
SSR 전용 애플리케이션이라면 어댑터 설정에서 SSG를 완전히 끌 수 있습니다.
adapters/node-server/vite.config.ts
import { nodeServerAdapter } from "@builder.io/qwik-city/adapters/node-server/vite";
import { extendConfig } from "@builder.io/qwik-city/vite";
import baseConfig from "../../vite.config";
export default extendConfig(baseConfig, () => ({
build: {
ssr: true,
rollupOptions: {
input: ["src/entry.express.tsx", "@qwik-city-plan"],
},
},
plugins: [
nodeServerAdapter({
name: "express",
// ⚠️ CRITICAL: Completely disable SSG – use a static sitemap.xml
ssg: null,
}),
],
}));
핵심 변경점: ssg: null은 자동 sitemap 생성을 중단하므로 public/에 넣은 파일이 그대로 제공됩니다.
정적 Sitemap 제공
public/sitemap.xml에 인덱싱하고 싶은 URL을 작성합니다.
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://yourdomain.com/</loc>
<lastmod>2024-12-01</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://yourdomain.com/about/</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
다국어 예시
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://yourdomain.com/</loc>
<priority>1.0</priority>
</url>
<url>
<loc>https://yourdomain.com/es/</loc>
<priority>0.8</priority>
</url>
<url>
<loc>https://yourdomain.com/de/</loc>
<priority>0.8</priority>
</url>
</urlset>
빌드 스크립트 조정
package.json에 qwik city collect가 들어 있다면 제거하세요—그렇지 않으면 SSG가 다시 강제로 실행됩니다.
{
"scripts": {
// Before
// "build:deploy": "npm run build.production && qwik city collect"
// After
"build:deploy": "npm run build.production"
}
}
빌드를 실행하고 확인합니다:
npm run build:deploy
ls -lh dist/sitemap.xml
cat dist/sitemap.xml
빈 파일이 아니라 전체 sitemap 내용이 보일 것입니다.
Node.js 서버는 이제 dist/sitemap.xml을 정적 에셋으로 제공합니다:
node server.js
curl http://localhost:3000/sitemap.xml
동적 Sitemap (SSR 라우트)
CMS 등에서 실시간으로 sitemap을 생성해야 한다면 SSR 라우트를 만들면 됩니다.
src/routes/sitemap.xml/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
const SUPPORTED_LOCALES = ['en-US', 'es', 'ru', 'de', 'fr', 'ja'];
export const onGet: RequestHandler = async ({ url }) => {
const origin = url.origin;
const urls = [
// Root URL
`
${origin}/
${new Date().toISOString().split('T')[0]}
weekly
1.0
`,
// Language‑specific URLs
...SUPPORTED_LOCALES
.filter(l => l !== 'en-US')
.map(l => `
${origin}/${l}/
weekly
0.8
`),
].join('\n');
const sitemap = `
${urls}
`;
return new Response(sitemap, {
status: 200,
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, max-age=86400', // 1 day
},
});
};
Note: 어댑터 설정에
ssg: null을 유지해 정적 생성 단계가 이 동적 라우트를 덮어쓰지 않도록 합니다.
왜 Node.js 어댑터에서도 SSG가 실행되는가
- Static Site Generation (SSG) – 빌드 시 페이지를 미리 렌더링하고, sitemap을 생성하며, 정적 파일을 씁니다.
- Server‑Side Rendering (SSR) – 런타임에 페이지를 즉시 렌더링합니다.
Node.js 어댑터는 SSR을 위한 것이지만 기본적으로 sitemap 생성과 onStaticGenerate가 있는 페이지를 미리 렌더링하기 위해 SSG를 트리거합니다. 라우트가 동적이거나 onStaticGenerate가 없으면 SSG는 내보낼 라우트가 0개라고 판단해 빈 sitemap을 기록합니다.
흔히 저지르는 실수
| 문제 | 원인 | 해결책 |
|---|---|---|
ssg: { exclude: ['*'] } | 파일을 제외하지만 여전히 SSG가 실행돼 sitemap을 덮어씀 | ssg: null 사용 |
빌드 스크립트에 qwik city collect | 빌드 후 SSG를 강제로 실행해 정적 파일을 덮어씀 | 해당 명령 제거 |
동적 콘텐츠를 public/에만 의존 | 동적 라우트는 정적 파일이 아니라 SSR 핸들러가 필요 | 위의 SSR 라우트 구현 |
dist/가 매 빌드마다 비워짐 | 정적 파일은 public/에 두고 dist/에 복사돼야 함 | public/sitemap.xml 유지 |
적용 후 체크리스트
-
ssg: null설정 후 “Starting Qwik City SSG…” 로그가 나타나지 않음 -
dist/sitemap.xml파일이 존재하고 빈 109바이트 파일보다 큼 -
cat dist/sitemap.xml출력에 기대한 URL들이 포함됨 - 프로덕션 서버가 전체 sitemap을 반환 (
curl https://yourdomain.com/sitemap.xml) - 빌드 로그에 SSG 관련 경고나 오류가 없음
선택 사항: /sitemap.xml/ 를 /sitemap.xml 로 리다이렉트
Trailing slash 로 요청될 경우 간단히 리다이렉트를 추가합니다:
import { createServer } from "http";
createServer((req, res) => {
if (req.url === "/sitemap.xml/") {
res.writeHead(301, { Location: "/sitemap.xml" });
res.end();
return;
}
// ...rest of server logic
}).listen(3000);
검색 엔진에는 무해하지만 URL을 깔끔하게 정리할 수 있습니다.
정적 Sitemap vs. SSR 라우트 선택 가이드
| 사용 사례 | 권장 방법 |
|---|---|
| 순수 SSR 앱, 라우트가 개발 시점에 고정, sitemap 변경이 드물 경우 | 정적 sitemap (public/sitemap.xml + ssg: null) |
| CMS, DB, 사용자 생성 콘텐츠 등에서 sitemap 데이터를 가져와야 할 때 | SSR 라우트 (src/routes/sitemap.xml/index.ts) |
| 실시간 업데이트 또는 로케일별 로직이 필요할 때 | SSR 라우트 |
| 별도 빌드 단계 없이 간단히 배포하고 싶을 때 | 정적 sitemap |
프로젝트에 맞는 방식을 적용하면 빈 sitemap 문제는 해결됩니다.