⚡ Blitzsicht · Built to be Found

Website 2026.
Das vollständige Handbuch.

Modulare Snippet-Library für Entwickler:innen und Agenturen, organisiert nach 8 Lifecycle-Phasen — von DNS-Setup bis Sustainability. 35 Layer, ~180 Snippets, Anti-Patterns markiert. Plattform-agnostisch. Stand 2026 inkl. AI-Search und BFSG. → Quick & Dirty Cheatsheet

35
Layer
~180
Snippets
9
Lifecycle-Phasen
∞
Anti-Patterns

⚡ Quickstart — wenn du nur 1 Stunde hast

Die fünf Layer, mit denen jede Site 2026 startet

  1. DNS & Email Domain, Records, SPF/DKIM/DMARC. Ohne läuft nichts — auch keine Mails an Kunden.
  2. Security Headers HTTPS, HSTS, CSP. Verhindert Browser-Warnungen, XSS und Mixed-Content-Probleme.
  3. JSON-LD Schema Was Google AI Overviews und ChatGPT-Search von dir sehen — Org, sameAs zu Wikidata.
  4. Core Web Vitals LCP < 2.5s, INP < 200ms, CLS < 0.1. Direkter Google-Ranking-Faktor.
  5. Forms & Anti-Spam Honeypot + Cloudflare Turnstile. Conversion-Mechanik ohne DSGVO-Kopfschmerzen.
01

DNS & Email — Records, SPF, DKIM, DMARC, DNSSEC

Foundation-Layer, der gerne übersehen wird. Falsche oder fehlende DNS-Records killen E-Mail-Zustellbarkeit (DKIM/DMARC fehlt → landet im Spam), kostbaren Trust (kein DNSSEC → Phishing-anfällig) und Performance (kein Anycast). Pflicht: SPF + DKIM + DMARC. Empfehlung: DNSSEC + CAA.

→ Layer 33 · Email-Deliverability — Reputation, Warmup & Bulk-Sender-Anforderungen vertiefend

DNS-Records — Standard-Set Pflicht
# Apex (Root) → IPv4 + IPv6
example.com.          A      76.76.21.21          # IP des Hosters
example.com.          AAAA   2606:4700:...:1234   # IPv6 — 2026 Pflicht

# www → CNAME auf Apex (oder ALIAS bei Cloudflare/Route53)
www.example.com.      CNAME  example.com.

# Subdomains für separate Services
api.example.com.      A      198.51.100.42
cdn.example.com.      CNAME  d1234.cloudfront.net.
mail.example.com.     A      198.51.100.20      # MX-Hostname

# MX — wer empfängt Mail (niedrigere Priorität = höhere Präferenz)
example.com.          MX     10 mail.example.com.
example.com.          MX     20 fallback.example.com.

# CAA — wer DARF SSL-Zertifikate ausstellen
example.com.          CAA    0 issue "letsencrypt.org"
example.com.          CAA    0 issue "sectigo.com"
example.com.          CAA    0 iodef "mailto:security@example.com"
WarumAAAA (IPv6) ist 2026 nicht mehr optional — Mobile-Provider routen IPv6-only, fehlt der Record gibt's Latenz-Penalties via NAT64. CAA verhindert, dass jemand mit kompromittiertem Domain-Zugriff via x-beliebiger CA Zertifikate ausstellt — limitiert auf deine erlaubten CAs. TTL: bei stabilen Records 3600s+, vor Migrationen 60s.
AntiCNAME auf Apex. RFC verbietet CNAME für die Root-Domain — Mail-Server suchen dort SOA und finden nichts. Lösung: A/AAAA direkt, oder CNAME-Flattening / ALIAS (Cloudflare, Route53, DNSimple). Anti: www als A-Record statt CNAME — bei IP-Wechsel zwei Records pflegen. Anti: TTL-Werte wie 86400 vor einer geplanten Migration — du bist 24h handlungsunfähig.
SPF — Sender Policy Framework Pflicht
# SPF: wer DARF Mail "von example.com" senden
# EIN einziger TXT-Record am Apex, mehrere Mechanismen

example.com.  TXT  "v=spf1 include:_spf.google.com include:spf.mailgun.org ~all"

# Mechanismen:
#   include:domain    → externe SPF-Records einbinden (Google, M365, Mailgun)
#   ip4:1.2.3.4       → spezifische IP erlauben
#   a, mx             → erlaube Hosts aus eigenen A/MX-Records
#   ~all              → SoftFail (Standard, sicher)
#   -all              → HardFail (strikt, erst wenn alles getestet)
#   +all              → erlaubt ALLES — NIEMALS verwenden

# Validieren:
dig +short example.com TXT | grep spf1
curl https://www.kitterman.com/spf/validate.html
AntiMehr als ein SPF-Record. RFC erlaubt nur einen — zwei Records → SPF wird komplett ignoriert, alle Mails landen im Spam. Anti: SPF-Lookup-Limit überschreiten (max 10 DNS-Lookups). Jedes include: zählt rekursiv. Mit M365 + Mailgun + SendGrid + Sales-Tool bist du schnell drüber → Mails fliegen. Lösung: SPF-Flattening-Tools oder Reduzierung der Sender. Anti: +all oder ?all — erlaubt jeder Welt, in deinem Namen zu spammen.
DKIM — DomainKeys Identified Mail Pflicht
# DKIM: kryptographische Signatur deiner Mails
# Public Key liegt im DNS, Mail-Server signiert mit Private Key

# Selector wird vom Mail-Provider vorgegeben (z.B. "google", "mg", "s1")
selector1._domainkey.example.com.  TXT
  "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC..."

# Pro Mail-Provider EIGENER Selector (keine Konflikte):
#   google._domainkey      → Google Workspace
#   selector1._domainkey   → Microsoft 365
#   mailgun._domainkey     → Mailgun
#   k1._domainkey          → Mailchimp

# Validieren:
dig +short google._domainkey.example.com TXT
curl "https://mxtoolbox.com/DKIMLookup.aspx"
WarumOhne DKIM signiert keiner deine Mails — Empfänger können nicht prüfen, ob die Mail wirklich von dir stammt. Gmail, Outlook, Yahoo werten DKIM-fail seit 2024 als Spam-Signal mit hoher Gewichtung. Bei Bulk-Sendern (≥5000 Mails/Tag an Gmail) ist DKIM seit Februar 2024 Pflicht.
DMARC — die Klammer um SPF + DKIM Pflicht
# DMARC sagt Empfängern: was tun, wenn SPF/DKIM fehlschlägt

# Phase 1: Monitoring (Reports sammeln, nichts blocken)
_dmarc.example.com.  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@example.com; pct=100; adkim=s; aspf=s"

# Phase 2: Quarantäne (nach 4-8 Wochen Monitoring)
_dmarc.example.com.  TXT  "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com; pct=100; adkim=s; aspf=s"

# Phase 3: Reject (Endziel)
_dmarc.example.com.  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; ruf=mailto:dmarc@example.com; pct=100; adkim=s; aspf=s; fo=1"

# Tags:
#   p=         → policy: none, quarantine, reject
#   rua=       → aggregate reports (täglich, JSON)
#   ruf=       → forensic reports (per Failure, sensibel!)
#   pct=       → wieviel % der Mails Policy anwenden (Rampup)
#   adkim/aspf → s=strict, r=relaxed (Subdomain-Match)
#   fo=1       → Forensic bei jedem Fail (sonst nur bei beiden)

# Reports auswerten: dmarcian, Postmark DMARC, Valimail (alle gratis bis Volumen X)
WarumDMARC ist die einzige Verteidigung gegen Domain-Spoofing — ohne kann jeder Phishing-Mails "von dir" senden. Niemals direkt mit p=reject starten — Phase 1 (none) für 4-8 Wochen, um vergessene Sender zu finden, sonst killst du legitime Mails (z.B. den HR-Mailversand vom externen Tool, das niemand mehr kannte).
Antip=reject ohne Monitoring-Phase. Du wirst Mails verlieren, die du gar nicht auf dem Schirm hast — Newsletter-Tools, CRM, Support-Plattform. Erst 4-8 Wochen p=none + Reports auswerten, dann graduell. Anti: rua-Adresse ohne automatisierten Parser — DMARC-Reports sind XML-JSON-Dumps, manuell unleserlich. Anti: ruf= aktivieren ohne dass Postfach DSGVO-konform ist — Forensic Reports enthalten Original-Mails inkl. PII.
DNSSEC + MTA-STS + BIMI Empfehlung
# DNSSEC: kryptographisch signierte DNS-Antworten
# Aktivieren beim Registrar (1 Klick bei Cloudflare, Hetzner, etc.)
# DS-Record beim TLD-Registry hinterlegen lassen
example.com.  DS  12345 13 2 ABC123...

# MTA-STS: TLS-erzwungene Mail-Zustellung (Schutz vor Downgrade-Attacks)
_mta-sts.example.com.  TXT  "v=STSv1; id=20260501T000000;"
# Plus Policy-File: https://mta-sts.example.com/.well-known/mta-sts.txt
version: STSv1
mode: enforce
mx: *.example.com
max_age: 604800

# TLS-RPT: Reports über fehlgeschlagene TLS-Mail-Zustellung
_smtp._tls.example.com.  TXT  "v=TLSRPTv1; rua=mailto:tls@example.com"

# BIMI: Logo neben Mail im Inbox (Trust-Signal)
# Voraussetzung: DMARC mit p=quarantine oder p=reject + VMC (Verified Mark Certificate)
default._bimi.example.com.  TXT
  "v=BIMI1; l=https://example.com/bimi/logo.svg; a=https://example.com/bimi/cert.pem"
WarumDNSSEC verhindert DNS-Cache-Poisoning — ohne kann ein Angreifer im Café-WLAN deine Domain auf seine Server umleiten. MTA-STS verhindert, dass Angreifer SMTP von TLS auf Klartext downgraden (Mail-Inhalte mitlesen). BIMI mit verifiziertem Logo neben deinen Mails in Gmail/Apple Mail — sichtbares Trust-Signal, deutlich höhere Open-Rate.
DNS-Provider Vergleich 2026 Nice-to-have
ProviderAnycastDNSSECAPIEmpfehlung
CloudflareJa, topJa, 1-KlickTopDefault-Wahl, frei
Route 53 (AWS)JaJaTopAWS-Stack-Lock-In
Hetzner DNSJa, EUJaOKBei Hetzner-Servern
desec.ioJaJa, PflichtOKFOSS, EU, gratis
Hover / GoDaddyNeinTeilweiseSchwachVermeiden für Prod
AntiDNS beim Registrar belassen. Standard-DNS bei vielen Registraren ist langsam und ohne Anycast — TTFB-Penalty. Domain registrieren ist eine Sache, DNS hosten eine andere. Anti: Single-Provider für Domain UND DNS — bei Account-Sperre verlierst du beides. Splitten: Domain bei z.B. Hover, DNS bei Cloudflare.
02

DACH Compliance — Rechtliches

Impressum, Datenschutz, TTDSG/TDDDG-konformer Cookie-Consent. Kein SEO-Thema, aber ohne fliegt die Site bei Abmahnung. Author-Boxen sind gleichzeitig E-E-A-T-Signal.

Impressum (DDG §5) — Mindestinhalt

  • Name und Anschrift (kein Postfach)
  • Vertretungsberechtigte (bei juristischer Person)
  • Kontakt: E-Mail + Telefon
  • Handelsregister + Nummer (falls eingetragen)
  • USt-ID (falls vorhanden)
  • Aufsichtsbehörde (bei behördlicher Zulassung)
  • Berufsbezeichnung + Kammer (reglementierte Berufe)
  • Streitschlichtung-Hinweis (B2C, Online-Vertrag)
  • Direkt erreichbar von jeder Seite (max 2 Klicks)

DSGVO + TDDDG — Cookie-Consent

  • Consent vor Setzen jedes nicht-essentiellen Cookies
  • Ablehnen genauso einfach wie Annehmen (Buttons gleich groß)
  • Granular: pro Zweck einzeln zustimmbar
  • Widerruf jederzeit möglich (Link in Footer)
  • Consent dokumentiert (Logs für 3 Jahre)
  • Datenschutzerklärung verlinkt mit Liste aller Verarbeiter
  • Auftragsverarbeitungsvertrag (AVV) mit jedem Drittanbieter
  • Dark Patterns vermeiden
Author-Box (E-E-A-T) Empfehlung
<aside class="author-box" aria-label="Über den Autor">
  <img src="/team/max.jpg" alt="Max Mustermann" width="80" height="80">
  <div>
    <p><strong>Max Mustermann</strong>, Senior Engineer</p>
    <p>10+ Jahre Erfahrung. TU München, Co-Author bei W3C.</p>
    <p><a rel="author" href="/team/max">Profil</a> · 
       <a href="https://www.linkedin.com/in/max" rel="me">LinkedIn</a></p>
  </div>
</aside>
WarumSichtbarer Author-Block + verlinkte Author-Page + Person-Schema = stärkstes E-E-A-T-Signal. rel="me" auf Social-Profile validiert Identität. Bei YMYL-Themen (Health, Finance, Legal) absolute Pflicht.
03

CMS-Architektur — Headless · Monolith · Git-based

Eine der frühesten Entscheidungen mit den größten Spätfolgen. Headless wirkt modern, ist aber für Solo-Sites overkill. Monolith (WordPress) bequem, aber Performance-/Sicherheits-Schmerz. Git-based für Tech-affine Teams optimal. Falsche Wahl kostet Jahre Lock-in.

CMS-Typ-Vergleich Pflicht
TypBeispieleStärkeSchwäche
MonolithWordPress, Drupal, TYPO3, CraftEditorial-UX, Plugin-Ökosystem, Kunde kann selbst pflegenPerformance, Security, Plugin-Hell
Headless APIStoryblok, Sanity, Contentful, Strapi, Directus, PayloadFrontend-Freiheit, Multi-ChannelEditorial-UX schwächer, Setup-Aufwand
Git-basedAstro Content, Markdown, MDX, Decap CMS, TinaCMSVersionierung, Rollback, Dev-Workflow, kostenlosNicht-Tech-Editor schwierig
Visual BuilderWebflow, Framer, Wix StudioDesigner-friendly, schnellVendor-Lock-In, kein Code-Export, teuer ab Skalierung
Hybrid (Block + Headless)WordPress + Faust, Drupal + Next.jsEditorial-UX + moderne FrontendDoppelte Komplexität
WarumFaustregel: Solo / Marketing-Site → Astro + Markdown (Git-based). SMB mit nicht-tech Editor → Storyblok oder Sanity (Headless mit guter Editorial-UX). Kunden-Pflege essentiell, Tech-Skill schwach → WordPress (Monolith). Designer-Team ohne Devs → Webflow (mit klarem Skalierungs-Limit-Bewusstsein).
AntiHeadless aus Trend-Gründen. Headless für eine 10-Page-Marketing-Site ist 5× Aufwand für 0 Mehrwert. Anti: Webflow für SEO-getriebene Sites mit hundert+ Pages — Skalierung wird teuer. Anti: WordPress mit 30+ Plugins — jedes Plugin ist Angriffsfläche + Performance-Killer. Anti: Custom-CMS bauen — Editorial-UX entwickeln dauert Jahre, bestehende Lösungen schlagen jeden Eigenbau.
Content-Modeling — Single-Type vs Component-driven Empfehlung
// Single-Type Modell (klassisch)
// Jede Page hat fixe Felder: title, hero_image, body_richtext, seo_meta
// → einfach, aber unflexibel: Designer kann keine neue Section bauen

// Component-driven (modern, empfohlen)
// Page = Sequenz von Blocks/Components
{
  "slug": "/leistungen/dtf-druck",
  "seo": { "title": "...", "description": "..." },
  "sections": [
    { "type": "hero", "variant": "split", "headline": "..." },
    { "type": "feature_grid", "columns": 3, "items": [...] },
    { "type": "testimonial", "author": "..." },
    { "type": "cta", "text": "...", "href": "..." }
  ]
}

// Im Code: Component-Resolver mappt type → Komponente
{sections.map(s => {
  const Component = COMPONENTS[s.type];
  return <Component {...s} />;
})}

// Storyblok: "Bloks", Sanity: "Portable Text + Custom Types"
// WordPress Gutenberg: Block Patterns + Custom Blocks (ACF)
WarumComponent-driven trennt Content-Struktur von Layout-Logik. Editor baut Pages aus vorhandenen Blocks zusammen — Designer-Freiheit ohne Code-Touch. Nachteil: höherer Setup-Aufwand am Anfang. Lohnt sich ab ~10 Pages oder mehrsprachigen Sites.
Live-Preview & Editorial-Workflow Empfehlung
# Pflicht-Features für Editor-tauglichen Stack:
#   - Live-Preview (sieht Änderung sofort, ohne Publish)
#   - Draft / Published / Scheduled (3 States)
#   - Visual-Editor mit Inline-Bearbeitung
#   - Versionsgeschichte + Rollback
#   - Branching für größere Änderungen
#   - Multi-User mit Rollen (Editor, Approver, Publisher)

# Astro + Sanity Live-Preview Setup:
npm i @sanity/preview-kit
# Pages mit isDraftMode prüfen, useLiveQuery() statt fetch()

# Storyblok Visual Editor:
# - Bridge-Skript einbinden, dom-Annotations setzen
# - data-blok-c="STORY_ID" auf jedem Component-Wrapper
# - Klick im Editor → springt direkt zur Quelle

# Decap CMS (für Astro/Hugo Git-based):
# /admin/index.html mit decap-cms.js
# - Editorial Workflow via Pull Requests
# - GitHub/GitLab als Backend, kein DB nötig
04

URL-Management — Slugs, Redirects, Migration, Vanity

URLs sind Versprechen. Jede Änderung bricht Backlinks, Lesezeichen, Crawler-Index. Wer Migration ohne 301-Map macht, verliert SEO-Authority systematisch. Slug-Strategie + Redirect-Map als Single Source of Truth ist Pflicht — egal wie klein die Site.

Slug-Konventionen Pflicht
# GOOD URLs:
/leistungen/dtf-druck
/blog/2026/04/website-2026-cheatsheet
/regensburg/handwerker-webdesign
/preise

# BAD URLs:
/index.php?page=services&id=42                # Query-Params + IDs
/Leistungen/DTF_Druck                          # UPPERCASE + Underscores
/de/de_DE/services-2024-04-final-FINAL-v3      # Dateinamen-Müll
/p/12345                                        # Numerische IDs ohne Kontext

# Regeln:
#   - lowercase
#   - Bindestriche statt Underscores (Google Standard)
#   - Keywords drin, aber kurz (3-5 Wörter max)
#   - Keine Stoppwörter ("der", "die", "das", "und")
#   - Keine Datumsangaben außer für Blog-Archiv-Sortierung
#   - Keine Dateiendungen (.html, .php) — moderne Router brauchen das nicht
#   - Trailing Slash konsistent (mit ODER ohne, NIEMALS beides)
#   - Maximal 3-4 Pfad-Segmente (/a/b/c/d ist Grenze)
#   - Sprach-Präfix nur bei Multi-Locale (siehe Layer International SEO)
Redirect-Map als Single Source of Truth Pflicht
# redirects.csv (oder JSON/YAML — committed in Git!)
old_path, new_path, status, added_at, reason
/services, /leistungen, 301, 2026-01-15, dt-Translation
/services/dtf, /leistungen/dtf-druck, 301, 2026-01-15, slug-rename
/blog/old-post, /blog/2024/03/old-post, 301, 2026-02-01, archive-restructure
/produkt/abc, /, 410, 2026-03-10, product-discontinued

# Vercel: vercel.json oder middleware.ts
{
  "redirects": [
    { "source": "/services", "destination": "/leistungen", "permanent": true },
    { "source": "/services/:slug*", "destination": "/leistungen/:slug*", "permanent": true }
  ]
}

# Cloudflare Workers / Pages:
const redirects = new Map([
  ['/services', '/leistungen'],
  ['/services/dtf', '/leistungen/dtf-druck']
]);

export default {
  async fetch(req) {
    const url = new URL(req.url);
    const dest = redirects.get(url.pathname);
    if (dest) return Response.redirect(new URL(dest, url), 301);
    return fetch(req);
  }
};

# Nginx:
location = /services { return 301 /leistungen; }
location ~ ^/services/(.*)$ { return 301 /leistungen/$1; }
AntiRedirect-Ketten (A→B→C). Google folgt max 5 Redirects, dann gibt's auf. Bei Migration immer auf finale Ziel-URL leiten, alte Redirects abkürzen. Anti: 302 statt 301 für permanente Verschiebungen — 302 = "temporär", PageRank wird nicht weitergegeben. Anti: JavaScript-Redirect statt HTTP — Crawler folgen oft nicht. Anti: Redirect-Map nicht versioniert — niemand weiß warum Redirects existieren, Cleanup wird unmöglich.
Slug-History & URL-Migration Empfehlung
# Pattern: jeder Content-Eintrag speichert seine Slug-History

# Datenbank-Schema:
CREATE TABLE posts (
  id UUID PRIMARY KEY,
  slug VARCHAR(255) UNIQUE NOT NULL,
  title VARCHAR(255),
  ...
);

CREATE TABLE post_slug_history (
  id UUID PRIMARY KEY,
  post_id UUID REFERENCES posts(id),
  old_slug VARCHAR(255) NOT NULL,
  changed_at TIMESTAMP NOT NULL,
  UNIQUE(old_slug)
);

# Bei Slug-Änderung: alten Slug in History pushen
# Beim Routing: erst aktuellen Slug prüfen, dann History (→ 301 zu aktuellem)

async function resolveSlug(slug) {
  let post = await db.posts.findOne({ slug });
  if (post) return { post, redirect: null };

  const history = await db.post_slug_history.findOne({ old_slug: slug });
  if (history) {
    post = await db.posts.findOne({ id: history.post_id });
    return { post, redirect: post.slug };  // 301
  }
  return { post: null, redirect: null };       // 404
}

