| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- 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<MetadataRoute.Sitemap> {
- 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<MetadataRoute.Sitemap> {
- 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<MetadataRoute.Sitemap> {
- const entries: MetadataRoute.Sitemap = [];
- const seen = new Set<number>();
- 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;
- }
|