fix(seo): pre-GSC audit — sitemap, redirects, OG tags, schema markup

- sitemap: replace /market-score with /padelnomics-score, add /opportunity-map,
  remove /billing/pricing (blocked by robots.txt), deduplicate articles query
- app: fix /market-score redirect chain (→ /en/padelnomics-score directly)
- base.html: move default OG tags inside {% block head %} so child overrides
  replace them instead of duplicating
- features, planner, directory: add JSON-LD WebPage + BreadcrumbList schema
- export pages: add meta descriptions and {% block head %} where missing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-03-10 10:53:20 +01:00
parent 927f77ae5e
commit 608f16f578
10 changed files with 96 additions and 11 deletions

View File

@@ -401,7 +401,7 @@ def create_app() -> Quart:
@app.route("/market-score") @app.route("/market-score")
async def legacy_market_score(): async def legacy_market_score():
return redirect("/en/market-score", 301) return redirect("/en/padelnomics-score", 301)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Blueprint registration # Blueprint registration

View File

@@ -6,6 +6,28 @@
<meta name="description" content="{{ t.dir_page_meta_desc | tformat(count=total_suppliers, countries=total_countries) }}"> <meta name="description" content="{{ t.dir_page_meta_desc | tformat(count=total_suppliers, countries=total_countries) }}">
<meta property="og:title" content="{{ t.dir_page_title }} - {{ config.APP_NAME }}"> <meta property="og:title" content="{{ t.dir_page_title }} - {{ config.APP_NAME }}">
<meta property="og:description" content="{{ t.dir_page_og_desc | tformat(count=total_suppliers, countries=total_countries) }}"> <meta property="og:description" content="{{ t.dir_page_og_desc | tformat(count=total_suppliers, countries=total_countries) }}">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebPage",
"name": "{{ t.dir_page_title }} - {{ config.APP_NAME }}",
"description": "{{ t.dir_page_meta_desc | tformat(count=total_suppliers, countries=total_countries) }}",
"url": "{{ config.BASE_URL }}/{{ lang }}/directory/",
"inLanguage": "{{ lang }}",
"isPartOf": {"@type": "WebSite", "name": "Padelnomics", "url": "{{ config.BASE_URL }}"}
},
{
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Home", "item": "{{ config.BASE_URL }}/{{ lang }}"},
{"@type": "ListItem", "position": 2, "name": "{{ t.dir_page_title }}", "item": "{{ config.BASE_URL }}/{{ lang }}/directory/"}
]
}
]
}
</script>
<style> <style>
:root { :root {
--dir-green: #15803D; --dir-green: #15803D;

View File

@@ -3,6 +3,7 @@
{% block paddle %}{% include "_payment_js.html" %}{% endblock %} {% block paddle %}{% include "_payment_js.html" %}{% endblock %}
{% block head %} {% block head %}
<meta name="description" content="{{ t.export_title }}">
<style> <style>
.exp-wrap { max-width: 640px; margin: 0 auto; padding: 3rem 0; } .exp-wrap { max-width: 640px; margin: 0 auto; padding: 3rem 0; }
.exp-hero { text-align: center; margin-bottom: 2rem; } .exp-hero { text-align: center; margin-bottom: 2rem; }

View File

@@ -2,6 +2,7 @@
{% block title %}Business Plan Details — {{ config.APP_NAME }}{% endblock %} {% block title %}Business Plan Details — {{ config.APP_NAME }}{% endblock %}
{% block head %} {% block head %}
<meta name="description" content="Business Plan Details — {{ config.APP_NAME }}">
<style> <style>
.bp-wrap { max-width: 680px; margin: 0 auto; padding: 3rem 0; } .bp-wrap { max-width: 680px; margin: 0 auto; padding: 3rem 0; }
.bp-hero { margin-bottom: 2rem; } .bp-hero { margin-bottom: 2rem; }

View File

@@ -1,6 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ t.export_success_title }} - {{ config.APP_NAME }}{% endblock %} {% block title %}{{ t.export_success_title }} - {{ config.APP_NAME }}{% endblock %}
{% block head %}
<meta name="description" content="{{ t.export_success_title }}">
{% endblock %}
{% block content %} {% block content %}
<main class="container-page" style="max-width:500px;margin:0 auto;padding:4rem 1rem;text-align:center"> <main class="container-page" style="max-width:500px;margin:0 auto;padding:4rem 1rem;text-align:center">
<div style="font-size:3rem;margin-bottom:1rem">&#10003;</div> <div style="font-size:3rem;margin-bottom:1rem">&#10003;</div>

View File

@@ -2,6 +2,10 @@
{% block title %}{{ t.export_waitlist_title }} - {{ config.APP_NAME }}{% endblock %} {% block title %}{{ t.export_waitlist_title }} - {{ config.APP_NAME }}{% endblock %}
{% block head %}
<meta name="description" content="{{ t.export_waitlist_title }} - {{ config.APP_NAME }}">
{% endblock %}
{% block content %} {% block content %}
<main class="container-page py-12"> <main class="container-page py-12">
<div class="card max-w-md mx-auto mt-8 text-center"> <div class="card max-w-md mx-auto mt-8 text-center">

View File

