Commit 707ec129 authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

[tag]0.1-prod

parents cacdd622 738e078d
Pipeline #54052 passed with stages
in 2 minutes and 22 seconds
...@@ -25,6 +25,15 @@ const STATIC_PAGE_SLUGS = [ ...@@ -25,6 +25,15 @@ const STATIC_PAGE_SLUGS = [
"video", "video",
]; ];
// Mỗi file sitemap con chứa tối đa 2.000 bài (giới hạn của Google: 50.000 URL/file)
const POSTS_PER_SITEMAP = 2000;
const POST_PAGE_SIZE = 100;
const POST_PAGES_PER_SITEMAP = POSTS_PER_SITEMAP / POST_PAGE_SIZE;
const POST_PAGE_RETRIES = 3;
const RETRY_DELAY_MS = 1500;
const POST_FILTERS = "is_hidden==false,is_active==true,type==news";
const normalizePath = (value?: string | null) => { const normalizePath = (value?: string | null) => {
const trimmed = value?.trim() ?? ""; const trimmed = value?.trim() ?? "";
if (!trimmed || trimmed === "/") return "/"; if (!trimmed || trimmed === "/") return "/";
...@@ -61,14 +70,26 @@ type PagedRows<T> = { ...@@ -61,14 +70,26 @@ type PagedRows<T> = {
}; };
}; };
// Chia nhỏ request để tránh nginx trả 504 khi lấy quá nhiều bản ghi một lúc
const POST_PAGE_SIZE = 500;
const POST_MAX_PAGES = 100; // đủ cho 50.000 bài viết
const POST_PAGE_RETRIES = 3;
const RETRY_DELAY_MS = 1500;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const fetchPostsCount = async (): Promise<number> => {
try {
const response = (await getApiV10Post({
page: 1,
pageSize: 1,
sortField: "published_at",
sortOrder: "desc",
priorityFeatured: false,
filters: POST_FILTERS,
})) as unknown as PagedRows<PostRow>;
return Math.max(0, Number(response?.responseData?.count ?? 0));
} catch (error) {
console.warn("[sitemap] Failed to fetch posts count, falling back to static sitemap only", error);
return 0;
}
};
const fetchPostPage = async (page: number): Promise<PagedRows<PostRow> | null> => { const fetchPostPage = async (page: number): Promise<PagedRows<PostRow> | null> => {
for (let attempt = 1; attempt <= POST_PAGE_RETRIES; attempt++) { for (let attempt = 1; attempt <= POST_PAGE_RETRIES; attempt++) {
try { try {
...@@ -77,7 +98,8 @@ const fetchPostPage = async (page: number): Promise<PagedRows<PostRow> | null> = ...@@ -77,7 +98,8 @@ const fetchPostPage = async (page: number): Promise<PagedRows<PostRow> | null> =
pageSize: POST_PAGE_SIZE, pageSize: POST_PAGE_SIZE,
sortField: "published_at", sortField: "published_at",
sortOrder: "desc", sortOrder: "desc",
filters: "is_hidden==false,is_active==true,type==news", priorityFeatured: false,
filters: POST_FILTERS,
})) as unknown as PagedRows<PostRow>; })) as unknown as PagedRows<PostRow>;
} catch (error) { } catch (error) {
console.warn(`[sitemap] Failed to fetch posts page ${page} (attempt ${attempt}/${POST_PAGE_RETRIES})`, error); console.warn(`[sitemap] Failed to fetch posts page ${page} (attempt ${attempt}/${POST_PAGE_RETRIES})`, error);
...@@ -87,24 +109,19 @@ const fetchPostPage = async (page: number): Promise<PagedRows<PostRow> | null> = ...@@ -87,24 +109,19 @@ const fetchPostPage = async (page: number): Promise<PagedRows<PostRow> | null> =
return null; return null;
}; };
const fetchAllNewsPosts = async (): Promise<PostRow[]> => { export async function generateSitemaps(): Promise<{ id: number }[]> {
const all: PostRow[] = []; const ids = [{ id: 0 }];
for (let page = 1; page <= POST_MAX_PAGES; page++) {
const response = await fetchPostPage(page);
if (!response) break;
const rows = response?.responseData?.rows ?? []; const count = await fetchPostsCount();
all.push(...rows); const totalPostSitemaps = Math.ceil(count / POSTS_PER_SITEMAP);
for (let i = 0; i < totalPostSitemaps; i++) {
const count = Number(response?.responseData?.count ?? 0); ids.push({ id: i + 1 });
if (rows.length < POST_PAGE_SIZE || (count > 0 && all.length >= count)) break;
} }
return all; return ids;
}; }
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const buildStaticSitemap = async (): Promise<MetadataRoute.Sitemap> => {
const now = new Date().toISOString(); const now = new Date().toISOString();
const entries: MetadataRoute.Sitemap = [ const entries: MetadataRoute.Sitemap = [
...@@ -126,16 +143,13 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> { ...@@ -126,16 +143,13 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}); });
} }
// Danh mục + bài viết (chạy song song) // Danh mục
const [categoryResponse, posts] = await Promise.all([ const categoryResponse = await getApiV10Category({
getApiV10Category({
page: 1, page: 1,
pageSize: 200, pageSize: 200,
sortField: "sort_order", sortField: "sort_order",
sortOrder: "asc", sortOrder: "asc",
}).catch(() => null), }).catch(() => null);
fetchAllNewsPosts(),
]);
const categoryRows = const categoryRows =
((categoryResponse as unknown as PagedRows<CategoryRow> | null)?.responseData?.rows ?? []) as CategoryRow[]; ((categoryResponse as unknown as PagedRows<CategoryRow> | null)?.responseData?.rows ?? []) as CategoryRow[];
...@@ -155,8 +169,26 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> { ...@@ -155,8 +169,26 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}); });
} }
return entries;
};
const buildPostsSitemap = async (chunk: number): Promise<MetadataRoute.Sitemap> => {
const now = new Date().toISOString();
const entries: MetadataRoute.Sitemap = [];
const seenPostSlugs = new Set<string>(); const seenPostSlugs = new Set<string>();
for (const post of posts) {
const startPage = chunk * POST_PAGES_PER_SITEMAP + 1;
const endPage = startPage + POST_PAGES_PER_SITEMAP - 1;
for (let page = startPage; page <= endPage; page++) {
const response = await fetchPostPage(page);
// Dừng khi một page fail hết retry để không lệch thứ tự phân trang
if (!response) break;
const rows = response?.responseData?.rows ?? [];
if (!rows.length) break;
for (const post of rows) {
const slug = post.slug?.trim(); const slug = post.slug?.trim();
if (!slug || !post.id || !post.title) continue; if (!slug || !post.id || !post.title) continue;
if (seenPostSlugs.has(slug)) continue; if (seenPostSlugs.has(slug)) continue;
...@@ -164,11 +196,19 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> { ...@@ -164,11 +196,19 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
entries.push({ entries.push({
url: `${SITE_URL}/${slug}`, url: `${SITE_URL}/${slug}`,
lastModified: toIso(post.updated_at) ?? toIso(post.published_at) ?? toIso(post.published_at) ?? now, lastModified: toIso(post.updated_at) ?? toIso(post.published_at) ?? now,
changeFrequency: "weekly", changeFrequency: "weekly",
priority: 0.7, priority: 0.7,
}); });
} }
}
return entries; return entries;
};
export default async function sitemap({ id = 0 }: { id?: number } = {}): Promise<MetadataRoute.Sitemap> {
const chunk = Math.max(0, Number(id) || 0);
if (chunk === 0) return buildStaticSitemap();
return buildPostsSitemap(chunk - 1);
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment