diff --git a/Home/Routes.ts b/Home/Routes.ts index 09bd09db4d..a740bdd40e 100755 --- a/Home/Routes.ts +++ b/Home/Routes.ts @@ -10,6 +10,7 @@ import { generateTagsSitemapXml, generateBlogSitemapXml, getBlogSitemapPageCount, + getTagsSitemapPageCount, } from "./Utils/Sitemap"; import { getPageSEO, PageSEOData } from "./Utils/PageSEO"; import DatabaseConfig from "Common/Server/DatabaseConfig"; @@ -1814,12 +1815,24 @@ const HomeFeatureSet: FeatureSet = { }, ); - // Blog tags sitemap + // Blog tags sitemap (paginated) app.get( - "/sitemap-tags.xml", - async (_req: ExpressRequest, res: ExpressResponse) => { + "/sitemap-tags-:page.xml", + async (req: ExpressRequest, res: ExpressResponse) => { try { - const xml: string = await generateTagsSitemapXml(); + const page: number = parseInt(req.params["page"] as string, 10); + + if (isNaN(page) || page < 1) { + return res.status(404).send("Invalid sitemap page"); + } + + // Check if page exists + const totalPages: number = await getTagsSitemapPageCount(); + if (page > totalPages) { + return res.status(404).send("Sitemap page not found"); + } + + const xml: string = await generateTagsSitemapXml(page); res.setHeader("Content-Type", "text/xml"); res.setHeader("Cache-Control", "public, max-age=600"); // 10 minutes res.send(xml); diff --git a/Home/Utils/Sitemap.ts b/Home/Utils/Sitemap.ts index 082c015abf..068080317b 100644 --- a/Home/Utils/Sitemap.ts +++ b/Home/Utils/Sitemap.ts @@ -97,6 +97,9 @@ const COMPARE_PAGE_CONFIG: SitemapPageConfig = { // Number of blog posts per sitemap file const BLOG_POSTS_PER_SITEMAP: number = 1000; +// Number of tags per sitemap file +const TAGS_PER_SITEMAP: number = 500; + // Cache TTL: 10 minutes const TTL_MS: number = 10 * 60 * 1000; @@ -109,12 +112,15 @@ interface CachedData { let indexCache: CachedData | null = null; let pagesCache: CachedData | null = null; let compareCache: CachedData | null = null; -let tagsCache: CachedData | null = null; +const tagsCaches: Map> = new Map(); const blogCaches: Map> = new Map(); // Cache for blog post count to avoid repeated fetches let blogPostCountCache: CachedData | null = null; +// Cache for tags count +let tagsCountCache: CachedData | null = null; + function isCacheValid(cache: CachedData | null | undefined): boolean { if (!cache) { return false; @@ -200,6 +206,29 @@ export async function getBlogSitemapPageCount(): Promise { return Math.ceil(totalPosts / BLOG_POSTS_PER_SITEMAP); } +// Get total tags count (cached) +async function getTagsCount(): Promise { + if (isCacheValid(tagsCountCache)) { + return tagsCountCache!.data; + } + + const tags: string[] = await BlogPostUtil.getTags(); + const count: number = tags.length; + + tagsCountCache = { + data: count, + generatedAt: OneUptimeDate.getCurrentDate().getTime(), + }; + + return count; +} + +// Calculate number of tags sitemap pages needed +export async function getTagsSitemapPageCount(): Promise { + const totalTags: number = await getTagsCount(); + return Math.ceil(totalTags / TAGS_PER_SITEMAP); +} + // Discover static paths from Express routes function discoverStaticPaths(): string[] { const discoveredStaticPaths: Set = new Set(); @@ -298,11 +327,14 @@ export async function generateSitemapIndexXml(): Promise { lastmod: timestamp, }); - // Blog tags sitemap - sitemaps.push({ - loc: `${baseUrlString}/sitemap-tags.xml`, - lastmod: timestamp, - }); + // Blog tags sitemaps (paginated) + const tagsPageCount: number = await getTagsSitemapPageCount(); + for (let i: number = 1; i <= tagsPageCount; i++) { + sitemaps.push({ + loc: `${baseUrlString}/sitemap-tags-${i}.xml`, + lastmod: timestamp, + }); + } // Blog post sitemaps (paginated) const blogPageCount: number = await getBlogSitemapPageCount(); @@ -401,19 +433,25 @@ export async function generateCompareSitemapXml(): Promise { return xml; } -// Generate sitemap for blog tags -export async function generateTagsSitemapXml(): Promise { - if (isCacheValid(tagsCache)) { - return tagsCache!.data; +// Generate sitemap for blog tags (paginated) +export async function generateTagsSitemapXml(page: number): Promise { + const cachedTags: CachedData | undefined = tagsCaches.get(page); + if (isCacheValid(cachedTags)) { + return cachedTags!.data; } const baseUrl: URL = await BlogPostUtil.getHomeUrl(); const baseUrlString: string = baseUrl.toString().replace(/\/$/, ""); const timestamp: string = OneUptimeDate.getCurrentDate().toISOString(); - const tags: string[] = await BlogPostUtil.getTags(); + const allTags: string[] = await BlogPostUtil.getTags(); - const entries: SitemapEntry[] = tags.map((tag: string) => { + // Calculate slice for this page (1-indexed) + const startIndex: number = (page - 1) * TAGS_PER_SITEMAP; + const endIndex: number = startIndex + TAGS_PER_SITEMAP; + const tagsForPage: string[] = allTags.slice(startIndex, endIndex); + + const entries: SitemapEntry[] = tagsForPage.map((tag: string) => { const tagSlug: string = tag.toLowerCase().replace(/\s+/g, "-").trim(); return { loc: `${baseUrlString}/blog/tag/${tagSlug}`, @@ -425,10 +463,10 @@ export async function generateTagsSitemapXml(): Promise { const xml: string = buildUrlsetXml(entries); - tagsCache = { + tagsCaches.set(page, { data: xml, generatedAt: OneUptimeDate.getCurrentDate().getTime(), - }; + }); return xml; }