# Vorteil: Slug-Renames brechen niemals Backlinks
# Beispiel: Blog-Post wird umbenannt, alle alten Links funktionieren weiter
Broken-Link-Detection & Vanity-URLs Nice-to-have
# Broken-Link-Detection (intern + extern)
npx linkinator https://example.com --recurse --skip="^https://twitter"
lychee --max-concurrency 4 https://example.com

# In CI integrieren — Build failt bei broken Internal Links
# GitHub Action: lycheeverse/lychee-action

# 404-Tracking (siehe Layer Operations) → Top-Broken-URLs identifizieren
# GSC → Indexierung → Nicht gefunden (404) — periodisch durchgehen

# Vanity URLs für Marketing/Print/Offline-Kampagnen
# /preisliste → 301 → /leistungen/preise (kürzer für Visitenkarten)
# /qr → 301 → /kontakt?utm_source=qr (für QR-Codes)
# /jobs → /karriere

# DevOps: Vanity-URLs in eigener Map, NICHT in Hauptrouting mischen
# Klar: was ist canonical (/karriere) vs Vanity-Eingangstor (/jobs)
05

Security & Delivery — Header

Alles, was vor dem ersten Byte HTML passiert. HTTPS, HTTP/3, Brotli und ein vernünftiges Header-Set. Google bewertet Sicherheit als Trust-Signal, Browser blocken alles ohne saubere Header zunehmend aggressiv.

Response Headers Pflicht
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; img-src 'self' data: https:;
  script-src 'self' 'nonce-{RANDOM}'; style-src 'self' 'unsafe-inline';
  font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';
  base-uri 'self'; form-action 'self'; upgrade-insecure-requests
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(),
  interest-cohort=(), browsing-topics=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
X-Frame-Options: DENY
WarumHSTS preload erzwingt HTTPS bevor der erste Request rausgeht. CSP mit nonce ist die einzige saubere Abwehr gegen Inline-XSS, 'unsafe-inline' für Script ist ein Rotes Tuch. frame-ancestors ersetzt X-Frame-Options. interest-cohort & browsing-topics blocken Googles Topics API.
Cache & Compression Empfehlung
# Static assets mit Hash im Namen — 1 Jahr cachen
Cache-Control: public, max-age=31536000, immutable

# HTML — kurz cachen, revalidate
Cache-Control: public, max-age=0, must-revalidate
Vary: Accept-Encoding

# Compression: brotli > gzip, HTTP/3 aktivieren
Content-Encoding: br
Alt-Svc: h3=":443"; ma=86400
WarumBrotli komprimiert HTML/CSS/JS ~20% besser als gzip. HTTP/3 (QUIC) reduziert TTFB messbar bei mobilen Verbindungen. Cloudflare, Vercel, Netlify machen das automatisch — Self-Hosting heißt aktiv konfigurieren.
06

HTML-Skelett — Document Shell

Das Grundgerüst. Falsche lang, fehlendes viewport oder kaputter Charset-Tag killen Indexierung und mobile UX. Reihenfolge im <head> ist relevant.

<head> Foundation Pflicht
<!DOCTYPE html>
<html lang="de-DE" dir="ltr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

  <!-- Theme adapts to OS preference -->
  <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
  <meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)">
  <meta name="color-scheme" content="light dark">

  <!-- Performance hints — early -->
  <link rel="preconnect" href="https://cdn.example.com" crossorigin>
  <link rel="preload" as="font" type="font/woff2" href="/fonts/main.woff2" crossorigin>
</head>
Warumlang="de-DE" statt nur "de" hilft hreflang-Konsistenz. viewport-fit=cover für iPhone Notch. Reihenfolge: charset → viewport → theme-color → preconnect → title. color-scheme verhindert weißes Aufblitzen beim Dark-Mode-Render.
07

Semantic HTML — DOM Structure

HTML5-Landmarks, eine h1, saubere Heading-Hierarchie. KI-Crawler segmentieren den Content nach genau diesen Elementen — Divs sind blind.

Page Skeleton Pflicht
<body>
  <a href="#main" class="skip-link">Zum Hauptinhalt springen</a>

  <header>
    <nav aria-label="Hauptnavigation"> ... </nav>
    <nav aria-label="Breadcrumb">
      <ol>
        <li><a href="/">Home</a></li>
        <li aria-current="page">Aktuelle Seite</li>
      </ol>
    </nav>
  </header>

  <main id="main">
    <article>
      <header>
        <h1>Genau eine H1 pro Seite</h1>
        <p><time datetime="2026-04-15">15. April 2026</time> · <a rel="author">Max</a></p>
      </header>
      <section aria-labelledby="s1">
        <h2 id="s1">Abschnitt</h2>
      </section>
    </article>
  </main>

  <footer>
    <address>Brand GmbH, Musterstr. 1, Regensburg</address>
  </footer>
</body>
WarumMehrere <nav> sind erlaubt — jede mit eigenem aria-label. Skip-Link ist WCAG 2.2 Pflicht. <time datetime> mit ISO-Format ist die einzige Datumsdarstellung, die KI-Crawler zuverlässig parsen.
08

Meta-Pflichtset — Title, Description, Robots

Was Google im SERP-Snippet anzeigt. Title und Description werden zwar oft umgeschrieben, aber sie sind das stärkste Signal. canonical und hreflang sind nicht-optional bei Multi-Locale.

SEO Core Tags Pflicht
<!-- 50–60 Zeichen, Marke ans Ende -->
<title>Primary Keyword | Modifier — Brand</title>

<!-- 150–160 Zeichen, Antwort + CTA -->
<meta name="description" content="Konkrete Antwort in einem Satz. Was, für wen, mit welchem Nutzen.">

<!-- Granular: Snippet-Länge, Image/Video-Preview -->
<meta name="robots" content="index,follow,max-snippet:-1,max-image-preview:large,max-video-preview:-1">

<!-- Self-referencing canonical, auch auf Startseite -->
<link rel="canonical" href="https://example.com/seite">

<!-- hreflang: ALLE Sprachen + x-default, immer mit Selbst-Referenz -->
<link rel="alternate" hreflang="de-DE" href="https://example.com/seite">
<link rel="alternate" hreflang="en-US" href="https://example.com/en/page">
<link rel="alternate" hreflang="x-default" href="https://example.com/seite">
Warummax-snippet:-1 erlaubt Google die volle Snippet-Länge — wichtig für Featured Snippets & AI Overviews. max-image-preview:large ist Voraussetzung für Discover-Traffic. hreflang ohne x-default ist halbgar; jede Variante muss bidirektional verlinken (Reciprocity).
09

Open Graph — Social Cards

Wie der Link in WhatsApp, LinkedIn, Slack, Discord, X aussieht. Heute auch wichtig: AI-Crawler OCR'n OG-Bilder und nehmen den Text als Kontext mit.

→ Layer 16 · OG-Image-Automation — Programmatische Erzeugung der Bilder pro Page

Open Graph + Twitter Pflicht
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="Brand">
<meta property="og:title" content="Klare Kernaussage in 60 Zeichen">
<meta property="og:description" content="Was es ist, für wen, warum jetzt klicken — max 180.">
<meta property="og:url" content="https://example.com/seite">
<meta property="og:locale" content="de_DE">

<!-- Bild: 1200×630, < 5MB, mit lesbarem Text-Overlay -->
<meta property="og:image" content="https://example.com/og/seite.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Beschreibung des Bildes inkl. Text-Overlay">

<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@brandhandle">
<meta name="twitter:image" content="https://example.com/og/seite.jpg">
WarumTwitter-Tags fallen auf OG zurück. Text-Overlay im OG-Bild ist 2026 wichtig: AI-Crawler wie GPTBot lesen das mit OCR und nutzen es als Kontext-Signal. og:image:alt wird oft vergessen, ist aber für A11y und AI-Indexierung relevant.
10

Icons & PWA — Manifest

Favicon-Set, Apple Touch, Web App Manifest. Eine PWA-tauglich aufgesetzte Site wird mobile besser bewertet und sieht auf dem Homescreen nicht aus wie eine Bookmark-Karteileiche.

Icon Set + Manifest Pflicht
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="mask-icon" href="/mask-icon.svg" color="#0a0a0a">
<link rel="manifest" href="/manifest.webmanifest">

// /manifest.webmanifest
{
  "name": "Brand — Full Name",
  "short_name": "Brand",
  "start_url": "/?source=pwa",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0a0a0a",
  "lang": "de-DE",
  "icons": [
    { "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icons/maskable.png", "sizes": "512x512", "purpose": "maskable" }
  ]
}
Warumpurpose: "maskable" Icon nötig für Android-Adaptive-Icons (sonst weißer Rand). start_url mit UTM oder Source-Param trackt PWA-Installs. Lighthouse PWA-Audit prüft diese Felder explizit.
11

Mobile-First Details — Safe Area, inputmode, Click-to-Call

Handwerker-Sites werden zu 70-80% mobil aufgerufen. Über das Viewport-Meta hinaus gibt's Details, die Mobile-Conversion entscheiden: Safe-Area für Notch, korrekte Tastaturen pro Inputtyp, native Sharing/Calling, Smart App Banners falls App existiert.

Safe Area Insets (Notch + Home Indicator) Pflicht
<!-- Pflicht: viewport-fit=cover (siehe Layer 02) -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

<style>
/* Sticky Header: Safe Area beachten */
header.sticky {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* Sticky Footer / Bottom Bar */
.bottom-bar {
  padding-bottom: max(1rem, env(safe-area-inset-bottom));
}

/* Full-Screen Hintergrund über Notch hinweg */
.hero {
  margin: calc(-1 * env(safe-area-inset-top)) calc(-1 * env(safe-area-inset-right))
          0 calc(-1 * env(safe-area-inset-left));
  padding-top: env(safe-area-inset-top);
}
</style>
WarumiPhone mit Notch + Home-Indicator-Bar verdecken sonst Content. env(safe-area-inset-*) respektiert dynamisch die Geräte-Geometrie. Bei Sticky-Bottom-Bars: max(1rem, env(...)) garantiert Mindestabstand auch ohne Home Indicator (alte iPhones, Android).
inputmode + autocomplete-Matrix Pflicht
Feldtyptypeinputmodeautocomplete
Name (voll)text—name
Vornametext—given-name
Nachnametext—family-name
E-Mailemailemailemail
Telefontelteltel
Straßetext—street-address
PLZtextnumericpostal-code
Stadttext—address-level2
Landtext—country-name
Kreditkarten-Nrtextnumericcc-number
OTP / 2FA-Codetextnumericone-time-code
URLurlurlurl
Suchesearchsearchoff
Numerische Eingabetextnumeric—
Dezimalzahltextdecimal—
Warumtype="number" nutzen klingt logisch — ist aber meist falsch (Spinner, Scroll-Wheel-Probleme, max-Werte). Lieber: type="text" inputmode="numeric" pattern="[0-9]*". autocomplete="one-time-code" für 2FA → iOS schlägt SMS-Code automatisch vor. autocomplete="off" für Suchfelder verhindert nervige History-Vorschläge.
Click-to-Call / SMS / WhatsApp / Mail Pflicht
<!-- Telefon: tel: mit internationaler Notation -->
<a href="tel:+4994112345670">0941 1234567-0</a>

<!-- SMS mit pre-filled body -->
<a href="sms:+4915112345678?body=Hallo,%20Anfrage%20zu...">SMS</a>

<!-- WhatsApp (international) -->
<a href="https://wa.me/4915112345678?text=Hallo" rel="noopener">WhatsApp</a>

<!-- E-Mail mit Subject + Body -->
<a href="mailto:info@example.com?subject=Anfrage&body=Guten%20Tag,">
  E-Mail
</a>

<!-- Geo-Link für Navigation (Maps öffnet je nach OS) -->
<a href="geo:49.0134,12.1016?q=Brand+GmbH+Regensburg">
  In Karten öffnen
</a>

<!-- Native Web Share (iOS, Android, Chrome) -->
<script>
shareBtn.addEventListener('click', async () => {
  if (navigator.share) {
    await navigator.share({
      title: document.title,
      text: 'Schau dir das an',
      url: location.href
    });
  } else {
    // Fallback: Copy to Clipboard
    navigator.clipboard.writeText(location.href);
  }
});
</script>
AntiTelefonnummer ohne tel:-Link. Mobile-User müssen tippen statt tappen — Conversion-Killer. Anti: tel-Link ohne Ländervorwahl (tel:0941...) — funktioniert für deutsche Nummern meistens, bei Roaming oder ausländischen Anrufern: bricht. Immer tel:+49....
Smart App Banner + Universal Links Nice-to-have
<!-- iOS Smart App Banner -->
<meta name="apple-itunes-app" content="app-id=123456789, app-argument=https://example.com/path">

<!-- iOS Universal Links: /.well-known/apple-app-site-association -->
{
  "applinks": {
    "apps": [],
    "details": [{
      "appID": "TEAMID.com.example.app",
      "paths": ["/order/*", "/account/*", "NOT /admin/*"]
    }]
  }
}

<!-- Android App Links: /.well-known/assetlinks.json -->
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.app",
    "sha256_cert_fingerprints": ["AB:CD:..."]
  }
}]

# Beide Files MÜSSEN:
#   - HTTPS-erreichbar sein
#   - Content-Type: application/json
#   - Ohne Redirect ausgeliefert werden
12

Accessibility & BFSG — WCAG 2.2 AA + Barrierefreiheitsstärkungsgesetz

A11y = SEO = rechtliche Pflicht (BFSG seit 28. Juni 2025 für viele B2C-Sites). Screenreader und KI-Crawler nutzen ähnliche Heuristiken, also doppelter Hebel. Bußgeld bei Nichtkonformität bis 100k €. Hier: technische WCAG 2.2 AA Patterns + rechtlicher Geltungsbereich + Pflicht-Erklärung.

A11y Patterns Pflicht
<!-- Skip-Link sichtbar bei Fokus -->
<style>
.skip-link { position: absolute; top: -100px; left: 0;
  background: #000; color: #fff; padding: 12px 20px; z-index: 100; }
.skip-link:focus { top: 0; }
:focus-visible { outline: 2px solid #000; outline-offset: 2px; }
</style>

<!-- Buttons vs Links: semantisch korrekt -->
<a href="/page">Navigation</a>           <!-- führt woanders hin -->
<button type="button">Action</button>     <!-- löst Aktion aus -->

<!-- Formulare: Labels + autocomplete -->
<label for="email">E-Mail</label>
<input type="email" id="email" autocomplete="email" required
       aria-describedby="email-help">

<!-- Bilder: aussagekräftige alts; dekorativ = leer -->
<img src="chart.png" alt="Umsatz Q1 2026: 1,2 Mio €, +18% YoY">
<img src="divider.svg" alt="">
Warumautocomplete ist WCAG 2.2 Pflicht für Login/Kontakt-Felder. :focus-visible statt :focus zeigt Fokus nur bei Tastatur-Nav. Alt-Texte für Charts: Inhalt beschreiben, nicht das Bild.

WCAG 2.2 AA Quick-Check

  • Kontrast Body-Text ≥ 4.5:1, Large-Text ≥ 3:1
  • Alle Funktionen per Tastatur erreichbar
  • Sichtbarer Focus-Indicator (2.4.7)
  • Touch-Targets ≥ 24×24px (2.5.8 — neu in 2.2)
  • Drag-Funktionen haben Klick-Alternative (2.5.7)
  • Login-Formulare nicht kognitiv-anspruchsvoll (3.3.8)
  • Konsistente Hilfe-Links (3.2.6)
  • Skip-Link am Anfang der Seite
  • Semantische Heading-Hierarchie
  • Fehlerhinweise programmatisch verknüpft
Geltungsbereich-Matrix Pflicht
KategorieBFSG-pflichtig?Bedingung
B2C-Online-ShopJa≥10 MA ODER >2 Mio € Jahresumsatz
Banking-/ZahlungsdiensteJaImmer (auch Klein-MA)
Personenbeförderung OnlineJaImmer
E-Books / Reader-AppsJaImmer
Telekommunikations-DiensteJaImmer
Reine B2B-Site (kein Verbraucher-Kontakt)NeinAchtung: Definition prüfen!
Klein-Solo (<10 MA, <2 Mio €) ohne ShopNeinAber: Kunden können's verlangen
Behörden-SitesJa, schon vorherBITV 2.0 (strenger als BFSG)
WarumDie "Klein-MA"-Ausnahme ist kein Freifahrtschein: B2B-Kunden können Barrierefreiheit als Vergaberichtlinie vorgeben (öffentliche Hand zwingend), Verbraucherschutzverbände klagen schon. Praxis-Tipp: wer eh WCAG 2.2 AA umsetzt (siehe Layer 11), erfüllt 95% der BFSG-Anforderungen automatisch — also einfach machen, unabhängig von Pflicht.
Anti"Wir sind klein, betrifft uns nicht". Der Schwellenwert kann fließend sein (Umsatz 1,9 Mio € heute, 2,1 Mio € nächstes Jahr — dann muss Barrierefreiheit retro). Anti: Accessibility-Overlay-Tools (UserWay, AccessiBe) als Lösung — diese werden 2024/25 in mehreren EU-Urteilen als nicht ausreichend eingestuft, oft sogar verschlimmernd (Screenreader-Konflikte). Lieber sauber bauen.
Erklärung zur Barrierefreiheit Pflicht
<!-- Pflicht-Seite unter /barrierefreiheit oder /accessibility -->
<!-- Verlinkt im Footer und im Impressum -->

<article>
<h1>Erklärung zur Barrierefreiheit</h1>

<p>Brand GmbH ist bemüht, ihre Website [example.com] im Einklang
mit dem Barrierefreiheitsstärkungsgesetz (BFSG) und der zugrunde
liegenden EU-Richtlinie 2019/882 barrierefrei zugänglich zu machen.</p>

<h2>Stand der Vereinbarkeit</h2>
<p>Diese Website ist [vollständig | teilweise | nicht] vereinbar mit
WCAG 2.1 AA (bzw. EN 301 549 V3.2.1).</p>

<h2>Nicht barrierefreie Inhalte</h2>
<ul>
  <li>PDFs vor 2024-01-01 sind nicht vollständig barrierefrei
      (werden bei nächster Überarbeitung umgestellt)</li>
  <li>Eingebettete YouTube-Videos: Untertitel werden ergänzt bis
      [Datum]</li>
</ul>

<h2>Kontakt bei Barrieren</h2>
<p>Sie können uns Barrieren melden:
<br>E-Mail: barrierefreiheit@example.com
<br>Telefon: 0941 1234567</p>

<h2>Schlichtungsverfahren</h2>
<p>Sollten unsere Antworten nicht zufriedenstellend sein, können
Sie sich an die Schlichtungsstelle nach §16 BFSG wenden:<br>
<a href="https://www.schlichtungsstelle-bfsg.de">schlichtungsstelle-bfsg.de</a></p>

<h2>Erstellung dieser Erklärung</h2>
<p>Diese Erklärung wurde am 28. Juni 2025 erstellt und zuletzt am
1. Mai 2026 überprüft.</p>
</article>
WarumPflicht-Inhalte: Stand der Vereinbarkeit, Liste nicht-barrierefreier Inhalte mit Begründung + Termin, Kontaktmöglichkeit für Meldungen, Hinweis auf Schlichtungsstelle, Datum der letzten Überprüfung. Mind. 1× pro Jahr aktualisieren. Im Footer als "Barrierefreiheit" verlinken, am besten parallel zu Impressum/Datenschutz.
Bußgelder & Durchsetzung Vorsicht
VerstoßSanktionZuständig
Fehlende Erklärung zur BarrierefreiheitBis 10.000 €Marktüberwachungsbehörde Land
Produkt nicht konform (Online-Shop, App)Bis 100.000 €Marktüberwachungsbehörde Land
Wiederholte VerstößeMarktverbot des ProduktsBAS / Länder
VerbraucherklagenAbmahnung + Unterlassungvzbv, Verbraucherschutzverbände
WarumDie Marktüberwachungsbehörden der Länder werden 2026 vermutlich noch behutsam, ab 2027 strikter durchgreifen. vzbv und einzelne Anwaltskanzleien haben bereits angekündigt, systematisch abzumahnen — ähnlich der Welle nach DSGVO-Inkrafttreten 2018. Risikoarm bleibt: WCAG 2.2 AA korrekt umsetzen + Erklärung publizieren + Kontaktstelle einrichten.
13

JSON-LD Schema — Basic Graph + Advanced Schemas

Der wichtigste Layer für Sichtbarkeit in Google AI Overviews, Perplexity, ChatGPT-Search. Alles in einem @graph bündeln, mit @id-Referenzen verlinken, Entitäten via sameAs auf Wikidata mappen.

Master @graph (Startseite) Pflicht
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://example.com/#organization",
      "name": "Brand GmbH",
      "url": "https://example.com",
      "logo": "https://example.com/logo.png",
      "sameAs": [
        "https://www.wikidata.org/wiki/Q12345",
        "https://www.linkedin.com/company/brand",
        "https://github.com/brand"
      ]
    },
    {
      "@type": "WebSite",
      "@id": "https://example.com/#website",
      "url": "https://example.com",
      "publisher": { "@id": "https://example.com/#organization" },
      "inLanguage": "de-DE",
      "potentialAction": {
        "@type": "SearchAction",
        "target": { "urlTemplate": "https://example.com/search?q={search_term_string}" },
        "query-input": "required name=search_term_string"
      }
    },
    {
      "@type": "WebPage",
      "@id": "https://example.com/#webpage",
      "url": "https://example.com/",
      "isPartOf": { "@id": "https://example.com/#website" },
      "datePublished": "2026-01-15T08:00:00+01:00",
      "dateModified": "2026-05-01T14:30:00+02:00",
      "inLanguage": "de-DE",
      "breadcrumb": { "@id": "https://example.com/#breadcrumb" }
    }
  ]
}
</script>
WarumEin @graph mit @id-Referenzen ist sauberer als 5 separate Blöcke und reduziert Duplikate. sameAs zu Wikidata ist 2026 das stärkste Entity-Linking-Signal — AI Overviews verifizieren Entitäten dort.
Article + Person (E-E-A-T) Empfehlung
{
  "@type": "Article",
  "headline": "Titel max 110 Zeichen",
  "image": ["cover-1x1.jpg", "cover-4x3.jpg", "cover-16x9.jpg"],
  "datePublished": "2026-04-15T08:00:00+02:00",
  "dateModified": "2026-05-01T14:30:00+02:00",
  "author": {
    "@type": "Person",
    "name": "Max Mustermann",
    "url": "https://example.com/team/max",
    "jobTitle": "Senior Engineer",
    "knowsAbout": ["Web Performance", "SEO", "Schema.org"],
    "alumniOf": "TU München",
    "sameAs": ["https://www.linkedin.com/in/maxmustermann"]
  },
  "publisher": { "@id": "https://example.com/#organization" }
}
WarumknowsAbout, alumniOf, jobTitle sind die wichtigsten Felder für Author-Authority. Google's E-E-A-T-Score korreliert mit Author-Profile-Tiefe. Author-Pages sind nicht optional — jede Person braucht eine eigene URL mit Person-Schema.
Product mit Offer + Reviews Empfehlung
{
  "@type": "Product",
  "name": "Produktname",
  "image": ["sku-1x1.jpg", "sku-4x3.jpg"],
  "sku": "SKU-123",
  "gtin13": "4006381333931",
  "brand": { "@type": "Brand", "name": "Brand" },
  "offers": {
    "@type": "Offer",
    "priceCurrency": "EUR",
    "price": "49.90",
    "priceValidUntil": "2026-12-31",
    "availability": "https://schema.org/InStock",
    "hasMerchantReturnPolicy": {
      "@type": "MerchantReturnPolicy",
      "applicableCountry": "DE",
      "merchantReturnDays": 14,
      "returnFees": "https://schema.org/FreeReturn"
    },
    "shippingDetails": {
      "@type": "OfferShippingDetails",
      "shippingRate": { "value": "4.95", "currency": "EUR" },
      "shippingDestination": { "addressCountry": "DE" }
    }
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.7", "reviewCount": 142
  }
}
WarumhasMerchantReturnPolicy + shippingDetails sind seit 2023 bei Google Shopping de-facto Pflicht. gtin13/mpn matchen Produkte über Shop-Grenzen hinweg im Knowledge Graph.
AntiFake Reviews oder AggregateRating ohne echte Reviews. Google's Spam-Update erkennt das, manuelle Penalty droht. Anti: availability: InStock hardcoden, wenn Stock dynamisch — bei OOS-Klick verliert man Trust.
Event + VideoObject + JobPosting Empfehlung
// Event
{
  "@type": "Event",
  "startDate": "2026-09-15T19:00:00+02:00",
  "eventStatus": "https://schema.org/EventScheduled",
  "eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
  "location": { "@type": "Place", "address": {...} },
  "offers": [{ "@type": "Offer", "price": "45.00", "priceCurrency": "EUR" }]
}

