|
@@ -1,36 +1,95 @@
|
|
|
import { MetadataRoute } from 'next';
|
|
import { MetadataRoute } from 'next';
|
|
|
-import { fetchBoardList } from '@/lib/api/forum/board';
|
|
|
|
|
|
|
+import { fetchBoardList, fetchAllPosts } from '@/lib/api/forum/board';
|
|
|
|
|
|
|
|
const BASE_URL = 'https://antooza.com';
|
|
const BASE_URL = 'https://antooza.com';
|
|
|
|
|
|
|
|
|
|
+// 게시글 수집 설정. sitemap 규격 상한은 50,000 건이라 여유가 있지만 무한 루프는 막는다.
|
|
|
|
|
+const POSTS_PER_PAGE = 100;
|
|
|
|
|
+const MAX_POST_PAGES = 50;
|
|
|
|
|
+
|
|
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+
|
|
|
// 정적 페이지
|
|
// 정적 페이지
|
|
|
|
|
+ // /login, /register 는 색인 가치가 없어 제외한다.
|
|
|
const staticPages: MetadataRoute.Sitemap = [
|
|
const staticPages: MetadataRoute.Sitemap = [
|
|
|
- { url: BASE_URL, changeFrequency: 'daily', priority: 1.0 },
|
|
|
|
|
- { url: `${BASE_URL}/login`, changeFrequency: 'monthly', priority: 0.3 },
|
|
|
|
|
- { url: `${BASE_URL}/register`, changeFrequency: 'monthly', priority: 0.3 },
|
|
|
|
|
|
|
+ { url: BASE_URL, lastModified: now, changeFrequency: 'daily', priority: 1.0 },
|
|
|
{ url: `${BASE_URL}/docs`, changeFrequency: 'weekly', priority: 0.5 },
|
|
{ url: `${BASE_URL}/docs`, changeFrequency: 'weekly', priority: 0.5 },
|
|
|
{ url: `${BASE_URL}/support/faq`, changeFrequency: 'monthly', priority: 0.4 },
|
|
{ url: `${BASE_URL}/support/faq`, changeFrequency: 'monthly', priority: 0.4 },
|
|
|
{ url: `${BASE_URL}/support/guide`, 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 },
|
|
{ url: `${BASE_URL}/support/contact`, changeFrequency: 'monthly', priority: 0.3 },
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
- // 동적: 게시판 목록
|
|
|
|
|
- let boardPages: MetadataRoute.Sitemap = [];
|
|
|
|
|
|
|
+ const [boardPages, postPages] = await Promise.all([fetchBoardPages(now), fetchPostPages()]);
|
|
|
|
|
+
|
|
|
|
|
+ return [...staticPages, ...boardPages, ...postPages];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 게시판 목록
|
|
|
|
|
+async function fetchBoardPages(now: Date): Promise<MetadataRoute.Sitemap> {
|
|
|
try {
|
|
try {
|
|
|
const res = await fetchBoardList();
|
|
const res = await fetchBoardList();
|
|
|
- if (res.success && res.data?.list) {
|
|
|
|
|
- boardPages = res.data.list
|
|
|
|
|
- .filter(b => b.isActive)
|
|
|
|
|
- .map(b => ({
|
|
|
|
|
- url: `${BASE_URL}/board/${b.code}`,
|
|
|
|
|
- changeFrequency: 'daily' as const,
|
|
|
|
|
- priority: 0.8,
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (!res.success || !res.data?.list) {
|
|
|
|
|
+ return [];
|
|
|
}
|
|
}
|
|
|
- } catch {
|
|
|
|
|
|
|
|
|
|
|
|
+ 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 에서 게시판이 통째로 빠져도 알 수 없다.
|
|
|
|
|
+ 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 [...staticPages, ...boardPages];
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ return entries;
|
|
|
|
|
+}
|