Yuvraj
5 days ago2 min read

Unquoted frontmatter dates lose a day

Web & TypeScriptTooling

Writing the loader for this blog, the frontmatter parser handed back something I did not ask for.

date: 2026-09-17

YAML does not treat that as a string. The timestamp type resolves it to a date, and gray-matter hands JavaScript a Date set to 2026-09-17T00:00:00Z. Nothing has gone wrong yet. It goes wrong at the moment anything formats it.

Link to this sectionWhere the day disappears

UTC midnight is the previous evening in every timezone west of Greenwich. So:

new Date("2026-09-17").getDate()  // 17 in London, 16 in New York

A note dated the 17th renders as the 16th for a reader in the Americas. In a daily stream that is not a cosmetic bug — the whole premise of the format is that the date is accurate, and the archive is sorted by it.

The same class of failure shows up twice more. toLocaleDateString() renders differently on the server and in the browser whenever their locale or timezone data differs, which React reports as a hydration mismatch and repairs by discarding the server's HTML. And relative time computed with Date.now() inside a component says "Today" at build time and is still saying it three weeks later, because it was baked into a static page.

Link to this sectionThe fix is to never build the Date

The loader rejects an unquoted date outright, with a message that says what to write instead:

if (value instanceof Date) {
  fail(file, `frontmatter \`${key}\` is an unquoted YAML date. Quote it.`);
}

And the formatter never parses:

const [year, month, day] = iso.split("-").map(Number);

Split on the hyphens, index into a month-name array, return a string. Identical on the server and in the browser, by construction, because no timezone was ever consulted.

Link to this sectionThe rule underneath

A calendar day and an instant are different types, and most date libraries only model the second one. When the thing you are storing is "the day this was published", every conversion to a timestamp is a chance to be wrong by one — so the useful discipline is to keep it a string from the file to the screen, and only build a Date at the one boundary that genuinely requires an instant. Here that is the RSS feed, which needs RFC-822 — and it pins the time to 12:00 GMT, because midday cannot be dragged across a date boundary by any real-world offset.

Sources

  1. 01YAML 1.1 timestamp typeyaml.org

First-hand: this post is about a system Yuvraj Singh built, ran or measured.