// VideoObject mit Transcript (entscheidend für AI-Indexierung)
{
  "@type": "VideoObject",
  "name": "Video Titel",
  "thumbnailUrl": ["thumb-1x1.jpg", "thumb-16x9.jpg"],
  "uploadDate": "2026-04-15T08:00:00+02:00",
  "duration": "PT4M33S",
  "transcript": "Vollständiges Transkript...",
  "hasPart": [{
    "@type": "Clip", "name": "Kapitel 1",
    "startOffset": 0, "endOffset": 60
  }]
}

// JobPosting
{
  "@type": "JobPosting",
  "title": "Senior Backend Engineer (m/w/d)",
  "datePosted": "2026-04-15",
  "validThrough": "2026-06-15T23:59:00+02:00",
  "employmentType": "FULL_TIME",
  "hiringOrganization": { "@id": "https://example.com/#organization" },
  "baseSalary": {
    "@type": "MonetaryAmount", "currency": "EUR",
    "value": { "minValue": 70000, "maxValue": 90000, "unitText": "YEAR" }
  }
}
WarumTranscript ist 2026 das wichtigste Video-Feld: AI-Suchmaschinen können kein Video, aber Text. JobPosting validThrough ist Pflicht für Google for Jobs.
AntiJobPosting nach Besetzung online lassen. 410 Gone oder validThrough in Vergangenheit setzen. Sonst Google-Strike. Anti: bei Cancellation Event-Schema löschen — stattdessen EventCancelled setzen, sonst behält Google die alte Info im Cache.
14

AI-Search Layer — 2026-Spezifikum

Der eigentliche Wettbewerbsvorteil 2026. llms.txt, granulare robots.txt für AI-Bots, Speakable-Markup, Entity-Linking. Die meisten Wettbewerber haben das nicht — wer es hat, wird in ChatGPT-Search, Perplexity und Google AI Overviews bevorzugt.

/llms.txt (Root) Empfehlung
# Brand GmbH

> Eine Zeile Pitch: was ihr macht, für wen, wo.

Längere Beschreibung mit Kontext, Spezialgebieten, USPs.

## Kerninformationen

