๐ [NextJS, SEO] AppRouter Dynamic Sitemap.xml ์์ฑํ๊ธฐ
๊ฒ์ ์์ง ์ต์ ํ(SEO)์์ ์ค์ํ ์์๋ก ์ฌ์ฉ๋๋ ์ฌ์ดํธ๋งต์, ์น์ฌ์ดํธ์ ํ์ด์ง์ ์ฝํ ์ธ ๋ฅผ ๊ตฌ์กฐ์ ์ผ๋ก ๋์ดํ ํ์ผ๋ก, ๊ฒ์ ์์ง๊ณผ ์ฌ์ฉ์์๊ฒ ์น์ฌ์ดํธ์ ๊ตฌ์กฐ์ ํ์ด์ง ๊ฐ ๊ด๊ณ๋ฅผ ๋ช ํํ ์ ๋ฌํ๋ ์ญํ ์ ํฉ๋๋ค.
์ด๋ฒ ํ๋ก์ ํธ์์ ๊ฐ ํ๋ ์ฐจ์ด์ฆ ๋ธ๋๋๋ง๋ค ์ฌ์ดํธ๋งต์ ์์ฑํ์ฌ ๊ฒ์ ๊ฒฐ๊ณผ์ ๊ฑธ๋ฆฌ๊ฒ ํ๊ธฐ ์ํด ์ฌ์ฉํด๋ดค์ต๋๋ค.
์ด๋ฒ์ NextJS AppRouter์์ ์ฌ์ดํธ๋งต์ ๋์ ์ผ๋ก ์์ฑํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์๊ฐํฉ๋๋ค.
๐ sitemap.xml Route Handler ํด๋ ๊ตฌ์กฐ ์ก๊ธฐ
app
ใด sitemap.xml
ใด route.ts
์์ ๊ฐ์ด ํด๋ ๊ตฌ์กฐ๋ฅผ ์ก์์ฃผ์ด https://xxx.com/sitemap.xml๋ก ์ง์ ํ์ ๋ ์ฌ์ดํธ๋งต ๋ฐ์ดํฐ๋ฅผ ๋ฟ๋ ค์ค ์ ์๋๋ก ํฉ๋๋ค.
๐ Api Route ์์ฑ
import { NextResponse } from 'next/server';
const formatDate = (date: Date): string => {
return date.toISOString().split("T")[0];
};
// ๋์ URL ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์ (์: ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ API์์ ๊ฐ์ ธ์ค๊ธฐ)
async function getDynamicUrls() {
// ์์ ๋ฐ์ดํฐ
return [
{ loc: 'https://example.com/page1' },
{ loc: 'https://example.com/page2' },
];
}
export async function GET() {
const urls = await getDynamicUrls();
// XML ์์ฑ
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls
.map(
({ loc }) => `
<url>
<loc>${loc}</loc>
<lastmod>${formatDate(new Date())}</lastmod>
</url>
`
)
.join('')}
</urlset>
`;
return new NextResponse(sitemap, {
headers: {
'Content-Type': 'application/xml',
},
});
}
API Route๋ฅผ ์ด์ฉํด ์์ฒญ์ ๋ฐ์ผ๋ฉด DB์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ์ฆ์ ๋์ ์ผ๋ก ์ฌ์ดํธ๋งต์ ์์ฑํ์ฌ ์๋ตํด์ค๋๋ค.
์ ๋ต์ ์ผ๋ก ์ค์ํ๊ฒ ๋ ธ์ถ์ด ํ์ํ ํ์ด์ง์ ์ฌ์ดํธ๋งต์ ๋์ ์ผ๋ก ์์ฑํด์ฃผ์๋ฉด ๋ฉ๋๋ค.
๐ ์์
์ด๋ฒ ํ๋ก์ ํธ์์ ์ ๋ ํ๋์ฐจ์ด์ฆ ๋ธ๋๋ ํ์ด์ง๋ง๋ค ์ฌ์ดํธ๋งต์ ์์ฑํด์ฃผ์๋ค๊ณ ํ๋๋ฐ์.
๊ทธ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
import { BrandService } from "@/services/brand";
import { NextResponse } from "next/server";
type Route = {
url: string;
priority: string;
};
const formatDate = (date: Date): string => {
return date.toISOString().split("T")[0];
};
const fetchRoutes = async (): Promise<Route[]> => {
const baseUrl = process.env.NEXT_PUBLIC_DOMAIN;
const staticRoutes: Route[] = [
{ url: `${baseUrl}/`, priority: "1" },
{ url: `${baseUrl}/search`, priority: "0.9" },
];
const brandRoute: Route[] = [];
let pageNo = 1;
while (1) {
const dataList = (await BrandService.getBrandList({ pageNo, pageSize: 1000 }, true))?.payload;
if (!dataList?.length) break;
dataList.forEach((item) =>
brandRoute.push({
url: `${baseUrl}/brand/${item.brandNm}`,
priority: "0.8",
})
);
pageNo++;
}
return [...staticRoutes, ...brandRoute];
};
const generateSitemap = (routes: Route[]) => {
const escapeXml = (str: string) => {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
${routes
.map(
({ url, priority }) => `
<url>
<loc>${escapeXml(url)}</loc>
<lastmod>${formatDate(new Date())}</lastmod>
<priority>${priority}</priority>
<changefreq>weekly</changefreq>
</url>
`
)
.join("")}
</urlset>`;
};
export async function GET() {
const routes = await fetchRoutes();
const sitemap = generateSitemap(routes);
return new NextResponse(sitemap, {
headers: {
"Content-Type": "application/xml",
},
});
}
ํ๋ฒ์ ๋ชจ๋ ๊ฐ์ ๊ฐ์ ธ์ค๋ฉด ์๋ฒ๊ฐ ์ํํ ๊ฒ ๊ฐ์ ๋๋ ์ ๊ฐ์ ธ์๊ณ ,
xml์ ํน์ ํน์๋ฌธ์๊ฐ ๊ปด์์ผ๋ฉด ์ค๋ฅ๊ฐ ๋ฐ์ํด escapeํด์ฃผ๋ ํจ์๋ก ๋ณํํด์ฃผ์์ต๋๋ค.
๊ทธ ๊ฒฐ๊ณผ ์ฑ๊ณต์ ์ผ๋ก ์ฌ์ดํธ๋งต์ ์์ฑํ ์ ์์๊ณ , 10000๊ฐ ์ด์์ ํ์ด์ง๋ค์ด ๊ตฌ๊ธ ์์น์ฝ์์ ์ธ์๋์์ต๋๋ค.
'React > Nextjs' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[NextJS, SEO] AppRouter Dynamic Metadata ์์ฑํ๊ธฐ (0) | 2024.11.21 |
---|---|
Vercel - Nextjs ๋ฐฉ๋ฌธ์ ์ ์ง๊ณ, ์น ๋ถ์ ์ด์ฉํ๊ธฐ (0) | 2024.10.10 |
[Nextjs] react-query๋ฅผ ์ด์ฉํ ์๋ฒ ๋ฐ์ดํฐ Prefetch ๋ฐฉ๋ฒ (0) | 2024.09.12 |
[Nextjs] searchParams๋ก URL ์ฟผ๋ฆฌ ์คํธ๋ง ํ์ฑํ๊ธฐ (0) | 2024.09.09 |
[Nextjs] Dynamic Route, ๋์ ๋ผ์ฐํ (0) | 2024.08.03 |
๋๊ธ