Repo-split Stage 3: frontend SSR service (in-repo, not yet live) (#13)

This commit is contained in:
emi 2026-07-21 17:48:55 +00:00
parent 9978754087
commit 4d86bbce83
2 changed files with 63 additions and 7 deletions

View file

@ -77,6 +77,19 @@ def _temp_text(f, unit: str) -> str:
return f"{v}°{unit}"
def _breadcrumb_jsonld(origin: str, breadcrumb: list[dict]) -> dict:
"""BreadcrumbList structured data. Google requires `item` on every ListItem
except the last, so unlinked intermediate crumbs (e.g. the country, which
has no page of its own) are omitted here even though the visible breadcrumb
shows them."""
crumbs = [c for c in breadcrumb[:-1] if c["href"]] + breadcrumb[-1:]
return {"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": i + 1, "name": c["name"],
**({"item": f"{origin}{c['href']}"} if c["href"] else {})}
for i, c in enumerate(crumbs)]}
def hub_payload() -> dict:
groups = cities.by_country()
return {
@ -204,6 +217,7 @@ def city_payload(request_origin: str, base: str, city: dict, history, recent=Non
"latitude": city["lat"], "longitude": city["lon"]}},
"creator": {"@type": "Organization", "name": "Thermograph"},
"isBasedOn": "https://open-meteo.com/ (ERA5 reanalysis)"},
_breadcrumb_jsonld(request_origin, breadcrumb),
],
}
return {
@ -229,7 +243,8 @@ def city_payload(request_origin: str, base: str, city: dict, history, recent=Non
}
def month_payload(city: dict, history, month_idx: int) -> dict:
def month_payload(base: str, city: dict, history, month_idx: int) -> dict:
name = city["name"]
title = cities.title_name(city)
month_name = MONTHS_TITLE[month_idx - 1]
month_slug = MONTHS[month_idx - 1]
@ -268,6 +283,9 @@ def month_payload(city: dict, history, month_idx: int) -> dict:
next_i = 1 if month_idx == 12 else month_idx + 1
n_years = year_range[1] - year_range[0]
unit = unit_for_country(city.get("country_code"))
breadcrumb = [{"name": "Home", "href": f"{base}/"}, {"name": "Climate", "href": f"{base}/climate"},
{"name": name, "href": f"{base}/climate/{city['slug']}"},
{"name": month_name, "href": None}]
return {
"month_name": month_name, "month_slug": month_slug,
"year_range": year_range, "n_years": n_years,
@ -279,6 +297,7 @@ def month_payload(city: dict, history, month_idx: int) -> dict:
"records": records,
"prev": {"name": MONTHS_TITLE[prev_i - 1], "slug": MONTHS[prev_i - 1]},
"next": {"name": MONTHS_TITLE[next_i - 1], "slug": MONTHS[next_i - 1]},
"breadcrumb": breadcrumb,
"canonical_path": f"/climate/{city['slug']}/{month_slug}",
"page_title": _titled(f"{title} in {month_name}: normal weather & records"),
"page_description": _clamp_desc(
@ -288,7 +307,8 @@ def month_payload(city: dict, history, month_idx: int) -> dict:
}
def records_payload(city: dict, history) -> dict:
def records_payload(request_origin: str, base: str, city: dict, history) -> dict:
display = cities.display_name(city)
title = cities.title_name(city)
years = history["date"].dt.year()
year_range = [int(years.min()), int(years.max())]
@ -321,12 +341,36 @@ def records_payload(city: dict, history) -> dict:
"dry_streak_days": dry_streak_days,
})
hemisphere = "Southern" if city["lat"] < 0 else "Northern"
breadcrumb = [{"name": "Home", "href": f"{base}/"}, {"name": "Climate", "href": f"{base}/climate"},
{"name": city["name"], "href": f"{base}/climate/{city['slug']}"},
{"name": "Records", "href": None}]
page_url = f"{request_origin}{base}/climate/{city['slug']}/records"
jsonld = {
"@context": "https://schema.org",
"@graph": [
{"@type": "Dataset",
"name": f"{display} monthly and seasonal weather records",
"description": f"Record high and low temperatures for {display} by month and by "
f"meteorological season, with the dates they occurred, from ~{n_years} "
f"years of daily climate history.",
"url": page_url,
"temporalCoverage": f"{year_range[0]}/{year_range[1]}",
"spatialCoverage": {"@type": "Place", "name": display,
"geo": {"@type": "GeoCoordinates",
"latitude": city["lat"], "longitude": city["lon"]}},
"creator": {"@type": "Organization", "name": "Thermograph"},
"isBasedOn": "https://open-meteo.com/ (ERA5 reanalysis)"},
_breadcrumb_jsonld(request_origin, breadcrumb),
],
}
return {
"year_range": year_range, "n_years": n_years,
"rows": rows,
"all_time_records": rec,
"monthly": _monthly_records_raw(history),
"seasonal": _seasonal_records_raw(history, city["lat"]),
"hemisphere": hemisphere,
"breadcrumb": breadcrumb,
"canonical_path": f"/climate/{city['slug']}/records",
"page_title": _titled(f"{title} weather records: hottest & coldest days since {year_range[0]}"),
"page_description": _clamp_desc(
@ -334,6 +378,7 @@ def records_payload(city: dict, history) -> dict:
f"{f' ({hi_date})' if hi_date else ''}; its coldest fell to "
f"{_temp_text(lo_rec, unit)}{f' ({lo_date})' if lo_date else ''}. "
f"Every day graded against {n_years} years of local history."),
"jsonld": jsonld,
}

View file

@ -112,11 +112,19 @@ def content_city(request: Request, slug: str):
except Exception: # noqa: BLE001 - today_vs_normal degrades to None
pass
token = history_token(history)
origin = _origin(request)
def build():
return payloads.city_payload(_origin(request), BASE, city, history, recent)
return payloads.city_payload(origin, BASE, city, history, recent)
return _cached(request, "content-city", cell["id"], slug, token, build)
# origin is folded into the cache key (not just slug) because the payload's
# jsonld.url is origin-qualified -- without this, whichever origin's request
# happens to populate the cache first would be served to every other origin
# too (and the ETag, derived from this same key, wouldn't vary either, so a
# different origin's client could get a false 304). A no-op in the default
# single-canonical-origin prod topology; load-bearing once something is
# genuinely served cross-origin (see Stage 5).
return _cached(request, "content-city", cell["id"], f"{slug}:{origin}", token, build)
@router.get("/content/city/{slug}/month/{month}")
@ -127,7 +135,7 @@ def content_city_month(request: Request, slug: str, month: str):
token = history_token(history)
def build():
return payloads.month_payload(city, history, payloads.MONTH_INDEX[month])
return payloads.month_payload(BASE, city, history, payloads.MONTH_INDEX[month])
return _cached(request, "content-month", cell["id"], f"{slug}:{month}", token, build)
@ -136,8 +144,11 @@ def content_city_month(request: Request, slug: str, month: str):
def content_city_records(request: Request, slug: str):
city, cell, history = _resolve_city(slug)
token = history_token(history)
origin = _origin(request)
def build():
return payloads.records_payload(city, history)
return payloads.records_payload(origin, BASE, city, history)
return _cached(request, "content-records", cell["id"], slug, token, build)
# See content_city's comment: origin folded into the cache key for the
# same jsonld.url / ETag-correctness reason.
return _cached(request, "content-records", cell["id"], f"{slug}:{origin}", token, build)