From ec6ec22421e714ba34c30f4c13539b601d63a251 Mon Sep 17 00:00:00 2001 From: JUSN Date: Fri, 24 Jul 2026 13:06:03 +0200 Subject: [PATCH] Web-Demo: 2 Gratis-Analysen direkt auf der Webseite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker (/demo auf behoerdenklar-proxy): - Turnstile-Verifikation serverseitig (Bot-Mauer) - Analyse 1 anonym (1x pro IP/30 Tage), Analyse 2 nur mit E-Mail (SHA-256-Hash mit Pepper, dauerhaft — nie Klartext gespeichert) - IP-Gesamtlimit 3/30 Tage, globales Tagesbudget 100 (~2,50 EUR Deckel) - Wegwerf-Mail-Blockliste, 5-MB-Limit, CORS nur eigene Domain - Prompt/Schema identisch zur App (ohne Antwort-Generator) Webseite: - demo.html im Papier-Design: Einwilligung, Upload mit Client-Downscaling (2000px/JPEG wie App), Ampel-Logik, Ergebnis-Karten, Warteliste-CTA mit vorbelegter E-Mail, gestaffelter Ablauf 1. anonym / 2. mit E-Mail - Startseite verlinkt Demo im Hero - Datenschutz: neuer Abschnitt 8 Gratis-Demo (Einwilligung, Turnstile, E-Mail-Hash), Folgeabschnitte neu nummeriert; Sitemap + /demo Ende-zu-Ende getestet: echte Analyse (Jobcenter-Testbrief korrekt erklärt, Ampel erkennt fruehere Absagefrist), Schummler-Szenarien (Browser-Loeschen, doppelte E-Mail) serverseitig abgefangen. Dabei entdeckt: Anthropic-Key war ungueltig -> erneuert (App-Analyse funktionierte wieder). Co-Authored-By: Claude Fable 5 --- .claude/agents/.claude.agent.md | 9 + .claude/agents/claude.agent.md | 9 + proxy/src/index.ts | 288 +++++++++++++++++ proxy/wrangler.toml | 4 + webseite/datenschutz.html | 37 ++- webseite/demo.html | 550 ++++++++++++++++++++++++++++++++ webseite/index.html | 4 + webseite/sitemap.xml | 1 + 8 files changed, 899 insertions(+), 3 deletions(-) create mode 100644 .claude/agents/.claude.agent.md create mode 100644 .claude/agents/claude.agent.md create mode 100644 webseite/demo.html diff --git a/.claude/agents/.claude.agent.md b/.claude/agents/.claude.agent.md new file mode 100644 index 0000000..0a7e1ce --- /dev/null +++ b/.claude/agents/.claude.agent.md @@ -0,0 +1,9 @@ +--- +name: .claude +description: Describe what this custom agent does and when to use it. +tools: Read, Grep, Glob, Bash # specify the tools this agent can use. If not set, all enabled tools are allowed. +--- + + + +Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation. \ No newline at end of file diff --git a/.claude/agents/claude.agent.md b/.claude/agents/claude.agent.md new file mode 100644 index 0000000..dcb3831 --- /dev/null +++ b/.claude/agents/claude.agent.md @@ -0,0 +1,9 @@ +--- +name: claude +description: Describe what this custom agent does and when to use it. +tools: Read, Grep, Glob, Bash # specify the tools this agent can use. If not set, all enabled tools are allowed. +--- + + + +Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation. \ No newline at end of file diff --git a/proxy/src/index.ts b/proxy/src/index.ts index 1f5d181..edb863f 100644 --- a/proxy/src/index.ts +++ b/proxy/src/index.ts @@ -16,12 +16,18 @@ interface Env { /** Geheimnis: `wrangler secret put ANTHROPIC_API_KEY` */ ANTHROPIC_API_KEY: string; + /** Geheimnis: `wrangler secret put TURNSTILE_SECRET_KEY` (Web-Demo) */ + TURNSTILE_SECRET_KEY: string; /** KV-Namespace für das Rate-Limit (Binding in wrangler.toml) */ RATE_LIMIT: KVNamespace; /** Erlaubte Anfragen pro Gerät und Tag (in wrangler.toml unter [vars]) */ TAGES_LIMIT: string; /** Erlaubte Anfragen pro IP-Adresse und Tag (in wrangler.toml unter [vars]) */ TAGES_LIMIT_IP: string; + /** Web-Demo: hartes Tagesbudget über alle Nutzer (Kosten-Deckel) */ + DEMO_TAGES_BUDGET: string; + /** Web-Demo: max. Analysen pro IP innerhalb von 30 Tagen */ + DEMO_LIMIT_IP: string; } const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages'; @@ -44,8 +50,290 @@ function fehler(status: number, typ: string, meldung: string): Response { ); } +// ============================================================ +// Web-Demo (/demo): 2 Gratis-Analysen direkt auf der Webseite +// ============================================================ +// +// Schutzschichten (jede allein wäre umgehbar, zusammen dicht genug): +// 1. Turnstile-Token (Bot-Mauer, wird serverseitig verifiziert) +// 2. Analyse 1 anonym: max. 1 pro IP / 30 Tage +// 3. Analyse 2 nur mit E-Mail: pro E-Mail (Hash) genau 1 — dauerhaft +// 4. IP-Gesamtlimit über 30 Tage (bremst Verlauf-Löscher) +// 5. Globales Tagesbudget (harter Kosten-Deckel für den Betreiber) + +/** Nur die eigene Webseite darf den Demo-Endpunkt aufrufen (CORS). */ +const DEMO_ORIGIN_MUSTER = /^https:\/\/([a-z0-9-]+\.)?behoerdenklar\.pages\.dev$/; + +/** ~5 MB Datei entsprechen ~6,7 Mio. Base64-Zeichen. */ +const DEMO_MAX_BASE64 = 7_000_000; + +/** Bekannte Wegwerf-E-Mail-Domains (kleine, pragmatische Liste). */ +const WEGWERF_DOMAINS = new Set([ + 'mailinator.com', 'guerrillamail.com', '10minutemail.com', 'temp-mail.org', + 'tempmail.com', 'trashmail.com', 'yopmail.com', 'sharklasers.com', + 'getnada.com', 'dispostable.com', 'maildrop.cc', 'throwawaymail.com', +]); + +/** Identisch zur App (src/services/analyse.ts) — gleiche Qualität in der Demo. */ +const DEMO_SYSTEM_PROMPT = `Du bist ein Assistent, der deutschen Behördenbriefe für Privatpersonen verständlich macht. Die Nutzer sind Deutsche, die Amtsdeutsch schwer verstehen, oder Menschen mit Deutsch als Fremdsprache. + +Deine Aufgabe: +1. Lies den fotografierten/hochgeladenen Brief vollständig. +2. Erkläre ihn in einfacher Alltagssprache (Sprachniveau A2/B1): kurze Sätze, keine Schachtelsätze, keine unerklärten Fachbegriffe. +3. Extrahiere Fristen und Termine exakt. Datumsangaben immer als ISO-Format (JJJJ-MM-TT). Wenn du ein Datum nicht sicher lesen kannst, lass das Feld null — erfinde niemals Daten. +4. Erstelle eine konkrete Checkliste, was der Nutzer tun muss. +5. Sei sachlich und beruhigend, nicht alarmierend. + +Wichtig: Wenn das Bild kein Behördenbrief ist oder unlesbar ist, schreibe das klar in kernaussage und erklaerung_einfach und lasse frist/termin null.`; + +/** Schlanke Demo-Variante des Analyse-Schemas (ohne Antwort-Optionen — + * der Antwort-Generator bleibt der App vorbehalten). */ +const DEMO_SCHEMA = { + type: 'object', + additionalProperties: false, + required: ['brieftyp', 'absender', 'kernaussage', 'erklaerung_einfach', 'fachbegriffe', 'frist', 'termin', 'checkliste'], + properties: { + brieftyp: { type: 'string', description: 'Kurze Kategorie des Briefs, z. B. "Einladung Jobcenter".' }, + absender: { type: 'string', description: 'Die Behörde, die den Brief geschickt hat.' }, + kernaussage: { type: 'string', description: 'Antwort auf "Was will das Amt von mir?" in 2-3 kurzen Sätzen, Sprachniveau A2.' }, + erklaerung_einfach: { type: 'string', description: 'Erklärung des gesamten Briefs in einfacher Alltagssprache (A2/B1). Kurze Sätze.' }, + fachbegriffe: { + type: 'array', + description: 'Fachbegriffe aus dem Brief mit einfacher Erklärung.', + items: { + type: 'object', additionalProperties: false, required: ['begriff', 'erklaerung'], + properties: { begriff: { type: 'string' }, erklaerung: { type: 'string' } }, + }, + }, + frist: { + description: 'Frist, bis wann reagiert werden muss. null wenn keine Frist im Brief steht.', + anyOf: [ + { + type: 'object', additionalProperties: false, required: ['datum', 'aktion'], + properties: { datum: { type: 'string', description: 'ISO JJJJ-MM-TT' }, aktion: { type: 'string' } }, + }, + { type: 'null' }, + ], + }, + termin: { + description: 'Persönlicher Termin. null wenn keiner im Brief steht.', + anyOf: [ + { + type: 'object', additionalProperties: false, required: ['datum', 'uhrzeit', 'ort'], + properties: { + datum: { type: 'string', description: 'ISO JJJJ-MM-TT' }, + uhrzeit: { anyOf: [{ type: 'string' }, { type: 'null' }] }, + ort: { anyOf: [{ type: 'string' }, { type: 'null' }] }, + }, + }, + { type: 'null' }, + ], + }, + checkliste: { type: 'array', description: 'To-do-Liste. Leer wenn nichts zu tun ist.', items: { type: 'string' } }, + }, +}; + +function demoCorsHeaders(origin: string | null): Record { + const erlaubt = origin && DEMO_ORIGIN_MUSTER.test(origin) ? origin : 'https://behoerdenklar.pages.dev'; + return { + 'access-control-allow-origin': erlaubt, + 'access-control-allow-methods': 'POST, OPTIONS', + 'access-control-allow-headers': 'content-type', + 'access-control-max-age': '86400', + }; +} + +/** Fehler für die Demo-Seite: { code, meldung } + CORS. */ +function demoFehler(status: number, code: string, meldung: string, origin: string | null): Response { + return Response.json({ code, meldung }, { status, headers: demoCorsHeaders(origin) }); +} + +/** SHA-256-Hash der E-Mail (mit Pepper) — es wird nie Klartext gespeichert. */ +async function emailHash(email: string, pepper: string): Promise { + const daten = new TextEncoder().encode(`bk-demo|${pepper}|${email}`); + const digest = await crypto.subtle.digest('SHA-256', daten); + return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +async function demoHandler(request: Request, env: Env): Promise { + const origin = request.headers.get('origin'); + if (request.method === 'OPTIONS') { + return new Response(null, { status: 204, headers: demoCorsHeaders(origin) }); + } + if (request.method !== 'POST') { + return demoFehler(405, 'methode', 'Nur POST erlaubt.', origin); + } + if (!origin || !DEMO_ORIGIN_MUSTER.test(origin)) { + return demoFehler(403, 'origin', 'Aufruf nur von der BehördenKlar-Webseite erlaubt.', origin); + } + + let body: { bild?: string; mimeType?: string; turnstileToken?: string; email?: string }; + try { + body = await request.json(); + } catch { + return demoFehler(400, 'json', 'Ungültige Anfrage.', origin); + } + + const { bild, mimeType, turnstileToken } = body; + const email = typeof body.email === 'string' ? body.email.trim().toLowerCase() : null; + + if (typeof bild !== 'string' || bild.length === 0 || bild.length > DEMO_MAX_BASE64) { + return demoFehler(400, 'datei', 'Die Datei fehlt oder ist zu groß (max. 5 MB).', origin); + } + if (mimeType !== 'image/jpeg' && mimeType !== 'image/png' && mimeType !== 'application/pdf') { + return demoFehler(400, 'datei', 'Nur Fotos (JPG/PNG) oder PDF sind möglich.', origin); + } + if (typeof turnstileToken !== 'string' || !turnstileToken) { + return demoFehler(403, 'turnstile', 'Sicherheitsprüfung fehlt. Bitte Seite neu laden.', origin); + } + + // Schicht 1: Turnstile serverseitig verifizieren (niemals nur im Browser!) + const ip = request.headers.get('cf-connecting-ip') ?? 'unbekannt'; + const pruefung = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ secret: env.TURNSTILE_SECRET_KEY, response: turnstileToken, remoteip: ip }), + }); + const pruefungJson = (await pruefung.json()) as { success?: boolean }; + if (!pruefungJson.success) { + return demoFehler(403, 'turnstile', 'Sicherheitsprüfung fehlgeschlagen. Bitte Seite neu laden und erneut versuchen.', origin); + } + + // E-Mail prüfen (nur für die 2. Analyse nötig) + let mailKey: string | null = null; + if (email) { + if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) { + return demoFehler(400, 'email', 'Bitte geben Sie eine gültige E-Mail-Adresse ein.', origin); + } + const domain = email.split('@')[1]; + if (WEGWERF_DOMAINS.has(domain)) { + return demoFehler(400, 'email', 'Wegwerf-E-Mail-Adressen sind nicht möglich. Bitte nutzen Sie Ihre normale Adresse.', origin); + } + mailKey = `demo:mail:${await emailHash(email, env.TURNSTILE_SECRET_KEY)}`; + } + + // Schichten 2-5: Limits lesen + const heute = new Date().toISOString().slice(0, 10); + const tagKey = `demo:tag:${heute}`; + const ipKey = `demo:ip:${ip}`; + const anonKey = `demo:anon:${ip}`; + const budget = parseInt(env.DEMO_TAGES_BUDGET || '100', 10); + const ipLimit = parseInt(env.DEMO_LIMIT_IP || '3', 10); + + const [tagWert, ipWert, anonWert, mailWert] = await Promise.all([ + env.RATE_LIMIT.get(tagKey), + env.RATE_LIMIT.get(ipKey), + env.RATE_LIMIT.get(anonKey), + mailKey ? env.RATE_LIMIT.get(mailKey) : Promise.resolve(null), + ]); + const tagZahl = parseInt(tagWert ?? '0', 10); + const ipZahl = parseInt(ipWert ?? '0', 10); + const anonZahl = parseInt(anonWert ?? '0', 10); + + // Schicht 5: globales Tagesbudget (Kosten-Deckel) + if (tagZahl >= budget) { + return demoFehler(429, 'budget', 'Die Gratis-Demo ist für heute ausgebucht. Kommen Sie morgen wieder — oder tragen Sie sich in die Warteliste ein.', origin); + } + // Schicht 4: IP-Gesamtlimit (30 Tage) + if (ipZahl >= ipLimit) { + return demoFehler(429, 'aufgebraucht', 'Die Gratis-Analysen für diesen Anschluss sind aufgebraucht. In der App gibt es 3 weitere gratis.', origin); + } + // Schicht 2/3: ohne E-Mail nur 1x, pro E-Mail nur 1x (dauerhaft) + if (!email && anonZahl >= 1) { + return demoFehler(403, 'email_noetig', 'Ihre erste Gratis-Analyse ist verbraucht. Für die zweite geben Sie bitte Ihre E-Mail-Adresse ein.', origin); + } + if (mailKey && mailWert !== null) { + return demoFehler(429, 'aufgebraucht', 'Mit dieser E-Mail-Adresse wurde die Gratis-Analyse schon genutzt. In der App gibt es 3 weitere gratis.', origin); + } + + // KI-Analyse — identischer Aufbau wie in der App + const dateiBlock = + mimeType === 'application/pdf' + ? { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: bild } } + : { type: 'image', source: { type: 'base64', media_type: mimeType, data: bild } }; + + const antwort = await fetch(ANTHROPIC_URL, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-api-key': env.ANTHROPIC_API_KEY, + 'anthropic-version': ANTHROPIC_VERSION, + }, + body: JSON.stringify({ + model: 'claude-sonnet-5', + max_tokens: MAX_TOKENS_OBERGRENZE, + system: DEMO_SYSTEM_PROMPT, + output_config: { format: { type: 'json_schema', schema: DEMO_SCHEMA } }, + messages: [ + { + role: 'user', + content: [ + dateiBlock, + { type: 'text', text: `Analysiere diesen Behördenbrief. Heute ist der ${heute} (wichtig für relative Datumsangaben wie "innerhalb von 14 Tagen").` }, + ], + }, + ], + }), + }); + + if (!antwort.ok) { + const status = antwort.status; + // Fehlerdetails ins Worker-Log (wrangler tail) — hilft bei der Diagnose + console.log('Demo-KI-Fehler', status, (await antwort.text()).slice(0, 500)); + if (status === 429 || status >= 500) { + return demoFehler(503, 'ki', 'Der KI-Dienst ist gerade ausgelastet. Bitte versuchen Sie es in einer Minute erneut.', origin); + } + return demoFehler(502, 'ki', 'Die Analyse ist fehlgeschlagen. Bitte versuchen Sie es mit einem neuen, gut beleuchteten Foto.', origin); + } + + const daten = (await antwort.json()) as { + stop_reason?: string; + content?: { type: string; text?: string }[]; + }; + + // Erst NACH erfolgreichem KI-Aufruf zählen (Fehler kosten kein Kontingent) + const MONAT_TTL = 60 * 60 * 24 * 30; + const schreiben: Promise[] = [ + env.RATE_LIMIT.put(tagKey, String(tagZahl + 1), { expirationTtl: 60 * 60 * 48 }), + env.RATE_LIMIT.put(ipKey, String(ipZahl + 1), { expirationTtl: MONAT_TTL }), + ]; + if (!email) { + schreiben.push(env.RATE_LIMIT.put(anonKey, String(anonZahl + 1), { expirationTtl: MONAT_TTL })); + } + if (mailKey) { + // bewusst OHNE TTL: „pro E-Mail für immer nur 1" — es liegt nur der Hash + schreiben.push(env.RATE_LIMIT.put(mailKey, heute)); + } + await Promise.all(schreiben); + + if (daten.stop_reason === 'refusal') { + return demoFehler(422, 'inhalt', 'Die KI konnte diesen Inhalt nicht verarbeiten. Bitte prüfen Sie, ob das Foto wirklich einen Behördenbrief zeigt.', origin); + } + const textBlock = [...(daten.content ?? [])].reverse().find((b) => b.type === 'text'); + if (!textBlock?.text) { + return demoFehler(502, 'ki', 'Die KI hat keine verwertbare Antwort geliefert. Bitte erneut versuchen.', origin); + } + + let analyse: unknown; + try { + analyse = JSON.parse(textBlock.text); + } catch { + return demoFehler(502, 'ki', 'Die Antwort war unlesbar. Bitte erneut versuchen.', origin); + } + + return Response.json( + { analyse, versuch: email ? 2 : 1 }, + { headers: demoCorsHeaders(origin) } + ); +} + export default { async fetch(request: Request, env: Env): Promise { + // Web-Demo hat einen eigenen Pfad mit eigenen Regeln + if (new URL(request.url).pathname === '/demo') { + return demoHandler(request, env); + } + if (request.method !== 'POST') { return fehler(405, 'invalid_request_error', 'Nur POST erlaubt.'); } diff --git a/proxy/wrangler.toml b/proxy/wrangler.toml index 5b9a7f1..7df1ae1 100644 --- a/proxy/wrangler.toml +++ b/proxy/wrangler.toml @@ -8,6 +8,10 @@ compatibility_date = "2026-07-01" [vars] TAGES_LIMIT = "20" TAGES_LIMIT_IP = "100" +# Web-Demo (/demo): hartes Tagesbudget über alle Nutzer (Kosten-Deckel, +# 100 Analysen ≈ 2,50 €/Tag Maximalschaden) + IP-Limit über 30 Tage +DEMO_TAGES_BUDGET = "100" +DEMO_LIMIT_IP = "3" [[kv_namespaces]] binding = "RATE_LIMIT" diff --git a/webseite/datenschutz.html b/webseite/datenschutz.html index 7fb5bc3..47ddc17 100644 --- a/webseite/datenschutz.html +++ b/webseite/datenschutz.html @@ -145,7 +145,38 @@ Ihre Adresse wird dann gelöscht.

