fix(blog): pin every date formatter to UTC - #229
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #222.
Every
Intl.DateTimeFormatinsrc/now carriestimeZone: '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, whichz.coerce.date()parses as UTC midnight — the single value guaranteed to land on the previous day in every western zone. The formatters had notimeZone, 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.tsxandTimeline.tsxrender 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:44formats bareYYYY-MM-DDhistory rows in the viewer's zone — same defect class, and acceptance criterion 4 ("noIntl.DateTimeFormatwithout an explicittimeZoneremains insrc/") covers it.Display-only. Stored
pubDatevalues,datetimeattributes, sort order (on timestamps, already zone-independent) and the RSS RFC-822 output are untouched; each formatter keeps its existingshort/longmonth style. No new dependencies.Evidence
Podcast and frontier-commits render an empty state locally (
EPISODES_MANIFEST_URLunset), so those paths were exercised with synthetic manifests fed throughEPISODES_MANIFEST_URL/FRONTIER_MANIFEST_URLcarrying boundary-hugging stamps —01:00Z(previous day in EDT) and23:30Z(next day in Tokyo). 16<time>elements acrossdist/blog,dist/podcast,dist/frontier-commits.Criterion 2 — visible text vs. the calendar date of its own
datetimeattribute:TZ=America/New_YorkTZ=America/New_YorkPre-fix sample:
Criterion 1 — three full builds, rendered date strings extracted and sorted:
Criterion 3 —
BlogApp.tsxis client-side, so no build check can prove it. NewBlogApp.test.tsxrenders the app underAmerica/Los_Angeles,UTCandAsia/Tokyo(the formatter is module-level, sovi.resetModules()rebuilds it per zone) and assertsApr 16, 2026in all three. Confirmed failing for the right reason with the pin removed:Criterion 4 —
grep -rn "Intl.DateTimeFormat" srcreturns 11 matches; all 11 carrytimeZone: 'UTC'.Criterion 5 — full gate suite on the rebased branch:
🤖 Generated with Claude Code