Skip to content

fix(blog): pin every date formatter to UTC - #229

Merged
schmug merged 1 commit into
mainfrom
claude/blissful-goodall-2bc08b
Aug 29, 2026
Merged

fix(blog): pin every date formatter to UTC#229
schmug merged 1 commit into
mainfrom
claude/blissful-goodall-2bc08b

Conversation

@schmug

@schmug schmug commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Closes #222.

Every Intl.DateTimeFormat in src/ now carries timeZone: 'UTC', so a post's rendered date is a property of its content rather than of the machine that built the page.

The bug

Blog frontmatter carries a bare pubDate: YYYY-MM-DD, which z.coerce.date() parses as UTC midnight — the single value guaranteed to land on the previous day in every western zone. The formatters had no timeZone, so every blog post rendered a day early on a negative-offset builder, with the visible text contradicting the <time datetime> attribute in the same element. Production is correct today only because Cloudflare Pages happens to build in UTC.

BlogApp.tsx and Timeline.tsx render client-side, where the unpinned zone is the viewer's — a reader in Los Angeles saw a different day than one in Berlin for the same post.

Scope note

The issue lists seven sites; this changes eight. src/components/mythos/Timeline.tsx:44 formats bare YYYY-MM-DD history rows in the viewer's zone — same defect class, and acceptance criterion 4 ("no Intl.DateTimeFormat without an explicit timeZone remains in src/") covers it.

Display-only. Stored pubDate values, datetime attributes, sort order (on timestamps, already zone-independent) and the RSS RFC-822 output are untouched; each formatter keeps its existing short/long month style. No new dependencies.

Evidence

Podcast and frontier-commits render an empty state locally (EPISODES_MANIFEST_URL unset), so those paths were exercised with synthetic manifests fed through EPISODES_MANIFEST_URL / FRONTIER_MANIFEST_URL carrying boundary-hugging stamps — 01:00Z (previous day in EDT) and 23:30Z (next day in Tokyo). 16 <time> elements across dist/blog, dist/podcast, dist/frontier-commits.

Criterion 2 — visible text vs. the calendar date of its own datetime attribute:

build parsed mismatches
pre-fix, TZ=America/New_York 16 12
post-fix, TZ=America/New_York 16 0

Pre-fix sample:

MISMATCH: 2026-04-16T00:00:00.000Z -> "Apr 15, 2026"   | UTC calendar date is Apr 16, 2026
MISMATCH: 2026-04-18T00:00:00.000Z -> "April 17, 2026" | UTC calendar date is Apr 18, 2026
MISMATCH: 2026-05-23T01:00:00.000Z -> "May 22, 2026"   | UTC calendar date is May 23, 2026

Criterion 1 — three full builds, rendered date strings extracted and sorted:

America/New_York:  16 <time> elements
UTC:               16 <time> elements
Asia/Tokyo:        16 <time> elements

diff EDT vs UTC    -> IDENTICAL
diff UTC vs Tokyo  -> IDENTICAL

Criterion 3BlogApp.tsx is client-side, so no build check can prove it. New BlogApp.test.tsx renders the app under America/Los_Angeles, UTC and Asia/Tokyo (the formatter is module-level, so vi.resetModules() rebuilds it per zone) and asserts Apr 16, 2026 in all three. Confirmed failing for the right reason with the pin removed:

AssertionError: expected 'Apr 15, 2026' to be 'Apr 16, 2026'
Tests  1 failed | 2 passed (3)

Criterion 4grep -rn "Intl.DateTimeFormat" src returns 11 matches; all 11 carry timeZone: 'UTC'.

Criterion 5 — full gate suite on the rebased branch:

format:check  All matched files use Prettier code style!
lint          clean
typecheck     131 files: 0 errors, 0 warnings, 1 hint (pre-existing)
test          34 files, 297 tests passing
build         26 pages built

🤖 Generated with Claude Code

Blog frontmatter carries a bare YYYY-MM-DD, which z.coerce.date() parses as
UTC midnight — the one value that falls on the previous day in every western
zone. The blog, podcast and frontier-commits formatters had no timeZone, so
they rendered in the build host's zone and every blog post came out a day
early on any negative-offset builder, with the visible text contradicting the
<time datetime> attribute in the same element. Production is correct today
only because Cloudflare Pages happens to build in UTC.

BlogApp.tsx and Timeline.tsx render client-side, where the unpinned zone is
the viewer's: a reader in Los Angeles saw a different day than one in Berlin
for the same post. Timeline.tsx is not in the issue's pointer list but is the
same defect, and acceptance criterion 4 covers it.

Display-only: stored pubDate values, datetime attributes, sort order (on
timestamps, already zone-independent) and the RSS RFC-822 output are untouched,
and each formatter keeps its existing short/long month style.

Verified — pre-fix EDT build, 16 <time> elements across dist/blog,
dist/podcast and dist/frontier-commits: 12 mismatch their own datetime
attribute. Post-fix: 0 mismatches, and the rendered date strings are identical
across TZ=America/New_York, TZ=UTC and TZ=Asia/Tokyo.

  npm test        34 files, 295 tests passing
  npm run typecheck   131 files, 0 errors, 0 warnings
  format:check / lint clean, npm run build 26 pages

Closes #222
@schmug
schmug enabled auto-merge (squash) August 29, 2026 12:29
@schmug
schmug merged commit 9ff97ae into main Aug 29, 2026
3 of 4 checks passed
@schmug
schmug deleted the claude/blissful-goodall-2bc08b branch August 29, 2026 12:34
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.

Pin all date formatters to UTC — blog dates render a day early off-UTC

1 participant