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}"
|
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:
|
def hub_payload() -> dict:
|
||||||
groups = cities.by_country()
|
groups = cities.by_country()
|
||||||
return {
|
return {
|
||||||
|
|
@ -204,6 +217,7 @@ def city_payload(request_origin: str, base: str, city: dict, history, recent=Non
|
||||||
"latitude": city["lat"], "longitude": city["lon"]}},
|
"latitude": city["lat"], "longitude": city["lon"]}},
|
||||||
"creator": {"@type": "Organization", "name": "Thermograph"},
|
"creator": {"@type": "Organization", "name": "Thermograph"},
|
||||||
"isBasedOn": "https://open-meteo.com/ (ERA5 reanalysis)"},
|
"isBasedOn": "https://open-meteo.com/ (ERA5 reanalysis)"},
|
||||||
|
_breadcrumb_jsonld(request_origin, breadcrumb),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return {
|
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)
|
title = cities.title_name(city)
|
||||||
month_name = MONTHS_TITLE[month_idx - 1]
|
month_name = MONTHS_TITLE[month_idx - 1]
|
||||||
month_slug = MONTHS[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
|
next_i = 1 if month_idx == 12 else month_idx + 1
|
||||||
n_years = year_range[1] - year_range[0]
|
n_years = year_range[1] - year_range[0]
|
||||||
unit = unit_for_country(city.get("country_code"))
|
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 {
|
return {
|
||||||
"month_name": month_name, "month_slug": month_slug,
|
"month_name": month_name, "month_slug": month_slug,
|
||||||
"year_range": year_range, "n_years": n_years,
|
"year_range": year_range, "n_years": n_years,
|
||||||
|
|
@ -279,6 +297,7 @@ def month_payload(city: dict, history, month_idx: int) -> dict:
|
||||||
"records": records,
|
"records": records,
|
||||||
"prev": {"name": MONTHS_TITLE[prev_i - 1], "slug": MONTHS[prev_i - 1]},
|
"prev": {"name": MONTHS_TITLE[prev_i - 1], "slug": MONTHS[prev_i - 1]},
|
||||||
"next": {"name": MONTHS_TITLE[next_i - 1], "slug": MONTHS[next_i - 1]},
|
"next": {"name": MONTHS_TITLE[next_i - 1], "slug": MONTHS[next_i - 1]},
|
||||||
|
"breadcrumb": breadcrumb,
|
||||||
"canonical_path": f"/climate/{city['slug']}/{month_slug}",
|
"canonical_path": f"/climate/{city['slug']}/{month_slug}",
|
||||||
"page_title": _titled(f"{title} in {month_name}: normal weather & records"),
|
"page_title": _titled(f"{title} in {month_name}: normal weather & records"),
|
||||||
"page_description": _clamp_desc(
|
"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)
|
title = cities.title_name(city)
|
||||||
years = history["date"].dt.year()
|
years = history["date"].dt.year()
|
||||||
year_range = [int(years.min()), int(years.max())]
|
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,
|
"dry_streak_days": dry_streak_days,
|
||||||
})
|
})
|
||||||
hemisphere = "Southern" if city["lat"] < 0 else "Northern"
|
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 {
|
return {
|
||||||
"year_range": year_range, "n_years": n_years,
|
"year_range": year_range, "n_years": n_years,
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
|
"all_time_records": rec,
|
||||||
"monthly": _monthly_records_raw(history),
|
"monthly": _monthly_records_raw(history),
|
||||||
"seasonal": _seasonal_records_raw(history, city["lat"]),
|
"seasonal": _seasonal_records_raw(history, city["lat"]),
|
||||||
"hemisphere": hemisphere,
|
"hemisphere": hemisphere,
|
||||||
|
"breadcrumb": breadcrumb,
|
||||||
"canonical_path": f"/climate/{city['slug']}/records",
|
"canonical_path": f"/climate/{city['slug']}/records",
|
||||||
"page_title": _titled(f"{title} weather records: hottest & coldest days since {year_range[0]}"),
|
"page_title": _titled(f"{title} weather records: hottest & coldest days since {year_range[0]}"),
|
||||||
"page_description": _clamp_desc(
|
"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"{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"{_temp_text(lo_rec, unit)}{f' ({lo_date})' if lo_date else ''}. "
|
||||||
f"Every day graded against {n_years} years of local history."),
|
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
|
except Exception: # noqa: BLE001 - today_vs_normal degrades to None
|
||||||
pass
|
pass
|
||||||
token = history_token(history)
|
token = history_token(history)
|
||||||
|
origin = _origin(request)
|
||||||
|
|
||||||
def build():
|
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}")
|
@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)
|
token = history_token(history)
|
||||||
|
|
||||||
def build():
|
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)
|
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):
|
def content_city_records(request: Request, slug: str):
|
||||||
city, cell, history = _resolve_city(slug)
|
city, cell, history = _resolve_city(slug)
|
||||||
token = history_token(history)
|
token = history_token(history)
|
||||||
|
origin = _origin(request)
|
||||||
|
|
||||||
def build():
|
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