A date that was yesterday in the browser
Vue SSR printed 14 Jul in UTC. The client printed 13 Jul in Berlin. Hydration failed, then the page rendered twice.
Case tiles flashed on first paint. Vue warned that the server HTML did not match. The mismatch was a calendar day. The vue container has TZ=UTC. Intl.DateTimeFormat in Node printed 14 Jul 2026. The browser in Berlin printed 13 Jul 2026 for the same ISO timestamp from the API. Hydration compared text nodes and gave up. The client rendered the tree again. TTFB looked fine. Time to a stable page did not.
The same class of bug hit locale. SSR used the path locale. A first visit without a cookie still had navigator.language on the client if a helper read it during setup. en on the server, ru in the browser, two dictionaries, failed hydrate.
The problem was formatting the same instant in two clocks. Visitors saw a flash and a double render. I needed the display string from SSR in serialized state, and no Intl on mount.
Format on the server, ship the string
PHP sends published_at as ISO-8601 UTC. The SSR entry formats it once for the request locale and puts the display string in serialized state. The client prints that string. It does not call Intl on mount. A locale change is a new URL and a new document. That is already how i18n works on this site.
- Dates that must follow the visitor clock, if any, are marked client-only and are empty in the SSR HTML.
- The vue process TZ stays UTC. Guessing Europe/Berlin in Docker would fix one office and break a crawler.
- Hydration warnings in the vue log are treated as deploy failures, not as console noise.
The flash went away. The document in the first HTML is the document after hydrate. Double render was the cost of formatting a date in two places.
What I took from this
Hydration compares text, not instants. Two time zones are two strings even when the ISO is the same.
Format once on the server. Ship the string. Intl on mount is a second calendar I did not want.
navigator.language in setup is a locale mismatch waiting for a first visit. Path locale is the source.