- [Über uns](https://example.com/ueber-uns): Geschichte, Team, Standort
- [Leistungen](https://example.com/leistungen): Vollständige Übersicht
- [Kontakt](https://example.com/kontakt): Adresse, Telefon, E-Mail

## Leistungen

- [Service A](https://example.com/service-a): Was, für wen, Preisrange
- [Service B](https://example.com/service-b): Was, für wen, Preisrange

## Optional

- [Newsletter](https://example.com/newsletter): Wöchentliche Updates
Warum/llms.txt ist ein Anthropic-Vorschlag, der bei Crawlern Traction gewinnt. Pflicht-Section ist H1 + Blockquote + H2-Sections. Praktischer Effekt 2026: einige Tools (Cursor, Anthropic-Crawler) lesen es priorisiert. Low-Effort-Wette mit Upside.
/robots.txt — Bot-differenziert Pflicht
# Standard: alles erlaubt
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /*?*utm_

# Google AI Training (Gemini-Training, NICHT Search)
User-agent: Google-Extended
Disallow: /

# OpenAI Training-Crawler
User-agent: GPTBot
Disallow: /

# OpenAI Live-Search (ChatGPT-Search) — meist erlauben!
User-agent: OAI-SearchBot
Allow: /
User-agent: ChatGPT-User
Allow: /

# Anthropic
User-agent: ClaudeBot
Allow: /
User-agent: anthropic-ai
Disallow: /

# Perplexity
User-agent: PerplexityBot
Allow: /

# Common Crawl + Apple Intelligence Training
User-agent: CCBot
Disallow: /
User-agent: Applebot-Extended
Disallow: /

Sitemap: https://example.com/sitemap.xml
WarumWichtige Unterscheidung 2026: Training-Bots vs. Live-Search-Bots. Standardstrategie: Training blocken (kein Mehrwert), Live-Search erlauben (= Sichtbarkeit in AI Overviews/ChatGPT-Search). Wer alles blockt, verschwindet aus AI-Suche.
Bot-Übersicht 2026 Nice-to-have
User-AgentZweckEmpfehlung
GooglebotGoogle Search IndexAllow
Google-ExtendedGemini-TrainingDisallow
GPTBotOpenAI TrainingDisallow
OAI-SearchBotChatGPT-Search LiveAllow
ChatGPT-UserUser-getriggertAllow
ClaudeBotAnthropic LiveAllow
anthropic-aiAnthropic TrainingDisallow
PerplexityBotPerplexity LiveAllow
CCBotCommon CrawlDisallow
Applebot-ExtendedApple AI TrainingDisallow
BingbotBing + CopilotAllow
Entity Linking via sameAs Empfehlung
"sameAs": [
  "https://www.wikidata.org/wiki/Q12345",    // Wikidata-ID anlegen!
  "https://de.wikipedia.org/wiki/Brand",
  "https://www.linkedin.com/company/brand",
  "https://github.com/brand",
  "https://www.youtube.com/@brand",
  "https://www.crunchbase.com/organization/brand"
]
WarumAI Overviews bauen ihren Knowledge Graph aus Wikidata und Wikipedia. Eigenen Wikidata-Eintrag anlegen (kostenlos, dauert 30 Min) ist der mit Abstand effektivste Hebel für Entity Recognition. Ohne Wikidata-ID bist du für KIs eine namenlose Domain.
15

AEO Content — Answer-First Pattern

Wie Content geschrieben sein muss, damit ihn LLMs als Quelle zitieren. Klassische SEO-Pyramide ist tot. Answer-First ist das neue Pattern.

Answer-First Struktur Pflicht
<article>
  <h1>Frage in Suchsprache?</h1>

  <!-- 1. Answer Block (80–120 Wörter) — direkt extrahierbar -->
  <p class="answer">
    Direkte, präzise Antwort in 2–3 Sätzen.
    Ein Datenpunkt, eine Quelle, ein konkretes Ergebnis.
  </p>

  <!-- 2. Key Facts als Liste -->
  <ul>
    <li><strong>Kernfakt 1:</strong> Wert + Einheit + Kontext</li>
  </ul>

  <!-- 3. Definitionsbox -->
  <dl>
    <dt>Begriff</dt>
    <dd>Definition mit Quelle.</dd>
  </dl>

  <!-- 4. Vertiefung in Sub-Sections -->
  <section><h2>Wie funktioniert es?</h2> ... </section>

  <!-- 5. FAQ-Block am Ende -->
  <section><h2>Häufige Fragen</h2> ... </section>
</article>
WarumLLMs extrahieren bevorzugt aus den ersten 200 Wörtern, aus Listen, Tabellen und Definitionslisten. "Eindeutige Statements statt Adjektive": KIs zitieren "43% schneller laut Studie X", nicht "unglaublich schnell". Daten + Quelle = Citation-Wahrscheinlichkeit.

Content-Checkliste pro Seite

  • Hauptfrage steht im Title und in der H1
  • Erster Absatz beantwortet die Frage in 80–120 Wörtern
  • Mindestens eine Bullet-Liste mit Kernfakten
  • Mindestens eine Definitionsliste oder Tabelle
  • Konkrete Zahlen + Quellen, keine Adjektive
  • Absätze max 3–4 Sätze
  • FAQ-Block am Ende mit 3–5 echten Fragen
  • Author-Box mit Person-Schema verlinkt
  • Letzte Aktualisierung sichtbar (<time>)
  • Internal Links zu thematisch verwandten Seiten
16

OG-Image-Automation — Programmatic Cards, Satori, AI-Generation

Layer OG-Tags hatte den Markup-Teil. Hier geht es um die Erzeugung der Bilder selbst — programmatisch pro Page, ohne Designer-Engpass. Vercel/Satori rendert React-JSX zu PNG, Cloudflare Workers können das gleiche, AI-Bild-Generation für Hero-Images.

Vercel OG (Satori) — Programmatic Cards Empfehlung
// app/api/og/route.tsx — Next.js App Router
import { ImageResponse } from 'next/og';

export const runtime = 'edge';

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const title = searchParams.get('title') ?? 'Brand';

  return new ImageResponse(
    (
      <div style={{
        background: '#1A2950',
        width: '100%', height: '100%',
        display: 'flex', flexDirection: 'column',
        padding: 80, fontFamily: 'Inter'
      }}>
        <div style={{ color: '#FF8C42', fontSize: 24, marginBottom: 40 }}>
          ⚡ Blitzsicht
        </div>
        <div style={{ color: '#fff', fontSize: 72, fontWeight: 700, lineHeight: 1.05 }}>
          {title}
        </div>
        <div style={{ color: '#A8B5D1', fontSize: 28, marginTop: 'auto' }}>
          blitzsicht.com
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

// In Page-Metadata referenzieren:
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  return {
    openGraph: {
      images: [`/api/og?title=${encodeURIComponent(post.title)}`]
    }
  };
}
WarumPro Blog-Post, pro Service-Page ein eigenes OG-Image — ohne Designer-Bottleneck. Satori (von Vercel) rendert React-JSX in < 100ms, Edge-Runtime weltweit. Funktioniert mit Cloudflare Workers via @cloudflare/workers-types + workers-og-Lib. Astro: astro-og-canvas oder satori direkt im Build-Step.
Build-Time vs Edge-Runtime Empfehlung
// Strategie A: Build-Time (Astro, statische Sites)
// → Pro Page wird Image im Build erzeugt, in /public/og/ gespeichert
// Vorteil: kein Runtime-Cost, CDN-cached
// Nachteil: Build-Zeit steigt linear mit Pages

// astro-og-canvas Beispiel:
import { OGImageRoute } from 'astro-og-canvas';
export const { getStaticPaths, GET } = OGImageRoute({
  param: 'slug',
  pages: await getCollection('blog'),
  getImageOptions: (path, page) => ({
    title: page.data.title,
    description: page.data.description,
    bgGradient: [[26, 41, 80]],
    border: { color: [255, 140, 66], width: 12 }
  })
});

// Strategie B: Edge-Runtime (dynamische Inhalte)
// → Image wird on-demand erzeugt, dann CDN-cached
// Vorteil: dynamische Daten möglich (User-Name, Datum)
// Nachteil: Cold-Start-Latenz, Cache-Konfiguration nötig

// Cache-Header bei Edge-Runtime kritisch:
return new ImageResponse(...,  {
  headers: {
    'Cache-Control': 'public, max-age=31536000, immutable'
  }
});
Hero-Images via AI-Generation Nice-to-have
# Tools für Marketing-Hero-Images (DACH-relevant):
#   - Midjourney (V6+, beste Quality)
#   - DALL-E 3 via OpenAI API (programmatic)
#   - Imagen 3 (Google, via Vertex AI)
#   - Flux 1.1 Pro (Black Forest Labs, EU-basiert)
#   - nano-banana (Gemini API, programmatic)

# Prompt-Template für B2B-Marketing:
#   "Photorealistic, [scene], natural lighting, shallow depth of field,
#    professional, no text, no logo, [color palette]"

# Lizenz-Frage:
#   - DALL-E 3: User besitzt Rechte (auch kommerziell), aber kein Copyright
#   - Midjourney: Pro-Plan = kommerzielle Nutzung erlaubt
#   - Flux: kommerziell nutzbar je nach Lizenz
#   - WICHTIG: AI-Bilder sind in DE/EU NICHT urheberrechtlich geschützt
#     → kann kopiert werden, kein Schutz für deine "Originale"

# AI-generated Alt-Texte (für vorhandene Bilder)
# Claude / GPT-4o mit Vision API: Bild rein, Alt-Text raus
# Kostet ~0.01-0.03 € pro Bild — bei 1000 Produkten = 10-30 €
# Vorsicht: Output IMMER manuell prüfen, KI halluziniert Details
AntiAI-Bilder mit sichtbarem KI-Look (6 Finger, Plastik-Haut, glitchy Hintergrund). Kunden riechen das, killt Trust. Lieber stockfoto-style oder echtes Foto. Anti: AI-Hero-Images bei Handwerker-Sites — User wollen echte Fotos vom echten Team (siehe Trust Signals Layer). Anti: Auto-Alt-Texte ohne Review — KI beschreibt "Mann mit Bart" statt "Werkstatt-Inhaber Max Mustermann an der Werkbank".
17

International SEO — hreflang Edge Cases

Multi-Locale-Sites scheitern fast immer an inkonsistenten hreflang-Tags. Reciprocity, x-default, ccTLD vs Subdir, IP-Redirect-Falle.

URL-Strategie wählen Pflicht
StrategieBeispielProContra
ccTLDexample.deStärkstes Geo-SignalHoster, SSL pro TLD
Subdomainde.example.comHosting-flexibelAuthority pro Subdomain
Subdirectoryexample.com/de/Authority gebündeltGeo-Signal schwächer
URL-Param?lang=de—Anti — Crawl-Probleme
WarumSubdirectory ist 95% der Fälle die richtige Wahl: Authority bleibt zentral, Setup einfach, hreflang reicht aus. ccTLD nur, wenn Brand-Wahrnehmung lokal sein muss (Banken, Recht, Lebensmittel).
hreflang Reciprocity Pflicht
<!-- AUF JEDER lokalisierten Seite ALLE Versionen aufführen -->
<!-- Inkl. SELBST-Referenz und x-default -->

<!-- Auf https://example.com/seite (Deutsch DE) -->
<link rel="alternate" hreflang="de-DE" href="https://example.com/seite">
<link rel="alternate" hreflang="de-AT" href="https://example.com/at/seite">
<link rel="alternate" hreflang="de-CH" href="https://example.com/ch/seite">
<link rel="alternate" hreflang="en-US" href="https://example.com/en/page">
<link rel="alternate" hreflang="x-default" href="https://example.com/seite">

<!-- Auf https://example.com/at/seite (Österreich) -->
<!-- IDENTISCHER Block, alle 5 Tags -->
WarumWenn Seite A auf B verlinkt, B aber nicht zurück → Google ignoriert beide hreflang. Reciprocity = Pflicht in beide Richtungen. x-default ist die Fallback-URL für nicht-gematchte Locales — meistens die englische oder die Hauptsprache.
AntiSprach-Code ohne Region. hreflang="de" matcht für Google alles (DE, AT, CH) — meist OK. Aber wenn Preise/Versand pro Land unterschiedlich → de-DE, de-AT, de-CH nutzen. Anti: hreflang="de-DE" auf eine Seite, deren Content englisch ist — Inkonsistenz triggert Page-Drop. Anti: hreflang="en-EN" — gibt's nicht. Korrekt: en-US, en-GB.
IP-Redirect: das Anti-Pattern Vorsicht
// FALSCH: User aus AT wird automatisch auf /at/ umgeleitet
if (geoip(req.ip).country === 'AT') {
  res.redirect(302, '/at/' + req.path);
}

// RICHTIG: Sprach-Switcher anzeigen, nie redirecten
// Banner: "Wir haben festgestellt, dass Sie aus Österreich kommen."
//        "Möchten Sie zur österreichischen Version wechseln?"
//        [Ja, zu AT] [Nein, hier bleiben]
AntiAuto-Redirect basierend auf IP. Googlebot crawlt aus US-IPs — wenn deine Site DE-User auf /de/ redirectet, sieht Googlebot nur die englische Version, /de/ wird nie indexiert. Anti: Sprache aus Accept-Language hart redirecten — VPN/Tor-Nutzer landen falsch. RICHTIG: Sprach-Switcher prominent + optional Banner-Hinweis mit Choice.
18

Performance-Budgets — Lighthouse, web-vitals, CrUX

Was nicht gemessen wird, regrediert. Synthetic (Lighthouse CI) im Build + Real User Monitoring im Prod. Festgelegte Budgets blocken Pull Requests.

→ Layer 19 · Core Web Vitals — Konkrete Vitals-Targets, gegen die das Budget greift

Asset-Budgets (Initial Page Load) Pflicht
AssetBudgetHard Limit
HTML (initial)≤ 30 KB50 KB
CSS (kritisch, inline)≤ 14 KB20 KB
CSS (gesamt)≤ 50 KB100 KB
JS (initial bundle)≤ 100 KB170 KB
JS (gesamt route)≤ 200 KB350 KB
Fonts (gesamt)≤ 100 KB200 KB
LCP-Image≤ 100 KB AVIF200 KB
Total Page Weight≤ 500 KB1 MB
Requests≤ 3050
3rd-Party-Domains≤ 48
WarumDiese Werte basieren auf 75th-Percentile Mobile-Verbindungen (LTE, ~5 Mbps). Wer drüber liegt, scheitert an LCP < 2.5s zuverlässig auf Bestandsgeräten. Hard Limit = CI failed.
Lighthouse CI Config Empfehlung
// .lighthouserc.json
{
  "ci": {
    "collect": {
      "url": ["https://example.com"],
      "numberOfRuns": 5,
      "settings": { "preset": "desktop" }
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:seo": ["error", { "minScore": 1.0 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "interaction-to-next-paint": ["error", { "maxNumericValue": 200 }]
      }
    }
  }
}
WarumnumberOfRuns: 5 ist Pflicht — einzelne Lighthouse-Runs streuen ±10%. Median aus 5 ist stabil.
AntiLokal Lighthouse-Score 100 feiern. Localhost ohne Throttling ist meaningless — Real-World ist 4G und Mid-Range-Android. Anti: Score-Optimierung über UX stellen (z.B. komplettes Lazy-Load der Hero-Section, weil LCP-Score steigt — Nutzer sehen 3 Sekunden weiße Seite).
RUM via web-vitals Empfehlung
// npm i web-vitals
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name, value: metric.value, rating: metric.rating,
    url: location.href,
    connection: navigator.connection?.effectiveType
  });
  (navigator.sendBeacon && navigator.sendBeacon('/api/vitals', body)) ||
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
}

onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
WarumsendBeacon ist der einzige zuverlässige Weg, Daten beim Page-Unload zu senden. connection.effectiveType hilft beim Slicen: schlechte INP auf 3G erwartbar, auf 4G ein Bug.
Uptime Monitoring Empfehlung
# Uptime Kuma (self-hosted, Docker) Setup:
# - HTTPS-Check alle 60s
# - SSL-Zertifikat: Warnung 21 Tage vor Ablauf
# - Keyword-Match (z.B. "Impressum") — erkennt White-Page-Fails
# - DNS-Resolver-Check
# - Telegram/Slack-Alert bei >= 2 Failures

# SSL-Grade-Check (extern)
curl "https://api.ssllabs.com/api/v3/analyze?host=example.com&publish=off"
AntiNur HTTP-200-Check. Site kann 200 zurückgeben und kaputt sein (DB-Error im Body, leere Page, Redirect-Loop). Immer Keyword-Match. Anti: Monitoring-Endpoints öffentlich exposen ohne Rate-Limit — werden für DoS missbraucht.
19

Core Web Vitals — Performance

Google rankt schnelle Sites besser. INP hat seit März 2024 FID abgelöst. Die Targets:

LCP
< 2.5s
Largest Contentful Paint
INP
< 200ms
Interaction to Next Paint
CLS
< 0.1
Cumulative Layout Shift
LCP — Hero Image Pflicht
<!-- LCP-Bild: fetchpriority high, eager, kein lazy -->
<img
  src="/hero-1280.avif"
  srcset="/hero-640.avif 640w, /hero-1280.avif 1280w, /hero-1920.avif 1920w"
  sizes="(max-width: 768px) 100vw, 1280px"
  width="1280"
  height="720"
  alt="Aussagekräftige Bildbeschreibung"
  fetchpriority="high"
  decoding="async">

<!-- Below-the-fold: lazy -->
<img src="/below.avif" width="800" height="600"
     alt="..." loading="lazy" decoding="async">
Warumwidth + height verhindern CLS. fetchpriority="high" sagt dem Browser: lade das vor allem anderen. AVIF ist ~50% kleiner als JPEG bei gleicher Qualität. Niemals loading="lazy" auf das LCP-Bild.
INP — JS Optimierung Empfehlung
<script src="/app.js" defer></script>
<script src="/analytics.js" async></script>
<link rel="modulepreload" href="/components/heavy.js">

// Lange Tasks brechen — yield zurück an Main Thread
async function processItems(items) {
  for (const item of items) {
    processItem(item);
    if (navigator.scheduling?.isInputPending()) {
      await new Promise(r => setTimeout(r, 0));
    }
  }
}
WarumINP misst die längste Reaktionszeit auf Interaktion. Hauptgrund für hohe INP: blockierende Long-Tasks >50ms. scheduler.yield() ist 2026 in Chromium verfügbar. Fallback: setTimeout(0).
20

Image & Video Deep — Formats, Picture, CDN

Bilder sind in 99% der Fälle der Hauptgrund für schlechte LCP. Modernes Format-Stacking, korrektes srcset/sizes, CDN-Transformation, Lazy-Loading mit Augenmaß.

<picture> — Format Stacking Pflicht
<picture>
  <!-- AVIF: 50% kleiner, 95%+ Browser 2026 -->
  <source
    type="image/avif"
    srcset="/img/hero-640.avif 640w, /img/hero-1280.avif 1280w, /img/hero-1920.avif 1920w"
    sizes="(max-width: 768px) 100vw, 1280px">

  <!-- WebP Fallback (alte Safari/Android) -->
  <source
    type="image/webp"
    srcset="/img/hero-640.webp 640w, /img/hero-1280.webp 1280w"
    sizes="(max-width: 768px) 100vw, 1280px">

  <!-- JPEG Default Fallback -->
  <img
    src="/img/hero-1280.jpg"
    srcset="/img/hero-640.jpg 640w, /img/hero-1280.jpg 1280w"
    sizes="(max-width: 768px) 100vw, 1280px"
    width="1280" height="720"
    alt="Konkrete Bildbeschreibung mit Kontext"
    fetchpriority="high"
    decoding="async">
</picture>
Warumsizes muss zur tatsächlichen CSS-Breite passen — wenn Bild im Layout 800px breit ist, lade nicht 1920px. width/height als HTML-Attribute reservieren Layout-Platz vor Decode → 0 CLS.
AntiBackground-Images für Hero/LCP-Content. CSS-background-image wird vom Browser deprioritized — LCP-Penalty. Anti: loading="lazy" auf Above-the-Fold-Bilder. Anti: srcset ohne sizes bei width-descriptors — Browser muss raten und nimmt meist das größte.
Image-CDN URL-Params Empfehlung
// Cloudflare Image Resizing
"https://example.com/cdn-cgi/image/width=1280,format=auto,quality=75/img/hero.jpg"

// imgproxy (self-hosted)
"https://img.example.com/insecure/rs:fit:1280:0/q:75/f:avif/plain/https://...jpg"

// Cloudinary
"https://res.cloudinary.com/demo/image/upload/w_1280,q_auto,f_auto/sample.jpg"

// Vercel/Next.js Image Optimization (built-in)
"/_next/image?url=%2Fimg%2Fhero.jpg&w=1280&q=75"
WarumOn-the-fly-Transformation eliminiert manuelles Variant-Generieren. format=auto serviert AVIF an moderne Browser, WebP/JPEG an alte. quality=75 ist visuell verlustfrei für 95% der Cases.
Video-Element optimal Empfehlung
<video
  poster="/v/poster.avif"
  preload="metadata"
  controls
  playsinline
  width="1280" height="720">
  <source src="/v/clip.av1.mp4" type="video/mp4; codecs=av01">
  <source src="/v/clip.h264.mp4" type="video/mp4; codecs=avc1">
  <track kind="captions" srclang="de"
         src="/v/clip.de.vtt" label="Deutsch" default>
  <track kind="captions" srclang="en"
         src="/v/clip.en.vtt" label="English">
</video>
Warumpreload="metadata" ist Default-richtig — "auto" lädt das ganze Video sofort, "none" killt Time-to-First-Frame. AV1-Codec ist 30% kleiner als H.264. Captions sind WCAG-Pflicht (1.2.2).
AntiAutoplay mit Sound. Browser blocken's, Nutzer hassen's. Wenn Autoplay sein muss: muted autoplay playsinline. Anti: preload="auto" auf Listing-Seiten mit mehreren Videos — Megabyte-Schleuder.
21

Security+ — SRI · security.txt · WAF · Dependencies

Komplement zu Layer 01 (Header). Hier geht's um den Rest der Defense-in-Depth-Pyramide: Subresource Integrity gegen kompromittierte CDN-Skripte, security.txt für Bug-Bounty-Workflow, Dependency-Hygiene gegen Supply-Chain-Attacks, WAF + Bot-Protection gegen aktive Angriffe.

Subresource Integrity (SRI) Pflicht
<!-- Skripte/Styles von externen CDNs MIT Hash absichern -->
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"></script>

<link
  rel="stylesheet"
  href="https://cdn.example.com/styles.css"
  integrity="sha384-..."
  crossorigin="anonymous">

# Hash erzeugen:
curl -s https://cdn.example.com/lib.js | \
  openssl dgst -sha384 -binary | openssl base64 -A
# → sha384-oqVuAfXRKap7fdgcCY5uykM6+R9G...

# Online-Generator: srihash.org
WarumSRI verhindert Polyfill.io-Style-Attacks: 2024 wurde polyfill.io übernommen und über 100.000 Sites mit Malware injiziert. Mit SRI-Hash hätten Browser das modifizierte Skript abgelehnt. Pflicht für jedes externe Skript — auch große CDNs (jsDelivr, unpkg) sind nicht 100% safe.
AntiSRI für eigenen Code mit häufigen Updates. Bei jedem Build neuer Hash → manuelle Pflege brutal. Lösung: eigenen Code via gleicher Origin (kein cross-origin), dann ist SRI nicht nötig. Anti: SRI ohne crossorigin="anonymous" — Browser verweigert die Validierung. Anti: Skripte ohne Versions-Pin laden (cdn.foo.com/lib/latest) — Hash funktioniert nicht, weil Inhalt sich ändert.
/.well-known/security.txt (RFC 9116) Empfehlung
# /.well-known/security.txt (Pflicht-Pfad)
# Jede Site sollte das haben — kostet 5 Min, schützt vor Frust auf beiden Seiten

Contact: mailto:security@example.com
Contact: https://example.com/security/report
Expires: 2027-01-01T00:00:00.000Z
Encryption: https://example.com/.well-known/pgp-key.txt
Acknowledgments: https://example.com/security/hall-of-fame
Preferred-Languages: de, en
Canonical: https://example.com/.well-known/security.txt
Policy: https://example.com/security/policy

# Optional aber empfohlen: signieren mit PGP
# gpg --clearsign security.txt
WarumSicherheitsforscher und Bug-Hunter brauchen einen klaren Kontaktweg. Ohne security.txt landen Reports im allgemeinen Support-Postfach, werden ignoriert, oder gehen direkt in die Öffentlichkeit (Full Disclosure). Expires-Feld erinnert dich, das Dokument zu pflegen — abgelaufenes security.txt ist schlimmer als keins.
Dependency-Hygiene Pflicht
# 1. Lockfile committen (package-lock.json, pnpm-lock.yaml, yarn.lock)
#    Reproducible Builds + verhindert Supply-Chain-Drift

# 2. Renovate (preferred) oder Dependabot konfigurieren
# renovate.json
{
  "extends": ["config:base"],
  "vulnerabilityAlerts": { "enabled": true, "automerge": true },
  "packageRules": [
    {
      "matchUpdateTypes": ["patch", "minor"],
      "matchCurrentVersion": "!/^0/",
      "automerge": true
    }
  ],
  "schedule": ["before 6am on Monday"]
}

# 3. Pre-Commit + CI: Audit-Checks
npm audit --audit-level=high
pnpm audit --audit-level=high
yarn npm audit --severity high

# 4. Snyk / Socket.dev für tiefere Analyse (FOSS + commercial)
npx snyk test
npx socket security audit

# 5. Lockfile-Lint: keine Github-URLs, keine ungetaggten Versionen
npx lockfile-lint --path package-lock.json --type npm \
  --validate-https --allowed-hosts npm
Antinpm install --no-save oder ohne Lockfile. Build heute funktioniert, morgen nicht — versteckte Version-Drifts. Anti: Auto-Merge für Major-Updates. Major = Breaking-Changes, müssen reviewt + getestet werden. Patch/Minor automatisch ist OK. Anti: Dependencies aus GitHub-URLs ("foo": "github:user/repo") — kein Hash-Pinning, jeder mit Repo-Zugriff kann Backdoor reinschmuggeln. Anti: Typosquatting nicht prüfen — node-ipc vs node_ipc, react-router-dom vs react-router-dom-v6. Socket.dev erkennt das.
WAF + Bot Protection Empfehlung
# Cloudflare (gratis Tier reicht für Solo/SMB)
#   - Web Application Firewall (OWASP-Regeln, kostenlos)
#   - Rate Limiting (10k Requests/Mo gratis, dann ab 5$/Mo)
#   - Bot Fight Mode (gratis)
#   - Country-Blocking (z.B. nur DACH erlauben)
#   - Challenge-Pages für verdächtige IPs

# Konfiguration über Cloudflare-Dashboard:
#   1. Security Level: Medium (für Standard-Sites)
#   2. Bot Fight Mode: Ein
#   3. WAF Managed Rules: OWASP + Cloudflare-Default ein
#   4. Rate Limiting: 100 req/min auf /api/* (für Form-Endpoints)
#   5. Page Rules: /admin/* → "I'm Under Attack"-Mode

# Alternative: AWS WAF (gleiche Features, andere Preisstruktur)
# Self-hosted: ModSecurity + Nginx (Aufwand, nur bei Compliance-Zwang)

# Fail2Ban auf Server-Level (gegen SSH-Brute-Force, Login-Versuche)
# /etc/fail2ban/jail.local
[sshd]
maxretry = 3
findtime = 600
bantime = 86400
AntiWAF zu aggressiv konfigurieren. "I'm Under Attack"-Mode global statt nur für /admin/* blockt legitime User mit JS-Challenge — Conversion-Killer. Anti: Country-Blocking ohne Bedacht — Cloud-Hoster (AWS, GCP) routen oft über andere Länder, du sperrst dich selber aus. Anti: Rate-Limit ohne Whitelist für eigene Healthchecks/Monitoring → false-positive Alerts.
22

Forms & Anti-Spam — Honeypot, Turnstile, UX-Patterns

Das Kontaktformular ist bei jeder Service-Site das wichtigste Conversion-Asset — und gleichzeitig das Spam-Einfallstor Nummer eins. Ohne Anti-Spam: 50+ Bot-Submissions/Tag. Mit Google reCAPTCHA: DSGVO-Risiko + Conversion-Killer durch hässliches Widget. Lösung 2026: Honeypot + Cloudflare Turnstile (gratis, EU-konform, unsichtbar).

Form Markup — A11y + UX Pflicht
<form action="/api/contact" method="POST" novalidate>
  <!-- Honeypot — versteckt für User, Bots füllen aus -->
  <div aria-hidden="true" style="position:absolute;left:-9999px">
    <label>Website (leer lassen)
      <input type="text" name="website" tabindex="-1" autocomplete="off">
    </label>
  </div>

  <!-- Echte Felder mit Best-Practice-Attributen -->
  <label for="name">Name</label>
  <input id="name" name="name" type="text"
         autocomplete="name" required minlength="2"
         aria-describedby="name-err">

  <label for="email">E-Mail</label>
  <input id="email" name="email" type="email"
         autocomplete="email" inputmode="email"
         required spellcheck="false">

  <label for="tel">Telefon (optional)</label>
  <input id="tel" name="tel" type="tel"
         autocomplete="tel" inputmode="tel">

  <label for="msg">Nachricht</label>
  <textarea id="msg" name="msg" rows="5" required
            minlength="10" maxlength="5000"></textarea>

  <!-- Cloudflare Turnstile (unsichtbar, DSGVO-konform) -->
  <div class="cf-turnstile" data-sitekey="YOUR_KEY"></div>

  <!-- Consent-Checkbox (Pflicht: Datenverarbeitung) -->
  <label>
    <input type="checkbox" name="consent" required>
    Ich stimme der <a href="/datenschutz">Datenverarbeitung</a> zu.
  </label>

  <button type="submit">Anfrage senden</button>
</form>
Warumautocomplete-Attribute nutzen Browser-AutoFill — füllt Felder in Sekunden, +30% Conversion. inputmode="email" öffnet auf Mobile die richtige Tastatur (mit @-Zeichen). novalidate deaktiviert Browser-Default-Validierung zugunsten eigener (kontrollierte UX). spellcheck="false" bei E-Mail/Tel verhindert nervige Rotunterstreichung.
Cloudflare Turnstile Empfehlung
<!-- Skript laden (async, defer) -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

// Server-Side Verification (Pflicht — Client-Token allein bedeutet nichts)
async function verifyTurnstile(token, ip) {
  const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
    method: 'POST',
    body: new URLSearchParams({
      secret: process.env.TURNSTILE_SECRET,
      response: token,
      remoteip: ip
    })
  });
  const data = await res.json();
  return data.success;
}

// Vorteile gegenüber reCAPTCHA:
//   - DSGVO: Cloudflare hat EU-Ziele, ADV verfügbar
//   - Unsichtbar in 95% der Fälle (kein "Ich bin kein Roboter")
//   - Kostenlos, unbegrenzt
//   - Kein Google-Tracking-Skript

// Alternativen:
//   - hCaptcha (auch DSGVO-OK)
//   - Friendly Captcha (DACH, EU-Hosting)
//   - mCaptcha (FOSS, self-hosted)
AntiGoogle reCAPTCHA v2/v3. Lädt Google-Skript bei JEDEM Pageload (auch ohne Form-Interaktion), überträgt IP an Google, ist DSGVO ohne Consent-Banner unzulässig (mehrere DPA-Entscheidungen 2023/24). reCAPTCHA v3 zusätzlich: Score-basiert, false-positives, frustriert legitime Nutzer. Anti: Turnstile-Token nur Client-Side prüfen — Bots können den Token-Mechanismus umgehen, Server-Verification ist Pflicht. Anti: Honeypot ohne aria-hidden + tabindex=-1 — Screenreader und Tastatur-Nav stolpern darüber.
Form-UX — Loading, Errors, Success Empfehlung
// Progressive Enhancement: Form funktioniert ohne JS, JS macht's besser
const form = document.querySelector('form');
const btn = form.querySelector('button[type=submit]');

form.addEventListener('submit', async (e) => {
  e.preventDefault();

  // Double-Submit-Schutz
  if (btn.disabled) return;
  btn.disabled = true;
  btn.textContent = 'Wird gesendet…';
  btn.setAttribute('aria-busy', 'true');

  try {
    const formData = new FormData(form);
    const res = await fetch(form.action, {
      method: 'POST',
      body: formData,
      headers: { 'Accept': 'application/json' }
    });

    if (!res.ok) throw new Error('Failed');

    // Success: Form ersetzen mit Bestätigung (Optimistic UI)
    form.outerHTML = `
      <div role="status" aria-live="polite" class="success">
        <h2>Vielen Dank!</h2>
        <p>Wir melden uns innerhalb von 24h.</p>
      </div>
    `;

    // Track Conversion
    window.plausible?.('Contact-Form-Submit');

  } catch (err) {
    // Konkrete, hilfreiche Error-Message
    showError('Etwas ist schiefgelaufen. Bitte ruf uns an: 0941 1234567');
    btn.disabled = false;
    btn.textContent = 'Erneut senden';
    btn.removeAttribute('aria-busy');
  }
});
AntiGenerische Error-Messages ("Es ist ein Fehler aufgetreten"). User strandet ohne Ausweg. Immer konkrete Fallback-Aktion (Telefon, alternative Mail). Anti: Form nach Submit nicht deaktivieren — User klickt 5× auf den Button, du bekommst 5 identische Anfragen. Anti: Success-Message mit Redirect zu Thank-You-Page ohne aria-live — Screenreader sagen nichts, blinde User wissen nicht ob's geklappt hat.
Server-Side Validation + Sanitization Pflicht
// Niemals Client-Validierung vertrauen — IMMER Server-seitig prüfen
import { z } from 'zod';
import DOMPurify from 'isomorphic-dompurify';

const ContactSchema = z.object({
  name: z.string().min(2).max(100).trim(),
  email: z.string().email().max(254).toLowerCase(),
  tel: z.string().max(30).optional(),
  msg: z.string().min(10).max(5000),
  consent: z.literal('on'),                // Pflicht-Checkbox
  website: z.string().max(0).optional(),     // Honeypot — MUSS leer sein
  cfTurnstileResponse: z.string()
});

export async function POST(req) {
  const body = await req.formData();
  const data = Object.fromEntries(body);

  // 1. Schema-Validierung
  const parsed = ContactSchema.safeParse(data);
  if (!parsed.success) return Response.json({ error: 'Invalid' }, { status: 400 });

  // 2. Honeypot-Check
  if (parsed.data.website) return Response.json({ ok: true });  // silent drop

  // 3. Turnstile-Verification
  const ok = await verifyTurnstile(parsed.data.cfTurnstileResponse, req.ip);
  if (!ok) return Response.json({ error: 'Bot detected' }, { status: 403 });

  // 4. Rate-Limit per IP (siehe Layer 20)

  // 5. Sanitize HTML aus User-Input (für E-Mail-Body)
  const safeMsg = DOMPurify.sanitize(parsed.data.msg, { ALLOWED_TAGS: [] });

  // 6. Send via Resend / Postmark / Mailgun
  await sendEmail({ ...parsed.data, msg: safeMsg });

  return Response.json({ ok: true });
}
WarumBots befüllen Forms direkt per HTTP-POST, umgehen Client-JS komplett. Server-Validation ist nicht optional. Honeypot-Spam wird still gedroppt (200 OK ohne Aktion) — Bots merken nicht, dass sie geblockt wurden, und versuchen es nicht mit anderem Vektor.
23

Trust Signals — Reviews, Logo Wall, Direct Contact

Conversion-Layer: was direkt am Money-Outcome hängt. Soziale Beweise, niederschwellige Kontakt-Kanäle, sichtbare Trust-Marker. Bei Handwerker- und Service-Sites entscheiden diese Elemente über die Anfragequote — alles andere optimiert nur den Traffic dorthin.

Reviews-Integration mit Schema Pflicht
<!-- Sichtbare Reviews + JSON-LD AggregateRating -->
<section aria-labelledby="reviews-h">
  <h2 id="reviews-h">Was Kunden sagen</h2>

  <div class="rating-summary">
    <strong>4.9 ★★★★★</strong> aus
    <a href="https://g.co/kgs/abc" rel="noopener">147 Google-Bewertungen</a>
  </div>

  <article class="review">
    <blockquote>Echte, ungekürzte Aussage des Kunden.</blockquote>
    <footer>— Max Mustermann, Geschäftsführer Müller GmbH ·
      <time datetime="2026-03-12">März 2026</time>
    </footer>
  </article>
</section>

<!-- JSON-LD: AggregateRating + Reviews (in Service oder LocalBusiness) -->
{
  "@type": "LocalBusiness",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.9",
    "reviewCount": "147",
    "bestRating": "5"
  },
  "review": [{
    "@type": "Review",
    "reviewRating": { "@type": "Rating", "ratingValue": "5" },
    "author": { "@type": "Person", "name": "Max Mustermann" },
    "reviewBody": "Echte Aussage...",
    "datePublished": "2026-03-12"
  }]
}
AntiFake-Reviews oder erfundene AggregateRating. Google's Spam-Update erkennt das (gleiches Sprachmuster, kein zweites Vorkommen im Web), Penalty droht. Anti: Reviews ohne Quelle ("Kunde X war zufrieden") — wirken erfunden, killen Trust. Anti: Reviews-Widget einer 3rd-Party-Plattform via iframe — DSGVO-Risiko (Daten-Transfer ohne Consent), Performance-Killer. Lieber Reviews via API holen, eigenes HTML rendern.
Logo Wall — Social Proof Empfehlung
<section aria-label="Referenz-Kunden">
  <p>Vertrauen von 100+ Unternehmen in der Region:</p>
  <ul class="logo-wall">
    <li><img src="/logos/kunde-a.svg" alt="Kunde A GmbH"
            width="120" height="40" loading="lazy"></li>
    <!-- 6-12 Logos optimal -->
  </ul>
</section>

<style>
.logo-wall {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 2rem;
  align-items: center;
  list-style: none;
  padding: 0;
}
.logo-wall img {
  filter: grayscale(1) opacity(0.6);
  transition: filter .2s;
  width: 100%;
  height: auto;
}
.logo-wall img:hover { filter: none; }
</style>
WarumGrayscale-Logos (Color-on-Hover) wirken hochwertiger und vermeiden Brand-Konflikte zwischen verschiedenen Kunden-CIs. 6-12 Logos ist der Sweet Spot — weniger wirkt mager, mehr wirkt austauschbar. Logos selbst hosten (SVG), nie hotlinken (DSGVO + Brand-Risiko bei Linkbruch).
AntiKunden-Logos ohne Erlaubnis. Markenrecht. Schriftlich einholen, am besten direkt im Werkvertrag. Anti: Carousel mit auto-rotation — schadet Lesbarkeit, klassischer Carousel-Anti-Pattern (Notre Dame Studie: ~1% Klickrate auf Slide 2).
Direct Contact — WhatsApp, Phone, Cal.com Pflicht
<!-- Click-to-Call (Mobile-First) -->
<a href="tel:+4994112345670" class="btn-call">
  📞 0941 1234567-0
</a>

<!-- Click-to-WhatsApp (mit Pre-Filled Message) -->
<a href="https://wa.me/4915112345678?text=Hallo,%20ich%20interessiere%20mich%20f%C3%BCr..."
   rel="noopener" target="_blank" class="btn-whatsapp">
  WhatsApp · Antwort < 1h
</a>

<!-- Cal.com Booking (DSGVO: erst nach Consent laden) -->
<button type="button" data-cal-link="max/30min">
  Termin online buchen →
</button>
<script src="https://app.cal.com/embed/embed.js" defer></script>

<!-- Sticky Mobile Action Bar (auf Mobile, untere Kante) -->
<div class="mobile-action-bar">
  <a href="tel:...">📞 Anrufen</a>
  <a href="https://wa.me/...">💬 WhatsApp</a>
  <a href="#kontakt">✉ Anfrage</a>
</div>

<style>
@media (max-width: 768px) {
  .mobile-action-bar {
    position: fixed; bottom: 0; left: 0; right: 0;
    display: grid; grid-template-columns: 1fr 1fr 1fr;
    background: var(--bg); border-top: 1px solid var(--border);
    padding: 8px env(safe-area-inset-right) calc(8px + env(safe-area-inset-bottom)) env(safe-area-inset-left);
    z-index: 50;
  }
}
</style>
WarumBei Handwerker-/Service-Sites ist Telefon der Conversion-Channel #1 — Mobile-User wollen direkt anrufen, nicht ein Formular ausfüllen. WhatsApp Business ist DACH-Standard für niederschwelligen Erstkontakt (vor allem bei Boomern und Gen X). Cal.com erlaubt Online-Booking ohne Hin-und-her, aber: Cal.com-Skript erst nach Consent laden (DSGVO).
AntiTelefonnummer als Bild oder Plain-Text ohne tel:-Link. Mobile-User können nicht mit einem Tap anrufen — Conversion-Killer. Anti: Telefonnummer-Maskierung gegen Spam (z.B. JS-Reveal-on-Click) — Crawler indexieren's nicht, lokales SEO leidet. Lieber: Spam-Filter beim Anrufannahme. Anti: Cal.com auto-load ohne Consent — überträgt User-Daten an Cal.com (auch self-hosted: Domain-Tracking).
Above-the-Fold Trust-Anker Empfehlung

Was gehört in den ersten Bildschirm (mobile)

  • Klare H1 mit Outcome (nicht Service): "In 90 Tagen 5–15 Aufträge mehr" statt "Webdesign"
  • 1 Sub-Headline mit Garantie/USP ("oder Geld zurück")
  • 1 Hero-Image mit Mensch-Bezug (Vertrauen entsteht über Gesichter, nicht Office-Stockphotos)
  • 2 CTAs — primär (Hauptanfrage) + sekundär (Live-Tool/Demo/Kostenloser Audit)
  • 1 Trust-Marker sichtbar: Sterne + Anzahl Reviews ODER bekannter Kunde ODER Auszeichnung
  • Slot-Verfügbarkeit echte Scarcity ("KW 22: 2 freie Slots") — nur wenn wahr
  • Social Proof als Ein-Satz-Aussage über H1 oder als Pill ("Mehr als 100 Handwerker vertrauen uns")
AntiHero-Carousel mit 5 Slides. Erst Slide 1 wird gesehen, alle anderen verschwendet. Eine starke Botschaft > fünf mittelmäßige. Anti: "Über 20 Jahre Erfahrung" als USP — Standard-Floskel, jeder sagt's. Lieber: konkrete Outcomes mit Zahlen. Anti: Newsletter-Popup nach 5s — User nicht mal angekommen, schon belästigt. Anti: Cookie-Banner so groß, dass er Above-the-Fold zerstört.
24

Auth 2026 — Passkeys, Magic Links, OAuth

Falls Member-Bereich oder Login existiert: Passwörter sind 2026 Legacy. Passkeys (WebAuthn) sind in allen großen Plattformen produktiv (Apple, Google, Microsoft, 1Password, Bitwarden), Magic Links der pragmatische Mittelweg, OAuth mit PKCE der Standard für 3rd-Party-Auth. httpOnly-Cookies schlagen localStorage in jeder Hinsicht.

→ Layer 21 · Security+ — Security-Querschnitt — SRI, WAF, Dependencies

Passkeys (WebAuthn) Empfehlung
// Registrierung (Server stellt challenge bereit)
const credential = await navigator.credentials.create({
  publicKey: {
    challenge: new Uint8Array(serverChallenge),
    rp: { name: 'Brand', id: 'example.com' },
    user: {
      id: new TextEncoder().encode(userId),
      name: 'user@example.com',
      displayName: 'Max Mustermann'
    },
    pubKeyCredParams: [
      { type: 'public-key', alg: -7 },    // ES256
      { type: 'public-key', alg: -257 }   // RS256
    ],
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred'
    },
    timeout: 60000
  }
});

// Login (gleicher Flow, navigator.credentials.get)
const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: new Uint8Array(serverChallenge),
    rpId: 'example.com',
    userVerification: 'preferred'
  }
});

// Server-Side: SimpleWebAuthn Library für Verifikation
// npm i @simplewebauthn/server @simplewebauthn/browser
import { verifyRegistrationResponse, verifyAuthenticationResponse }
  from '@simplewebauthn/server';
WarumPasskeys = phishingsicher (gebunden an Domain) + kein Passwort-Reuse + biometrisch verifiziert + Sync via iCloud/Google. Conversion: User klickt 1×, Face-ID, eingeloggt — keine "Passwort vergessen"-Schleife. SimpleWebAuthn ist die Standard-Lib (TypeScript, gut dokumentiert, FOSS).
AntiPasskey-only ohne Fallback. Nicht jeder User hat einen Passkey-fähigen Authenticator (alte Geräte, Linux ohne YubiKey). Immer Magic-Link oder OAuth als Fallback. Anti: rpId mit Subdomain-Mismatch — Passkey für example.com funktioniert nicht auf app.example.com ohne explizite Konfiguration.
Magic Links Empfehlung
// Server: Token erzeugen, signieren, per Mail senden
import { SignJWT, jwtVerify } from 'jose';

async function requestMagicLink(email) {
  const token = await new SignJWT({ email, purpose: 'login' })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('15m')
    .setJti(crypto.randomUUID())            // für Single-Use-Tracking
    .sign(secret);

  const url = `https://example.com/auth/verify?t=${token}`;
  await sendMail({
    to: email,
    subject: 'Dein Login-Link',
    text: `Klicke hier zum Einloggen (15 Min gültig): ${url}`
  });

  // JTI in Redis speichern → Single-Use erzwingen
  await redis.set(`magic:${jti}`, 'pending', 'EX', 900);
}

// Verify (Server-Endpoint)
async function verify(token) {
  const { payload } = await jwtVerify(token, secret);
  const used = await redis.get(`magic:${payload.jti}`);
  if (used !== 'pending') throw new Error('Already used or expired');
  await redis.del(`magic:${payload.jti}`);   // Single-Use

  // Session erstellen (httpOnly cookie)
  return createSession(payload.email);
}
WarumMagic Links eliminieren Passwörter ganz, sind einfacher als Passkeys (kein Geräte-Setup), funktionieren auf jedem Browser. 15 Min Gültigkeit + Single-Use via JTI ist der Mindeststandard. Bekannte Implementierungen: Supabase Auth, Clerk, Lucia, Auth.js — alle FOSS oder günstig.
OAuth mit PKCE Pflicht
// PKCE = Proof Key for Code Exchange — Pflicht 2026 (auch für Confidential Clients)

// 1. Code Verifier (random 43-128 chars) + Challenge erzeugen
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64url(await crypto.subtle.digest('SHA-256',
  new TextEncoder().encode(verifier)));

// 2. Authorize-URL bauen, User redirecten
const authUrl = new URL('https://oauth.provider.com/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://example.com/auth/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('state', csrfToken);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// Verifier in httpOnly Cookie speichern (NICHT localStorage!)
res.cookie('pkce_verifier', verifier, { httpOnly: true, secure: true, sameSite: 'lax' });

// 3. Callback: Token-Exchange mit Verifier
const tokens = await fetch('https://oauth.provider.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: 'https://example.com/auth/callback',
    client_id: CLIENT_ID,
    code_verifier: verifier
  })
});
AntiOAuth Implicit Flow. Seit OAuth 2.1 deprecated. Immer Authorization Code + PKCE. Anti: Tokens in localStorage. JS-zugänglich → XSS klaut alles. Lieber httpOnly-Cookie + Same-Site=Strict. Anti: state-Parameter weglassen — CSRF-Lücke. Anti: redirect_uri nicht exakt validieren (Wildcard!) — Open-Redirect-Angriff.
Session Management Pflicht
// httpOnly + Secure + SameSite Cookie für Sessions
res.cookie('session', sessionToken, {
  httpOnly: true,           // JS kann nicht zugreifen → XSS-Schutz
  secure: true,             // Nur HTTPS
  sameSite: 'lax',           // CSRF-Basisschutz, erlaubt Top-Level-Nav
  maxAge: 1000 * 60 * 60 * 24 * 7,  // 7 Tage
  path: '/',
  domain: '.example.com'      // Subdomain-Sharing, mit Bedacht
});

// Session-Rotation bei Privilege-Escalation
// Beispiel: nach Login NEUE Session-ID erzeugen, alte invalidieren
async function rotateSession(oldId, userId) {
  const newId = crypto.randomUUID();
  await redis.del(`session:${oldId}`);
  await redis.set(`session:${newId}`, JSON.stringify({ userId }), 'EX', 86400 * 7);
  return newId;
}

// Logout: Session in DB löschen, Cookie clearen
async function logout(req, res) {
  await redis.del(`session:${req.cookies.session}`);
  res.clearCookie('session');
}
AntiJWT als Session-Token + localStorage. JWTs sind nicht serverseitig invalidierbar (außer mit Blocklist) — bei Account-Sperre läuft Token bis Expiry weiter. Stateful Sessions (Redis/DB) erlauben sofortiges Invalidate. Anti: SameSite=None ohne Notwendigkeit — öffnet CSRF-Loch. Anti: Lange Session-Lifetimes ohne Re-Auth-Prompts für sensitive Aktionen (Passwort ändern, Zahlungen).
25

Platform Features — View Transitions, :has(), Container Queries, Popover

Was eine Site sich "2026" anfühlen lässt — alle nativ in Chromium und Safari, kein JS-Framework nötig. View Transitions ersetzen SPA-Animationen, :has() killt 50% von BEM-Komplexität, Container Queries machen Component-Driven Design real, Popover API ersetzt Floating-UI-Libraries.

View Transitions API Empfehlung
/* Cross-Document View Transitions (Multi-Page-Apps!) */
/* Funktioniert mit Astro, klassischem MPA, ohne SPA-Framework */
@view-transition {
  navigation: auto;
}

/* Element-spezifische Transitions */
.hero-image {
  view-transition-name: hero;
}

/* Custom Animation */
::view-transition-old(hero) {
  animation: fade-out 200ms ease-out;
}
::view-transition-new(hero) {
  animation: fade-in 200ms ease-in;
}

/* Same-Document (z.B. nach State-Update) */
<script>
if (document.startViewTransition) {
  document.startViewTransition(() => {
    updateDOM();
  });
}
</script>
WarumCross-Document View Transitions sind der Game-Changer für MPAs — du bekommst SPA-Feel ohne SPA-Komplexität (kein Hydration-Overhead, kein Router-Lib, kein State-Management). Astro hat First-Class-Support. Browser-Support 2026: Chrome/Edge stabil, Safari ab 18.2 stabil, Firefox in Progressive Rollout.
Speculation Rules API Empfehlung
<!-- Modernste Form von Prefetching, ersetzt rel="prefetch" -->
<script type="speculationrules">
{
  "prerender": [{
    "where": { "href_matches": "/blog/*" },
    "eagerness": "moderate"
  }],
  "prefetch": [{
    "where": { "href_matches": "/*" },
    "eagerness": "conservative"
  }]
}
</script>

<!-- Eagerness-Level: -->
<!--   immediate: sofort nach Page-Load (sparsam!) -->
<!--   eager: bei Hover/Focus (~200ms vor Click) -->
<!--   moderate: bei Hover/Focus mit Delay -->
<!--   conservative: nur bei mousedown/touchstart -->
WarumPrerender lädt die ganze Folge-Seite inkl. JS-Execution im Hintergrund — Click-to-Render unter 100ms. Eagerness "moderate" ist Sweet-Spot: nicht jeder Hover prerendert, nur eindeutige Intent-Signals. Spart Crawl-Budget gegenüber prefetch-Spam.
Anti"immediate" eagerness mit vielen Links. Browser prerendert alles → Bandbreitenverschwendung, Server-Last, Analytics-Spam (jede prerendered Page zählt sonst als Pageview). Anti: Prerender für Pages mit Side-Effects (z.B. Newsletter-Confirm-Link via GET) — Confirm wird ausgelöst, ohne dass User klickt.
CSS :has() + Container Queries Empfehlung
/* :has() — Parent-Selector (vorher: nur via JS möglich) */

/* Card hat ein Image → anderes Layout */
.card:has(img) {
  display: grid;
  grid-template-columns: auto 1fr;
}

/* Form-Validation ohne JS-Klassen */
form:has(input:invalid) button[type=submit] {
  opacity: 0.5;
  pointer-events: none;
}

/* Empty-State ohne JS */
.list:not(:has(.list-item)) .empty-state {
  display: block;
}

/* Container Queries — Component-Driven Design */
.card-container {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 400px) {
  .card { display: grid; grid-template-columns: 1fr 2fr; }
}
@container card (min-width: 700px) {
  .card-title { font-size: 2rem; }
}

/* Cascade Layers — Specificity-Hell vermeiden */
@layer reset, base, components, utilities;

@layer reset { /* normalize.css */ }
@layer components { .button { padding: 1rem; } }
@layer utilities { .p-2 { padding: .5rem !important; } }
/* Reihenfolge oben definiert Priorität — !important nicht mehr nötig */
Warum:has() killt 50% des klassischen "Klassen-an-Parent-toggeln-mit-JS"-Patterns. Container Queries sind der Schlüssel zu echtem Component-Driven CSS — eine Card weiß nicht, ob sie in der Sidebar oder im Hero rendert, sie passt sich ihrem Container an. Cascade Layers ersetzen das Specificity-Wettrüsten mit !important.
Popover API + Anchor Positioning Nice-to-have
<!-- Native Popover — kein Floating-UI-Lib mehr nötig -->
<button popovertarget="menu">Menu</button>
<div id="menu" popover>
  Inhalt — automatisch:
  <ul>
    <li>Top-Layer-Rendering (über allem){' '}</li>
    <li>Light-Dismiss (ESC, Click outside){' '}</li>
    <li>Fokus-Management{' '}</li>
  </ul>
</div>

<style>
/* Anchor Positioning — Popover relativ zu Button */
[popovertarget="menu"] {
  anchor-name: --menu-btn;
}
#menu {
  position-anchor: --menu-btn;
  inset-area: bottom span-right;
  margin-block-start: .5rem;
}
</style>

<!-- Dialog für Modals -->
<dialog id="my-modal">
  <form method="dialog">
    <p>Modal-Inhalt</p>
    <button>Schließen</button>
  </form>
</dialog>
<script>document.querySelector('#my-modal').showModal();</script>
WarumPopover API + Dialog-Element ersetzen ~80% der Anwendungsfälle für Floating-UI / Headless-UI / Reach-UI — kein npm-Package, kein JS-Lib, A11y eingebaut (Fokus-Trap, ESC-Dismiss). Anchor Positioning erspart die manuelle position-calculation.
26

Platform Snippets — Astro · Next.js · WordPress

Die gleichen Layer, übersetzt in die drei häufigsten DACH-Stacks. Astro für Marketing/Content, Next.js für App-lastige Projekte, WordPress weil 40% des Webs.

Astro astro.config.mjs Pflicht
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
import robotsTxt from 'astro-robots-txt';

export default defineConfig({
  site: 'https://example.com',
  trailingSlash: 'never',
  build: { format: 'file' },
  prefetch: { prefetchAll: true, defaultStrategy: 'viewport' },
  integrations: [
    sitemap({
      i18n: { defaultLocale: 'de', locales: { de: 'de-DE', en: 'en-US' } },
      filter: (page) => !page.includes('/admin')
    }),
    robotsTxt({
      policy: [
        { userAgent: '*', allow: '/', disallow: ['/admin'] },
        { userAgent: 'GPTBot', disallow: '/' },
        { userAgent: 'CCBot', disallow: '/' }
      ]
    })
  ]
});
Warumprefetch.viewport lädt Links erst beim Scroll ins Viewport. trailingSlash: 'never' + format: 'file' verhindert Redirect-Ketten und Canonical-Konflikte.
AntiTrailing Slash mischen. /about und /about/ sind für Google zwei URLs. Inkonsistenz halbiert den PageRank-Pass-Through. Anti: output: 'server' verwenden, wenn die Site eigentlich statisch sein könnte — kostet TTFB ohne Mehrwert.
Next.js generateMetadata (App Router) Pflicht
// app/layout.tsx — Root Metadata
import type { Metadata } from 'next';

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: { default: 'Brand', template: '%s | Brand' },
  alternates: {
    canonical: '/',
    languages: { 'de-DE': '/', 'en-US': '/en' }
  },
  openGraph: {
    type: 'website', locale: 'de_DE',
    siteName: 'Brand',
    images: [{ url: '/og.jpg', width: 1200, height: 630 }]
  },
  twitter: { card: 'summary_large_image' },
  robots: { index: true, googleBot: { 'max-snippet': -1 } }
};

// app/blog/[slug]/page.tsx — Dynamic
export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return {
    title: post.title,
    description: post.excerpt,
    alternates: { canonical: `/blog/${slug}` }
  };
}
WarummetadataBase macht alle relativen URLs (OG-Bilder, Canonical) automatisch absolut — Hauptgrund für kaputte OG-Previews. template: '%s | Brand' spart Repetition.
AntiuseEffect für SEO-Tags. Tags müssen im SSR-HTML stehen, nicht client-seitig injiziert. Crawler führen JS oft nicht aus oder priorisieren initial-HTML. Das alte next/head aus dem Pages Router ist im App Router obsolet — nie mischen.
Next.js sitemap.ts & robots.ts Pflicht
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();
  return [
    { url: 'https://example.com', lastModified: new Date(), priority: 1 },
    ...posts.map(p => ({
      url: `https://example.com/blog/${p.slug}`,
      lastModified: p.updatedAt,
      changeFrequency: 'monthly' as const
    }))
  ];
}

// app/robots.ts
export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      { userAgent: '*', allow: '/', disallow: ['/api/'] },
      { userAgent: 'GPTBot', disallow: '/' },
      { userAgent: 'OAI-SearchBot', allow: '/' }
    ],
    sitemap: 'https://example.com/sitemap.xml'
  };
}
WordPress functions.php — JSON-LD & Cleanup Empfehlung
// JSON-LD Article Schema sauber, ohne 12 Plugins
add_action('wp_head', function() {
  if (is_singular('post')) {
    global $post;
    $schema = [
      '@context' => 'https://schema.org',
      '@type' => 'Article',
      'headline' => get_the_title(),
      'datePublished' => get_the_date('c'),
      'dateModified' => get_post_modified_time('c', true, $post),
      'author' => ['@type' => 'Person', 'name' => get_the_author()],
      'mainEntityOfPage' => get_permalink()
    ];
    echo '<script type="application/ld+json">'
       . wp_json_encode($schema, JSON_UNESCAPED_SLASHES) . '</script>';
  }
}, 5);