-

8. Reichweitenmessung (Webseite)

+

8. Gratis-Demo (Webseite)

+

+ Auf unserer Webseite können Sie zwei Brief-Analysen kostenlos + ausprobieren, ohne die App zu installieren. Dabei gilt: +

+
    +
  • + Ihr Brief-Foto wird — nur mit Ihrer Einwilligung + (Häkchen vor dem Hochladen, Art. 6 Abs. 1 lit. a, Art. 9 Abs. 2 + lit. a DSGVO) — über unseren Server (Cloudflare) an Anthropic (USA) + zur Analyse geschickt. Es wird auf unserem Server nicht gespeichert; + bei Anthropic wird es nach spätestens 30 Tagen gelöscht (wie in + Abschnitt 3a beschrieben). Das Ergebnis sehen nur Sie in Ihrem + Browser; wir speichern es nicht. +
  • +
  • + Zum Schutz vor Missbrauch (Art. 6 Abs. 1 lit. f + DSGVO) prüfen wir Anfragen mit Cloudflare Turnstile (Bot-Erkennung + durch Cloudflare, Inc., USA), zählen Analysen pro IP-Adresse + (Speicherung max. 30 Tage) und führen ein tägliches Gesamtlimit. +
  • +
  • + Für die zweite Gratis-Analyse geben Sie Ihre + E-Mail-Adresse an. Wir speichern sie ausschließlich als + nicht umkehrbare Prüfsumme (Hash), um das Kontingent von zwei + Analysen durchzusetzen — die Adresse selbst wird nicht gespeichert + und nicht für E-Mails verwendet. In die Warteliste (Abschnitt 7) + kommen Sie nur, wenn Sie sich dort separat eintragen. +
  • +
