Compare commits
3 Commits
v202603100
...
v202603100
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97ba13c42a | ||
|
|
1bd5bae90d | ||
|
|
608f16f578 |
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **SEO audit fixes** — sitemap: replaced `/market-score` with `/padelnomics-score`, added `/opportunity-map`, removed `/billing/pricing` (blocked by robots.txt), deduplicated articles query (was producing 4 entries per article instead of 2). Fixed `/market-score` redirect chain (1 hop instead of 2). Moved default OG tags inside `{% block head %}` so child templates replace rather than duplicate them. Added JSON-LD WebPage + BreadcrumbList schema to features, planner, and directory pages. Added meta descriptions to export pages.
|
||||
|
||||
### Changed
|
||||
- **Opportunity Score v7 → v8** — better spread and discrimination across the full 0-100 range. Addressable market weight reduced (20→15 pts) with steeper sqrt curve (ceiling 1M, was LN/500K). Economic power reduced (15→10 pts). Supply deficit increased (40→50 pts) with market existence dampener: countries with zero padel venues get max 5 pts supply deficit (factor 0.1), scaling linearly to full credit at 50+ venues. NULL nearest-court distance now treated as 0 (assume nearby) instead of 0.5. Added `country_percentile` output column (PERCENT_RANK within country). Target: P5-P95 spread ≥40 pts (was 22), zero-venue countries avg <30.
|
||||
- **Opportunity Score v6 → v7 (calibration fix)** — two fixes for inflated scores in saturated markets. (1) `dim_locations` now sources venue coordinates from `dim_venues` (deduplicated OSM + Playtomic) instead of `stg_padel_courts` (OSM only), making Playtomic-only venues visible to spatial lookups. (2) Country-level supply saturation dampener on the 40-pt supply deficit component: saturated countries (Spain ~4.5/100k) get dampened supply deficit (×0.55 → 22 pts max), emerging markets (Germany ~0.7/100k) are nearly unaffected (×0.93 → ~37 pts).
|
||||
|
||||
@@ -401,7 +401,7 @@ def create_app() -> Quart:
|
||||
|
||||
@app.route("/market-score")
|
||||
async def legacy_market_score():
|
||||
return redirect("/en/market-score", 301)
|
||||
return redirect("/en/padelnomics-score", 301)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Blueprint registration
|
||||
|
||||
@@ -6,6 +6,28 @@
|
||||
<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: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>
|
||||
:root {
|
||||
--dir-green: #15803D;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% block paddle %}{% include "_payment_js.html" %}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta name="description" content="{{ t.export_title }}">
|
||||
<style>
|
||||
.exp-wrap { max-width: 640px; margin: 0 auto; padding: 3rem 0; }
|
||||
.exp-hero { text-align: center; margin-bottom: 2rem; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% block title %}Business Plan Details — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta name="description" content="Business Plan Details — {{ config.APP_NAME }}">
|
||||
<style>
|
||||
.bp-wrap { max-width: 680px; margin: 0 auto; padding: 3rem 0; }
|
||||
.bp-hero { margin-bottom: 2rem; }
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ t.export_success_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta name="description" content="{{ t.export_success_title }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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">✓</div>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
{% 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 %}
|
||||
<main class="container-page py-12">
|
||||
<div class="card max-w-md mx-auto mt-8 text-center">
|
||||
|
||||
@@ -9,6 +9,35 @@
|
||||
<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 }}">
|
||||
<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 %}
|
||||
|
||||
{% macro slider(name, label, min, max, step, value, tip='') %}
|
||||
|
||||
@@ -7,6 +7,28 @@
|
||||
<meta property="og:title" content="{{ t.features_title_prefix }} | {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="{{ t.features_meta_desc }}">
|
||||
<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 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -26,9 +26,10 @@ STATIC_PATHS = [
|
||||
"/imprint",
|
||||
"/suppliers",
|
||||
"/markets",
|
||||
"/market-score",
|
||||
"/padelnomics-score",
|
||||
"/planner/",
|
||||
"/directory/",
|
||||
"/opportunity-map",
|
||||
]
|
||||
|
||||
|
||||
@@ -65,16 +66,16 @@ async def _generate_sitemap_xml(base_url: str) -> str:
|
||||
for lang in LANGS:
|
||||
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.
|
||||
# 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(
|
||||
"""SELECT url_path, COALESCE(updated_at, published_at) AS lastmod
|
||||
"""SELECT url_path, MAX(COALESCE(updated_at, published_at)) AS lastmod
|
||||
FROM articles
|
||||
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"""
|
||||
)
|
||||
for article in articles:
|
||||
|
||||
@@ -29,15 +29,16 @@
|
||||
<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 }}">
|
||||
{% 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:description" content="">
|
||||
<meta property="og:type" content="website">
|
||||
<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 name="twitter:card" content="summary_large_image">
|
||||
|
||||
<script>window.__GEO = {country: "{{ user_country }}", city: "{{ user_city }}"};</script>
|
||||
{% block head %}{% endblock %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav-bar" id="main-nav">
|
||||
|
||||
Reference in New Issue
Block a user