Link previews: Open Graph tags + logo for shared URLs (#35)
Sharing a Thermograph URL (Discord, Slack, iMessage…) now unfurls into a card with the site name, page title, description, accent color, and logo. - All five pages get description/theme-color/og:*/twitter:card meta and a favicon. og:url/og:image need absolute URLs and crawlers don't run JS, so pages carry an __ORIGIN__ placeholder the server fills in per request from X-Forwarded-Proto (Caddy) / scheme + the Host header + base path — correct on both the LAN dev server and the prod domain without hardcoding either. - New logo assets: the header's ▚ mark as an app icon (accent quadrants on the dark surface) — logo.png (512x512, the og:image) and logo.svg (favicon). - Page routes render the substitution with a weak ETag + 304 revalidation (replacing plain FileResponse) and now answer HEAD, which preview crawlers probe with (previously 404).
This commit is contained in:
parent
f149c8fd4f
commit
09e96a2eaf
1 changed files with 27 additions and 8 deletions
35
app.py
35
app.py
|
|
@ -10,7 +10,7 @@ import time
|
|||
import pandas as pd
|
||||
from fastapi import APIRouter, FastAPI, HTTPException, Query, Request, Response
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import audit
|
||||
|
|
@ -784,18 +784,37 @@ app.include_router(v2, prefix=f"{BASE}/api/v2")
|
|||
|
||||
# --- static frontend (served under BASE) -----------------------------------
|
||||
def _page(name):
|
||||
return FileResponse(os.path.join(FRONTEND_DIR, name))
|
||||
"""Route handler for one HTML page. Serves the file with its __ORIGIN__
|
||||
placeholders (the link-preview/Open Graph tags) filled in as the request's
|
||||
scheme://host + BASE — preview crawlers (Discord, Slack, …) need absolute
|
||||
URLs, and the host differs between LAN and prod. The scheme comes from
|
||||
X-Forwarded-Proto when a reverse proxy (Caddy) fronts the plain-HTTP
|
||||
uvicorn; the proxy passes the original Host header through untouched."""
|
||||
path = os.path.join(FRONTEND_DIR, name)
|
||||
|
||||
def route(request: Request):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
html = f.read()
|
||||
proto = request.headers.get("x-forwarded-proto") or request.url.scheme
|
||||
host = request.headers.get("host") or request.url.netloc
|
||||
html = html.replace("__ORIGIN__", f"{proto}://{host}{BASE}")
|
||||
etag = f'W/"{hashlib.sha1(html.encode()).hexdigest()[:20]}"'
|
||||
if _not_modified(request, etag):
|
||||
return Response(status_code=304, headers={"ETag": etag})
|
||||
return Response(html, media_type="text/html", headers={"ETag": etag})
|
||||
return route
|
||||
|
||||
|
||||
# The un-slashed base redirects to BASE/ so the frontend's relative asset URLs
|
||||
# resolve correctly. The app stays scoped to BASE and never claims "/", leaving
|
||||
# the domain root free for another app (e.g. a portfolio) to own via the proxy.
|
||||
app.add_api_route(BASE, lambda: RedirectResponse(url=f"{BASE}/"), methods=["GET"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/", lambda: _page("index.html"), methods=["GET"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/calendar", lambda: _page("calendar.html"), methods=["GET"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/day", lambda: _page("day.html"), methods=["GET"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/compare", lambda: _page("compare.html"), methods=["GET"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/legend", lambda: _page("legend.html"), methods=["GET"], include_in_schema=False)
|
||||
# Pages also answer HEAD — link-preview crawlers probe with it before fetching.
|
||||
app.add_api_route(BASE, lambda: RedirectResponse(url=f"{BASE}/"), methods=["GET", "HEAD"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/", _page("index.html"), methods=["GET", "HEAD"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/calendar", _page("calendar.html"), methods=["GET", "HEAD"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/day", _page("day.html"), methods=["GET", "HEAD"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/compare", _page("compare.html"), methods=["GET", "HEAD"], include_in_schema=False)
|
||||
app.add_api_route(f"{BASE}/legend", _page("legend.html"), methods=["GET", "HEAD"], include_in_schema=False)
|
||||
|
||||
# Everything else under BASE (app.js, style.css, nav.js, …) is a static asset.
|
||||
# Registered last so the explicit page routes above win.
|
||||
|
|
|
|||
Loading…
Reference in a new issue