Repo-split Stage 3: frontend SSR service (in-repo, not yet live) (#13)
This commit is contained in:
parent
9978754087
commit
4d86bbce83
2 changed files with 63 additions and 7 deletions
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue