From 90bded90b7229d66a8445089116780cb9405e31d 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.
---
content.py | 24 +++++++++++++++++++-----
templates/about.html.j2 | 2 +-
templates/base.html.j2 | 5 +++++
templates/city.html.j2 | 10 ++++------
templates/records.html.j2 | 6 ++----
tests/test_content.py | 20 ++++++++++++++++++++
6 files changed, 51 insertions(+), 16 deletions(-)
diff --git a/content.py b/content.py
index 391a361..b010014 100644
--- a/content.py
+++ b/content.py
@@ -14,6 +14,7 @@ import polars as pl
from fastapi import HTTPException, Request, Response
from fastapi.responses import PlainTextResponse
from jinja2 import Environment, FileSystemLoader, select_autoescape
+from markupsafe import Markup
import cities
import city_events
@@ -61,11 +62,22 @@ def _c(f: float) -> int:
return round((f - 32) * 5 / 9)
-def _temp(f) -> str:
- """A Fahrenheit value shown in both units: '72°F (22°C)'."""
+def _temp(f):
+ """A Fahrenheit temperature as a client-convertible span, e.g.
+ '72°F'. Renders °F by default
+ (so crawlers and no-JS visitors see a real value); climate.js rewrites it to
+ the active unit on load and on toggle. Returns '—' for a missing value."""
if f is None:
return "—"
- return f"{round(f)}°F ({_c(f)}°C)"
+ return Markup('{}°F').format(f, round(f))
+
+
+def _temp_bare(f):
+ """Like _temp() but with no unit letter ('72°'), for the range strip where the
+ axis already implies the unit. Still carries data-temp-f so it converts."""
+ if f is None:
+ return "—"
+ return Markup('{}°').format(f, round(f))
def _fmt(metric: str, v) -> str:
@@ -119,6 +131,8 @@ def _range_bar(low_f, high_f) -> dict | None:
_env.globals["temp_class"] = temp_class
+_env.globals["temp"] = _temp
+_env.globals["temp_bare"] = _temp_bare
def _month_doy(month_idx: int) -> int:
@@ -402,10 +416,10 @@ def _month_context(request, city, history, month_idx: int) -> dict:
stats = []
if tmax:
stats.append(("Average high", _temp(tmax["mean"])))
- stats.append(("Typical high range", f"{_temp(tmax['p10'])} to {_temp(tmax['p90'])}"))
+ stats.append(("Typical high range", _temp(tmax['p10']) + " to " + _temp(tmax['p90'])))
if tmin:
stats.append(("Average low", _temp(tmin["mean"])))
- stats.append(("Typical low range", f"{_temp(tmin['p10'])} to {_temp(tmin['p90'])}"))
+ stats.append(("Typical low range", _temp(tmin['p10']) + " to " + _temp(tmin['p90'])))
if precip:
stats.append(("Average daily precipitation", f"{precip['mean']:.2f} in"))
# Record high and low for every metric in this calendar month (mirrors the
diff --git a/templates/about.html.j2 b/templates/about.html.j2
index 3467de2..5d20af3 100644
--- a/templates/about.html.j2
+++ b/templates/about.html.j2
@@ -23,7 +23,7 @@
within ±7 days of that day-of-year — a 15-day seasonal window across all years. The
observed value is placed on that distribution as an empirical
percentile, then mapped to a grade from “Below Normal” through “High” to “Near Record”.
- Everything is relative to the location: a 60°F day can be “Above Normal” in one place and
+
Everything is relative to the location: a {{ temp(60) }} day can be “Above Normal” in one place and
“Below Normal” in another. Precipitation is graded only among days that actually rained, so “Heavy”
means heavy for a rainy day here. See the grade guide for the full scale.
diff --git a/templates/base.html.j2 b/templates/base.html.j2
index b5125be..e7a5615 100644
--- a/templates/base.html.j2
+++ b/templates/base.html.j2
@@ -60,5 +60,10 @@
Thermograph grades weather by its percentile against ~45 years of local
climate history. Climate data via Open-Meteo (ERA5 reanalysis).
+
+
+
+