Skip to content

blog: harden /blog routes (close #65, #66, #67, #68, #69, #70, #71) - #73

Merged
thiras merged 9 commits into
mainfrom
claude/blog-sweep
May 11, 2026
Merged

thiras merged 9 commits into
mainfrom
claude/blog-sweep

Conversation

@alpergundogdu

@alpergundogdu alpergundogdu commented May 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

All seven blog-PR-review follow-ups (#65–#71) bundled into one PR. Each is a single commit; the changes don't entangle.

Bugs

Hardening

  • Closes lib/blog.ts getPost accepts unvalidated string instead of Slug #67 — getPost(slug: string) now calls parseSlug first and returns null on failure. Today this is redundant (static export + dynamicParams = false only routes pre-validated slugs), but it locks the invariant at the function boundary instead of relying on transitive framework guarantees.
  • Closes Assert SITE_URL is XML-safe in sitemap-pages route #71 — app/sitemap-pages.xml/route.ts interpolates SITE_URL raw into <loc> bodies. SLUG_RE guards the per-post slug; the origin had no equivalent guard. Module-top assert added against <, >, &, ", '.

Refactor

  • Closes Slug brand type-design follow-ups (unique symbol, IsoDate, dead conditional) #69 — Slug brand backed by a module-local unique symbol, so no external module can write "foo" as Slug. Same pattern applied as IsoDate; parseEntry consumes it, so PostMeta.date surfaces as branded for downstream consumers. Also drops the dead {post.summary ? <p>…</p> : null} guard on the blog index — summary is required + trimmed-non-empty at parse.

Docs

Tests

  • Closes Add regression tests for parseSlug (SLUG_RE) #70 — First test in the repo. Wires up Vitest (the natural choice given Next 16 + React 19 + ESM) and adds boundary cases for parseSlug (5 accepts, 9 rejects, plus non-string inputs). 15 tests pass.

Test plan

  • pnpm lint — clean
  • pnpm exec tsc --noEmit — clean
  • pnpm exec prettier --check . — clean
  • pnpm test — 15/15 pass
  • pnpm build — static export succeeds; 14 routes prerendered including /blog, /blog/[slug] × 3
  • out/blog/index.html contains id="blog-h"; out/blog/<post>/index.html contains id="post-h"
  • out/sitemap-pages.xml opens with a valid <?xml … ?> prolog (origin assert ran without throwing)
  • Eyeball at pnpm dev:
    • /blog/ and /blog/<post>/ look unchanged
    • VoiceOver / NVDA: each region is now announced with its heading instead of "unnamed"
    • Temporarily rename content/blog/ → build throws with actionable message

Frame hard-codes aria-labelledby={`${id}-h`}, so each <Frame id="..."/>
needs a matching <h1 id="...-h">. The two routes added in #61 declared
ids "blog" and "post" but never wired up the corresponding headers, so
screen readers announced both sections as unnamed.

Closes #65
Previously a missing or mis-named content/blog/ silently fell back to
an empty post list — the blog index rendered "nothing yet.", the
sitemap omitted post URLs, and generateStaticParams returned []. CI
passed; prod silently regressed.

Throw with an actionable message instead; .gitkeep is the escape hatch
for the legitimate empty-posts case.

Closes #66
Today output: "export" + dynamicParams = false means the framework only
routes pre-validated slugs to this handler. parseSlug at the function
boundary makes the invariant explicit instead of relying on transitive
framework guarantees, so a future caller from an unguarded path (RSS
feed, search, new route) can't regress it.

Closes #67
SLUG_RE guards per-post slug interpolation into <loc> bodies. The
origin had no equivalent guard — a future preview-URL accident
introducing &, <, >, ", or ' would silently emit invalid XML.

Closes #71
…ditional

- Slug brand is now backed by a module-local `unique symbol`, so no
  external module can write `"foo" as Slug` — parseSlug is the only
  callable construction path.
- IsoDate mirrors the pattern; parseEntry consumes it so PostMeta.date
  surfaces as a branded value to downstream consumers (RSS feed,
  archive page, etc.) without them re-testing ISO_DATE_RE.
- {post.summary ? <p>…</p> : null} on the blog index is dead defensive
  code — summary is required + trimmed-non-empty at parse — so emit
  the <p> unconditionally.

Closes #69
- Chrome.tsx: a bare `#section` from /blog/foo/ resolves to
  /blog/foo/#section, not /blog/#section as the comment claimed.
- SLUG_RE: drop the CodeQL rule ID (rots when the rule is renamed).
- Memoise comment: drop the parentheticals enumerating call sites
  (count is correct but rots with use).
- Sort tie-break: drop the HFS+ aside (irrelevant on modern macOS).
- openGraph metadata: 8-line block trimmed to the two non-obvious
  gotchas — shallow merge + og→twitter fallback timing.

Closes #68
SLUG_RE is the central guard against stored-XSS via slug interpolation;
the regex was tightened in #61 to forbid empty strings, leading/trailing
hyphens, and consecutive hyphens, but none of those tighter constraints
were protected against regression.

This is the first test in the repo. Vitest is the natural choice given
the Next 16 + React 19 + ESM stack.

Closes #70
Copilot AI review requested due to automatic review settings May 11, 2026 14:59
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 11, 2026 •

Copy link
Copy Markdown

Deploying website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5086cb0
Status: ✅  Deploy successful!
Preview URL: https://99e4ed36.website-70y.pages.dev
Branch Preview URL: https://claude-blog-sweep.website-70y.pages.dev

View logs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR bundles several follow-ups to harden the /blog feature: it tightens invariants at the lib/blog.ts boundary (slug/date branding + validation), prevents silent regressions in build output (missing posts directory now throws), improves a11y labeling for blog Frames, hardens sitemap XML generation, and introduces the repo’s first test setup using Vitest.

Changes:

  • Add Vitest configuration and a pnpm test script, plus initial parseSlug regression tests.
  • Harden blog source handling: unfakeable Slug brand, branded IsoDate, validate getPost inputs, and throw if content/blog/ is missing.
  • Fix /blog route a11y (aria-labelledby targets now exist) and add an XML-safety assert for SITE_URL in the sitemap route.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
vitest.config.ts Adds Vitest runner configuration (include/exclude patterns).
package.json Introduces test script and adds vitest devDependency.
pnpm-lock.yaml Locks Vitest/Vite and related transitive dependencies.
lib/blog.ts Hardens slug/date types + parsing, validates getPost, and throws on missing posts directory.
lib/blog.test.ts Adds initial boundary tests for parseSlug.
components/site/Chrome.tsx Tightens/clarifies hash-anchor comment for /blog/* rendering.
app/sitemap-pages.xml/route.ts Adds XML-safety assert for SITE_URL used in <loc> bodies.
app/blog/page.tsx Fixes blog heading id for Frame labeling and removes dead summary conditional.
app/blog/[slug]/page.tsx Fixes post heading id for Frame labeling.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread package.json
Comment thread lib/blog.ts
Vite 8 (pulled in transitively by Vitest 4 in 3dc0749) declares
`engines.node: "^20.19.0 || >=22.12.0"`. The previous >=20.9.0
floor would let pnpm install on Node 20.9–20.18 with a peer-warning
that the dev workflow won't actually run there.

CI runs Node 24, so the practical floor is well clear of the new
minimum.

Addresses PR #73 review.
#67 added a parseSlug check at the getPost boundary so unguarded callers
can't reach readEntries with junk input. Add a regression test exercising
the same boundary cases as parseSlug (uppercase, double-hyphen, empty,
underscore, angle brackets) and asserting null.

The invalid-slug short-circuit also means no filesystem read happens
for these inputs — captured in a comment on the test block.

Addresses PR #73 review.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request enhances the blog system by introducing branded types for slugs and dates, adding unit tests with Vitest, and improving error handling for missing content directories. It also includes UI refinements like header IDs and security checks for sitemap generation. Feedback highlights a potential security risk in error logging where unsanitized URLs are included, and suggests performance optimizations such as module-level caching and property filtering during data parsing.

Comment on lines +8 to +12
if (/[<>&"']/.test(SITE_URL)) {
throw new Error(
`[sitemap] SITE_URL contains XML-significant characters: ${SITE_URL}`,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The validation for SITE_URL is a good security measure to prevent XML injection. However, the error message includes the raw SITE_URL. If this error is ever displayed in an environment that renders HTML (like some CI logs or error monitoring tools), it could potentially lead to a secondary injection if SITE_URL itself was maliciously crafted. Consider sanitizing the output in the error message or just stating that the URL is invalid.

Comment thread lib/blog.ts
Comment on lines 149 to 150
if (a.date !== b.date) return b.date.localeCompare(a.date);
return a.slug.localeCompare(b.slug);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The addition of a tie-break on slug for sorting ensures consistent build output across different environments. However, since fs.readdirSync and subsequent file parsing are expensive operations, please implement module-level caching to prevent redundant work. Additionally, ensure that unused properties are filtered out during the initial parsing phase to simplify data retrieval and avoid 'no-unused-vars' warnings.

References
  1. Implement module-level caching for expensive file-system operations to prevent redundant parsing during build processes.
  2. Filter out unused properties during the initial parsing phase to simplify data retrieval and avoid eslint 'no-unused-vars' warnings.

@thiras
thiras merged commit c779033 into main May 11, 2026
8 checks passed
@thiras
thiras deleted the claude/blog-sweep branch May 11, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants