diff --git a/src/content-indexer/indexers/changelog.ts b/src/content-indexer/indexers/changelog.ts index 2873347bd..3321359f2 100644 --- a/src/content-indexer/indexers/changelog.ts +++ b/src/content-indexer/indexers/changelog.ts @@ -16,6 +16,48 @@ export interface ChangelogIndexerConfig { branchId: string; } +const MAX_HEADINGS = 3; + +/** + * Format a date string (YYYY-MM-DD) to long date format (e.g., "January 8, 2026") + */ +const formatLongDate = (dateString: string): string => { + const [year, month, day] = dateString.split("-").map(Number); + const date = new Date(year, month - 1, day); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); +}; + +/** + * Extract H2 headings from markdown content and build a description string. + * Caps at MAX_HEADINGS to stay within the ~155 character meta description limit. + * Example output: "Week of January 8, 2026: updates to Developer Experience and Node." + */ +const buildChangelogDescription = ( + content: string, + date: string, +): string => { + const headings = Array.from(content.matchAll(/^## (.+)$/gm)) + .map((m) => m[1].trim()) + .slice(0, MAX_HEADINGS); + + const prefix = `Week of ${formatLongDate(date)}`; + + if (headings.length === 0) { + return `${prefix}: changelog updates.`; + } + + const joined = + headings.length === 1 + ? headings[0] + : `${headings.slice(0, -1).join(", ")} and ${headings[headings.length - 1]}`; + + return `${prefix}: updates to ${joined}.`; +}; + /** * Parse a changelog filename (e.g., "2025-11-20.md") into date components */ @@ -89,11 +131,15 @@ export const buildChangelogIndex = async ( const route = `${year}/${Number(month)}/${Number(day)}`; const fullPath = `changelog/${route}`; + // Generate a unique description from H2 headings + const description = buildChangelogDescription(content, date); + // Create path index entry const pathIndexEntry: ChangelogPathIndexEntry = { type: "changelog", date, // ISO date string like "2025-12-11" filePath: filename, // Filename like "2025-12-11.md" + description, }; // Generate hash-based objectID from path (consistent with docs/SDK) @@ -107,6 +153,7 @@ export const buildChangelogIndex = async ( objectID, indexerType: "changelog", title: `Changelog - ${date}`, + description, content, // Raw markdown - truncateRecord will clean it path: fullPath, pageType: "Changelog" as const, diff --git a/src/content-indexer/types/pathIndex.ts b/src/content-indexer/types/pathIndex.ts index 9abccfc28..302fbc9c2 100644 --- a/src/content-indexer/types/pathIndex.ts +++ b/src/content-indexer/types/pathIndex.ts @@ -25,6 +25,7 @@ export interface ChangelogPathIndexEntry { type: "changelog"; date: string; // ISO date string like "2025-12-11" filePath: string; // Filename like "2025-12-11.md" + description?: string; // Auto-generated from H2 headings } export type PathIndexEntry = diff --git a/src/content-indexer/uploaders/preview-changelog.ts b/src/content-indexer/uploaders/preview-changelog.ts index 9b2317f35..d1f1c42e8 100644 --- a/src/content-indexer/uploaders/preview-changelog.ts +++ b/src/content-indexer/uploaders/preview-changelog.ts @@ -47,7 +47,8 @@ export const uploadChangelogFile = async ( console.info(` 📄 ${filename} -> ${redisKey}`); // New files need reindex (to add the route to the index); content-only edits don't - // because the changelog index only stores date + filePath, not content. + // trigger a reindex. Note: the index also stores a description derived from H2 headings, + // so heading changes in preview will leave the description stale until a full reindex. return { reindexNeeded: isNew }; };