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).

+ + + + diff --git a/templates/city.html.j2 b/templates/city.html.j2 index a3a1435..dcbf276 100644 --- a/templates/city.html.j2 +++ b/templates/city.html.j2 @@ -76,14 +76,14 @@ {% if m.bar %}{% endif %} {% if m.range_lo_f is not none and m.range_hi_f is not none %} - {{ m.range_lo_f | round | int }}°–{{ m.range_hi_f | round | int }}° + {{ temp_bare(m.range_lo_f) }}–{{ temp_bare(m.range_hi_f) }} {% else %}{% endif %} {% endfor %}

Each bar spans the 10th-percentile daily low to the 90th-percentile daily high — the range most days fall within — coloured cold → hot on a shared - −10 °F to 115 °F scale, so the whole year's rhythm reads at a glance.

+ {{ temp(-10) }} to {{ temp(115) }} scale, so the whole year's rhythm reads at a glance.

@@ -97,10 +97,8 @@

Record extremes

All {{ name }} weather records →

diff --git a/templates/records.html.j2 b/templates/records.html.j2 index 61ce920..3e7e32a 100644 --- a/templates/records.html.j2 +++ b/templates/records.html.j2 @@ -22,10 +22,8 @@

Record high and low temperatures for {{ display }} — by month, by season, and all-time — with the dates they occurred, across {{ year_range[0] }}–{{ year_range[1] }} of daily climate history.{% if all_time.tmax and all_time.tmin %} Its hottest day on record reached - {{ all_time.tmax.max | round | int }}°F ({{ ((all_time.tmax.max - 32) * 5 / 9) | round | int }}°C) - on {{ all_time.tmax.max_date }}; its coldest fell to - {{ all_time.tmin.min | round | int }}°F ({{ ((all_time.tmin.min - 32) * 5 / 9) | round | int }}°C) - on {{ all_time.tmin.min_date }}.{% endif %}

+ {{ temp(all_time.tmax.max) }} on {{ all_time.tmax.max_date }}; its coldest fell to + {{ temp(all_time.tmin.min) }} on {{ all_time.tmin.min_date }}.{% endif %}

Records by month

diff --git a/tests/test_content.py b/tests/test_content.py index 812757f..e46d441 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -171,3 +171,23 @@ def test_hub_glossary_about(client): assert client.get(f"{B}/glossary/percentile").status_code == 200 assert client.get(f"{B}/glossary/not-a-term").status_code == 404 assert client.get(f"{B}/about").status_code == 200 + + +def test_climate_pages_carry_convertible_temps(client): + # Every temperature is a client-convertible span (default °F text so crawlers + # and no-JS visitors still see a real value); the old inline "(NN°C)" dual form + # is gone (the toggle now supplies Celsius). + import re + for path in (f"{B}/climate/{SLUG}", f"{B}/climate/{SLUG}/july", f"{B}/climate/{SLUG}/records"): + b = client.get(path).text + assert 'class="temp" data-temp-f=' in b + assert "°F" in b + assert re.search(r"\d+°F \(-?\d+°C\)", b) is None # no leftover dual-unit strings + + +def test_seo_pages_load_unit_and_account_scripts(client): + # Header parity with the interactive pages: the °F/°C toggle, account/bell, and + # the temperature converter are all loaded. + b = client.get(f"{B}/climate/{SLUG}").text + for src in (f"{B}/units.js", f"{B}/account.js", f"{B}/climate.js"): + assert f'src="{src}"' in b