@@ -9,6 +9,35 @@
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}"> <meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/planner.css') }}?v={{ v }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/planner.css') }}?v={{ v }}">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebPage",
"name": "{{ t.planner_page_title }} - {{ config.APP_NAME }}",
"description": "{{ t.planner_meta_desc }}",
"url": "{{ config.BASE_URL }}/{{ lang }}/planner/",
"inLanguage": "{{ lang }}",
"isPartOf": {"@type": "WebSite", "name": "Padelnomics", "url": "{{ config.BASE_URL }}"}
},
{
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Home", "item": "{{ config.BASE_URL }}/{{ lang }}"},
{"@type": "ListItem", "position": 2, "name": "{{ t.nav_planner }}", "item": "{{ config.BASE_URL }}/{{ lang }}/planner/"}
]
},
{
"@type": "SoftwareApplication",
"name": "Padelnomics Padel Court Planner",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "EUR"}
}
]
}
</script>
{% endblock %} {% endblock %}
{% macro slider(name, label, min, max, step, value, tip='') %} {% macro slider(name, label, min, max, step, value, tip='') %}

View File

@@ -7,6 +7,28 @@
<meta property="og:title" content="{{ t.features_title_prefix }} | {{ config.APP_NAME }}"> <meta property="og:title" content="{{ t.features_title_prefix }} | {{ config.APP_NAME }}">
<meta property="og:description" content="{{ t.features_meta_desc }}"> <meta property="og:description" content="{{ t.features_meta_desc }}">
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}"> <meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebPage",
"name": "{{ t.features_title_prefix }} | {{ config.APP_NAME }}",
"description": "{{ t.features_meta_desc }}",
"url": "{{ config.BASE_URL }}/{{ lang }}/features",
"inLanguage": "{{ lang }}",
"isPartOf": {"@type": "WebSite", "name": "Padelnomics", "url": "{{ config.BASE_URL }}"}
},
{
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Home", "item": "{{ config.BASE_URL }}/{{ lang }}"},
{"@type": "ListItem", "position": 2, "name": "{{ t.features_title_prefix }}", "item": "{{ config.BASE_URL }}/{{ lang }}/features"}
]
}
]
}
</script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@@ -26,9 +26,10 @@ STATIC_PATHS = [
"/imprint", "/imprint",
"/suppliers", "/suppliers",
"/markets", "/markets",
"/market-score", "/padelnomics-score",
"/planner/", "/planner/",
"/directory/", "/directory/",
"/opportunity-map",
] ]
@@ -65,16 +66,16 @@ async def _generate_sitemap_xml(base_url: str) -> str:
for lang in LANGS: for lang in LANGS:
entries.append(_url_entry(f"{base}/{lang}{path}", alternates)) entries.append(_url_entry(f"{base}/{lang}{path}", alternates))
# Billing pricing — no lang prefix, no hreflang
entries.append(_url_entry(f"{base}/billing/pricing", []))
# Published articles — both lang variants with accurate lastmod. # Published articles — both lang variants with accurate lastmod.
# Exclude noindex articles (thin data) to keep sitemap signal-dense. # Exclude noindex articles (thin data) to keep sitemap signal-dense.
# GROUP BY url_path: articles table has one row per language per url_path,
# but the for-lang loop already creates both lang entries per path.
articles = await fetch_all( articles = await fetch_all(
"""SELECT url_path, COALESCE(updated_at, published_at) AS lastmod """SELECT url_path, MAX(COALESCE(updated_at, published_at)) AS lastmod
FROM articles FROM articles
WHERE status = 'published' AND noindex = 0 AND published_at <= datetime('now') WHERE status = 'published' AND noindex = 0 AND published_at <= datetime('now')
ORDER BY published_at DESC GROUP BY url_path
ORDER BY MAX(published_at) DESC
LIMIT 25000""" LIMIT 25000"""
) )
for article in articles: for article in articles:

View File

@@ -29,15 +29,16 @@
<link rel="alternate" hreflang="de" href="{{ config.BASE_URL }}/de{{ path_suffix }}"> <link rel="alternate" hreflang="de" href="{{ config.BASE_URL }}/de{{ path_suffix }}">
<link rel="alternate" hreflang="x-default" href="{{ config.BASE_URL }}/en{{ path_suffix }}"> <link rel="alternate" hreflang="x-default" href="{{ config.BASE_URL }}/en{{ path_suffix }}">
{% endif %} {% endif %}
<meta name="twitter:card" content="summary_large_image">
<script>window.__GEO = {country: "{{ user_country }}", city: "{{ user_city }}"};</script>
{% block head %}
<meta property="og:title" content="{{ config.APP_NAME }}"> <meta property="og:title" content="{{ config.APP_NAME }}">
<meta property="og:description" content=""> <meta property="og:description" content="">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="{{ config.BASE_URL }}{{ request.path }}"> <meta property="og:url" content="{{ config.BASE_URL }}{{ request.path }}">
<meta property="og:image" content="{{ url_for('static', filename='images/logo.png', _external=True) }}"> <meta property="og:image" content="{{ url_for('static', filename='images/logo.png', _external=True) }}">
<meta name="twitter:card" content="summary_large_image"> {% endblock %}
<script>window.__GEO = {country: "{{ user_country }}", city: "{{ user_city }}"};</script>
{% block head %}{% endblock %}
</head> </head>
<body> <body>
<nav class="nav-bar" id="main-nav"> <nav class="nav-bar" id="main-nav">