+ +

9. Reichweitenmessung (Webseite)

Auf dieser Webseite nutzen wir Cloudflare Web Analytics (Cloudflare, Inc., USA) — eine datenschutzfreundliche Besucherstatistik @@ -157,14 +188,14 @@ DSGVO).

-

9. Keine Rechtsberatung

+

10. Keine Rechtsberatung

BehördenKlar erklärt Briefe verständlich, ersetzt aber keine Rechtsberatung. Bei rechtlich wichtigen Entscheidungen wenden Sie sich an eine Beratungsstelle oder eine Anwältin/einen Anwalt.

-

10. Änderungen

+

11. Änderungen

Wir passen diese Datenschutzerklärung an, wenn sich die App oder die Rechtslage ändert. Die aktuelle Fassung finden Sie immer auf dieser diff --git a/webseite/demo.html b/webseite/demo.html new file mode 100644 index 0000000..6b94143 --- /dev/null +++ b/webseite/demo.html @@ -0,0 +1,550 @@ + + + + + + Gratis-Demo: Behördenbrief online erklären lassen | BehördenKlar + + + + + + + + + + + + + + + + +

+ +
+ 📬 BehördenKlar + Formular D-01 · Gratis-Demo +
+ +
+

Betreff

+

Probieren Sie es aus —
mit Ihrem eigenen Brief.

