Skip to content

fix(backlinks): read history dates as calendar dates, not UTC midnight - #300

Open
kevin9327 wants to merge 1 commit into
every-app:mainfrom
kevin9327:fix/backlinks-date-timezone
Open

kevin9327 wants to merge 1 commit into
every-app:mainfrom
kevin9327:fix/backlinks-date-timezone

Conversation

@kevin9327

Copy link
Copy Markdown

Problem

Backlink history dates render one day early for every reader west of UTC, and the month axis one
month early on the 1st.

formatCompactDate and formatMonthLabel parse with new Date(value). For a date-only string that
is UTC midnight, and toLocaleDateString then renders it in the reader's own timezone:

new Date("2026-03-01") rendered as a local date
  America/New_York      Feb 28, 2026
  America/Los_Angeles   Feb 28, 2026
  UTC                   Mar 1, 2026

The values reaching these are calendar dates rather than instants, and the server makes them so
deliberately — normalizeHistoryDate is value.slice(0, 10), applied to every history date. The
repo's own fixture is date: "2026-02-01".

So on the backlinks page, in the Americas:

  • the firstSeen / lastSeen columns show the day before the day the link was first seen
  • the history chart's tooltip does too
  • a month bucket of 2026-03-01 is labelled Feb 26

Fix

parseDisplayDate reads a date-only value (YYYY-MM-DD or YYYY-MM) as a local calendar date and
leaves anything carrying a time to Date, where the offset is real and local rendering is correct.
Both formatters go through it; formatRelativeTimestamp is untouched because it is given an
instant.

Tests

The assertion that matters does not depend on where it runs:

expect(parseDisplayDate("2026-03-01").getTime()).toBe(new Date(2026, 2, 1).getTime());

UTC midnight and local midnight are the same instant only in UTC, so this pins the behaviour
everywhere else. Against main, on my machine (UTC+9):

AssertionError: expected 1772323200000 to be 1772290800000

That difference is 32,400,000 ms — exactly nine hours.

Three describes, one invariant each: the date-only parse, that a value with a time is left alone,
and that each formatter shows the date it was given. I kept the existing truncateMiddle tests as
they were.

How I tested

Windows 11, Node 22, pnpm 10.30.1.

npx vitest run src/client/features/backlinks/   → 3 files, 17 tests passed
npx tsc --noEmit                                 → clean
npx prettier --check <the two files>             → clean

I did not run the whole suite or e2e; this is a leaf utility with no other callers than the two
formatters it serves.

One thing I noticed and did not change

truncateMiddle slices on UTF-16 code units, so a cut can land inside a surrogate pair and leave a
lone surrogate. I checked whether it is reachable and it is not: its only caller passes
extractUrlPath, which returns URL.pathname, and that is always percent-encoded ASCII. Mentioning
it rather than fixing it, since a fix would be speculative.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

ywwenjin163 pushed a commit to ywwenjin163/open-seo that referenced this pull request Sep 12, 2026
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.

1 participant