Skip to content

Commit 62b8887

Browse files
mhartingtonmhessdevclaude
authored
fix(site): add lastmod dates to sitemap entries (#7759)
Page routes now include lastmod derived from file modification time, and changelog entries use their frontmatter date. This helps search engines prioritize recently changed pages and improves crawl efficiency. Co-authored-by: Marc Hess <marc@marchess.dev> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 032322c commit 62b8887

1 file changed

Lines changed: 52 additions & 7 deletions

File tree

apps/site/src/lib/sitemap.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { Dirent } from "node:fs";
2-
import { readdir } from "node:fs/promises";
2+
import { readdir, stat } from "node:fs/promises";
33
import path from "node:path";
44
import { changelogSource } from "@/lib/changelog-source";
55
import { getBaseUrl } from "@/lib/url";
66

77
type SitemapEntry = {
88
url: string;
9+
lastModified?: string;
910
changeFrequency?: "daily" | "weekly" | "monthly";
1011
priority?: number;
1112
};
@@ -77,8 +78,13 @@ function getEntryMetadata(pathname: string): Omit<SitemapEntry, "url"> {
7778
};
7879
}
7980

81+
type PageRoute = {
82+
pathname: string;
83+
filePath: string;
84+
};
85+
8086
/** Recursively collect public page routes from the App Router tree. */
81-
async function collectPageRoutes(directory: string, segments: string[] = []): Promise<string[]> {
87+
async function collectPageRoutes(directory: string, segments: string[] = []): Promise<PageRoute[]> {
8288
let entries: Dirent<string>[];
8389

8490
try {
@@ -112,26 +118,62 @@ async function collectPageRoutes(directory: string, segments: string[] = []): Pr
112118
return [];
113119
}
114120

115-
return [routeSegments.length === 0 ? "/" : `/${routeSegments.join("/")}`];
121+
return [{
122+
pathname: routeSegments.length === 0 ? "/" : `/${routeSegments.join("/")}`,
123+
filePath: entryPath,
124+
}];
116125
}),
117126
);
118127

119128
return routes.flat();
120129
}
121130

131+
/** Get the last modified date of a file as an ISO date string (YYYY-MM-DD). */
132+
async function getFileLastModified(filePath: string): Promise<string | undefined> {
133+
try {
134+
const fileStat = await stat(filePath);
135+
return fileStat.mtime.toISOString().split("T")[0];
136+
} catch {
137+
return undefined;
138+
}
139+
}
140+
122141
/** Generate sitemap entries for all public pages in the site app. */
123142
export async function getSiteSitemapEntries(baseUrl = getBaseUrl()): Promise<SitemapEntry[]> {
124-
const [pathnames, changelogEntries] = await Promise.all([
143+
const [pageRoutes, changelogPages] = await Promise.all([
125144
collectPageRoutes(APP_DIRECTORY),
126-
changelogSource.getPages().map((page) => page.url),
145+
changelogSource.getPages(),
127146
]);
128147

129-
const allPathnames = [...new Set([...pathnames, ...changelogEntries])];
148+
// Build a map of pathname → lastmod from page file stats
149+
const pageLastModMap = new Map<string, string | undefined>();
150+
await Promise.all(
151+
pageRoutes.map(async ({ pathname, filePath }) => {
152+
pageLastModMap.set(pathname, await getFileLastModified(filePath));
153+
}),
154+
);
155+
156+
// Changelog entries use their frontmatter date as lastmod
157+
const changelogLastModMap = new Map<string, string>();
158+
for (const page of changelogPages) {
159+
const date = page.data.date instanceof Date
160+
? page.data.date.toISOString().split("T")[0]
161+
: String(page.data.date).split("T")[0];
162+
changelogLastModMap.set(page.url, date);
163+
}
164+
165+
const allPathnames = [
166+
...new Set([
167+
...pageRoutes.map((route) => route.pathname),
168+
...changelogPages.map((page) => page.url),
169+
]),
170+
];
130171

131172
return allPathnames
132173
.sort((left, right) => left.localeCompare(right))
133174
.map((pathname) => ({
134175
url: new URL(pathname, baseUrl).toString(),
176+
lastModified: changelogLastModMap.get(pathname) ?? pageLastModMap.get(pathname),
135177
...getEntryMetadata(pathname),
136178
}));
137179
}
@@ -155,8 +197,11 @@ ${items}
155197
/** Render a URL sitemap document. */
156198
export function renderSitemapXml(entries: SitemapEntry[]): string {
157199
const items = entries
158-
.map(({ url, changeFrequency, priority }) => {
200+
.map(({ url, lastModified, changeFrequency, priority }) => {
159201
const metadata = [
202+
lastModified
203+
? ` <lastmod>${escapeXml(lastModified)}</lastmod>`
204+
: null,
160205
changeFrequency
161206
? ` <changefreq>${escapeXml(changeFrequency)}</changefreq>`
162207
: null,

0 commit comments

Comments
 (0)