From ed48936dad6d9b2704bee20cc33a63c36fe7e3a3 Mon Sep 17 00:00:00 2001 From: Deeman Date: Fri, 6 Mar 2026 09:55:13 +0100 Subject: [PATCH] feat: add styled 404/500 error pages with i18n support Custom error templates extending base.html with centered layout. 404 is context-aware: detects /markets/{country}/{city} paths and shows city-specific message with link back to country overview. Both pages support EN/DE translations. Co-Authored-By: Claude Sonnet 4.6 --- web/src/padelnomics/app.py | 35 +++++++++++++++++++++++--- web/src/padelnomics/locales/de.json | 13 +++++++++- web/src/padelnomics/locales/en.json | 13 +++++++++- web/src/padelnomics/templates/404.html | 23 +++++++++++++++++ web/src/padelnomics/templates/500.html | 16 ++++++++++++ 5 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 web/src/padelnomics/templates/404.html create mode 100644 web/src/padelnomics/templates/500.html diff --git a/web/src/padelnomics/app.py b/web/src/padelnomics/app.py index 1085bf3..1149d71 100644 --- a/web/src/padelnomics/app.py +++ b/web/src/padelnomics/app.py @@ -5,7 +5,7 @@ import json import time from pathlib import Path -from quart import Quart, Response, abort, g, redirect, request, session, url_for +from quart import Quart, Response, abort, g, redirect, render_template, request, session, url_for from .analytics import close_analytics_db, open_analytics_db from .core import ( @@ -270,12 +270,39 @@ def create_app() -> Quart: from .sitemap import sitemap_response return await sitemap_response(config.BASE_URL) - # Ensure unhandled exceptions are always logged (Granian doesn't show - # Quart's debug error page, so without this 500s are silent). + # ------------------------------------------------------------------------- + # Error pages + # ------------------------------------------------------------------------- + + def _error_lang() -> str: + """Best-effort language from URL path prefix (no g.lang in error handlers).""" + path = request.path + if path.startswith("/de/"): + return "de" + return "en" + + @app.errorhandler(404) + async def handle_404(error): + import re + lang = _error_lang() + t = get_translations(lang) + country_slug = None + country_name = None + m = re.match(r"^/(?:en|de)/markets/([^/]+)/[^/]+/?$", request.path) + if m: + country_slug = m.group(1) + country_name = country_slug.replace("-", " ").title() + return await render_template( + "404.html", lang=lang, t=t, country_slug=country_slug, + country_name=country_name or "", + ), 404 + @app.errorhandler(500) async def handle_500(error): app.logger.exception("Unhandled 500 error: %s", error) - return "Internal Server Error", 500 + lang = _error_lang() + t = get_translations(lang) + return await render_template("500.html", lang=lang, t=t), 500 # Health check @app.route("/health") diff --git a/web/src/padelnomics/locales/de.json b/web/src/padelnomics/locales/de.json index d3be9b9..be8981b 100644 --- a/web/src/padelnomics/locales/de.json +++ b/web/src/padelnomics/locales/de.json @@ -1825,5 +1825,16 @@ "affiliate_pros_label": "Vorteile", "affiliate_cons_label": "Nachteile", "affiliate_at_retailer": "bei {retailer}", - "affiliate_our_picks": "Unsere Empfehlungen" + "affiliate_our_picks": "Unsere Empfehlungen", + + "error_404_title": "Seite nicht gefunden", + "error_404_heading": "Diese Seite gibt es nicht", + "error_404_message": "Die gesuchte Seite wurde verschoben oder existiert noch nicht.", + "error_404_city_message": "Die Marktanalyse für diese Stadt ist noch nicht verfügbar.", + "error_404_back_home": "Zur Startseite", + "error_404_back_country": "Zurück zur {country}-Übersicht", + "error_500_title": "Etwas ist schiefgelaufen", + "error_500_heading": "Etwas ist schiefgelaufen", + "error_500_message": "Wir arbeiten an einer Lösung. Bitte versuche es gleich noch einmal.", + "error_500_back_home": "Zur Startseite" } \ No newline at end of file diff --git a/web/src/padelnomics/locales/en.json b/web/src/padelnomics/locales/en.json index 62b2a4e..b878eb0 100644 --- a/web/src/padelnomics/locales/en.json +++ b/web/src/padelnomics/locales/en.json @@ -1828,5 +1828,16 @@ "affiliate_pros_label": "Pros", "affiliate_cons_label": "Cons", "affiliate_at_retailer": "at {retailer}", - "affiliate_our_picks": "Our picks" + "affiliate_our_picks": "Our picks", + + "error_404_title": "Page Not Found", + "error_404_heading": "This page doesn't exist", + "error_404_message": "The page you're looking for may have been moved or doesn't exist yet.", + "error_404_city_message": "The market analysis for this city isn't available yet.", + "error_404_back_home": "Back to Home", + "error_404_back_country": "Back to {country} overview", + "error_500_title": "Something Went Wrong", + "error_500_heading": "Something went wrong", + "error_500_message": "We're working on fixing this. Please try again in a moment.", + "error_500_back_home": "Back to Home" } \ No newline at end of file diff --git a/web/src/padelnomics/templates/404.html b/web/src/padelnomics/templates/404.html new file mode 100644 index 0000000..23d128e --- /dev/null +++ b/web/src/padelnomics/templates/404.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}{{ t.error_404_title }} — {{ config.APP_NAME }}{% endblock %} + +{% block content %} +
+
+

404

+

{{ t.error_404_heading }}

+ {% if country_slug %} +

{{ t.error_404_city_message }}

+ + {{ t.error_404_back_country.replace('{country}', country_name) }} + + {% else %} +

{{ t.error_404_message }}

+ + {{ t.error_404_back_home }} + + {% endif %} +
+
+{% endblock %} diff --git a/web/src/padelnomics/templates/500.html b/web/src/padelnomics/templates/500.html new file mode 100644 index 0000000..1a10578 --- /dev/null +++ b/web/src/padelnomics/templates/500.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}{{ t.error_500_title }} — {{ config.APP_NAME }}{% endblock %} + +{% block content %} +
+
+

500

+

{{ t.error_500_heading }}

+

{{ t.error_500_message }}

+ + {{ t.error_500_back_home }} + +
+
+{% endblock %}