// Emojis raus — spart 14KB pro Page
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');

// REST API für nicht-Logged-In disablen
add_filter('rest_authentication_errors', function($r) {
  if (!empty($r) || is_user_logged_in()) return $r;
  return new WP_Error('rest_disabled', 'REST restricted', ['status' => 401]);
});
AntiYoast + RankMath + AIO-SEO parallel. Jedes Plugin schreibt eigene OG/JSON-LD-Tags — Crawler sehen widersprüchliche Signale, Title und Description doppelt. Genau eines aktiv, Rest deinstallieren (nicht nur deaktivieren). Anti: Page Builder (Elementor, WPBakery) für SEO-kritische Landingpages — Bloat-HTML, INP-Killer. Lieber Block-Editor + ACF.
27

Webhooks & Background Jobs — Queues, Retries, Idempotenz

Form-Submit triggert E-Mail + CRM-Eintrag + Slack-Notification + Analytics-Event. Synchron in der Request-Chain ist fragil: einer hängt, alles failt. Production-Setup braucht Queue + Retries + Idempotenz + Dead-Letter-Queue. Ohne das brennt das System leise im Hintergrund.

Queue-Pattern statt Sync-Chain Pflicht
// FALSCH: alles synchron im Request
async function handleContactForm(data) {
  await sendEmail(data);          // 800ms — kann timeouten
  await createCRMEntry(data);      // 1.2s — kann fehlen
  await notifySlack(data);         // 400ms — Slack-API down?
  await trackAnalytics(data);      // 200ms
  // User wartet 2.6s, EIN Fehler killt alles
}

