Date sitemap lastmod from the build pipeline; warn on skipped pages

- bunext build records when each page's client bundle hash last changed
  in .bunext/page-lastmod.json (sitemap.lastmod_file to relocate). The
  hash covers the page's whole import graph, so component edits re-date
  every page that uses them. The sitemap uses these dates as lastmod
- Add sitemap.default_lastmod
- BREAKING: use_file_lastmod now defaults to false (file mtimes are reset
  by checkouts); it is a fallback after the build record
- Warn when a page or __root.tsx is skipped because its default export
  is not at the start of a line (e.g. export { Page as default })
- Replace leaking module mocks in tests (log, grab-root-file-path) with
  spies and fixtures

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-14 18:25:38 +01:00
co-authored by Claude Opus 5.5
parent be4bebaa22
commit 3a75880ae4
26 changed files with 877 additions and 54 deletions
+19 -7
View File
@@ -7,6 +7,7 @@ import grabOrigin from "./grab-origin";
import grabRelativePath from "./grab-relative-path";
import isDevelopment from "./is-development";
import { log } from "./log";
import { readPageLastmodRecord } from "./page-lastmod-record";
const ErrorPagePaths = ["/404", "/500"];
/**
* Protocol limits for a single sitemap file
@@ -131,7 +132,8 @@ function parseLastmod(lastmod) {
return isNaN(date.getTime()) ? undefined : date;
}
export async function grabSitemapEntries(params) {
const use_file_lastmod = params?.use_file_lastmod ?? true;
const use_file_lastmod = params?.use_file_lastmod ?? false;
const lastmod_record = readPageLastmodRecord(params?.lastmod_file);
const entries = [];
const pages = grabAllPages({ exclude_api: true });
for (let i = 0; i < pages.length; i++) {
@@ -145,17 +147,26 @@ export async function grabSitemapEntries(params) {
file_path: page.local_path,
route: page_path,
});
// When the page's client bundle last changed, per `bunext build`
const build_lastmod = lastmod_record?.pages[grabRelativePath(page.local_path)]?.lastmod;
if (page_sitemap_paths) {
entries.push(...page_sitemap_paths.map(toSitemapEntry));
for (const page_sitemap_path of page_sitemap_paths) {
const entry = toSitemapEntry(page_sitemap_path);
entries.push({
...entry,
lastmod: entry.lastmod || build_lastmod,
});
}
continue;
}
if (page.url_path.match(/\[/))
continue;
entries.push({
path: page_path,
lastmod: use_file_lastmod
? statSync(page.local_path).mtime
: undefined,
lastmod: build_lastmod ||
(use_file_lastmod
? statSync(page.local_path).mtime
: undefined),
});
}
const additional_paths = typeof params?.additional_paths == "function"
@@ -172,10 +183,11 @@ export async function grabSitemapEntries(params) {
continue;
seen.add(path);
final_entries.push({
changefreq: params?.default_changefreq,
priority: params?.default_priority,
...entry,
path,
lastmod: entry.lastmod || params?.default_lastmod,
changefreq: entry.changefreq || params?.default_changefreq,
priority: entry.priority ?? params?.default_priority,
});
}
return final_entries;