From 33912efa6ccd54af9560bd07280b0f93b338238a Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Sun, 19 Jul 2026 11:25:55 -0700 Subject: [PATCH] Default climate pages to the city's own temperature unit (#189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search Console shows impressions skewing hard to °C countries — India, South Africa, New Zealand, the UK, Australia — while every climate page rendered °F into the HTML. Search snippets quote the server-rendered text, so a reader in Delhi saw a temperature they had to convert in their head before the page was worth a click. Climate pages now render temperatures in the city's country convention: °F for the US and the handful of other Fahrenheit countries, °C everywhere else. This happens server-side, so it holds with no JS and shows up in the snippet. data-temp-f keeps the raw Fahrenheit in every case — it stays the conversion source of truth, and climate.js repaints from it on toggle exactly as before. The visitor's stored choice still wins over everything; the page default only sits between that and the locale guess, so a first-time visitor stops seeing a °F→°C repaint on load for the majority of our traffic. The unit rides on a ContextVar rather than a parameter because _temp() is reached from both directions — templates call it as a Jinja global, and the context builders call it directly to pre-render the records tables into strings. Threading an argument would mean touching ~20 call sites across three levels of nesting, where missing one yields a silently wrong unit instead of an error. The scope wraps the context builder as well as the render, since the builder is evaluated as an argument and runs first, and it resets in a finally: the page handlers are sync `def`, so Starlette runs them on a recycled threadpool thread and a value left set would leak into the next request that thread serves. Conversion rounds half-up to match JS's Math.round rather than Python's round-half-to-even, so the server prints the number climate.js would and there is no flicker on load for a visitor whose unit already matched. Pages with no single city — the homepage strip, the hub, the interactive views — set no scope, emit no data-unit-default, and keep today's locale behaviour. Precipitation, wind and humidity stay imperial; that inconsistency is worth a follow-up but is outside this change. Two existing assertions were passing for the wrong reason: base.html.j2 mentions "°F/°C" in an HTML comment, so a bare `"°F" in html` check succeeded even on a page whose every temperature was Celsius. They now read the rendered spans. Claude-Session: https://claude.ai/code/session_013dRZmX9D3JEntfMKWMTWZ8 --- static/units.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/static/units.js b/static/units.js index c4b0187..14f12ba 100644 --- a/static/units.js +++ b/static/units.js @@ -20,8 +20,20 @@ function defaultUnit() { } return "F"; // no region subtag to go on → keep the historical default } +// A climate page knows which city it is about, so it publishes that country's +// convention on . It sits *between* the stored choice and +// the locale guess: better than the locale (a UK visitor reading about Phoenix +// wants the °F the page already rendered, and repainting it to °C would just be a +// flash), but never better than an explicit toggle. Pages with no single city — +// the homepage, the interactive views — emit no attribute and fall through. +function pageDefault() { + const v = document.body && document.body.dataset.unitDefault; + return (v === "C" || v === "F") ? v : null; +} const _stored = localStorage.getItem(UNIT_KEY); -let _unit = (_stored === "C" || _stored === "F") ? _stored : defaultUnit(); +let _unit = (_stored === "C" || _stored === "F") + ? _stored + : (pageDefault() || defaultUnit()); const _unitCbs = []; export function getUnit() { return _unit; }