// RICHTIG: Queue für Side-Effects
async function handleContactForm(data) {
  await db.contacts.create(data);            // 50ms — kritisch
  await queue.add('contact:notify', data);   // 10ms — async
  return { ok: true };                       // User-Response: 60ms
}

// Worker (separater Prozess)
queue.process('contact:notify', async (job) => {
  const data = job.data;
  await Promise.allSettled([          // einer hängt → andere laufen
    sendEmail(data),
    createCRMEntry(data),
    notifySlack(data),
    trackAnalytics(data)
  ]);
});

// Tools 2026:
//   - BullMQ + Redis (FOSS, Node)
//   - Trigger.dev (managed, gute DX)
//   - Inngest (managed, event-driven)
//   - QStash (Upstash, HTTP-based, serverless-friendly)
//   - Cloudflare Queues (gut für Cloudflare-Stack)
//   - PostgreSQL + pg-boss (kein extra Service)
WarumSynchrone Chains skalieren nicht: jede zusätzliche Integration verschlechtert P99-Latenz und erhöht Failure-Rate exponentiell. Queue-Pattern entkoppelt User-Request von Side-Effects. Faustregel: alles > 200ms im Request-Path gehört in die Queue.
Idempotenz & Retries Pflicht
// Webhook-Empfang muss idempotent sein
// (Stripe, GitHub, etc. retry'n bei nicht-2xx Response)

async function handleStripeWebhook(event) {
  const processed = await redis.get(`webhook:${event.id}`);
  if (processed) return new Response('OK', { status: 200 });

  try {
    await processEvent(event);
    await redis.set(`webhook:${event.id}`, '1', 'EX', 86400 * 30);
    return new Response('OK', { status: 200 });
  } catch (err) {
    // 5xx zurück → Stripe retry'd nach exponentiellem Backoff
    return new Response('Failed', { status: 500 });
  }
}

// Outgoing Retries mit Exponential Backoff
async function sendWithRetry(url, body, maxRetries = 5) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const res = await fetch(url, { method: 'POST', body });
      if (res.ok) return res;
      if (res.status < 500) throw new Error('4xx');  // nicht retry-bar
    } catch (err) {
      if (i === maxRetries - 1) throw err;
      await sleep(Math.min(1000 * 2 ** i, 30000));  // 1s, 2s, 4s, 8s, 16s
    }
  }
}
AntiWebhook-Empfang ohne Idempotenz. Stripe/GitHub schicken ein Event mehrfach (Retry bei Timeout). Doppelte Side-Effects: doppelte Mails, doppelte DB-Einträge, doppelte Charges. Anti: Lineare Retries (1s, 1s, 1s) — DDoS gegen den eigenen Service bei Outage. Exponential Backoff Pflicht. Anti: 4xx-Errors retry'n — Bug, kein temporäres Problem.
Webhook-Signatur-Verifikation Pflicht
// JEDER eingehende Webhook MUSS verifiziert werden
// Sonst kann jeder einen "Stripe-Webhook" faken → Bestellung gratis

// Stripe Webhook Verification
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET);

export async function POST(req) {
  const sig = req.headers.get('stripe-signature');
  const body = await req.text();   // raw body, NICHT JSON-parsen!

  try {
    const event = stripe.webhooks.constructEvent(
      body, sig, process.env.STRIPE_WEBHOOK_SECRET
    );
    // Signatur OK → weiter verarbeiten
    await handleEvent(event);
    return new Response('OK');
  } catch (err) {
    return new Response('Invalid signature', { status: 400 });
  }
}

// GitHub Webhook (HMAC-SHA256)
import { createHmac, timingSafeEqual } from 'crypto';

function verifyGithub(body, signature, secret) {
  const hmac = createHmac('sha256', secret);
  hmac.update(body);
  const expected = 'sha256=' + hmac.digest('hex');
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

// timingSafeEqual statt === verhindert Timing-Attacks
Dead-Letter-Queue + Monitoring Empfehlung
// Jobs die N-mal failed → in DLQ statt verloren
queue.process('mail:send', {
  attempts: 5,
  backoff: { type: 'exponential', delay: 5000 },
  removeOnFail: false           // behalten für Inspektion
}, async (job) => { ... });

// DLQ-Worker (manuelle Re-Queue möglich)
queue.on('failed', async (job, err) => {
  if (job.attemptsMade >= 5) {
    await dlq.add('failed-job', {
      original: job.name,
      data: job.data,
      error: err.message,
      failedAt: new Date()
    });
    notifyOps(`Job ${job.name} in DLQ: ${err.message}`);
  }
});

// Monitoring-Dashboard:
//   - Queue-Längen (sollte gegen 0 tendieren)
//   - Job-Latenz (P50, P95, P99)
//   - Failure-Rate
//   - DLQ-Wachstum (Alarm wenn > 10/Tag)

// BullMQ kommt mit Bull Board (Web-UI)
// Trigger.dev und Inngest haben eingebaute Dashboards
28

Operations — 404, Critical CSS, Search Console, ISR, Backups

Querschnittsthemen, die zwischen die Layer fallen, aber kritisch für Production sind. 404/500-Seiten als eigene Templates, Critical-CSS-Inlining für instant LCP, Search Console Setup, ISR/Edge-Caching, Resource Hints Matrix, Backup-Strategie.

404 / 500 — eigene Templates Pflicht
<!-- /404.html — KEIN Whitescreen, KEIN Standard-Server-Error -->
<main>
  <h1>404 — Seite nicht gefunden</h1>
  <p>Die angeforderte Seite existiert nicht (mehr).</p>

  <!-- 1. Suche -->
  <form action="/search" role="search">
    <label for="q">Suche:</label>
    <input id="q" name="q" type="search">
    <button>Suchen</button>
  </form>

  <!-- 2. Hauptlinks -->
  <nav aria-label="Wichtige Seiten">
    <ul>
      <li><a href="/">Startseite</a></li>
      <li><a href="/leistungen">Leistungen</a></li>
      <li><a href="/kontakt">Kontakt</a></li>
    </ul>
  </nav>

  <!-- 3. Track 404s für SEO-Analyse -->
  <script>
    fetch('/api/track-404', {
      method: 'POST',
      body: JSON.stringify({ url: location.href, ref: document.referrer })
    });
  </script>
</main>

<!-- HTTP-Status MUSS 404 sein, NICHT 200 ("Soft 404") -->
WarumEine 404-Seite, die HTTP 200 zurückgibt ("Soft 404"), wird von Google als duplicate Content erkannt und schadet Ranking. 404-Tracking zeigt dir, welche broken Links existieren — typische Quelle: Backlinks von alten Domain-Versionen, falsch geschriebene Marketing-URLs.
AntiStandard-Server-404 (Apache/Nginx Default). Hässlich, kein Brand-Anker, keine Navigation — User bouncen sofort. Anti: 404 → Redirect auf Startseite. Verschleiert das Problem, macht Debugging unmöglich, Google-Soft-404-Penalty. Anti: 500-Seite mit Stacktrace im Production-Mode. Information Disclosure, Security-Risiko.
Critical CSS Inlining Empfehlung
<!-- Above-the-Fold CSS inline (max ~14KB) -->
<!-- Rest async laden, blockiert Render nicht -->

<head>
  <style>
    /* Critical CSS — nur was Above-the-Fold sichtbar ist */
    /* Reset, Layout-Container, Header, Hero — alles drüber raus */
    body { font-family: system-ui; margin: 0; }
    .hero { min-height: 100vh; display: grid; }
    /* ... max 14KB für TCP-Slow-Start-Optimierung */
  </style>

  <!-- Full CSS async via media-trick -->
  <link rel="preload" href="/main.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/main.css"></noscript>
</head>

// Tooling für automatische Extraction:
// - Critters (Vite/Astro Plugin)
// - critical (npm — Penthouse-basiert)
// - beasties (Cloudflare Fork von Critters, aktiv)
// - Next.js: experimental.optimizeCss in next.config.js
WarumExternes CSS blockt Render bis es geladen ist — auf 4G ~200ms Penalty fürs LCP. Inline-Critical-CSS rendert sofort, Rest lädt parallel. 14KB-Limit ist nicht willkürlich: TCP-Slow-Start sendet ca. 14KB im ersten Roundtrip. Drunter = ein Roundtrip, drüber = zwei.
AntiKomplettes CSS inlinen. Verhindert Caching → bei jedem Page-Load wird das gesamte CSS neu übertragen. Nur Above-the-Fold inline. Anti: Critical CSS einmal manuell extrahieren und vergessen — bei jeder Layout-Änderung neu generieren oder Build-Step automatisieren.
Resource Hints Matrix Empfehlung
HintWasWannBeispiel
preconnectTCP+TLS-Handshake3rd-party Origins, die du bald brauchstFonts CDN, Analytics
dns-prefetchNur DNS-LookupFallback für ältere Browser, sehr viele OriginsTracking-Pixel
preloadResource SOFORT ladenCritical Asset auf aktueller Seite (LCP-Image, Font)Hero-Font
modulepreloadJS-Module SOFORT ladenBekannte ES-Module-ImportsApp-Entry
prefetchResource für NÄCHSTE SeiteWahrscheinlich-nächste Navigation"Next Article"
prerenderKomplette Seite vorrendernSehr sicher dass Nutzer dorthin gehtPagination "Next"
<!-- Reihenfolge im <head>: preconnect → preload → prefetch -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="preload" href="/fonts/main.woff2" as="font"
      type="font/woff2" crossorigin>
<link rel="preload" href="/hero.avif" as="image"
      type="image/avif" fetchpriority="high">
<link rel="prefetch" href="/blog/article-2">
Antipreload-Spam. Wenn alles preload ist, ist nichts preload — Browser priorisiert nicht mehr. Max 2-3 preloads pro Seite. Anti: preconnect ohne crossorigin für CORS-Resources (Fonts) — Browser muss zwei TLS-Handshakes machen. Anti: prerender für externe Seiten — DSGVO-Risiko (Nutzer hat externe Site nie besucht, Daten fließen trotzdem).
Search Console + Indexing API Pflicht
# 1. Verifikation (3 Methoden, wähle eine)

# A) HTML-Tag (einfachste)
<meta name="google-site-verification" content="abc123...">
<meta name="msvalidate.01" content="def456...">     <!-- Bing -->

# B) DNS TXT (auch für Subdomains, eleganter)
example.com.  TXT  "google-site-verification=abc123..."

# C) Datei im Root
# /google1234567890.html — von Google bereitgestellt

# 2. Sitemap einreichen
# Search Console → Sitemaps → /sitemap.xml hinzufügen

# 3. Indexing API (Google) — sofortige Indexierung neuer URLs
# Setup: Service Account anlegen, API aktivieren
curl -X POST "https://indexing.googleapis.com/v3/urlNotifications:publish" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/new-page",
    "type": "URL_UPDATED"
  }'

# 4. IndexNow (Bing/Yandex/Seznam — Pendant zu Indexing API)
curl "https://api.indexnow.org/IndexNow?url=https://example.com/page&key=YOURKEY"
WarumGoogle's Indexing API ist offiziell nur für JobPosting + LiveStream-Schemas vorgesehen — funktioniert in der Praxis aber für alle URLs und beschleunigt die Indexierung von Tagen auf Stunden. IndexNow ist herstellerübergreifend (Microsoft, Yandex, Seznam), Cloudflare hat es 1-Klick-integriert.
ISR / Edge Caching Empfehlung
// Next.js — Incremental Static Regeneration
export const revalidate = 3600;  // alle 60 Min revalidieren

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(p => ({ slug: p.slug }));
}

// On-Demand Revalidation (wenn neue Daten reinkommen)
import { revalidatePath } from 'next/cache';

export async function POST(req: Request) {
  const { secret, slug } = await req.json();
  if (secret !== process.env.REVALIDATE_SECRET) return new Response('401', { status: 401 });
  revalidatePath(`/blog/${slug}`);
  return Response.json({ revalidated: true });
}

// Cloudflare Workers — stale-while-revalidate
const cache = caches.default;
const cached = await cache.match(req);
if (cached) {
  ctx.waitUntil(refreshCache(req));  // async refresh
  return cached;                       // stale sofort an User
}
WarumISR + On-Demand-Revalidation ist die beste Kombi: statische Performance (CDN-Cache, ms-Antwortzeit) + dynamische Aktualität (Webhooks aus CMS). stale-while-revalidate liefert sofort die alte Version aus dem Cache, refresht im Hintergrund — null Wartezeit für User.
CORS + Rate Limiting für API-Routen Empfehlung
// CORS: nur erlaubte Origins, niemals "*"
const ALLOWED = ['https://example.com', 'https://app.example.com'];

export async function POST(req: Request) {
  const origin = req.headers.get('origin');
  if (origin && !ALLOWED.includes(origin)) {
    return new Response('CORS blocked', { status: 403 });
  }

  // Rate Limit per IP — Upstash, Cloudflare, eigenes Redis
  const ip = req.headers.get('cf-connecting-ip') ?? 'unknown';
  const { success } = await ratelimit.limit(ip);
  if (!success) return new Response('Too Many Requests', { status: 429 });

  return new Response(JSON.stringify({ ok: true }), {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': origin || ALLOWED[0],
      'Access-Control-Allow-Methods': 'POST',
      'Vary': 'Origin'
    }
  });
}
AntiAccess-Control-Allow-Origin: * auf Endpoints mit Auth. Erlaubt jeder Webseite, im Namen eines eingeloggten Users zu agieren. Anti: Rate Limit nur auf IP — bei NAT (Mobile-Provider, Office) trifft's unschuldige User. Kombiniere mit User-ID oder Session-Token. Anti: Keine Rate Limits auf Public-Endpoints (Kontaktformular) — wird in 24h zu Spam-Maschine.
Backup-Strategie Pflicht

3-2-1-Regel für Website-Backups

  • 3 Kopien der Daten (1 Live, 2 Backups)
  • 2 verschiedene Medien (Disk + Object Storage)
  • 1 Off-Site (anderes Rechenzentrum, andere Region)
  • Code: Git-Repo (GitHub/GitLab) — selbst ist auch ein Backup
  • DB: täglicher Dump in S3/B2/Hetzner Storage Box, 30 Tage Retention
  • User-Uploads: gleichermaßen ins Object Storage spiegeln
  • Restore-Test mind. 1× pro Quartal — ungetestetes Backup ist kein Backup
  • Backup-Verschlüsselung mit Key NICHT auf demselben Server
  • Disaster-Recovery-Doc: wer macht was in welcher Reihenfolge
AntiBackup auf demselben Server. Disk-Failure = beides weg. Anti: Backup nie restored — bei Restore-Versuch stellt sich heraus, dass mysqldump --single-transaction fehlte und die DB inkonsistent ist. Anti: Verschlüsselungs-Key im selben Backup. Anti: Kein Off-Site — Brand im RZ, alles weg (siehe OVH Strasbourg 2021).
29

GSC & Keywords — Tracking, Research, AI-Search-Sichtbarkeit

Eine Website wird nicht "fertig" gelaunched — sie wird laufend gegen Realität gemessen. Google Search Console + Bing Webmaster Tools liefern die einzigen echten Daten zu deiner Sichtbarkeit (alles andere ist Schätzung). Keyword-Tracking + AI-Search-Visibility sind 2026 zwei separate Disziplinen — wer nur Position-1-Rankings misst, übersieht 40% des Traffics.

Search Console — Top-5 Reports Pflicht
ReportWas misst erWie oft prüfen
PerformanceKlicks, Impressionen, CTR, Position pro Query/Page/Land/DeviceWöchentlich
IndexierungWelche URLs sind im Index, welche nicht und warumWöchentlich
Core Web VitalsReal-User LCP/INP/CLS aus CrUX, mobile + desktopMonatlich
Manuelle MaßnahmenPenalties durch Google-ReviewerWöchentlich (Alert!)
SicherheitsproblemeMalware, Hacking, Social EngineeringWöchentlich (Alert!)
WarumPerformance-Report ist die einzige Quelle, die zeigt, für welche Queries deine Site wirklich rankt — Tools wie Sistrix schätzen, GSC weiß. Indexierungs-Report zeigt "Discovered – currently not indexed" — typischer Grund: Crawl-Budget verschwendet auf Filter-/Sort-URLs ohne canonical. Manuelle Maßnahmen + Sicherheitsprobleme: E-Mail-Alert aktivieren, Reaktionszeit kritisch.
URL Inspection & Live Test Empfehlung
# 1. URL Inspection Tool — Status einer URL im Index
#    GSC → URL eingeben oben → "URL prüfen"
# Zeigt:
#   - Indexiert: ja/nein + Reason
#   - Letztes Crawl-Datum
#   - User-Agent (Mobile-First seit 2024)
#   - Canonical-URL (was Google als kanonisch sieht — oft != deine!)
#   - Strukturierte Daten validiert
#   - Mobile-Usability-Status

# 2. Live Test — wie würde Google die URL JETZT crawlen
#    Klick "Live-URL testen" — fetch direkt Googlebot-Sicht
# Zeigt:
#   - Gerendertes HTML (nach JS-Execution!)
#   - Screenshot wie Googlebot es sieht
#   - HTTP-Response (Status, Headers)
#   - Page Resources die geblockt sind (JS/CSS/Img)

# 3. Indexierung anfordern (nach Page-Update)
#    Klick "Indexierung beantragen" — landet in Priority-Queue
#    Limit: ~10-15 Anfragen/Tag pro Property

# 4. Removals Tool — URL temporär aus Index entfernen
#    GSC → Entfernung → 6 Monate temporär
#    Bei DSGVO-Anfragen: parallel auch noindex/410 setzen
Anti"Indexierung anfordern" als Standard-Workflow. Spam-Verdacht bei Massen-Anfragen, Limit greift schnell. Lieber: gute interne Verlinkung + sitemap.xml, Google findet's von alleine. Anti: URL-Removals nutzen statt richtigem Fix — temporär heißt 6 Monate, dann zurück. Bei echtem Löschen: 410 Gone + noindex + sitemap-Entfernung + Removals = vier Schritte.
GSC API — Automation Empfehlung
# Daily Export der Performance-Daten — sonst nur 16 Monate History in GSC UI
# API-Daten in eigene DB → unbegrenzte History + Custom-Dashboards

curl -X POST "https://www.googleapis.com/webmasters/v3/sites/https%3A%2F%2Fexample.com%2F/searchAnalytics/query" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2026-04-01",
    "endDate": "2026-04-30",
    "dimensions": ["query", "page", "country", "device"],
    "rowLimit": 25000,
    "dataState": "all"
  }'

# Direkt-Export zu BigQuery (offizieller Bulk Export, kostenlos)
#   GSC → Einstellungen → Bulk-Datenexport → BigQuery-Projekt verbinden
#   Tägliche Daten ab Aktivierung, 50M Zeilen/Tag möglich

# Tools mit GSC-Anbindung:
#   - Google Looker Studio (frei) — direkter Connector
#   - Plausible Cloud — GSC-Integration für CTR-Daten
#   - SEO Gets, SEOmonitor — Monitoring + Alerts
WarumGSC-UI zeigt nur 16 Monate History und limitiert auf 1000 Zeilen — viele Long-Tail-Keywords werden abgeschnitten. BigQuery Bulk Export löst beides: vollständige Daten + unbegrenzte Aufbewahrung. Damit lassen sich Dashboards bauen, die Anomalien (plötzlicher CTR-Drop) automatisch erkennen.
Bing Webmaster Tools Empfehlung
# bing.com/webmasters — kostenlos, oft unterschätzt
# 2026 wichtiger denn je, weil:
#   - Microsoft Copilot zieht aus Bing-Index
#   - DuckDuckGo, Yahoo, Ecosia nutzen Bing-Index
#   - Bing-Marktanteil DACH ~5-8% (kein Rauschen!)

# Setup:
#   1. Verifikation (TXT-Record oder HTML-File)
#   2. Sitemap einreichen
#   3. GSC-Daten importieren (1-Klick-Migration aus Google)
#   4. IndexNow API aktivieren (sofortige Indexierung)

# Eindeutige Vorteile gegenüber GSC:
#   - Keyword-Research-Tool integriert (Volume + Trend)
#   - Backlinks-Report ausführlicher als GSC
#   - SEO-Tool: vollautomatischer Site-Scan + Empfehlungen
#   - Inspection: 10.000 URL-Submissions/Tag (vs ~15 bei Google)
WarumBing-Daten sind 2026 strategisch wichtig: Copilot in Windows + Edge + Microsoft 365 nutzt Bing-Index als primäre Quelle. Wer in Bing nicht rankt, ist für Hunderte Millionen Office-/Edge-User unsichtbar. Dazu: Bing's Keyword-Research ist kostenlos und brauchbar — spart einen Sistrix-Account für Solo-Devs/Agenturen.
Keyword-Research — Tool-Vergleich DACH 2026 Pflicht
ToolDACH-DatenqualitätPreis/MoStärke
SistrixSpitze (DE-Standard)~100 €DACH-Sichtbarkeitsindex, historische Daten seit 2008
SE RankingSolide~50 €Bestes Preis/Leistung, gute UI
Mangools (KWFinder)OK~30 €Einsteiger, übersichtlich
AhrefsTop global~110 €Backlinks, Content-Gap, Site-Audit
SemrushTop global~130 €All-in-one, PPC-Daten
Google Keyword PlannerLive-Daten0 € (mit Ads-Account)Search Volume direkt von Google, gerundet
Google TrendsLive-Daten0 €Saisonalität, Vergleich, Breakout-Topics
AnswerThePublicOK0–11 €Long-Tail-Fragen visualisiert
People-Also-Ask ScrapingLive0 € (Custom)Echte AI-relevante Fragen
WarumFür DACH ist Sistrix der Marktführer — keine andere Datenbank hat vergleichbare Tiefe für deutsche SERPs. Wer global denkt: Ahrefs oder Semrush. Solo/Agentur-Bootstrap: SE Ranking + GSC + Bing Webmaster reichen oft. Wirklich gratis: GSC + Bing + Trends + Keyword Planner liefern 80% der relevanten Daten.
AntiSearch Volume als einzigen Wert betrachten. "100k Searches/Monat" ohne Suchintent = nutzlos (zu viel Konkurrenz oder falsche Intent). Lieber: niedriges Volume + hohe Intent + niedriger Wettbewerb. Anti: Keyword-Listen kaufen — veraltet, ohne lokalen Kontext, ohne Intent. Anti: nur Single-Keyword denken — moderne SEO ist Topic Clustering (Hauptbegriff + 20 verwandte Long-Tails auf einer Page).
Keyword-Tracking-Setup Pflicht
# Tracking IMMER getrennt nach:

