feat(sitemap): implement paginated tags sitemap generation with caching

This commit is contained in:
Nawaz Dhandala
2026-01-25 18:51:23 +00:00
parent 30cb030470
commit d2e82fe50e
2 changed files with 69 additions and 18 deletions

View File

@@ -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);

View File

@@ -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<T> {
let indexCache: CachedData<string> | null = null;
let pagesCache: CachedData<string> | null = null;
let compareCache: CachedData<string> | null = null;
let tagsCache: CachedData<string> | null = null;
const tagsCaches: Map<number, CachedData<string>> = new Map();
const blogCaches: Map<number, CachedData<string>> = new Map();
// Cache for blog post count to avoid repeated fetches
let blogPostCountCache: CachedData<number> | null = null;
// Cache for tags count
let tagsCountCache: CachedData<number> | null = null;
function isCacheValid<T>(cache: CachedData<T> | null | undefined): boolean {
if (!cache) {
return false;
@@ -200,6 +206,29 @@ export async function getBlogSitemapPageCount(): Promise<number> {
return Math.ceil(totalPosts / BLOG_POSTS_PER_SITEMAP);
}
// Get total tags count (cached)
async function getTagsCount(): Promise<number> {
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<number> {
const totalTags: number = await getTagsCount();
return Math.ceil(totalTags / TAGS_PER_SITEMAP);
}
// Discover static paths from Express routes
function discoverStaticPaths(): string[] {
const discoveredStaticPaths: Set<string> = new Set();
@@ -298,11 +327,14 @@ export async function generateSitemapIndexXml(): Promise<string> {
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<string> {
return xml;
}
// Generate sitemap for blog tags
export async function generateTagsSitemapXml(): Promise<string> {
if (isCacheValid(tagsCache)) {
return tagsCache!.data;
// Generate sitemap for blog tags (paginated)
export async function generateTagsSitemapXml(page: number): Promise<string> {
const cachedTags: CachedData<string> | 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<string> {
const xml: string = buildUrlsetXml(entries);
tagsCache = {
tagsCaches.set(page, {
data: xml,
generatedAt: OneUptimeDate.getCurrentDate().getTime(),
};
});
return xml;
}