Set page title server-side for link preview compatibility

When a specific relation URL is loaded, fetch_relation_name() makes a
lightweight GET /relation/{id}.json call to get the name tag and pass
it to the template. The <title> and og:title are then set server-side,
so forum link previews and crawlers see the route name in the HTML
without executing JavaScript.

Errors are swallowed silently so a slow or failed API response just
falls back to the default title.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-02-27 21:01:25 +00:00
parent 7a4cdfcca7
commit c2355a053d
3 changed files with 26 additions and 2 deletions

View file

@ -119,6 +119,27 @@ def nearest_coord_index(lon: float, lat: float, route_coords: list[Coord]) -> in
return best_i return best_i
def fetch_relation_name(relation_id: int) -> str | None:
"""Return the name tag of a relation, or None if unavailable.
Uses a short timeout and swallows all errors intended for best-effort
use in page titles where a fallback is acceptable.
"""
url = f"{OSM_API}/relation/{relation_id}.json"
try:
resp = requests.get(url, headers={"User-Agent": "osm-pt-geojson/1.0"}, timeout=10)
except requests.RequestException:
return None
if resp.status_code != 200:
return None
data: dict[str, Any] = resp.json()
for elem in data.get("elements", []):
if elem["type"] == "relation" and elem["id"] == relation_id:
name: str | None = elem.get("tags", {}).get("name")
return name
return None
def fetch_sibling_routes(relation_id: int) -> list[dict[str, Any]]: def fetch_sibling_routes(relation_id: int) -> list[dict[str, Any]]:
"""Return sibling route relations from the same route_master, excluding self. """Return sibling route relations from the same route_master, excluding self.

View file

@ -9,6 +9,7 @@ from osm_geojson.pt.core import (
OsmError, OsmError,
build_route_coords, build_route_coords,
fetch_relation_full, fetch_relation_full,
fetch_relation_name,
fetch_route_master_routes, fetch_route_master_routes,
fetch_sibling_routes, fetch_sibling_routes,
make_geojson, make_geojson,
@ -96,7 +97,8 @@ def index() -> ResponseReturnValue:
@app.route("/<int:relation_id>") @app.route("/<int:relation_id>")
def route_page(relation_id: int) -> ResponseReturnValue: def route_page(relation_id: int) -> ResponseReturnValue:
"""Render the page with a relation pre-loaded.""" """Render the page with a relation pre-loaded."""
return render_template("index.html", relation_id=relation_id, error=None) name = fetch_relation_name(relation_id)
return render_template("index.html", relation_id=relation_id, error=None, route_name=name)
@app.route("/load", methods=["POST"]) @app.route("/load", methods=["POST"])

View file

@ -3,7 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>OSM Public Transport → GeoJSON</title> <title>{% if route_name %}{{ route_name }} {% endif %}OSM Public Transport → GeoJSON</title>
{% if route_name %}<meta property="og:title" content="{{ route_name }} OSM Public Transport → GeoJSON">{% endif %}
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}"> <link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">