# 1. Brand vs Non-Brand
#    Brand:    "blitzsicht", "blitzsicht.com", "blitzsicht webdesign"
#    Non-Brand: "webdesign handwerker", "website handwerker regensburg"
#    → Brand-Traffic ist nicht "SEO-Erfolg", sondern Marketing-Frucht
#    → Non-Brand-Wachstum ist der echte SEO-KPI

# 2. Mobile vs Desktop
#    SERPs unterscheiden sich teils massiv (Local Pack, AI Overviews)
#    Mobile-First Indexing: Mobile-Position ist die "echte"

# 3. Geo (lokale SEO)
#    "webdesign regensburg" rankt in München anders als in Regensburg
#    → Tracking je Stadt für lokale Service-Pages

# 4. SERP-Feature
#    Position 1 Organic ≠ Featured Snippet ≠ Local Pack ≠ AI Overview
#    Klick-Verteilung Position 1 (klassisch): ~28%
#    Klick-Verteilung mit AI Overview drüber: ~10%

# Trackbare Keyword-Buckets (typisches Setup):
#   - Money-Keywords (Service-Pages, hohe Intent): 20-50
#   - Long-Tail-Targets (Blog-Themen): 100-500
#   - Brand-Defense (Variants, Typos): 10-30
#   - Wettbewerber-Brands: 10-20 (für Vergleich)

# KPIs pro Bucket:
#   - Avg Position
#   - Share of Voice (SoV)
#   - Klick-Anteil (CTR × Impressions)
#   - SERP-Feature-Coverage (Featured Snippet, AI Overview, Local Pack)
AntiNur Money-Keywords tracken. 60-80% deines Traffics kommt von Long-Tails, die du nie "geplant" hattest. Erst GSC-Performance-Report durchforsten, dann Long-Tail-Tracking-Liste bauen. Anti: Position 1 als einziges Ziel — bei Queries mit AI Overview ist Position 1 unter dem AI-Block fast unsichtbar. Anti: Tracking ohne Geo-Context für lokale Services. Anti: Brand-Traffic in den SEO-KPI mischen — verfälscht den echten organischen Erfolg, vor allem nach Werbekampagnen.
AI-Search-Visibility Tracking Empfehlung
# 2026-Spezifikum: AI-Suchmaschinen sind eigene Discovery-Channels
# Klassische SEO-Tools messen das größtenteils noch nicht ausreichend

# WAS messen:
#   1. Wirst du in AI-Antworten ZITIERT (mit Link)?
#   2. Wirst du nur ERWÄHNT (ohne Link)?
#   3. Bei welchen Queries / Intents?
#   4. Wo in der Antwort (Top, Footnote, ignoriert)?

# WO messen (4 Kanäle):
#   - Google AI Overviews (ehemals SGE)
#   - ChatGPT Search (web-enabled)
#   - Perplexity
#   - Microsoft Copilot
#   - (optional) Claude Web Search, Gemini Deep Research

# TOOLS (junges Segment, Stand 2026):
#   - Profound — AI-Search-Visibility-Plattform
#   - Athena (athenahq.ai) — Mention-Tracking in LLMs
#   - Otterly.ai — AI-Search-Monitoring DACH
#   - Peec AI — Brand-Tracking in ChatGPT/Perplexity
#   - Sistrix AI Visibility (neu, in Beta)

# DIY-Setup (gratis, manuell, monatliche Stichproben):
#   1. Liste Top-30 Money-Queries
#   2. Skript: für jede Query → ChatGPT-Search + Perplexity API
#   3. Parse Antwort → finde Brand-Mention + Citation-Link
#   4. Log in DB: query, model, mentioned (bool), cited (bool), position
#   5. Trend über Wochen tracken
WarumBei 2026er-Suchanfragen ersetzt die AI-Antwort zunehmend den klassischen 10-Blue-Links-SERP — wer dort nicht zitiert wird, verliert Traffic, selbst bei Position-1-Ranking. Die Hebel für AI-Visibility sind teilweise andere als für klassisches SEO: starke Author-Schemas, Wikidata-Entry, klare Daten-Statements (siehe Layer 09 Answer-First), llms.txt, und vor allem Erwähnungen auf Drittseiten (LLMs gewichten breite Quellenbasis).
AntiAI-Search ignorieren, weil "noch klein". Pro-Suchanfrage-Anteil mag klein sein, aber Wachstum ist hoch und vor allem: AI-User haben höhere Conversion-Intent. Wer wartet, bis es 30% sind, hat 2-3 Jahre Aufholbedarf. Anti: AI-Search nur bei Brand-Queries tracken — der eigentliche Hebel sind Non-Brand-Service-Queries ("bester Webdesigner für Handwerker"). Anti: Klassisches Keyword-Stuffing in der Hoffnung, AIs zitieren mehr — LLMs erkennen Geschwurbel und bestrafen es härter als Google.
Monitoring-Stack — Empfehlung Solo/Agentur Nice-to-have

Pragmatischer 2026-Stack (für Blitzsicht-Kunde / SMB)

  • GSC + Bing Webmaster — beides verifizieren, beides täglich automatisch exportieren
  • BigQuery Bulk Export aus GSC für unbegrenzte History (gratis bis Volumen)
  • Looker Studio-Dashboard für Klick/Position/CTR-Trends pro Keyword-Bucket
  • Sistrix Smart (~50 €/Mo) für DACH-Sichtbarkeitsindex und SERP-Feature-Tracking
  • Plausible für Conversion-Attribution (welcher organische Klick → welche Anfrage)
  • Otterly.ai oder DIY-Skript für AI-Search-Monitoring (monatlich, 30 Money-Queries)
  • E-Mail-Alerts: GSC Manuelle Maßnahmen + Sicherheitsprobleme + Indexing-Drops >10%
  • Wöchentlicher 30-Min-Slot: GSC-Performance-Report durchgehen, Anomalien notieren
  • Monatlicher Report-Termin: Top-Klicks, neue Long-Tails, AI-Visibility-Score
  • Quartalsweise Keyword-Audit: Cannibalisierung, abgewanderte Money-Terms, neue Topics
30

Analytics DSGVO-konform — Plausible · Matomo · Server-Side

Google Analytics ist seit dem Schrems-II-Urteil und mehreren DPA-Entscheidungen (Österreich, Frankreich, Italien 2022) ohne EU-US Data Privacy Framework + DPA + IP-Anonymisierung + Consent in der EU rechtlich grenzwertig bis verboten. Cookieless oder Server-Side ist 2026 der pragmatische Standard.

Analytics-Stack-Vergleich Pflicht
ToolHostingCookiesConsentUse-Case
Plausible Cloud (EU)EU (DE)NeinNein¹SMB, Marketing
Plausible Self-HostedEigeneNeinNeinVolle Kontrolle
Matomo Self-HostedEigeneOptionalNein (cookieless)Tiefere Analyse
Umami Self-HostedEigeneNeinNeinMinimal, FOSS
GA4 + Server-Side + Consent v2HybridJaJaE-Commerce mit Ads
GA4 ohne Consent———Verboten in EU
Warum¹ Auch cookieless Analytics ohne PII fällt unter §25 TDDDG, wenn Daten auf dem Endgerät zugegriffen werden. Plausible argumentiert, dass kein Zugriff aufs Endgerät stattfindet. Mehrere DPAs (u.a. Frankreich CNIL) haben das akzeptiert. Sichere Wette: Plausible/Umami ohne Consent, GA4 nur mit.
Plausible Setup Empfehlung
<!-- Standard -->
<script defer
        data-domain="example.com"
        src="https://plausible.io/js/script.js"></script>

<!-- Mit Outbound-Links + File-Downloads -->
<script defer
        data-domain="example.com"
        src="https://plausible.io/js/script.outbound-links.file-downloads.js"></script>

// Custom Events (Goals)
window.plausible('Signup');
window.plausible('Purchase', { props: { plan: 'Pro', value: 49 } });
Consent Banner (TCF v2.2) Pflicht
// REGEL 1: VOR jedem nicht-essentiellen Tracker = NICHTS laden
if (consent.analytics === true) {
  loadScript('https://www.googletagmanager.com/gtag/js?id=G-XXXXX');
}

// REGEL 2: Buttons gleich groß, gleiche Visual Hierarchy
// Falsch: [Akzeptieren] (groß, grün) [Nur Notwendige] (klein, grau)
// Richtig: [Alles ablehnen] [Alles akzeptieren] [Einstellungen] — alle gleich

// REGEL 3: Granular pro Zweck
const consent = {
  necessary: true,
  preferences: false,
  analytics: false,
  marketing: false
};

// REGEL 4: Persistieren mit Timestamp + Version
localStorage.setItem('consent', JSON.stringify({
  ...consent, timestamp: new Date().toISOString(), version: '1.2'
}));

// REGEL 5: Widerruf so einfach wie Erteilung
// → Permanenter Footer-Link "Cookie-Einstellungen ändern"
AntiAkzeptieren-Button visuell betonen. Bayerisches Datenschutzbüro & CNIL verfolgen das aktiv (Dark Patterns). Anti: Cookies VOR Consent setzen — selbst nicht "anonyme" Session-IDs. Anti: Consent ohne Zweck-Granularität ("Alles oder nix"-Banner sind unzulässig). Anti: "Weitersurfen = Zustimmung" — explizite Action ist Pflicht.

DSGVO-Compliance-Checkliste pro Drittanbieter

  • Auftragsverarbeitungsvertrag (AVV / DPA) abgeschlossen und gespeichert
  • Server-Standort dokumentiert (EU-only oder DPF-zertifiziert)
  • In Datenschutzerklärung mit Zweck, Rechtsgrundlage, Speicherdauer aufgeführt
  • Bei US-Anbietern: aktiver DPF-Status auf dataprivacyframework.gov verifiziert
  • IP-Anonymisierung aktiviert (wo möglich)
  • Consent vor erstem Datenfluss (Tag-Manager: erst nach Consent feuern)
  • Widerrufmöglichkeit dokumentiert und im Footer verlinkt
  • Jährliches Re-Audit der Verarbeiterliste
Häufige DSGVO-Fallen Vorsicht
AntiGoogle Fonts hotlinken (fonts.googleapis.com). LG München I 2022: 100€ Schadenersatz pro Aufruf. Lokal hosten oder via Bunny Fonts (EU). Anti: YouTube-Embeds ohne youtube-nocookie.com + Consent. Anti: reCAPTCHA v3 ohne Consent (lädt Google-Skript, IP-Übertragung). Alternative: hCaptcha, Cloudflare Turnstile. Anti: Stripe-Elements ohne Consent — auch Payment-Provider sind 3rd-Party-Skripte.
31

Marketing Automation — UTM, A/B-Testing, Server-Side Tracking

Wo Layer 18 (Analytics) das Messen abdeckt, geht's hier um die Conversion-Optimierung selbst. UTM-Disziplin als Voraussetzung für Attribution, A/B-Testing ohne CLS-Penalty, Server-Side Conversion Tracking als 2026-Standard (weil 3rd-Party-Cookies sterben), Heatmaps via Microsoft Clarity (gratis, DSGVO-machbar).

UTM-Konventionen Pflicht
# Standardisierte UTM-Parameter (sonst keine saubere Attribution)

utm_source     # wer (google, facebook, newsletter, partner_xy)
utm_medium     # wie (cpc, organic, email, social, referral)
utm_campaign   # welche Kampagne (q2-2026-handwerker, black-friday)
utm_content    # Variante (banner-blue, banner-orange, headline-a)
utm_term       # Keyword (nur Paid-Search relevant)
utm_id         # interne Kampagnen-ID (für Cross-Tool-Matching)

# BEISPIELE — strikte Naming-Convention:
#   ?utm_source=google&utm_medium=cpc&utm_campaign=2026q2_handwerker_de
#   ?utm_source=newsletter&utm_medium=email&utm_campaign=may_promo&utm_content=cta_top
#   ?utm_source=facebook&utm_medium=social&utm_campaign=brand_awareness

# Konventionen:
#   - lowercase (Plausible/GA case-sensitive)
#   - underscore_statt_bindestrich (oder umgekehrt — konsequent!)
#   - Datum im Format YYYYqQ oder YYYYMM
#   - keine Leerzeichen, keine Umlaute

# URL-Builder zentral hosten — nicht jeder baut UTMs frei zusammen:
#   1. Google Campaign URL Builder (web)
#   2. Eigenes Internal-Tool mit Dropdowns für Source/Medium
#   3. Notion-DB als Single Source of Truth
AntiUTMs ad-hoc bauen. "Google" vs "google" vs "Google Ads" vs "googleads" → 4 verschiedene Sources in den Reports, Attribution kaputt. Anti: UTMs auf interne Links setzen — überschreibt die echte Source der Session, korrumpiert komplette Attribution. Anti: UTMs in canonical URL — Google indexiert getrackte URL als kanonisch, alle Tracking-Daten landen falsch. Canonical IMMER ohne UTM.
A/B-Testing ohne CLS Empfehlung
<!-- Server-Side A/B-Testing: keine Flicker, kein CLS -->
<!-- Beispiel via Edge Middleware (Vercel, Cloudflare Workers) -->

// middleware.ts
export function middleware(req) {
  const bucket = req.cookies.get('ab-bucket') || (Math.random() < 0.5 ? 'a' : 'b');
  const res = NextResponse.rewrite(`/variant-${bucket}/...`);
  res.cookies.set('ab-bucket', bucket, { maxAge: 86400 * 30 });
  return res;
}

<!-- Client-Side A/B-Testing OHNE CLS -->
<!-- 1. CSS für beide Varianten inline (display:none default) -->
<!-- 2. Skript IM <head> (synchron!) entscheidet, welche Variante -->
<!-- 3. Body bereits richtig gerendert, kein Flicker -->

<head>
  <style>
    .variant-a, .variant-b { display: none; }
    [data-variant="a"] .variant-a { display: block; }
    [data-variant="b"] .variant-b { display: block; }
  </style>
  <script>
    // SYNCHRON, vor jedem Render — kein FOUC
    const v = localStorage.getItem('ab') || (Math.random() < .5 ? 'a' : 'b');
    localStorage.setItem('ab', v);
    document.documentElement.dataset.variant = v;
  </script>
</head>

<!-- Tools: Vercel Edge Config + Statsig, GrowthBook (FOSS), PostHog -->
AntiVWO / Optimize / Google Optimize Client-Side ohne Anti-Flicker-CSS. User sieht Variante A, dann switch zu B → CLS, schlechte UX. Lighthouse-Penalty. Anti: Test-Bucket im Cookie ohne Sticky — User refreshed, bekommt neue Variante, Test-Daten verfälscht. Anti: Mehrere Tests parallel ohne Multivariate-Setup — Confounding-Effekte unauflösbar.
Server-Side Conversion Tracking Empfehlung
// Browser-Cookie-Apocalypse: 3rd-Party-Cookies sind 2026 weitgehend tot
// → Conversion-Tracking via Server-Side APIs

// Meta Conversions API (CAPI) — Pixel-Replacement
await fetch(`https://graph.facebook.com/v19.0/${PIXEL_ID}/events`, {
  method: 'POST',
  body: JSON.stringify({
    data: [{
      event_name: 'Purchase',
      event_time: Math.floor(Date.now() / 1000),
      action_source: 'website',
      event_source_url: 'https://example.com/order/confirm',
      user_data: {
        em: [hashSHA256(email.toLowerCase())],
        ph: [hashSHA256(phone)],
        client_ip_address: req.ip,
        client_user_agent: req.headers['user-agent']
      },
      custom_data: { value: 49.90, currency: 'EUR' }
    }],
    access_token: META_ACCESS_TOKEN
  })
});

// Google Enhanced Conversions (für Google Ads Conversion-Optimierung)
// Enhanced Conversions API: gleiche Logik, anderer Endpoint
// LinkedIn CAPI, TikTok Events API, Snapchat CAPI: gleiches Pattern

// DSGVO: Hashing reicht NICHT als Anonymisierung
// → Server-Side Tracking nur NACH Consent + in Datenschutzerklärung dokumentieren
// Vorteil: kein Browser-Skript = nicht von Adblockern blockiert
WarumBrowser-Pixel werden zunehmend von ITP (Safari), ETP (Firefox), Brave, Adblockern blockiert — Conversion-Daten verloren. Server-Side-API umgeht das, weil der Request vom eigenen Server kommt. Vorteil für ROAS: Meta/Google können Anzeigen besser optimieren mit vollständigeren Daten. Pflicht-Anteil: Consent-Status mit übertragen (data_processing_options bei Meta).
Microsoft Clarity (gratis Heatmaps) Nice-to-have
<!-- Microsoft Clarity — kostenlos, unbegrenzt, gut DSGVO-machbar -->
<!-- Bietet: Heatmaps, Session Recordings, Funnels, Rage Clicks -->

<!-- Setup nach Consent -->
<script>
if (consent.analytics) {
  (function(c,l,a,r,i,t,y){
    c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
    t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
    y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
  })(window, document, "clarity", "script", "YOUR_PROJECT_ID");

  // PII-Schutz: User-IDs anonymisieren, sensible Inputs maskieren
  clarity("set", "email", hashSHA256(userEmail));
  clarity("identify", hashSHA256(userId));
}
</script>

// CSS: Sensible Felder maskieren (Pflicht)
<input type="email" data-clarity-mask="True">
<div data-clarity-region="checkout"> ... </div>

// Was du daraus lernst:
//   - Wo klicken User auf nicht-klickbare Elemente (Confusion)
//   - Rage Clicks (Frustration → UX-Problem)
//   - Scroll-Depth (Content gelesen oder ignoriert)
//   - Funnel-Drop-Offs (wo bricht der Checkout)
AntiClarity ohne Consent + ohne PII-Maskierung. Session-Recording + Eingaben-Capture ist DSGVO-Pflicht-Consent + ADV mit Microsoft. Anti: Hotjar als Default — teurer, gleiche Funktionalität, kein DACH-Vorteil. Clarity gewinnt fast immer auf Kosten/Funktion-Achse.
32

Logging & Observability — Structured Logs, Errors, Tracing

Layer Monitoring deckte Performance + SEO. Hier geht's um Application-Layer-Sicht: Wenn etwas kaputt ist, wie findet man's? Strukturiertes Logging, Error-Tracking via Sentry/PostHog, OpenTelemetry-Basics, Log-Retention mit PII-Scrubbing.

Structured Logging (JSON) Pflicht
// pino — schnellster Node-Logger, JSON-Output by default
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  redact: ['req.headers.authorization', 'req.headers.cookie', '*.password'],
  formatters: {
    level: (label) => ({ level: label })  // "level": "info" statt 30
  },
  base: { env: process.env.NODE_ENV, service: 'api' }
});

// Strukturiert loggen (NICHT String-concatenation)
logger.info({ userId, orderId, amount: 49.90 }, 'Order created');
logger.error({ err, userId, route: req.url }, 'Payment failed');

// Falsch:
console.log(`User ${userId} created order ${orderId}`);
// → unsearchable, nicht filterbar, kein Stack-Trace

// Korrelations-IDs für Request-Tracing
import { randomUUID } from 'crypto';
const requestId = req.headers['x-request-id'] ?? randomUUID();
const child = logger.child({ requestId });
// Alle Logs innerhalb dieses Requests haben requestId
Anticonsole.log() in Production. String-Logs sind unsearchable. Bei Outage stundenlanges grep statt 1-Klick-Filter. Anti: Logging ohne Redaction von Passwörtern/Tokens — Log-Dateien werden zu DSGVO-Hochrisiko-Daten. Anti: Fehler abfangen ohne sie zu loggen — silent failures sind Production-Hölle. Anti: Log-Level debug in Production lassen — Disk-Full-Outage in 2 Wochen.
Error-Tracking (Sentry) Pflicht
// Sentry: Error-Tracking + Performance + Replay (FOSS-Self-Host möglich)
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 0.1,        // 10% der Requests gesampled
  replaysSessionSampleRate: 0.0,
  replaysOnErrorSampleRate: 1.0, // Replay nur bei Errors
  beforeSend(event, hint) {
    // PII-Scrubbing
    if (event.user?.email) {
      event.user.email = hashSHA256(event.user.email);
    }
    // 4xx ignorieren (kein echter Bug)
    if (event.tags?.statusCode < 500) return null;
    return event;
  },
  ignoreErrors: [
    'ResizeObserver loop limit exceeded',  // Browser-Quirk
    'Network request failed',             // User-Connection
    'Non-Error promise rejection captured'
  ]
});

// Manuelle Captures mit Kontext
Sentry.captureException(err, {
  tags: { route: '/api/contact', formStep: 'submit' },
  extra: { formData: sanitize(data) },
  user: { id: hashSHA256(userId) }
});

