Give the site one definition of a calendar date - #288
Conversation
The footer, both "Member since" lines and the API-key list each formatted their own date. The footer picked up the runtime's time zone, so a counter started near midnight UTC rendered as one day on the server and another in the browser; the other three passed no locale either, so a join date read 1/2/2026 or 02/01/2026 depending on the visitor. All four now call absoluteDateFrom, which is already pinned to en-US and UTC and already returns '' rather than "Invalid Date" for a malformed value. Its comment no longer describes it as the News page's formatter, because it is now the site's. Closes #286 Closes #287 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dmccoystephenson
left a comment
There was a problem hiding this comment.
Self-review: the substitution itself is sound — absoluteDateFrom is a strict improvement at all four sites, and I confirmed by grep that no other calendar-date formatter remains in the codebase, so the new comment's "only calendar-date formatter" claim holds. One real edge case found in the footer, noted inline. The three profile/API-key sites have no equivalent problem: each interpolates into a label (Member since, Created) already conditional on the object existing, and their createdAt comes from an API contract rather than a writable data file.
| month: 'long', | ||
| day: 'numeric' | ||
| }) : null; | ||
| const formattedDate = startDate ? absoluteDateFrom(startDate) : null; |
There was a problem hiding this comment.
This changes what an unparseable startDate renders, and the surrounding guard was not updated to match.
The old inline formatter returned the string "Invalid Date" for a bad value. absoluteDateFrom returns ''. But the clause below (line 59) is still guarded on startDate being truthy, not on the formatted result — so a malformed date now leaves the footer reading 5 visits since with nothing after it: a dangling preposition rather than a visibly wrong date.
This is reachable rather than theoretical: startDate traces back to data/visits.json, a runtime-persisted file bind-mounted in compose.yml and therefore editable outside the app. Neither rendering is good, but an empty trailing clause reads as a broken template, while "Invalid Date" at least announced itself.
Suggest guarding on the formatted value instead — {visits != null && formattedDate && ( on line 59 — so an unparseable date hides the counter entirely rather than half a sentence. That would also make this line's startDate ? ternary redundant, since absoluteDateFrom already returns '' for an empty or malformed input.
The counter was guarded on startDate being present, but rendered formattedDate. The old formatter returned "Invalid Date" for a bad value, so something was always printed; absoluteDateFrom returns '', which left the footer reading "5 visits since " with nothing after it. Guard on the formatted result instead, so an unparseable date hides the counter rather than half a sentence. data/visits.json is bind-mounted and editable outside the app, so this is reachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
Four call sites still formatted their own dates instead of using the shared helper. All four now call
absoluteDateFromfromutils/relativeTime.ts.components/BottomBar.tsx— the footer's "visits since" date passeden-USbut pinned no time zone, so it formatted in whatever zone the runtime was in. This one renders on the server and again in the browser: a counter started near midnight UTC produced two different calendar days, which is a React hydration mismatch that could visibly rewrite the footer after load. (Footer's "visits since" date is formatted in the runtime's time zone, so the server and the browser can disagree #286)pages/u/[username].tsx,pages/account.tsx×2 — "Member since" twice and the API-key list's "Created" passed no locale or time zone at all. These render client-side only, so there is no hydration bug here, but a join date read1/2/2026for a US visitor and02/01/2026for a UK one, and an instant near midnight UTC showed a different day depending on where the visitor was. (Profile "Member since" and API-key dates bypass the site's date formatter, so they print in the runtime's locale and time zone #287)absoluteDateFromwas already pinned toen-USand UTC and already returns''rather thanInvalid Datefor an unparseable value, so no new logic was needed — only its doc comment changed, because it no longer describes something the News page owns.Visible change: the three profile/API-key dates go from
1/2/2026toJanuary 2, 2026. That is intended — it is how the rest of the site prints a date. The footer's output is unchanged for any date it was already formatting correctly.Test plan
npm run lint— no warnings or errorsnpm test— 246 tests across 37 files passnpm run build— clean;/accountand/u/[username]still build static,/resources/[slug]still server-renderedabsoluteDateFrom's existing tests already cover the behaviour relied on here: UTC pinning either side of midnight (__tests__/relativeTime.test.ts:45-46) and''for a malformed input (line 50)Closes #286
Closes #287