From 625c08928482756966aba68e8188b66a636974e7 Mon Sep 17 00:00:00 2001 From: Deeman Date: Sun, 22 Feb 2026 10:29:59 +0100 Subject: [PATCH] fix: post-flatten dev scripts, lead form validation, copy improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix dev_run.sh and dev_setup.sh cd path (../.. after repo flatten) - Quote form: re-render step 9 inline on validation error instead of flash + redirect to step 1; phone/email errors now show field-level - Supplier FAQ: move differentiation Q to top, fix Q10 email to hello@ (was leads@), rename Q1 to "How do I get listed?" - Replace Innenhalle → Indoorhalle throughout DE locale and seed script Co-Authored-By: Claude Sonnet 4.6 --- research/product_ideas.md | 54 +++++++++++++++++++ web/scripts/dev_run.sh | 2 +- web/scripts/dev_setup.sh | 2 +- web/src/padelnomics/leads/routes.py | 43 ++++++++------- web/src/padelnomics/locales/de.json | 12 ++--- web/src/padelnomics/locales/en.json | 2 +- .../public/templates/suppliers.html | 10 ++-- web/src/padelnomics/scripts/seed_content.py | 6 +-- 8 files changed, 96 insertions(+), 35 deletions(-) diff --git a/research/product_ideas.md b/research/product_ideas.md index 41465ab..722697e 100644 --- a/research/product_ideas.md +++ b/research/product_ideas.md @@ -65,6 +65,60 @@ Annual "Listed" tier = low-friction entry for suppliers who want visibility with - Make it **Pro-only** as a built-in perk (headline differentiator for Pro upgrade) - Possibly also available as a paid add-on boost for Growth tier +## Company Registry Intelligence (Data / Programmatic SEO) + +Use public company registries to extract data on padel businesses — financials from annual +filings, geocoded addresses, incorporation dates — and build: + +- **Padel Industry Profitability Index** — aggregate revenue/profit/assets across all filed + padel companies, updated annually. Unique linkable asset; journalists and investors will cite it. +- **Company directory with financial health signals** — one page per company with location, + incorporation date, filed accounts summary, directors. Programmatic SEO at scale. +- **Regional profitability map** — geocoded addresses + aggregated financials per region. + *"Which UK regions have the most profitable padel clubs?"* +- **Growth timeline articles** — incorporation dates chart padel's rise. + *"UK padel company formations: 3 in 2018 → 47 in 2023 → 120+ in 2024"* +- **Cross-country comparison** — once multi-country data is collected: + *"Where is padel most commercially mature?"* + +### Data sources + +| Country | Registry | API | Cost | Has Financials | +|---------|----------|-----|------|----------------| +| UK | Companies House | REST API | Free (600 req/5min) | Yes — turnover, P&L, assets | +| Germany | Handelsregister | No official API | Northdata (freemium) | Limited | +| Netherlands | KVK | Paid API | Monthly sub | Some | +| France | INSEE Sirene | Bulk download | Free | No (accounts at INPI) | +| Spain | Registro Mercantil | No free API | Commercialised | Paywall since 2011 | +| Sweden | Bolagsverket | Good API | Free | Yes | + +### Start with UK + +- API key: developer.company-information.service.gov.uk (~5 min to get, free) +- `GET /search/companies?q=padel` — full-text search works +- SIC codes: `93110` (operation of sports facilities), `93130` (fitness facilities) +- `GET /company/{number}/filing-history?category=accounts` → parse iXBRL for: + Turnover, ProfitLoss, TotalAssets, NetAssets, AverageNumberEmployees +- Geocode postcodes via free OS Postcode API + +### Caveats +- ~60% of small UK companies file abridged accounts (balance sheet only, no turnover) +- Many padel clubs don't have "padel" in company name — cross-ref with existing venue DB +- Holding structures can obscure revenue (rolls up to parent) +- Germany: use northdata.de (no free official API) +- Spain: genuinely closed since 2011 + +### Technical sketch +``` +companies_house_fetch.py → search + paginate all padel companies +filings_fetch.py → download + parse iXBRL accounts per company +geocode.py → postcode → lat/lng (OS Postcode API) +aggregate.py → SQL: profitability index, regional groupings, YoY growth +page_generator.py → one page per company + index pages per region +``` +Stack: `requests`, `lxml` (XBRL parsing), `sqlite3`. No heavy dependencies. + + ## Supplier Reviews (implementation sketch) - New table: `supplier_reviews (id, lead_forward_id, supplier_id, rating INT 1-5, body TEXT, created_at)` - Email trigger: worker job checks `lead_forwards` created 60+ days ago with no review → enqueue review request email diff --git a/web/scripts/dev_run.sh b/web/scripts/dev_run.sh index d1ad062..d81d5ea 100755 --- a/web/scripts/dev_run.sh +++ b/web/scripts/dev_run.sh @@ -10,7 +10,7 @@ set -euo pipefail -cd "$(dirname "$0")/.." +cd "$(dirname "$0")/../.." # -- Colors & helpers -------------------------------------------------------- diff --git a/web/scripts/dev_setup.sh b/web/scripts/dev_setup.sh index f507e85..381f6ed 100755 --- a/web/scripts/dev_setup.sh +++ b/web/scripts/dev_setup.sh @@ -8,7 +8,7 @@ set -euo pipefail -cd "$(dirname "$0")/.." +cd "$(dirname "$0")/../.." # -- Colors & helpers ------------------------------------------------------- diff --git a/web/src/padelnomics/leads/routes.py b/web/src/padelnomics/leads/routes.py index 5455346..471f30d 100644 --- a/web/src/padelnomics/leads/routes.py +++ b/web/src/padelnomics/leads/routes.py @@ -273,31 +273,38 @@ async def quote_request(): form = await request.form services = form.getlist("services_needed") - # Validate mandatory fields - errors = [] + # Validate mandatory fields — collect field names for inline error display + field_errors = [] if not form.get("country"): - errors.append("Country is required") + field_errors.append("country") if not form.get("timeline"): - errors.append("Timeline is required") + field_errors.append("timeline") if not form.get("stakeholder_type"): - errors.append("Stakeholder type is required") + field_errors.append("stakeholder_type") if not form.get("contact_name", "").strip(): - errors.append("Full name is required") - if not form.get("contact_email", "").strip(): - errors.append("Email is required") - if not form.get("contact_phone", "").strip(): - errors.append("Phone number is required") + field_errors.append("contact_name") contact_email_raw = form.get("contact_email", "").strip() - if contact_email_raw and is_disposable_email(contact_email_raw): - errors.append("Please use a permanent email address, not a temporary one.") + if not contact_email_raw: + field_errors.append("contact_email") + elif is_disposable_email(contact_email_raw): + field_errors.append("contact_email") contact_phone_raw = form.get("contact_phone", "").strip() - if contact_phone_raw and not is_plausible_phone(contact_phone_raw): - errors.append("Please enter a valid phone number.") - if errors: + if not contact_phone_raw: + field_errors.append("contact_phone") + elif not is_plausible_phone(contact_phone_raw): + field_errors.append("contact_phone") + if field_errors: if is_json: - return jsonify({"ok": False, "errors": errors}), 422 - await flash("; ".join(errors), "error") - return redirect(url_for("leads.quote_request")) + return jsonify({"ok": False, "errors": field_errors}), 422 + form_data = {k: v for k, v in form.items() if not k.startswith("_") and k != "csrf_token"} + form_data["services_needed"] = services + return await render_template( + "quote_request.html", + data=form_data, + step=9, + steps=_get_quote_steps(lang), + errors=field_errors, + ) heat = calculate_heat_score(form) diff --git a/web/src/padelnomics/locales/de.json b/web/src/padelnomics/locales/de.json index d1f6545..ce5a59a 100644 --- a/web/src/padelnomics/locales/de.json +++ b/web/src/padelnomics/locales/de.json @@ -104,7 +104,7 @@ "landing_roi_monthly_cf": "Monatlicher Cashflow", "landing_roi_payback": "Amortisationszeit", "landing_roi_annual_roi": "Jährlicher ROI", - "landing_roi_note": "Annahmen: Innenhalle Mietmodell, 8 €/m² Miete, Personalkosten, 5 % Zinsen, 10-jähriges Darlehen. Amortisation und ROI basieren auf der Gesamtinvestition.", + "landing_roi_note": "Annahmen: Indoorhalle Mietmodell, 8 €/m² Miete, Personalkosten, 5 % Zinsen, 10-jähriges Darlehen. Amortisation und ROI basieren auf der Gesamtinvestition.", "landing_roi_cta": "Jetzt planen →", "landing_journey_title": "Deine Reise", "landing_journey_01": "Analysieren", @@ -792,7 +792,7 @@ "label_build_buy": "Kauf/Bau", "label_rent": "Miete", "label_courts": "Plätze", - "label_indoor_hall": "Innenhalle", + "label_indoor_hall": "Indoorhalle", "label_outdoor_land": "Außenfläche", "label_playing_surface": "Spielfläche", "wiz_capex": "CAPEX", @@ -1017,7 +1017,7 @@ "sup_proof_q2": "Endlich eine Plattform, die den Padel-Baumarkt versteht. Wir kennen das Budget, den Zeitplan und den Standorttyp, bevor wir überhaupt Erstkontakt aufnehmen.", "sup_proof_cite2": "— Padel-Court-Installationsunternehmen, Skandinavien", "sup_faq_h2": "Anbieter-FAQ", - "sup_faq_q1": "Wie beanspruche ich mein Inserat?", + "sup_faq_q1": "Wie werde ich gelistet?", "sup_faq_a1_pre": "Finde dein Unternehmen in unserem", "sup_faq_a1_post": "und klicke auf „Ist das dein Unternehmen?“ Wir überprüfen deine Identität und geben dir Zugang, um einen Plan auszuwählen und dein Profil zu aktualisieren.", "sup_faq_dir_link": "Verzeichnis", @@ -1160,7 +1160,7 @@ "features_meta_desc": "60+ anpassbare Variablen, 6 Analyse-Tabs, Sensitivitätsanalyse und professionelle Finanzprojektionen für deine Padelplatz-Investition.", "features_card_1_body": "Jede Annahme ist anpassbar: Platzbaukosten, Miete, Stundensätze, Auslastungskurven, Finanzierungskonditionen, Exit-Multiplikatoren. Nichts ist fest vorgegeben — Dein Modell spiegelt deine Realität wider.", "features_card_2_body": "Annahmen, Investition (CAPEX), Betriebsmodell, Cashflow, Renditen & Exit sowie Kennzahlen. Jeder Tab mit interaktiven Diagrammen, die sich in Echtzeit aktualisieren, wenn du Eingaben anpasst.", - "features_card_3_body": "Innenhallenmodelle (Anmietung eines Bestandsgebäudes oder Neubau) und Außenanlagen mit Saisonalitätsanpassungen. Szenarien direkt nebeneinander vergleichen, um den besten Ansatz für deinen Markt zu finden.", + "features_card_3_body": "Indoorhallenmodelle (Anmietung eines Bestandsgebäudes oder Neubau) und Außenanlagen mit Saisonalitätsanpassungen. Szenarien direkt nebeneinander vergleichen, um den besten Ansatz für deinen Markt zu finden.", "features_card_4_body": "Sieh dir an, wie sich deine IRR und Cash-Rendite bei unterschiedlichen Auslastungsraten und Preisen verändern. Ermittle deinen Break-even-Punkt sofort mit der integrierten Sensitivitätsmatrix.", "features_card_5_body": "IRR, MOIC, DSCR, Cash-on-Cash-Rendite, Break-even-Auslastung, RevPAH, Schuldenrendite — die Kennzahlen, die Banken und Investoren in einem Padelplatz-Businessplan sehen möchten.", "features_card_6_body": "Unbegrenzte Szenarien speichern. Verschiedene Standorte, Platzzahlen, Finanzierungsstrukturen und Preisstrategien testen. Laden und vergleichen, um den optimalen Plan für deine Investition zu finden.", @@ -1179,7 +1179,7 @@ "landing_journey_05_desc": "Launch-Playbook, Performance-Benchmarks und Wachstumsanalysen für deinen Betrieb.", "landing_feature_1_body": "Jede Annahme ist anpassbar: Platzbaukosten, Miete, Preisgestaltung, Auslastung, Finanzierungskonditionen, Exit-Szenarien. Nichts ist fest vorgegeben.", "landing_feature_2_body": "Annahmen, Investition (CAPEX), Betriebsmodell, Cashflow, Renditen & Exit sowie Kennzahlen — jeder Tab mit interaktiven Diagrammen.", - "landing_feature_3_body": "Innenhallenmodelle (Miete oder Neubau) und Außenanlagen mit Saisonalität. Szenarien direkt nebeneinander vergleichen.", + "landing_feature_3_body": "Indoorhallenmodelle (Miete oder Neubau) und Außenanlagen mit Saisonalität. Szenarien direkt nebeneinander vergleichen.", "landing_feature_4_body": "Sieh dir an, wie sich deine Renditen bei unterschiedlichen Auslastungsraten und Preisen verändern. Break-even-Punkt sofort ermitteln.", "landing_feature_5_body": "IRR, MOIC, DSCR, Cash-on-Cash-Rendite, Break-even-Auslastung, RevPAH, Schuldenrendite — die Kennzahlen, die Banken und Investoren sehen möchten.", "landing_feature_6_body": "Unbegrenzte Szenarien speichern. Verschiedene Standorte, Platzzahlen und Finanzierungsstrukturen testen. Den optimalen Plan finden.", @@ -1192,7 +1192,7 @@ "landing_faq_a3": "Wenn du über den Planer Angebote anforderst, teilen wir deine Projektdetails (Anlagentyp, Platzzahl, Glas, Beleuchtung, Land, Budget, Zeitplan) mit passenden Anbietern aus unserem Verzeichnis. Diese kontaktieren dich direkt mit ihren Angeboten.", "landing_faq_a4": "Das Durchsuchen des Verzeichnisses ist für alle kostenlos. Anbieter erhalten standardmäßig einen Basiseintrag. Kostenpflichtige Pläne (Basic ab 39 €/Monat, Growth ab 199 €/Monat, Pro ab 499 €/Monat) schalten Anfrageformulare, vollständige Beschreibungen, Logos, verifizierte Badges und Prioritätsplatzierung frei.", "landing_faq_a5": "Das Modell verwendet reale Standardwerte auf Basis europäischer Marktdaten. Jede Annahme ist anpassbar, sodass du deine lokalen Gegebenheiten abbilden kannst. Die Sensitivitätsanalyse zeigt, wie sich die Ergebnisse in verschiedenen Szenarien verändern, und hilft dir, die Bandbreite möglicher Ergebnisse zu verstehen.", - "landing_seo_p1": "Padel ist der am schnellsten wachsende Sport in Europa — die Nachfrage nach Plätzen übersteigt das Angebot in Deutschland, Österreich, der Schweiz und darüber hinaus bei weitem. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Innenhalle mit 6–8 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 2–3 Mio. € (Neubau), mit Amortisationszeiten von 3–5 Jahren für gut gelegene Anlagen.", + "landing_seo_p1": "Padel ist der am schnellsten wachsende Sport in Europa — die Nachfrage nach Plätzen übersteigt das Angebot in Deutschland, Österreich, der Schweiz und darüber hinaus bei weitem. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Indoorhalle mit 6–8 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 2–3 Mio. € (Neubau), mit Amortisationszeiten von 3–5 Jahren für gut gelegene Anlagen.", "landing_seo_p2": "Die entscheidenden Faktoren für den Erfolg sind Standort (treibt die Auslastung), Baukosten (CAPEX), Miet- oder Grundstückskosten sowie die Preisstrategie. Unser Finanzplaner ermöglicht es dir, alle diese Variablen interaktiv zu modellieren und die Auswirkungen auf IRR, MOIC, Cashflow und Schuldendienstdeckungsgrad in Echtzeit zu sehen. Ob du als Unternehmer deine erste Anlage prüfst, als Immobilienentwickler Padel in ein Mixed-Use-Projekt integrierst oder als Investor eine bestehende Paddelhalle bewertest — Padelnomics gibt dir die finanzielle Klarheit für fundierte Entscheidungen.", "landing_final_cta_sub": "Modelliere deine Investition und lass dich mit verifizierten Platz-Anbietern aus {total_countries} Ländern zusammenbringen.", "landing_jsonld_org_desc": "Professionelle Planungsplattform für Padelplatz-Investitionen. Finanzplaner, Anbieterverzeichnis und Marktinformationen für Padel-Unternehmer.", diff --git a/web/src/padelnomics/locales/en.json b/web/src/padelnomics/locales/en.json index f9bab0c..5b7a930 100644 --- a/web/src/padelnomics/locales/en.json +++ b/web/src/padelnomics/locales/en.json @@ -1017,7 +1017,7 @@ "sup_proof_q2": "Finally a platform that understands the padel construction market. We know the budget, the timeline, and the venue type before we even make first contact.", "sup_proof_cite2": "— Padel court installation company, Scandinavia", "sup_faq_h2": "Supplier FAQ", - "sup_faq_q1": "How do I claim my listing?", + "sup_faq_q1": "How do I get listed?", "sup_faq_a1_pre": "Find your company in our", "sup_faq_a1_post": "and click “Is this your company?” We’ll verify your identity and give you access to choose a plan and upgrade your profile.", "sup_faq_dir_link": "directory", diff --git a/web/src/padelnomics/public/templates/suppliers.html b/web/src/padelnomics/public/templates/suppliers.html index 4208baa..27ebacd 100644 --- a/web/src/padelnomics/public/templates/suppliers.html +++ b/web/src/padelnomics/public/templates/suppliers.html @@ -632,6 +632,10 @@

