From 595a3b13d8941a6d61342d191104da6115f39192 Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Thu, 16 Jul 2026 10:48:30 -0700 Subject: [PATCH] =?UTF-8?q?Give=20climate/city/record=20pages=20full=20hea?= =?UTF-8?q?der=20parity=20+=20working=20=C2=B0F/=C2=B0C=20(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server-rendered SEO pages shared base.html.j2's nav but carried no JS, so they lacked the °F/°C toggle and account/bell, and their temperatures were baked as dual-unit strings that couldn't respond to a toggle. - Load units.js + account.js + climate.js on base.html.j2, so every SEO page gets the same header as the interactive pages (nav + °F/°C toggle + account + bell). - Emit each temperature as (default °F text, real for crawlers/no-JS); climate.js rewrites them to the active unit on load and on toggle. Tint classes stay pinned to absolute °F. Drops the inline "(NN°C)". - Make account.js base-aware: derive the app base from import.meta.url so its api/v2 fetches and the alerts links resolve from deep /climate/{slug} URLs instead of 404ing. Equivalent on the depth-1 interactive pages. - Default the unit from the browser locale for first-time visitors (no stored pref): en-US → °F, everyone else → °C. A stored choice always wins. --- static/account.js | 19 +++++++++++++------ static/climate.js | 18 ++++++++++++++++++ static/units.js | 19 ++++++++++++++++++- 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 static/climate.js diff --git a/static/account.js b/static/account.js index 8e42d03..94b3f21 100644 --- a/static/account.js +++ b/static/account.js @@ -8,10 +8,17 @@ let currentUser = null; // {id, email, display_name} or null const authCbs = []; // notified on login/logout so pages can re-gate +// The app's base path (e.g. "/thermograph"), derived from this module's own URL +// — it's always served from `${base}/account.js`, so this resolves correctly on +// both the depth-1 interactive pages and the deep SEO URLs (/climate/{slug}/…), +// where a directory-relative "api/v2/…" would otherwise resolve wrong and 404. +const APP_BASE = new URL(".", import.meta.url).pathname.replace(/\/$/, ""); +const u = (path) => `${APP_BASE}/${String(path).replace(/^\//, "")}`; + // --- shared fetch helper ----------------------------------------------------- -// Base-relative URL (resolves under /thermograph automatically), cookie sent, -// and a constant X-TG-Auth header the server can require as cheap CSRF defense -// (trivial same-origin, impossible cross-site without a preflight we never grant). +// Absolute app-base URL (so it works from any page depth), cookie sent, and a +// constant X-TG-Auth header the server can require as cheap CSRF defense (trivial +// same-origin, impossible cross-site without a preflight we never grant). export async function apiFetch(path, { method = "GET", json, form } = {}) { const opts = { method, credentials: "same-origin", headers: { "X-TG-Auth": "1" } }; if (json !== undefined) { @@ -21,7 +28,7 @@ export async function apiFetch(path, { method = "GET", json, form } = {}) { opts.headers["Content-Type"] = "application/x-www-form-urlencoded"; opts.body = new URLSearchParams(form).toString(); } - return fetch(path, opts); + return fetch(u(path), opts); } // Parse a JSON response, throwing a friendly Error on failure. fastapi-users @@ -285,7 +292,7 @@ function renderHeader() { - Manage alerts → + Manage alerts →
@@ -293,7 +300,7 @@ function renderHeader() { ${USER_IC}${escapeHtml(name)}
`; diff --git a/static/climate.js b/static/climate.js new file mode 100644 index 0000000..d30f9b6 --- /dev/null +++ b/static/climate.js @@ -0,0 +1,18 @@ +// Client-side temperature converter for the server-rendered climate/city/record +// pages. Those pages render every temperature as `72°F` (see content.py `_temp`/`_temp_bare`) so the +// value is real in the HTML for crawlers and no-JS visitors. Here we rewrite each +// span to the active unit on load and whenever the °F/°C toggle changes — reusing +// the same unit machinery as the interactive pages. Importing units.js also boots +// its toggle-injection IIFE, so this one script gives the pages a working toggle. +import { fmtTemp, onUnitChange } from "./units.js"; + +function paint() { + for (const el of document.querySelectorAll(".temp[data-temp-f]")) { + const f = parseFloat(el.dataset.tempF); + if (!Number.isNaN(f)) el.textContent = fmtTemp(f, !el.hasAttribute("data-bare")); + } +} + +paint(); +onUnitChange(paint); diff --git a/static/units.js b/static/units.js index 8a81e68..c4b0187 100644 --- a/static/units.js +++ b/static/units.js @@ -4,7 +4,24 @@ // stored/plotted values stay in °F and only the numbers shown flip. const UNIT_KEY = "thermograph:unit"; -let _unit = (localStorage.getItem(UNIT_KEY) === "C") ? "C" : "F"; + +// First-time default: pick °F only for the countries that actually use it (the US +// plus a handful of territories/nations), otherwise °C — read off the browser +// locale's region subtag (en-US → F, de-DE → C). A stored choice always wins, so +// this only affects visitors who've never toggled. Not persisted, so an unstored +// visitor keeps re-deriving the same locale default until they pick one. +const F_REGIONS = new Set(["US", "PR", "GU", "VI", "AS", "MP", "UM", "BS", "BZ", "KY", "PW", "FM", "MH", "LR"]); +function defaultUnit() { + const langs = (navigator.languages && navigator.languages.length) + ? navigator.languages : [navigator.language]; + for (const l of langs) { + const m = /-([A-Za-z]{2})\b/.exec(l || ""); + if (m) return F_REGIONS.has(m[1].toUpperCase()) ? "F" : "C"; + } + return "F"; // no region subtag to go on → keep the historical default +} +const _stored = localStorage.getItem(UNIT_KEY); +let _unit = (_stored === "C" || _stored === "F") ? _stored : defaultUnit(); const _unitCbs = []; export function getUnit() { return _unit; }