// Alternativen 2026:
//   - PostHog (FOSS, all-in-one: Errors + Analytics + Replay)
//   - GlitchTip (FOSS Sentry-API-kompatibel)
//   - HighlightIO (FOSS, gute UI)
//   - Bugsnag, Rollbar, Honeybadger (commercial)
AntiReplays mit 100% Sample-Rate. Datenmenge explodiert, Sentry-Bill auch. 1.0 nur bei Errors, sonst < 0.1. Anti: Source-Maps nicht uploaden — Stack-Traces sind minified, debuggen unmöglich. Anti: User-PII ungehashed in Events — DSGVO-Verstoß.
OpenTelemetry — Distributed Tracing Nice-to-have
// OpenTelemetry: vendor-neutraler Standard für Traces/Metrics/Logs
// → kann an Datadog, Honeycomb, Grafana Tempo, Jaeger gleichzeitig

import { trace, context } from '@opentelemetry/api';

const tracer = trace.getTracer('api');

async function handleRequest(req) {
  return tracer.startActiveSpan('handleRequest', async (span) => {
    span.setAttribute('http.url', req.url);
    span.setAttribute('user.id', hashSHA256(userId));

    try {
      const result = await tracer.startActiveSpan('db.query', async (s) => {
        const r = await db.query(...);
        s.end();
        return r;
      });
      span.setStatus({ code: 1 });  // OK
      return result;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: 2 });  // ERROR
      throw err;
    } finally {
      span.end();
    }
  });
}

// Traces zeigen: HTTP-Request → DB-Query → External-API → Antwort
// Mit Latenz pro Span. Bottlenecks identifizieren in Sekunden.

// 2026 Stack-Empfehlung:
//   - Grafana Stack (Loki + Tempo + Mimir) self-hosted
//   - Honeycomb (managed, beste UX, teuer ab Skalierung)
//   - Datadog (managed, all-in-one, sehr teuer)
//   - Axiom (managed, günstig, gut für Logs)
WarumOTEL ist 2026 der vendor-neutrale Standard — wer heute nativ einen Anbieter integriert, sitzt morgen im Lock-In. OTEL-Instrumentierung kann jederzeit auf anderen Backend zeigen. Für Solo/SMB oft overkill — Sentry + structured Logs reichen lange.
Log-Retention & PII-Scrubbing Pflicht

DSGVO-konformes Log-Management

  • Retention: Standard 30 Tage, Audit-Logs 6-12 Monate, danach automatisch löschen
  • PII-Scrubbing beim Logging (nicht erst in Storage): E-Mail, Tel, IP, Name, Adresse
  • IP-Adressen: nicht direkt loggen — entweder gar nicht oder hash mit Salt-Rotation
  • Auth-Tokens: NIE loggen, auch nicht in Error-Stack-Traces
  • Request-Bodies: NIEMALS komplett loggen — gezielte Felder mit Allowlist
  • Datenschutzerklärung: Logging als Verarbeitung dokumentieren
  • Audit-Trail für Login/Logout/Password-Change separat (rechtlich wertvoll)
  • Log-Zugriff: nur Ops-Team, mit Audit (wer hat wann welche Logs gelesen)
  • Backup-Encryption: Logs in Backup ebenfalls verschlüsselt
  • Right-to-Erasure: User-bezogene Logs müssen auf Antrag gelöscht werden — daher hashed-User-IDs nutzen, nicht Klartext
33

Email-Deliverability — Reputation, Warmup, Suppression, Mail-Trennung

Layer DNS hatte SPF/DKIM/DMARC. Records korrekt setzen ist Voraussetzung — ausreichend ist es nicht. Sender-Reputation entsteht aus Verhalten über Zeit. Warmup neuer Domains, transaktional vs. Marketing trennen, Bounces handlen, Spam-Komplekt verstehen. 2026 verschärfen Gmail/Yahoo die Anforderungen weiter.

Mail-Subdomain-Trennung Pflicht
# Pflicht-Trennung — separate Subdomains für separate Use-Cases

# Apex-Domain für rein menschliche Mails:
@example.com             # Geschäftliche Korrespondenz, niemals automatisiert

# Transaktional (Bestellbestätigungen, Password-Reset):
@send.example.com        # oder @notify, @mail
#   - Hohe Open-Rate (90%+), niedrige Bounce-Rate
#   - Wenn das schlecht wird, ist alles kaputt

# Marketing (Newsletter, Kampagnen):
@news.example.com        # oder @marketing
#   - Niedrigere Open-Rate (15-25%), höhere Spam-Rate
#   - DARF Reputation der Hauptdomain NICHT mit runterziehen

# Cold Outreach (Vertrieb):
@example-team.com         # EIGENE Domain (nicht Subdomain!)
#   - Spam-Komplekt-Risiko hoch
#   - Bei Domain-Burn: separate Domain entsorgen, Hauptmarke geschützt

# Bounces / Returns (Bounce-Adresse für Mail-Server):
bounces@bounce.example.com  # Provider-spezifisch (Postmark, Mailgun)
WarumSender-Reputation wird per Subdomain gemessen. Wenn Marketing-Newsletter Spam-Reports hat, leidet @send.example.com nicht — Transaktional bleibt unberührt. Cold Outreach auf Hauptdomain ist klassischer Anfänger-Fehler: ein Spam-Report-Sturm kann die komplette Geschäfts-E-Mail-Kommunikation blocken.
AntiNewsletter von @example.com (Apex) senden. Schlechte Marketing-Reputation killt Geschäfts-Mails. Anti: Cold Outreach auf Subdomain — wenn die geblockt wird, ist die Hauptdomain mit dran. Eigene Domain für Cold. Anti: "noreply@..." als From — User können nicht antworten, Mail-Provider werten das als Spam-Signal.
Domain-Warmup (neue Sender-Domain) Pflicht
# Neue Sender-Domain hat KEINE Reputation
# 5000 Mails am Tag 1 = direkter Spam-Folder

# Warmup-Schedule (typisch 4-6 Wochen):
#   Tag 1-3:    50 Mails/Tag an aktive Empfänger
#   Tag 4-7:    100 Mails/Tag
#   Woche 2:    500 Mails/Tag
#   Woche 3:    1.500 Mails/Tag
#   Woche 4:    5.000 Mails/Tag
#   Woche 5+:   10.000+ Mails/Tag

# Wichtig:
#   - Nur an aktive Empfänger (öffnen, klicken, antworten)
#   - Spam-Rate < 0.1% halten (Gmail-Threshold seit Feb 2024)
#   - Bounce-Rate < 2%
#   - Volumen langsam steigern, kein 10× Sprung

# Tools:
#   - Mailflow (DACH, AI-basiert)
#   - Warmup Inbox
#   - Lemwarm
#   - Smartlead.ai (auch Cold Outreach mit eingebautem Warmup)

# Manueller Warmup (kostenlos, dauert länger):
#   1. Eigene Mailadressen in 5+ Mailboxen anlegen (Gmail, Outlook, iCloud)
#   2. Mails an sich selbst senden, antworten, weiterleiten, archivieren
#   3. Volumen langsam hochfahren
Reputation-Monitoring & Bounce-Handling Pflicht
# Reputation-Monitoring (täglich/wöchentlich)

# Google Postmaster Tools (FREE, Pflicht für Gmail-Volumen)
#   - Spam-Rate Domain + IP
#   - Reputation-Score (Bad/Low/Medium/High)
#   - DKIM/SPF/DMARC-Compliance
#   - Auth-Score

# Microsoft SNDS (Outlook/Hotmail)
#   - Smart Network Data Services
#   - IP-Reputation, Filter-Result, Complaint-Rate

# MailTester.com (manuell pro Test-Mail)
#   - Score 0-10 (10 = perfekt)
#   - Zeigt Spam-Trigger (Wörter, fehlende Header, Auth)

# GlockApps Inbox Insight
#   - Inbox-Placement-Test (Inbox / Promotions / Spam)
#   - Across 30+ Provider

# Bounce-Handling (Pflicht):
#   Hard Bounce (User existiert nicht): SOFORT aus Liste, NIE wieder senden
#   Soft Bounce (temporär): 3 Versuche, dann suppression
#   Spam-Komplekt: SOFORT aus Liste, suppression-Liste
#   Unsubscribe: SOFORT, RFC 8058 (List-Unsubscribe-Post)

# Suppression-Liste persistieren (alle Provider haben das eingebaut)
# Eigene zusätzlich für Cross-Provider-Konsistenz
AntiBounces ignorieren. Jeder Hard-Bounce zählt gegen Reputation. Wer 5% Bounce-Rate fährt, kommt in Spam. Anti: Liste re-importieren ohne Suppression-Check. Unsubscribe-User bekommen wieder Mails → Spam-Report → Reputation tot. Anti: Re-Engagement-Kampagnen an inaktive (> 6 Mo) Empfänger ohne Vorsicht. Lieber sterben lassen.
Gmail/Yahoo Bulk-Sender-Anforderungen 2024+ Pflicht

Pflicht ab 5000 Mails/Tag an Gmail oder Yahoo

  • SPF + DKIM + DMARC alle drei korrekt (siehe Layer DNS)
  • DMARC Policy mind. p=none (besser quarantine)
  • One-Click-Unsubscribe: List-Unsubscribe-Header (RFC 8058) List-Unsubscribe: <mailto:unsub@..>, <https://..> + List-Unsubscribe-Post: List-Unsubscribe=One-Click
  • Spam-Rate < 0.3% (Google empfiehlt < 0.1%)
  • Sender-Adresse-Authentifizierung: Domain in From-Header MUSS mit DKIM-Signing-Domain alignen
  • TLS-Zustellung: MTA-STS empfohlen
  • Klare Sender-Identität: From-Display-Name = Brand, nicht "info"
  • Funktionierender Reply-To oder echte From-Adresse
  • Verifizierte Postmaster-Tools-Anmeldung bei Google + Microsoft
  • BIMI (mit VMC) optional aber Trust-Boost
34

Sustainability — Green Web, Carbon-Footprint, Renewables

Eine Webseite verursacht durchschnittlich ~1g CO₂ pro Pageview. Bei 100k Pageviews/Monat = 1,2 Tonnen/Jahr. Hebel: grünes Hosting, schlanke Pages (siehe Performance-Budgets Layer 14), bewusste Asset-Wahl. 2026 zunehmend als Differenzierungs-Anker genutzt — vor allem bei B2B-Vergaben (öffentliche Hand, Großkonzerne mit ESG-Reporting).

Green Hosting Auswahl Empfehlung
HosterStandortStromVerifiziert
HetznerDE / FI100% Wasserkraft (FI), 100% Grün (DE seit 2017)Green Web Foundation
CloudflareGlobal100% Renewables-Match seit 2019Cloudflare Sustainability Report
VercelGlobal (AWS-basiert)AWS Region-abhängigTeilweise
AWS Frankfurt (eu-central-1)DE100% Renewables seit 2023AWS Sustainability
NetcupDE100% ÖkostromTÜV Süd zertifiziert
IONOSDE100% ÖkostromTÜV Süd
OVHcloudFR / DEMix, 80% Renewables-Ziel 2025Eigene Reports
Webflow / SquarespaceUSAAWS / GCP — variiertTeilweise
WarumFür Blitzsicht-Stack ist Hetzner die offensichtliche Wahl: 100% Wasserkraft (FI-RZ), 100% Grünstrom (DE seit 2017), Green-Web-Foundation-verifiziert, EU-Hosting für DSGVO. Dazu signifikant günstiger als AWS/Vercel. Cloudflare als CDN davor: Renewables-Match + globale Edge-Performance.
AntiGreenwashing-Claims ohne Verifikation. "CO2-neutral durch Kompensation" ohne Zertifikat = Marketing-Behauptung. Green Web Foundation (thegreenwebfoundation.org) ist die unabhängige Standard-Verifikation. Anti: Hosting-Standort ignorieren — DE-Site auf US-East ist beides schlecht (Latenz + meistens schlechterer Strom-Mix).
websitecarbon.com Benchmark Empfehlung
# Manuell: websitecarbon.com (gibt CO2-Score pro Pageview)
# Bewertung:
#   A+ : < 0.095g CO2 / Pageview (Top 5%)
#   A  : < 0.186g
#   B  : < 0.341g
#   C  : < 0.493g
#   D-F: schlechter

# Programmatisch — beasties / lighthouse-plugin-carbon
npx lighthouse-plugin-carbon https://example.com

# Hauptfaktoren (in dieser Reihenfolge):
#   1. Total Page Weight (KB) — Performance-Budgets aus Layer 14
#   2. Hosting-Strom-Mix (grün vs grau)
#   3. CDN-Edge-Caching (kürzere Strecken = weniger Energie)
#   4. Anzahl Requests

# Niedrig hängende Früchte:
#   - AVIF statt JPEG (50% kleiner)
#   - Variable Fonts statt 4 Font-Files
#   - Autoplay-Videos raus (~80% des Traffics einer Page)
#   - 3rd-Party-Skripte minimieren (Tracking, Chat-Widgets)
Sustainability-Badge + Kommunikation Nice-to-have
<!-- Green Web Foundation Verifikation (gratis, ~2 Wochen) -->
<!-- 1. https://www.thegreenwebfoundation.org/check/ → Domain prüfen -->
<!-- 2. Hoster reicht Nachweis über Strom-Mix ein -->
<!-- 3. Domain wird in DB gelistet -->

<!-- Footer-Badge mit Link zur Verifikation -->
<a href="https://www.thegreenwebfoundation.org/green-web-check/?url=example.com"
   rel="noopener">
  <img src="/img/green-hosting.svg"
       alt="Green Web verifiziert — diese Site läuft auf 100% Ökostrom"
       width="120" height="40" loading="lazy">
</a>

<!-- /sustainability oder /nachhaltigkeit Page -->
<!-- Liste konkrete Maßnahmen, nicht nur Behauptungen: -->
<!--   - Hosting Hetzner (Wasserkraft, FI-RZ)                                    -->
<!--   - CDN Cloudflare (Renewables-Match)                                       -->
<!--   - Page-Weight < 500 KB (Performance-Budget, siehe Layer 14)               -->
<!--   - AVIF/WebP statt JPEG                                                    -->
<!--   - Plausible (kein Tracking-Pixel-Bombardement)                            -->
<!--   - websitecarbon.com Score: A+ / 0.08g CO2 pro Visit                       -->
WarumSustainability ist 2026 ein echter Trust-Marker — vor allem in B2B-Pitches, öffentlichen Vergaben (ESG-Reporting-Pflicht ab CSRD 2024/25 für vielen Mittelstand), und bei jüngeren Endkunden. Konkret beziffern (Score, gCO2, Hosting-Standort) schlägt vage "Wir sind grün". Pflicht: Behauptungen verifizierbar machen, sonst Greenwashing-Vorwurf.
Anti"CO2-neutral"-Behauptung ohne Belege. Wettbewerbsrechtlich angreifbar (UWG, abmahnfähig). EU-Green-Claims-Direktive (in Umsetzung) wird 2026/27 strenger. Lieber "100% Ökostrom (Hetzner-Verifikation)" + konkrete Zahl als nebulös "klimafreundlich". Anti: Sustainability-Page mit autoplay-Hintergrundvideo — sofort unglaubwürdig.
35

Documentation Sites — Starlight, Mintlify, Search, Versioning

Falls eine Doku-Site existiert (API-Docs, Tutorials, Knowledge-Base): andere Anforderungen als Marketing-Site. Search ist primärer Navigation-Pfad, llms.txt entscheidet ob deine Docs in Cursor/Claude/Copilot landen, Versioning für Breaking-Changes essentiell, MDX für interaktive Beispiele.

Doku-Stack-Vergleich 2026 Pflicht
StackStärkeSchwächeBeispiel-Nutzer
Astro StarlightFOSS, schnell, gut anpassbar, Markdown-firstKein eingebauter Multi-Lang-Versions-SwitcherBun, Cloudflare
MintlifyBeste Editor-UX, AI-Search eingebaut, gehostedVendor-Lock, $$$ ab SkalierungAnthropic, Cursor, Resend
NextraNext.js-Stack, MDX-starkUpdate-Trägheit, weniger InnovationSWR, Turbo
DocusaurusFOSS, Versioning eingebaut, große CommunityReact-only, Bundle-Size hochReact, Jest, Babel
VitePressFOSS, Vite-fast, Vue-stackMarkdown-only (kein MDX nativ)Vite, Vue, Vitest
FumadocsFOSS, modernes Next.js, Search/Search-AI eingebautJung, kleine CommunityBetter Auth
GitBookWYSIWYG, Editor-friendlyVendor-Lock, Performance schwachKlassische Knowledge-Base
WarumFür Tech-Teams mit Dev-Skill: Astro Starlight oder Fumadocs — gratis, schnell, voller Markdown/MDX-Support. Für Editorial-getriebene Docs ohne Dev-Resources: Mintlify (lohnt sich oft trotz Kosten — Tools, Search, Theming "out of the box"). GitBook nur wenn explizit non-Tech-Editoren primär arbeiten.
llms.txt für Docs Empfehlung
# Layer AI-Search hatte llms.txt für Marketing-Sites
# Doku-Sites haben spezifische Anforderungen — Cursor/Claude/Copilot sollen
# die Docs als Quelle nutzen, nicht halluzinieren

# /llms.txt — Index aller Doku-Sektionen
# /llms-full.txt — Komplette Docs als ein Markdown-Bundle

# Anthropic-Docs Beispiel:
https://docs.anthropic.com/llms.txt
https://docs.anthropic.com/llms-full.txt

# Auto-Generation aus Astro Content Collections:
// scripts/generate-llms.ts
import { getCollection } from 'astro:content';

const docs = await getCollection('docs');

// llms.txt — Index
const llmsIndex = `# Project Docs

> Kurz-Pitch was die Lib/API macht.

## Getting Started
${docs.filter(d => d.data.section === 'start')
       .map(d => `- [${d.data.title}](${d.slug}): ${d.data.description}`)
       .join('\n')}

## API Reference
${docs.filter(d => d.data.section === 'api')
       .map(d => `- [${d.data.title}](${d.slug})`)
       .join('\n')}
`;

// llms-full.txt — Volltext
const llmsFull = docs
  .map(d => `# ${d.data.title}\n\n${d.body}`)
  .join('\n\n---\n\n');
Warum2026 fragen Devs nicht mehr Google, sondern Cursor/Claude/Copilot direkt im Editor. Wer keine LLM-konsumierbaren Docs hat, taucht in den Antworten nicht auf. llms-full.txt ist das einzelne File, das eine LLM mit allen Docs lädt — kein Crawling, kein RAG nötig.
Search-Lösungen Empfehlung
# Search ist die primäre Navigation in Docs (häufiger als Sidebar)

# Algolia DocSearch (FREE für FOSS-Docs)
#   - Beste Search-Quality, blitzschnell
#   - Crawler indexiert wöchentlich
#   - Bewerbung nötig: docsearch.algolia.com/apply

# Pagefind (FOSS, statisch, kein Backend)
npm i pagefind
# In Astro/Hugo/11ty: nach Build wird Index in /pagefind/ generiert
# Komplett client-side, kein API-Call
# Astro Starlight nutzt Pagefind by default

# Meilisearch (FOSS, self-hosted)
#   - Typo-tolerant, faceted, fast
#   - Eigener Server (~50MB RAM, Hetzner CX11 reicht)
#   - SaaS-Variante: Meilisearch Cloud

# Typesense (FOSS, self-hosted, Algolia-Alternative)
#   - Vergleichbar mit Meilisearch, anderer Fokus

# AI-Search (Mintlify-built-in oder via Inkeep)
#   - LLM-gestützte Antworten basierend auf Docs
#   - 100-300 €/Mo
#   - DSGVO: prüfen welcher Provider hinter dem LLM steht
Versioning-Strategie Empfehlung
# Bei Breaking-Changes: alte Docs müssen erreichbar bleiben
# SDK v1 nutzt Doku-v1, SDK v2 nutzt Doku-v2 — beides parallel

# Strategie A: URL-Prefix (sauberste Lösung)
/docs/v1/getting-started
/docs/v2/getting-started
/docs/latest/getting-started   # → 302 zu aktueller Version

# Strategie B: Branch in Git, separate Deploys
/docs   → main branch (latest)
/v1.docs.example.com → v1 branch

# Strategie C: Docusaurus eingebautes Versioning
#   docusaurus.config.js: presets-classic, docs-versions
#   Snapshot der Docs in /versioned_docs/

# Wichtig:
#   - Banner auf alten Versionen: "Du liest v1. Aktuelle Version: v2.x"
#   - Canonical-Tag immer auf neueste Version (für SEO)
#   - Migration-Guide zwischen Major-Versionen prominent
#   - Sunset-Date kommunizieren (wann v1-Docs offline gehen)

# Anti-Pattern: alte Docs einfach löschen → Backlinks brechen, Trust kaputt
MDX für interaktive Docs Nice-to-have
// MDX = Markdown + JSX-Components
// Erlaubt eingebettete interaktive Elemente in der Doku

---
title: API-Beispiel
---

## Live-Beispiel

<CodePlayground lang="typescript">
const result = await api.users.create({
  email: 'test@example.com'
});
</CodePlayground>

## API-Schema

<ApiSchema endpoint="POST /users" />

## Try it

<TryEndpoint method="POST" url="/api/users" />

// Components-Library für Docs:
//   - Callout/Admonition (Note, Warning, Tip)
//   - CodeBlock mit Copy-Button + Sprach-Tabs
//   - APIPlayground (live API-Calls testen)
//   - Diagrams via Mermaid oder Excalidraw
//   - Video-Embeds mit Capture-Frames

// Achtung: MDX-Components müssen accessible sein
// (siehe Layer Accessibility) — sonst sind Docs für Screenreader unbrauchbar
AntiCode-Snippets ohne Sprach-Tag. Syntax-Highlighting kaputt + Copy-Button kann Sprache nicht erkennen. Anti: Lange MDX-Pages mit 20+ Components — Bundle-Size explodiert, Page-Load schlecht. Lieber splitten. Anti: Code-Beispiele die "alle Features" zeigen — User wollen das minimale Beispiel zum Kopieren, nicht den Show-Off.