diff --git a/.gitignore b/.gitignore index 28e4c9f..cbcf3ae 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ __pycache__/ /icons/ .claude/worktrees/ Ban Yaro - Google Play package/ +/unsplash/ diff --git a/MARKETING.md b/MARKETING.md new file mode 100644 index 0000000..d232fe2 --- /dev/null +++ b/MARKETING.md @@ -0,0 +1,72 @@ +# đŸ Ban Yaro â Marketing-Cockpit + +**Single Source of Truth fĂŒrs Marketing.** Vor jeder Aktion hier prĂŒfen, danach updaten â so wird nichts doppelt gemacht, vergessen oder ĂŒbersehen. Pflege: RenĂ© + Claude. + +_Stand: 2026-06-03_ + +> Diese Datei = Planung & Checkliste. FĂŒr **Live-Daten** (User-Meilenstein, Kanal-Tracking) lohnt zusĂ€tzlich ein Marketing-Tab im **Admin-Bereich** â siehe âAusbau" unten. + +## đ Kanal-Ăberblick +| Kanal / Bereich | Status | NĂ€chster Schritt | +|---|---|---| +| Flyer Print | đą 1000 gedruckt (03.06.) | lokal verteilen | +| Flyer Digital | đĄ Idee | Doppelseiten-PDF + Empfehlungs-QR | +| Lokal (Ebersberg) | ⏠offen | TierĂ€rzte, Hundeschulen, FutterlĂ€den, Tierheim | +| Online-Communities | ⏠offen | FB-Gruppen Landkreis EBE + nebenan.de | +| Empfehlung / Referral | đĄ Infra da (`referral_code`) | Empfehlungs-QR + Tracking sichtbar machen | +| Influencer | đĄ 2 Runden (Mai), kaum Resonanz | Runde 3 erst ab ~50 aktiven Usern | +| Presse / Blogs | đĄ 1 Runde, kaum Resonanz | keine Massenwelle; Nische zuerst | +| Verzeichnisse / Listings | ⏠offen | Product Hunt, PWA-Dirs, Google Business EBE | +| SEO / KI-Auffindbarkeit | đĄ technisch optimiert | Backlinks (Blog-Testberichte) | +| Landing Page | đĄ Redesign-Briefing da | 3 Einstiege, Outcomes statt Features | +| App Store (iOS) | đą in Review (1.0 (3), 03.06.) | Freigabe abwarten | +| Play Store (Android) | đŽ ON HOLD | 12 Closed-Tester / 14 Tage fehlen | +| Merch / NFC-Halsband | đĄ recherchiert | 20 Tags fĂŒr Beta (~33 âŹ) | + +Legende: đą lĂ€uft/erledigt · đĄ angefangen · ⏠offen · đĄ Idee · đŽ blockiert + +## âł Gates / Trigger (nicht zu frĂŒh starten) +- **Influencer & Presse Runde 3** erst ab **~50 aktiven Usern** â vorher zu frĂŒh (GroĂredaktionen fragen zuerst nach Zahlen). â Bei jeder Session aktuelle User-Zahl checken. +- iOS-App ist nativ gebaut & in Review â **ĂŒberholt** die alte âiOS erst ab 10k via Rork/PWABuilder"-Strategie. + +## đ Backlog (konkret als NĂ€chstes) +- [ ] **Flyer lokal verteilen (Ebersberg)** â TierĂ€rzte (Wartezimmer), Hundeschulen/Welpengruppen, FutterlĂ€den, Hundesalons, Tierheim, Hundewiesen-AushĂ€nge, hundefreundliche CafĂ©s. Persönlich erklĂ€ren; AufhĂ€nger: Giftköder-Radar + âDaten in Deutschland". **Lokal bĂŒndeln, nicht streuen** (Community-Dichte fĂŒr Gassi-Treffen/Giftköder). +- [ ] **Digitaler Doppelseiten-Flyer (PDF)** mit **Empfehlungs-QR** fĂŒr Online-/Gruppen-Verteilung. Quelle: `promotion/flyer_a5_*.html`. _Offene Frage: generischer `?ref=empfehlung`-Link vs. pro-User `referral_code`._ +- [ ] **Lokale FB-Gruppen + nebenan.de** â Flyer-Foto + Link posten. +- [ ] **Verzeichnisse** â Product Hunt, progressivewebappstore.com, pwafire.org/directory, Google Business (Ebersberg). +- [ ] **Landing-Page-Redesign** nach Briefing (3 Zielgruppen-Einstiege Hundebesitzer/ZĂŒchter/WelpenkĂ€ufer, Outcomes statt Features, ZĂŒchter-SaaS prominent, Datenschutz als Argument, GrĂŒnder-Story + Foto). +- [ ] **Messung einbauen** â âWie hast du von uns gehört?" im Onboarding + QR-refs pro Kanal. + +## â Erledigt +- [x] 1000 Flyer A5 (zweiseitig) gedruckt â 03.06.2026 +- [x] iOS-App nativ gebaut + eingereicht (1.0 (3), in Review) â Details im Repo `banyaro-ios` +- [x] Influencer-Outreach Runde 1 (5) + Runde 2 (13) â Mai 2026 +- [x] SEO-Grundlagen (llms.txt, Landing About-Section) + +## đ Messung â was bringt wirklich Nutzer? +- **Onboarding-Frage âWie hast du von uns gehört?"** (1 Klick) = billigste & wichtigste Kontrolle. _(noch einzubauen)_ +- **QR-refs pro Ort/Kanal** (z. B. `banyaro.app/?ref=tierarzt-grafing`) â ab nĂ€chster Flyer-Charge. +- **`referral_code`** (in DB, `routes/auth.py`) â Empfehlungen zĂ€hlbar. +- Aktive User aktuell: _[aus Admin eintragen]_ + +## đ Details je Kanal + +### Influencer +2 Runden im Mai gesendet (`partner@banyaro.app`; DKIM/SPF/DMARC aktiv), **kaum Resonanz** â zu frĂŒh (wenige User), teils falsche Adressen (z. B. GEO â richtig `chefredaktion@geo.de`). +**Runde 3:** keine Massenwelle ohne PR-Agentur; **Hundeschulen/-trainer zuerst** (kleines Netzwerk, empfehlen aktiv Tools, Trainingsfeature ist stark), persönliche Mails, AufhĂ€nger = neue Features + echte Nutzerzahlen. +â **Wer schon kontaktiert wurde:** AI-Memory `project_influencer_outreach` (Runde 1: verpinscht, missyminzi, wanderlust_samoyed, viviundholly, doguniversity, dogstv; Runde 2: nami.and.tommy, brina.explores, heimatherzen, pfotentick, flummis_diary, verwolft, wildwildwilli, knutini_, ninja.vom.wolfstor, pupsonality, osman_theparson, babybearyuki, dogswiss). **Vor neuer Runde dort prĂŒfen.** + +### Play Store (Android TWA) +PWABuilder-Paket fertig (`Ban Yaro - Google Play package/`, Package `app.banyaro.twa`). **BLOCKER:** Google verlangt 12 Closed-Tester ĂŒber 14 Tage â Tester fehlen (Engpass, nicht die Technik). assetlinks.json + Play-Console-Eintrag stehen bereit. Nicht priorisieren bis Tester da. + +### Merch / NFC-Halsband +Tag recherchiert: **HID Laundry Tag 16 mm** (shopnfc, SKU RE-ICO2-16, ~1 âŹ/Stk ab 500), fĂŒr `banyaro.app/hund/{id}`. Beta: 20 Stk (~33 âŹ) an erste Nutzer. + +### Flyer +Print: A5 zweiseitig, Quelle `promotion/flyer_a5_allgemein.html` + `flyer_a5_rueckseite.html`, QR â banyaro.app. Vorderseite = alle Hundebesitzer, RĂŒckseite stark ZĂŒchter-fokussiert. + +## đ Ausbau: Live-Tool im Admin-Bereich (optional) +Diese Datei deckt Planung/Checkliste ab (Claude pflegt sie). Der **Admin-Bereich** lohnt sich fĂŒr die Teile mit echten Daten: +- **User-Meilenstein-Anzeige** (aktive User) â blendet automatisch den âOutreach Runde 3"-Hinweis ein, sobald ~50 erreicht. +- **Kanal-Tracking**: Auswertung âWie gehört?" + QR-ref-ZĂ€hler + `referral_code`-Statistik. +- Optional: das Kanal-Board (Status/Backlog) als editierbare Admin-Seite. diff --git a/VERSION b/VERSION index 41edc23..0948691 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1141 \ No newline at end of file +1155 \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index df5124d..e954c83 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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( + '', + '' + ) + 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( + '', + f'' + ).replace( + '
Hundewelpen von geprĂŒften ZĂŒchtern
+{count_text}
+ {litters_html or 'Aktuell keine WĂŒrfe eingetragen.
Schau bald wieder vorbei!
Keine Orte in der NĂ€he gefunden.
'; - } else { - sugEl.innerHTML = suggestions.map(s => ` - `).join(''); - sugEl.querySelectorAll('.diary-location-suggestion').forEach(el => { - el.addEventListener('click', () => _setName(el.dataset.name)); - }); - } - sugEl.style.display = ''; - } catch (err) { - UI.toast.error(err?.message?.includes('GPS') || lat == null - ? 'GPS nicht verfĂŒgbar.' : 'Ortssuche fehlgeschlagen.'); - } finally { - UI.setLoading(btn, false); - } - } - - document.getElementById('diary-location-btn')?.addEventListener('click', _showSuggestions); + if (_locLat != null) _diaryPicker.setValue(_locLat, _locLon, _locName); + }, 50); document.getElementById('diary-form-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ diff --git a/backend/static/js/pages/forum.js b/backend/static/js/pages/forum.js index e2dfe19..512ed6d 100644 --- a/backend/static/js/pages/forum.js +++ b/backend/static/js/pages/forum.js @@ -640,6 +640,17 @@ function _fmtDate(iso) { } catch (err) { UI.toast.error(err.message); } }); + // Liker-Liste anzeigen (Klick auf die Zahl) + const _thLikeCount = document.getElementById('thread-like-count'); + if (_thLikeCount) { + _thLikeCount.style.cursor = 'pointer'; + _thLikeCount.title = 'Wer hat geliked?'; + _thLikeCount.addEventListener('click', e => { + e.stopPropagation(); + if ((thread.likes || 0) > 0) _showLikers('thread', thread.id); + }); + } + // Report thread document.getElementById('thread-report-btn')?.addEventListener('click', () => { _showReportForm('thread', thread.id); @@ -812,9 +823,9 @@ function _fmtDate(iso) { // Like container.querySelectorAll('.forum-post-like:not([data-bound])').forEach(btn => { btn.dataset.bound = '1'; + const postId = parseInt(btn.dataset.postId); btn.addEventListener('click', async () => { if (!uid) { UI.toast.info('Bitte erst anmelden.'); return; } - const postId = parseInt(btn.dataset.postId); try { const res = await API.forum.like('post', postId); btn.classList.toggle('active', res.liked); @@ -822,6 +833,16 @@ function _fmtDate(iso) { if (countEl) countEl.textContent = res.count; } catch (err) { UI.toast.error(err.message); } }); + // Klick auf die Zahl â Liker-Liste + const countEl = btn.querySelector('.forum-post-like-count'); + if (countEl) { + countEl.style.cursor = 'pointer'; + countEl.title = 'Wer hat geliked?'; + countEl.addEventListener('click', e => { + e.stopPropagation(); + if (parseInt(countEl.textContent) > 0) _showLikers('post', postId); + }); + } }); // Report @@ -874,6 +895,28 @@ function _fmtDate(iso) { }); } + // ---------------------------------------------------------- + // Liker-Liste â wer hat geliked? + // ---------------------------------------------------------- + async function _showLikers(targetType, targetId) { + try { + const likers = await API.forum.likers(targetType, targetId); + if (!likers.length) { UI.toast.info('Noch keine Likes.'); return; } + const rows = likers.map(l => ` +