diff --git a/content.py b/content.py index a555c82..4bb1c04 100644 --- a/content.py +++ b/content.py @@ -147,6 +147,19 @@ def origin(request: Request) -> str: return f"{proto}://{host}" +def _breadcrumb_jsonld(o: str, breadcrumb: list[tuple[str, str | None]]) -> 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[1]] + breadcrumb[-1:] + return {"@type": "BreadcrumbList", + "itemListElement": [ + {"@type": "ListItem", "position": i + 1, "name": nm, + **({"item": f"{o}{href}"} if href else {})} + for i, (nm, href) in enumerate(crumbs)]} + + def _respond_html(request: Request, template: str, **ctx) -> Response: o = origin(request) html = _env.get_template(template).render(base=BASE, origin=o, base_url=f"{o}{BASE}", **ctx) @@ -392,11 +405,7 @@ def _city_context(request, city, cell, history) -> dict: "latitude": city["lat"], "longitude": city["lon"]}}, "creator": {"@type": "Organization", "name": "Thermograph"}, "isBasedOn": "https://open-meteo.com/ (ERA5 reanalysis)"}, - {"@type": "BreadcrumbList", - "itemListElement": [ - {"@type": "ListItem", "position": i + 1, "name": nm, - **({"item": f"{o}{href}"} if href else {})} - for i, (nm, href) in enumerate(breadcrumb)]}, + _breadcrumb_jsonld(o, breadcrumb), ], } return { @@ -573,11 +582,7 @@ def _records_context(request, city, history) -> dict: "latitude": city["lat"], "longitude": city["lon"]}}, "creator": {"@type": "Organization", "name": "Thermograph"}, "isBasedOn": "https://open-meteo.com/ (ERA5 reanalysis)"}, - {"@type": "BreadcrumbList", - "itemListElement": [ - {"@type": "ListItem", "position": i + 1, "name": nm, - **({"item": f"{o}{href}"} if href else {})} - for i, (nm, href) in enumerate(breadcrumb)]}, + _breadcrumb_jsonld(o, breadcrumb), ], } return { diff --git a/tests/test_content.py b/tests/test_content.py index 83b1579..070e2be 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -56,6 +56,25 @@ def test_city_page_renders_stats_in_html(client): assert "°F" in b +def _jsonld_breadcrumbs(html: str) -> list[dict]: + import json, re + m = re.search(r'', html, re.S) + assert m, "no JSON-LD block on page" + graph = json.loads(m.group(1))["@graph"] + return [n for n in graph if n["@type"] == "BreadcrumbList"] + + +@pytest.mark.parametrize("path", [f"/climate/{SLUG}", f"/climate/{SLUG}/records"]) +def test_breadcrumb_jsonld_items_valid_for_google(client, path): + # Google: every ListItem except the last must carry an ``item`` URL, so + # unlinked intermediate crumbs (the country) must not appear in JSON-LD. + (bc,) = _jsonld_breadcrumbs(client.get(B + path).text) + els = bc["itemListElement"] + assert [e["position"] for e in els] == list(range(1, len(els) + 1)) + for e in els[:-1]: + assert e["item"].startswith("http"), f"crumb {e['name']!r} missing item" + + def test_city_404(client): assert client.get(f"{B}/climate/nope-not-a-city").status_code == 404