Karten-Ausbau (OSM), Forum-Erweiterung, UI-Komponenten, Refactor Tagebuch/Gassi (DRY), Landing/SEO — APP_VER 1155

This commit is contained in:
rene 2026-06-03 17:24:47 +02:00
parent 2d907f6370
commit 10e39ed135
18 changed files with 871 additions and 405 deletions

View file

@ -511,11 +511,11 @@ async def sitemap():
urls = [
("https://banyaro.app/", "weekly", "1.0"),
("https://banyaro.app/zuechter", "weekly", "0.9"),
("https://banyaro.app/info", "monthly", "0.8"),
("https://banyaro.app/presse", "monthly", "0.7"),
("https://banyaro.app/wiki/rassen", "weekly", "0.8"),
("https://banyaro.app/knigge", "monthly", "0.7"),
("https://banyaro.app/wurfboerse", "daily", "0.8"),
("https://banyaro.app/wiki/rassen", "weekly", "0.8"),
("https://banyaro.app/help", "monthly", "0.7"),
("https://banyaro.app/knigge", "monthly", "0.7"),
("https://banyaro.app/partner", "monthly", "0.6"),
]
try:
@ -526,12 +526,6 @@ async def sitemap():
for r in rassen:
urls.append((f"https://banyaro.app/wiki/rasse/{r['slug']}", "monthly", "0.7"))
events = conn.execute(
"SELECT id FROM events WHERE datum >= date('now') LIMIT 200"
).fetchall()
for e in events:
urls.append((f"https://banyaro.app/api/events/{e['id']}", "weekly", "0.5"))
# Öffentliche Züchter-Profile
breeders = conn.execute(
"SELECT bp.zwingername FROM breeder_profiles bp "
@ -1348,12 +1342,47 @@ async def public_dog_page(dog_id: int):
# ------------------------------------------------------------------
@app.get("/teilen/{token}")
async def invite_page(token: str):
return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"})
from fastapi.responses import HTMLResponse
with open(f"{STATIC_DIR}/index.html", encoding="utf-8") as _f:
_html = _f.read()
_html = _html.replace(
'<link rel="canonical" href="https://banyaro.app/">',
'<meta name="robots" content="noindex"><link rel="canonical" href="https://banyaro.app/">'
)
return HTMLResponse(content=_html, headers={"Cache-Control": "no-store, no-cache"})
@app.get("/breeder/{zwingername}")
async def breeder_profile_page(zwingername: str):
return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"})
from fastapi.responses import HTMLResponse
from urllib.parse import unquote
from database import db as _db
import html as _html_mod
name = unquote(zwingername)
desc = f"Hundezüchter {_html_mod.escape(name)} auf Ban Yaro — Wurfbörse, Stammbaum und mehr."
try:
with _db() as conn:
bp = conn.execute(
"SELECT bp.rasse, bp.beschreibung FROM breeder_profiles bp "
"JOIN users u ON u.id = bp.user_id WHERE bp.zwingername=? AND u.rolle='breeder' LIMIT 1",
(name,)
).fetchone()
if bp and bp["beschreibung"]:
desc = _html_mod.escape(bp["beschreibung"][:160])
except Exception:
pass
with open(f"{STATIC_DIR}/index.html", encoding="utf-8") as _f:
_page = _f.read()
_page = _page.replace(
'<link rel="canonical" href="https://banyaro.app/">',
f'<link rel="canonical" href="https://banyaro.app/breeder/{_html_mod.escape(zwingername)}">'
).replace(
'<title>Ban Yaro</title>',
f'<title>{_html_mod.escape(name)} — Hundezüchter auf Ban Yaro</title>'
f'\n <meta name="description" content="{desc}">'
f'\n <meta name="robots" content="index, follow">'
)
return HTMLResponse(content=_page, headers={"Cache-Control": "no-store, no-cache"})
@app.get("/litters")
@ -1471,6 +1500,7 @@ async def ausweis_page(dog_id: int, request: Request):
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">
<title>Heimtierausweis {esc(dog["name"])}</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
@ -1657,6 +1687,14 @@ async def knigge_page():
footer{background:#1a1a1a;color:#aaa;text-align:center;padding:1.5rem;font-size:.82rem;margin-top:2rem}
footer a{color:#C4843A}
</style>
<script type="application/ld+json">
{{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[
{{"@type":"Question","name":"Muss mein Hund in der Öffentlichkeit an der Leine?","acceptedAnswer":{{"@type":"Answer","text":"In Deutschland gilt Leinenpflicht in Innenstädten, Parks, auf Kinderspielplätzen und in Tiergehegen. In ländlichen Gebieten gibt es je nach Bundesland Ausnahmen. In der Brut- und Setzzeit (MärzJuli) besteht vielerorts erweiterte Leinenpflicht auch auf Feldwegen."}}}},
{{"@type":"Question","name":"Darf ich meinen Hund im öffentlichen Nahverkehr mitnehmen?","acceptedAnswer":{{"@type":"Answer","text":"Kleine Hunde in einer Transporttasche fahren in der Regel kostenlos. Größere Hunde benötigen oft einen Kinderfahrschein und müssen angeleint und mit Maulkorb reisen. Die Regeln variieren je nach Verkehrsbetrieb."}}}},
{{"@type":"Question","name":"Wie verhalte ich mich bei der Begegnung mit anderen Hunden?","acceptedAnswer":{{"@type":"Answer","text":"Beim Aufeinandertreffen von Hunden: immer den anderen Hundehalter fragen, ob eine Begegnung erwünscht ist. Leinenstress vermeiden, indem du Abstand hältst oder ausweichst. Einen ängstlichen oder aggressiven Hund nie bedrängen lassen."}}}},
{{"@type":"Question","name":"Muss ich den Kot meines Hundes beseitigen?","acceptedAnswer":{{"@type":"Answer","text":"Ja, in Deutschland ist die Beseitigung von Hundekot auf öffentlichen Flächen gesetzlich vorgeschrieben. Bei Verstoß drohen Bußgelder von 25300 €. Bitte immer Kotbeutel dabeihaben."}}}},
{{"@type":"Question","name":"Brauche ich eine Haftpflichtversicherung für meinen Hund?","acceptedAnswer":{{"@type":"Answer","text":"In den meisten deutschen Bundesländern ist eine Hundehaftpflichtversicherung Pflicht. Sie deckt Schäden ab, die dein Hund an Personen oder Sachen verursacht. Ausnahme: In Bayern ist sie freiwillig, wird aber dringend empfohlen."}}}}
]}}</script>
</head>
<body>
<header>
@ -1727,7 +1765,9 @@ async def help_page():
k for k in by_kat.keys() if k not in KAT_LABEL
]
import json as _json
sections_html = ""
faq_items = []
for kat in kat_order:
label = KAT_LABEL.get(kat, kat.replace("_", " ").title())
items = "".join(
@ -1736,6 +1776,13 @@ async def help_page():
for a in by_kat[kat]
)
sections_html += f'<section><h2>{_html.escape(label)}</h2>{items}</section>'
for a in by_kat[kat]:
faq_items.append({
"@type": "Question",
"name": a["frage"],
"acceptedAnswer": {"@type": "Answer", "text": a["antwort"]}
})
faq_json_ld = _json.dumps(faq_items, ensure_ascii=False)
html = f"""<!DOCTYPE html>
<html lang="de">
@ -1744,6 +1791,8 @@ async def help_page():
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hilfe & FAQ Ban Yaro</title>
<meta name="description" content="Antworten zu Ban Yaro und Ban Yaro Go: Installation, Standort, Account, Features.">
<link rel="canonical" href="https://banyaro.app/help">
<meta name="robots" content="index, follow">
<link rel="icon" href="/icons/icon-180.png">
<style>
:root {{
@ -1814,6 +1863,11 @@ async def help_page():
.contact p {{ margin: .25rem 0; color: var(--c-text-sec); }}
nav.top {{ margin-bottom: 1.5rem; }}
</style>
<script type="application/ld+json">{{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": {faq_json_ld}
}}</script>
</head>
<body>
<nav class="top"><a href="/"> banyaro.app</a></nav>
@ -1851,6 +1905,8 @@ async def konto_loeschen():
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Konto löschen Ban Yaro</title>
<link rel="canonical" href="https://banyaro.app/konto-loeschen">
<meta name="robots" content="noindex">
<link rel="stylesheet" href="/css/design-system.css">
<style>
body { font-family: var(--font-sans); background: var(--c-bg); color: var(--c-text);
@ -1900,6 +1956,7 @@ async def force_update():
from fastapi.responses import HTMLResponse
html = """<!DOCTYPE html><html><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Ban Yaro Update</title>
<style>body{font-family:sans-serif;display:flex;align-items:center;justify-content:center;
height:100vh;margin:0;background:#0f1623;color:#fff;flex-direction:column;gap:16px}
@ -1971,6 +2028,8 @@ async def partner_landing():
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ban Yaro Partner Werde Teil der ersten 100</title>
<meta name="description" content="Werde Ban Yaro Partner. Gib deiner Community exklusive Gründer-Lizenzen — nur 100 Plätze weltweit, nie wieder erhältlich.">
<link rel="canonical" href="https://banyaro.app/partner">
<meta name="robots" content="index, follow">
<meta property="og:title" content="Ban Yaro Partner">
<meta property="og:description" content="Gib deiner Community etwas Besonderes. 100 Gründer-Plätze. Exklusiv. Für immer.">
<meta property="og:image" content="https://banyaro.app/icons/icon-512.png">
@ -2256,6 +2315,7 @@ async def passport_share_page(token: str):
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Hundepass {dog['name']}</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
@ -2329,6 +2389,119 @@ async def passport_share_page(token: str):
return HTMLResponse(html)
# ------------------------------------------------------------------
# Wurfbörse /wurfboerse — SSR-Seite mit korrektem Canonical
# ------------------------------------------------------------------
@app.get("/wurfboerse")
async def wurfboerse_page():
from fastapi.responses import HTMLResponse
from database import db as _db
import html as _h
import json as _json
litters_html = ""
rows = []
ld_items = []
try:
with _db() as conn:
rows = conn.execute(
"""SELECT l.id, l.welpen_verfuegbar, l.preis_spanne, l.status,
bp.zwingername, bp.rasse,
wr.name AS rasse_name
FROM litters l
JOIN breeder_profiles bp ON bp.id = l.breeder_id
JOIN users u ON u.id = bp.user_id
LEFT JOIN wiki_rassen wr ON wr.id = bp.breed_id
WHERE l.sichtbar=1 AND u.rolle='breeder'
AND (l.sichtbar_bis IS NULL OR l.sichtbar_bis >= date('now'))
ORDER BY l.created_at DESC LIMIT 60"""
).fetchall()
for i, r in enumerate(rows, 1):
rasse_label = _h.escape(r["rasse_name"] or r["rasse"] or "")
zw = _h.escape(r["zwingername"] or "")
verfueg = r["welpen_verfuegbar"]
preis = _h.escape(r["preis_spanne"] or "")
status_map = {"geplant": "Geplant", "geboren": "Geboren", "verfuegbar": "Verfügbar"}
status_label = status_map.get(r["status"], r["status"])
litters_html += f"""<div class="litter-card">
<div class="litter-breed">{rasse_label or "Unbekannte Rasse"}</div>
<div class="litter-breeder">Züchter: <a href="/breeder/{_h.escape(r['zwingername'] or '')}">{zw}</a></div>
<div class="litter-meta">
<span class="badge">{status_label}</span>
{f'<span>{verfueg} Welpen verfügbar</span>' if verfueg else ''}
{f'<span>{preis}</span>' if preis else ''}
</div>
</div>"""
ld_items.append({
"@type": "ListItem",
"position": i,
"name": f"{r['rasse_name'] or r['rasse'] or 'Welpen'}{r['zwingername'] or 'Züchter'}",
"url": f"https://banyaro.app/breeder/{r['zwingername']}"
})
except Exception:
pass
count_text = f"{len(rows)} Würfe" if litters_html else "Aktuell keine Würfe eingetragen"
ld_json = _json.dumps({
"@context": "https://schema.org",
"@type": "ItemList",
"name": "Wurfbörse — Hundewelpen bei Ban Yaro",
"description": "Aktuelle Würfe von geprüften Züchtern auf Ban Yaro",
"url": "https://banyaro.app/wurfboerse",
"numberOfItems": len(rows),
"itemListElement": ld_items
}, ensure_ascii=False)
html = f"""<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wurfbörse Hundewelpen bei Ban Yaro</title>
<meta name="description" content="Seriöse Hundewelpen von geprüften Züchtern auf Ban Yaro. Jetzt Welpen finden, Züchter kontaktieren und Stammbaum einsehen.">
<link rel="canonical" href="https://banyaro.app/wurfboerse">
<meta name="robots" content="index, follow">
<meta property="og:title" content="Wurfbörse — Hundewelpen bei Ban Yaro">
<meta property="og:description" content="Seriöse Hundewelpen von geprüften Züchtern auf Ban Yaro.">
<meta property="og:image" content="https://banyaro.app/icons/icon-512.png">
<link rel="icon" href="/icons/icon-180.png">
<script type="application/ld+json">{ld_json}</script>
<style>
*,*::before,*::after{{box-sizing:border-box;margin:0;padding:0}}
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#fbfaf6;color:#1c1917;padding:0 0 4rem}}
.hero{{background:linear-gradient(135deg,#C4843A,#e8a857);color:#fff;padding:2.5rem 1.5rem;text-align:center}}
.hero h1{{font-size:clamp(1.6rem,4vw,2.2rem);font-weight:800;margin-bottom:.5rem}}
.hero p{{opacity:.9;font-size:1rem}}
.container{{max-width:720px;margin:2rem auto;padding:0 1rem}}
.count{{font-size:.85rem;color:#78716c;margin-bottom:1.5rem}}
.litter-card{{background:#fff;border-radius:12px;padding:1.2rem 1.4rem;margin-bottom:1rem;
box-shadow:0 1px 4px rgba(0,0,0,.08);border-left:4px solid #C4843A}}
.litter-breed{{font-size:1.05rem;font-weight:700;color:#1c1917;margin-bottom:.3rem}}
.litter-breeder{{font-size:.875rem;color:#57534e;margin-bottom:.5rem}}
.litter-breeder a{{color:#C4843A;text-decoration:none}}
.litter-meta{{display:flex;gap:.6rem;flex-wrap:wrap;font-size:.8rem;color:#78716c}}
.badge{{background:#fef3c7;color:#92400e;border-radius:100px;padding:.1rem .6rem;font-weight:600}}
.cta{{display:block;text-align:center;margin-top:2.5rem}}
.cta a{{background:#C4843A;color:#fff;border-radius:100px;padding:.85rem 2rem;
font-size:1rem;font-weight:700;text-decoration:none;display:inline-block}}
.empty{{text-align:center;padding:3rem 1rem;color:#78716c}}
</style>
</head>
<body>
<div class="hero">
<h1>Wurfbörse</h1>
<p>Hundewelpen von geprüften Züchtern</p>
</div>
<div class="container">
<p class="count">{count_text}</p>
{litters_html or '<div class="empty"><p>Aktuell keine Würfe eingetragen.<br>Schau bald wieder vorbei!</p></div>'}
<div class="cta"><a href="/">Zur Ban Yaro App</a></div>
</div>
</body>
</html>"""
return HTMLResponse(content=html, headers={"Cache-Control": "max-age=1800"})
# SPA Fallback — ALLE nicht-API-Routen gehen zur index.html
@app.get("/{full_path:path}")
async def spa_fallback(full_path: str):