{{ t.sup_faq_h2 }}

+
+ {{ t.sup_faq_q3 }} +

{{ t.sup_faq_a3 }}

+
{{ t.sup_faq_q1 }}

{{ t.sup_faq_a1_pre }} {{ t.sup_faq_dir_link }} {{ t.sup_faq_a1_post }}

@@ -640,10 +644,6 @@ {{ t.sup_faq_q2 }}

{{ t.sup_faq_a2 }}

-
- {{ t.sup_faq_q3 }} -

{{ t.sup_faq_a3 }}

-
{{ t.sup_faq_q4 }}

{{ t.sup_faq_a4 }}

@@ -670,7 +670,7 @@
{{ t.sup_faq_q10 }} -

{{ t.sup_faq_a10_pre }} {{ config.LEADS_EMAIL }} {{ t.sup_faq_a10_post }}

+

{{ t.sup_faq_a10_pre }} {{ config.EMAIL_FROM }} {{ t.sup_faq_a10_post }}

diff --git a/web/src/padelnomics/scripts/seed_content.py b/web/src/padelnomics/scripts/seed_content.py index 044ce9e..23ee2f7 100644 --- a/web/src/padelnomics/scripts/seed_content.py +++ b/web/src/padelnomics/scripts/seed_content.py @@ -123,7 +123,7 @@ pricing, rent, financing terms, and see how the numbers change in real time.\ _DE_BODY = """\ {{ padel_context }} -Diese Analyse modelliert eine **{{ dblCourts }} Doppel- + {{ sglCourts }} Einzelcourt-Innenhalle** \ +Diese Analyse modelliert eine **{{ dblCourts }} Doppel- + {{ sglCourts }} Einzelcourt-Indoorhalle** \ auf Basis lokaler Marktdaten für {{ city }}, {{ country_name }}. {% if currency_note %} > **Hinweis:** {{ currency_note }} @@ -200,7 +200,7 @@ TEMPLATES = [ "meta_description_pattern": ( "Was kostet es, eine Padelhalle in {{ city }} zu eröffnen? " "Vollständige Investitionsanalyse: CAPEX, Umsatzmodell und Rendite " - "für eine {{ dblCourts }}+{{ sglCourts }} Court Innenhalle." + "für eine {{ dblCourts }}+{{ sglCourts }} Court Indoorhalle." ), "body_template": _DE_BODY, }, @@ -588,7 +588,7 @@ CITIES = [ "padel_context_de": ( "Chicagos große hispanische Gemeinschaft und etablierte Schlägertsportkultur " "machen es zu einem natürlichen Padelmarkt. Die harten Winter treiben die " - "Nachfrage nach Innenhallen und reduzieren den Outdoor-Wettbewerb, " + "Nachfrage nach Indoorhallen und reduzieren den Outdoor-Wettbewerb, " "der wärmere Märkte beeinflusst." ), "currency_note_en": _EUR_NOTE_EN,