+

+ Kein Konto. Keine App-Installation. Laden Sie ein Foto Ihres + Behördenbriefs hoch — in etwa einer Minute lesen Sie ihn in einfacher + Sprache. 2 Analysen gratis. +

+
+ +
+

§ 1 Brief hochladen

+
+

Antrag auf Klartext · 2 Gratis-Analysen

+ +
+ + + + + + + +
+ +

+ Geht mit Foto (JPG/PNG) oder PDF, max. 5 MB. Tipp: gutes Licht, + Brief von oben, ganzer Text im Bild. +

+
+ +
+ +

Ihr Brief wird analysiert…

+

Das dauert bis zu einer Minute. Bitte lassen Sie die Seite geöffnet.

+
+ +
+
+
+ +
+

§ 2 Ihr Brief in Klartext

+ +
+ Brief +

+ +
+ +
+ Einfach erklärt +

+
+ + + + + +

+ ⚖️ BehördenKlar erklärt Briefe, ersetzt aber keine Rechtsberatung. Bei + wichtigen Entscheidungen hilft eine Beratungsstelle oder ein Anwalt. +

+
+ + + + + +
+ + + + + + + diff --git a/webseite/index.html b/webseite/index.html index 342d204..025758c 100644 --- a/webseite/index.html +++ b/webseite/index.html @@ -322,6 +322,10 @@ Jetzt anmelden → Bald im App Store +

+ Erst ausprobieren? Testen + Sie es direkt im Browser — mit Ihrem eigenen Brief, 2 Analysen gratis. +

diff --git a/webseite/sitemap.xml b/webseite/sitemap.xml index 0f8ecc0..95fc61c 100644 --- a/webseite/sitemap.xml +++ b/webseite/sitemap.xml @@ -7,4 +7,5 @@ https://behoerdenklar.pages.dev/uk2026-07-23 https://behoerdenklar.pages.dev/ru2026-07-23 https://behoerdenklar.pages.dev/pl2026-07-23 + https://behoerdenklar.pages.dev/demo2026-07-24