๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
React/Nextjs

[NextJS, SEO] AppRouter Dynamic Sitemap.xml ์ƒ์„ฑํ•˜๊ธฐ

by LasBe 2024. 11. 22.
๋ฐ˜์‘ํ˜•

๐Ÿ“’ [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, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&apos;");
  };

  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๊ฐœ ์ด์ƒ์˜ ํŽ˜์ด์ง€๋“ค์ด ๊ตฌ๊ธ€ ์„œ์น˜์ฝ˜์†”์— ์ธ์‹๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€


์˜คํ”ˆ ์ฑ„ํŒ