κΈ°μ‘΄ νλ‘ νΈμλ(Next.js) μλ²μ μ¬μ΄νΈλ§΅ μμ± λ‘μ§μ λ°±μλμ κ²μκΈ λͺ©λ‘ APIλ₯Ό νμ©νκ³ μμλ€. μ΄ κ³Όμ μμ λͺ κ°μ§ λ¬Έμ κ° λ°μνλ€.
export const dynamic = 'force-static'; μ€μ μ ν΅ν΄ λΉλ μμ μ μ¬μ΄νΈλ§΅μ 미리 μμ±νμ¬ μλ΅ μλλ₯Ό μ΅μ ννλ€.
import { MetadataRoute } from "next";
// export const dynamic = 'force-dynamic'; κΈ°μ‘΄ SSR μ€μ
export const dynamic = 'force-static'; // SSG λ°©μμΌλ‘ μ ν
μ΄ λ°©λ²μΌλ‘ μλ΅μλλ ν¬κ² μ€μΌ μ μμμ§λ§ μλ‘μ΄ μ½ν μΈ κ° μΆκ°λ λλ§λ€ μ 체 νλ‘μ νΈλ₯Ό μ¬λΉλν΄μΌ νλ―λ‘ μ€μκ° λ°μ μΈ‘λ©΄μμ μ μ°μ±μ΄ λ¨μ΄μ‘λ€.
μ²μμλ sitemap μμ±μ μν΄ λ³λμ μ μ© μ»¨νΈλ‘€λ¬λ₯Ό λ§λλ κ²μ΄ κ³Όλνκ² λκ»΄μ Έ κΈ°μ‘΄ κ²μκΈ λͺ©λ‘ APIλ₯Ό κ·Έλλ‘ νμ©νμλ€.
κ·Έλ¬λ νμ΄μ§ κΈ°λ° APIμ΄λ€ 보λ μ 체 λ°μ΄ν°λ₯Ό μμ°¨μ μΌλ‘ μ¬λ¬ λ² νΈμΆν΄μΌ νκ³ λ³Έλ¬Έ κ°μ sitemapμ λΆνμν νλκΉμ§ ν¬ν¨λμ΄ μ€λ²ν€λκ° ν¬κ³ κΈ μκ° λμ΄λ μλ‘ μλ΅ μλκ° κΈμ¦νμλ€.
λ°λΌμ κ·Όλ³Έμ μΈ ν΄κ²°μ μν΄ κΈ°μ‘΄ λλ©μΈμ /sitemap μλ ν¬μΈνΈλ₯Ό μΆκ°νμλ€.
@Query("""
SELECT new com.pyomin.cool.dto.SitemapDto(
p.slug,
COALESCE(p.updatedAt, p.createdAt)
)
FROM Post p
WHERE p.status = :status
ORDER BY COALESCE(p.updatedAt, p.createdAt) DESC, p.id DESC
""")
List<SitemapDto<String>> findAllForSitemap(PostStatus status);
sitemap μμ±μ νμν λ°μ΄ν°λ§ μ΅μνμΌλ‘ μ‘°ννλλ‘ DTO νλ‘μ μ
μ μ¬μ©νμκ³ lastmod μ€μ μ μν΄ COALESCE()λ₯Ό μ¬μ©νμ¬ μμ μΌμ΄ μμΌλ©΄ μμ±μΌμ μ‘°ννλλ‘ μ²λ¦¬νμλ€. SitemapDtoλ μλμ κ°μ΄ μ€κ³νμλ€.
package com.pyomin.cool.dto;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
public record SitemapDto<K>(K key, Instant lastModified) {
public SitemapDto(K key, LocalDateTime lastModifiedLdt) {
this(
key,
lastModifiedLdt == null
? null
: lastModifiedLdt.atZone(ZoneId.of("Asia/Seoul")).toInstant()
);
}
}
λ¬Έμμ΄ slugλΏλ§ μλλΌ μΆνμ λ€λ₯Έ νμ
μ μλ³μλ ν¬ν¨ν μ μλλ‘ μ λ€λ¦μΌλ‘ μ€κ³νμμΌλ©° sitemap νμ€ νλμΈ <lastmod>μ W3C Datetime νμ€μ μΆ©μ‘±νκΈ° μν΄ DBμμ μ‘°νν LocalDateTimeμ μλΉμ€ νμμ‘΄(Asia/Seoul) κΈ°μ€μΌλ‘ 보μ ν ν Instant νμ
μΌλ‘ λ³ννλ€.
μ΄ν μΉ΄ν
κ³ λ¦¬λ κ°μ λ°©μμΌλ‘ μλ ν¬μΈνΈλ₯Ό μ€κ³ν ν κ° λλ©μΈλ³ μ¬μ΄νΈλ§΅ λ°μ΄ν°λ₯Ό λκΈ°μ μΌλ‘ κΈ°λ€λ¦¬μ§ μκ³ Promise.all()μ μ¬μ©νμ¬ λ³λ ¬λ‘ νΈμΆνλ€.
λν revalidate μ΅μ
μ ν΅ν΄ μ ν΄μ§ μ£ΌκΈ°λ§λ€ μ¬μ΄νΈλ§΅μ λ°±κ·ΈλΌμ΄λμμ μ¬μμ±νλλ‘ μ€μ νλ€.
import { MetadataRoute } from "next";
import { getApiBase } from "@/lib/api/apiBase";
export const revalidate = 86400; // ISR (ν루λ§λ€ μ¬μμ±)
const SITE_URL = 'https://pyomin.com';
type SlugSitemap = { slug: string; lastModified: string };
type SitemapResponse<T> = { sitemap: T[] };
async function fetchPostSitemap(): Promise<SlugSitemap[]> {
const res = await fetch(`${getApiBase()}/posts/sitemap`);
if (!res.ok) throw new Error('posts/sitemap μ€ν¨');
const json = (await res.json()) as SitemapResponse<SlugSitemap>;
return json.sitemap ?? [];
}
// μΉ΄ν κ³ λ¦¬ fetch μλ΅
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const [posts, categories] = await Promise.all([fetchPostSitemap(), fetchCategorySitemap()]); // λμ νΈμΆ
const postPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${SITE_URL}/posts/${encodeURIComponent(post.slug)}`,
lastModified: post.lastModified,
changeFrequency: 'monthly',
priority: 0.8,
}));
// μ΄ μΈ μ¬μ΄νΈλ§΅ μμ± λ° return
}
κ²°κ³Όμ μΌλ‘ μ¬μ΄νΈλ§΅ μμ± μκ°μ 1μ΄ μ΄λ΄λ‘ λ¨μΆν μ μμμΌλ©° μλ΅ μλ κ°μ μ ν΅ν΄ λ€μ΄λ² Yeti λ΄μ νμμμ λ¬Έμ λ₯Ό ν΄κ²°νκ³ μ μμ μΈ μΈλ±μ±μ μ λν μ μμλ€.

