import { MetadataRoute } from 'next'; import { fetchBoardList, fetchAllPosts } from '@/lib/api/forum/board'; const BASE_URL = 'https://bitforum.io'; // 게시글 수집 설정. sitemap 규격 상한은 50,000 건이라 여유가 있지만 무한 루프는 막는다. const POSTS_PER_PAGE = 100; const MAX_POST_PAGES = 50; export default async function sitemap(): Promise { const now = new Date(); // 정적 페이지 // /login, /register 는 색인 가치가 없어 제외한다. const staticPages: MetadataRoute.Sitemap = [ { url: BASE_URL, lastModified: now, changeFrequency: 'daily', priority: 1.0 }, { url: `${BASE_URL}/news`, lastModified: now, changeFrequency: 'daily', priority: 0.7 }, { url: `${BASE_URL}/docs`, changeFrequency: 'weekly', priority: 0.5 }, { url: `${BASE_URL}/support/faq`, changeFrequency: 'monthly', priority: 0.4 }, { url: `${BASE_URL}/support/guide`, changeFrequency: 'monthly', priority: 0.4 }, { url: `${BASE_URL}/support/contact`, changeFrequency: 'monthly', priority: 0.3 }, ]; const [boardPages, postPages] = await Promise.all([fetchBoardPages(now), fetchPostPages()]); return [...staticPages, ...boardPages, ...postPages]; } // 게시판 목록 async function fetchBoardPages(now: Date): Promise { try { const res = await fetchBoardList(); if (!res.success || !res.data?.list) { return []; } return res.data.list .filter(board => board.isActive) .map(board => ({ url: `${BASE_URL}/board/${board.code}`, lastModified: now, changeFrequency: 'daily' as const, priority: 0.8, })); } catch (error) { // 조용히 삼키면 sitemap 에서 게시판이 통째로 빠져도 알 수 없다. // 실제로 운영 sitemap 의 게시판이 0 건이었는데 이 빈 catch 때문에 드러나지 않았다. console.error('[sitemap] 게시판 목록 조회 실패', error); return []; } } // 게시글 // 목록 페이지가 클라이언트 렌더링이라 초기 HTML 에 글 링크가 없다. // sitemap 에 넣지 않으면 크롤러가 개별 게시글을 발견할 경로가 없다. async function fetchPostPages(): Promise { const entries: MetadataRoute.Sitemap = []; const seen = new Set(); try { for (let page = 1; page <= MAX_POST_PAGES; page++) { const res = await fetchAllPosts({ page, perPage: POSTS_PER_PAGE }); if (!res.success || !res.data) { break; } // 공지/전체공지도 실제로 열리는 페이지이므로 함께 담는다. const posts = [...(res.data.speaker ?? []), ...(res.data.notice ?? []), ...(res.data.list ?? [])]; for (const post of posts) { // 비밀글은 비로그인 사용자가 열 수 없어 제외한다. if (post.isSecret || seen.has(post.id)) { continue; } seen.add(post.id); entries.push({ url: `${BASE_URL}/post/${post.id}`, lastModified: post.createdAt ? new Date(post.createdAt) : undefined, changeFrequency: 'weekly', priority: 0.6, }); } // 목록이 한 페이지 분량보다 적으면 마지막 페이지다. if ((res.data.list?.length ?? 0) < POSTS_PER_PAGE) { break; } } } catch (error) { console.error('[sitemap] 게시글 조회 실패', error); } return entries; }