fix(blog-list): avoid timezone shift when rendering string dates#79
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates BlogList date rendering so string dates display consistently by extracting and formatting the literal YYYY-MM-DD portion, avoiding timezone-based day shifts during parsing.
Changes:
- Add string-date parsing via an ISO date-prefix regex and
getDatePartsFromString. - Introduce
formatBlogDateto format string dates via extracted parts, with fallback to existingDate/timestamp normalization. - Update
BlogCardto render the preformatted date string.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return classNames.filter(Boolean).join(' '); | ||
| }; | ||
|
|
||
| // 2025-6-26 16:00:00 |
There was a problem hiding this comment.
The example comment shows 2025-6-26 16:00:00, but ISO_DATE_PREFIX_RE requires zero-padded month/day (\d{2}), so that example would not match and the string would fall back to new Date(value) (reintroducing the timezone-shift behavior). Either update the example to YYYY-MM-DD ... or relax the regex to accept 1–2 digit month/day (and normalize accordingly).
| // 2025-6-26 16:00:00 | |
| // 2025-06-26 16:00:00 |
| const date = | ||
| value instanceof Date | ||
| ? value | ||
| : typeof value === 'string' | ||
| ? new Date(value) | ||
| : new Date(value); |
There was a problem hiding this comment.
normalizeDate now contains a nested ternary where both non-Date branches are identical (new Date(value)), which adds complexity without changing behavior. Consider reverting to value instanceof Date ? value : new Date(value) to keep the helper straightforward.
| const date = | |
| value instanceof Date | |
| ? value | |
| : typeof value === 'string' | |
| ? new Date(value) | |
| : new Date(value); | |
| const date = value instanceof Date ? value : new Date(value); |
| const [, year, month, day] = match; | ||
|
|
||
| return { | ||
| year: Number(year), | ||
| month: Number(month), | ||
| day: Number(day), |
There was a problem hiding this comment.
getDatePartsFromString will happily accept out-of-range values like 2026-99-99 (regex match) and new Date(year, month - 1, day) will normalize them into a different valid date rather than returning undefined. If the intent is to treat invalid inputs as missing dates (matching the previous Invalid Date -> undefined behavior), add range checks (and ideally verify the constructed Date’s Y/M/D match the parsed parts).
| const [, year, month, day] = match; | |
| return { | |
| year: Number(year), | |
| month: Number(month), | |
| day: Number(day), | |
| const [, yearString, monthString, dayString] = match; | |
| const year = Number(yearString); | |
| const month = Number(monthString); | |
| const day = Number(dayString); | |
| if ( | |
| !Number.isInteger(year) || | |
| !Number.isInteger(month) || | |
| !Number.isInteger(day) || | |
| month < 1 || | |
| month > 12 || | |
| day < 1 || | |
| day > 31 | |
| ) { | |
| return undefined; | |
| } | |
| const normalizedDate = new Date(year, month - 1, day); | |
| if ( | |
| Number.isNaN(normalizedDate.getTime()) || | |
| normalizedDate.getFullYear() !== year || | |
| normalizedDate.getMonth() !== month - 1 || | |
| normalizedDate.getDate() !== day | |
| ) { | |
| return undefined; | |
| } | |
| return { | |
| year, | |
| month, | |
| day, |
| if (typeof value === 'string') { | ||
| const dateParts = getDatePartsFromString(value); | ||
|
|
||
| if (dateParts) { | ||
| return formatter.format( | ||
| new Date(dateParts.year, dateParts.month - 1, dateParts.day), | ||
| ); | ||
| } |
There was a problem hiding this comment.
When dateFormatOptions includes an explicit timeZone, formatting a string date via new Date(year, month - 1, day) (local-midnight) can still shift the rendered day when the formatter time zone differs from the client’s local zone. Consider either (a) documenting that the string-date fast path ignores timeZone semantics and is intended to be a locale-only display, or (b) adjusting the construction strategy based on the requested timeZone (e.g., use a UTC-based Date when dateFormatOptions.timeZone is set).
Summary
YYYY-MM-DDportion for display2025-12-31 16:00:00Testing
npx biome check src/blog-list/index.tsxnpx tsc --noEmit -p tsconfig.jsonnpm run preflight(script is not defined in this repository)