Skip to content

feat(seo): make sitemap and RSS generation deterministic (#189) - #211

Merged
truthixify merged 6 commits into
wraith-protocol:developfrom
Promazin:wave-10/189-deterministic-sitemap-rss
Oct 23, 2026
Merged

truthixify merged 6 commits into
wraith-protocol:developfrom
Promazin:wave-10/189-deterministic-sitemap-rss

Conversation

@Promazin

Copy link
Copy Markdown
Contributor

Summary

Closes #189

Every build was stamping all sitemap <lastmod> entries with today's date and the
RSS <lastBuildDate> with the current time, dirtying tracked files on every run
even when no content changed.

Changes

scripts/sitemap.ts — rewritten, now the single entrypoint

scripts/gen-sitemap.mjs is retired. All callers (pnpm build, pnpm generate:sitemap)
now go through tsx scripts/sitemap.ts.

Deterministic date derivation per route type:

Route type Source
Blog posts date frontmatter in the MDX file
Case studies integrationDate field in case-studies.json
Static pages git log -1 --date=short on the page's source file(s)
Tag archives / authors Omitted — no single trustworthy date

--check mode: compares the regenerated XML against public/sitemap.xml in
memory and exits 1 if they differ, without touching the file.

scripts/gen-rss.mjs

Replaced new Date().toISOString() in <lastBuildDate> with posts[0].publishedAt
(the most-recent post's publication date). Feed files are now stable across builds
when content is unchanged.

package.json

  • generate:sitemap → tsx scripts/sitemap.ts (was node scripts/gen-sitemap.mjs)
  • Added check:sitemap → tsx scripts/sitemap.ts --check

CI (.github/workflows/ci.yml)

Added pnpm check:sitemap to the build job so a stale public/sitemap.xml fails
the pull request before it merges.

Tests

  • src/__tests__/sitemap.test.ts — new: covers <lastmod> omission, correct root
    URL, byte-identical output, no wall-clock date leakage
  • src/__tests__/rss.test.ts — two new cases: lastBuildDate from newest post,
    byte-identical output across calls

Generated output (re-committed)

public/sitemap.xml and public/feed.xml + all tag feeds regenerated with the new
deterministic logic. Running either generator again produces no diff.

Tested

  • npx tsx scripts/sitemap.ts → 45 routes, succeeds
  • npx tsx scripts/sitemap.ts --check → passes on fresh output, exits 1 on stale
  • node scripts/gen-rss.mjs run twice → byte-identical public/feed.xml
  • <lastBuildDate> is now Wed, 12 Aug 2026 (newest post date), not wall-clock time

@Promazin
Promazin requested a review from truthixify as a code owner October 21, 2026 13:08
@vercel

vercel Bot commented Oct 21, 2026

Copy link
Copy Markdown

@Promazin is attempting to deploy a commit to the truthixify's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Oct 21, 2026

Copy link
Copy Markdown

@Promazin Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sitemap is deterministic only in a full-history clone. scripts/sitemap.ts:36-44 derives static dates with git log, while every CI checkout in .github/workflows/ci.yml uses actions/checkout@v4 with its default fetch-depth: 1. In that shallow checkout, path history is missing (or collapses to the synthetic PR commit), so check:sitemap regenerates different XML and the required build job currently fails as stale; the same command passes in a full-history worktree. Either fetch full history anywhere the generator/check runs, or derive dates from repository content rather than Git history, then regenerate and commit the output. Please also rebase onto #210's EPIPE fix so the unit job is green.

…ocol#189)

- Rewrite scripts/sitemap.ts as the single entrypoint for both the
  build pipeline and pnpm generate:sitemap.  scripts/gen-sitemap.mjs
  is retired; all callers now go through tsx scripts/sitemap.ts.

- Derive per-route lastmod from trustworthy sources only:
  • blog posts  → date frontmatter in the MDX file
  • case studies → integrationDate field in case-studies.json
  • static pages → git log date of the page's source file(s)
  • tag archives / author pages → omitted entirely (no single date)
  Running the generator twice on unchanged content now produces
  byte-identical output.

- Fix RSS lastBuildDate: use the publication date of the most-recent
  post instead of new Date().toISOString(), so feed.xml is stable
  across builds when no post has been added or changed.

- Add --check mode to scripts/sitemap.ts: exits 1 when
  public/sitemap.xml is stale.  Hook it into the CI build job via
  pnpm check:sitemap so a stale file fails the pull request.

- Add pnpm generate:sitemap and pnpm check:sitemap scripts to
  package.json.

- Add src/__tests__/sitemap.test.ts covering lastmod omission,
  deterministic output, and correct root URL rendering.

- Extend src/__tests__/rss.test.ts with two new cases: lastBuildDate
  derived from the newest post, and byte-identical output across calls.

- Regenerate public/sitemap.xml and public/feed.xml (+ tag feeds)
  with the new deterministic logic.

- Update README.md to document the new scripts and check mode.
…temap

actions/checkout@v4 defaults to fetch-depth: 1 (shallow clone), which
means git log -1 -- <path> in gitLastModified() either returns empty or
resolves to the synthetic PR merge commit.  check:sitemap then regenerates
different XML and the build job fails as stale.

Add fetch-depth: 0 to the unit-tests and build job checkouts so every
git log call in scripts/sitemap.ts sees the real commit history.

Regenerate public/sitemap.xml against the full local history so the
committed file matches what CI will produce after this fix.
@Promazin
Promazin force-pushed the wave-10/189-deterministic-sitemap-rss branch from f03d06f to 8a776c3 Compare October 22, 2026 03:21
@Promazin
Promazin requested a review from truthixify October 22, 2026 03:25

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High severity: scripts/sitemap.ts:38-40 interpolates repository-controlled paths into an execSync shell string. getBlogRoutes feeds it filenames returned from src/content/blog, so a PR can add a crafted filename containing a quote and shell metacharacters and execute commands during pnpm build/CI. Use execFileSync or spawnSync with an argument array such as git, [log, -1, --format=%cd, --date=short, --, ...repoPaths] and add a filename-with-metacharacters regression test. The shallow-checkout fix is otherwise correct.

@Promazin
Promazin requested a review from truthixify October 23, 2026 15:08

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved exact head 8269785 after repeating the remote full-diff malware scan. gitLastModified now invokes git with an argument array rather than a shell, the malicious-filename regression coverage is present, the preview request-drain fix is bounded, toml is patched, and all repository-owned build, unit, smoke, link, Lighthouse, and accessibility gates are green. The remaining Vercel status is an external authorization failure.

@truthixify
truthixify merged commit ef18bda into wraith-protocol:develop Oct 23, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Wave 10][Carryover] Make sitemap and RSS generation deterministic

2 participants