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
⚡ Quickstart — wenn du nur 1 Stunde hast
Die fünf Layer, mit denen jede Site 2026 startet
- DNS & Email Domain, Records, SPF/DKIM/DMARC. Ohne läuft nichts — auch keine Mails an Kunden.
- Security Headers HTTPS, HSTS, CSP. Verhindert Browser-Warnungen, XSS und Mixed-Content-Probleme.
- JSON-LD Schema Was Google AI Overviews und ChatGPT-Search von dir sehen — Org, sameAs zu Wikidata.
- Core Web Vitals LCP < 2.5s, INP < 200ms, CLS < 0.1. Direkter Google-Ranking-Faktor.
- Forms & Anti-Spam Honeypot + Cloudflare Turnstile. Conversion-Mechanik ohne DSGVO-Kopfschmerzen.
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
# 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"
# 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
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: 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"
# 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)
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: 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"
| Provider | Anycast | DNSSEC | API | Empfehlung |
|---|---|---|---|---|
| Cloudflare | Ja, top | Ja, 1-Klick | Top | Default-Wahl, frei |
| Route 53 (AWS) | Ja | Ja | Top | AWS-Stack-Lock-In |
| Hetzner DNS | Ja, EU | Ja | OK | Bei Hetzner-Servern |
| desec.io | Ja | Ja, Pflicht | OK | FOSS, EU, gratis |
| Hover / GoDaddy | Nein | Teilweise | Schwach | Vermeiden für Prod |
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
<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>
rel="me" auf Social-Profile validiert Identität. Bei YMYL-Themen (Health, Finance, Legal) absolute Pflicht.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.
| Typ | Beispiele | Stärke | Schwäche |
|---|---|---|---|
| Monolith | WordPress, Drupal, TYPO3, Craft | Editorial-UX, Plugin-Ökosystem, Kunde kann selbst pflegen | Performance, Security, Plugin-Hell |
| Headless API | Storyblok, Sanity, Contentful, Strapi, Directus, Payload | Frontend-Freiheit, Multi-Channel | Editorial-UX schwächer, Setup-Aufwand |
| Git-based | Astro Content, Markdown, MDX, Decap CMS, TinaCMS | Versionierung, Rollback, Dev-Workflow, kostenlos | Nicht-Tech-Editor schwierig |
| Visual Builder | Webflow, Framer, Wix Studio | Designer-friendly, schnell | Vendor-Lock-In, kein Code-Export, teuer ab Skalierung |
| Hybrid (Block + Headless) | WordPress + Faust, Drupal + Next.js | Editorial-UX + moderne Frontend | Doppelte Komplexität |
// 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)
# 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
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.
# 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)
# 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; }
# 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 (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)
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.
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
'unsafe-inline' für Script ist ein Rotes Tuch. frame-ancestors ersetzt X-Frame-Options. interest-cohort & browsing-topics blocken Googles Topics API.# 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
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.
<!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>
lang="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.Semantic HTML — DOM Structure
HTML5-Landmarks, eine h1, saubere Heading-Hierarchie. KI-Crawler segmentieren den Content nach genau diesen Elementen — Divs sind blind.
<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>
<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.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.
<!-- 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">
max-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).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 -->
<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">
og:image:alt wird oft vergessen, ist aber für A11y und AI-Indexierung relevant.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.
<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" }
]
}
purpose: "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.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.
<!-- 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>
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).| Feldtyp | type | inputmode | autocomplete |
|---|---|---|---|
| Name (voll) | text | — | name |
| Vorname | text | — | given-name |
| Nachname | text | — | family-name |
| Telefon | tel | tel | tel |
| Straße | text | — | street-address |
| PLZ | text | numeric | postal-code |
| Stadt | text | — | address-level2 |
| Land | text | — | country-name |
| Kreditkarten-Nr | text | numeric | cc-number |
| OTP / 2FA-Code | text | numeric | one-time-code |
| URL | url | url | url |
| Suche | search | search | off |
| Numerische Eingabe | text | numeric | — |
| Dezimalzahl | text | decimal | — |
type="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.<!-- 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>
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....<!-- 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
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.
<!-- 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="">
: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
| Kategorie | BFSG-pflichtig? | Bedingung |
|---|---|---|
| B2C-Online-Shop | Ja | ≥10 MA ODER >2 Mio € Jahresumsatz |
| Banking-/Zahlungsdienste | Ja | Immer (auch Klein-MA) |
| Personenbeförderung Online | Ja | Immer |
| E-Books / Reader-Apps | Ja | Immer |
| Telekommunikations-Dienste | Ja | Immer |
| Reine B2B-Site (kein Verbraucher-Kontakt) | Nein | Achtung: Definition prüfen! |
| Klein-Solo (<10 MA, <2 Mio €) ohne Shop | Nein | Aber: Kunden können's verlangen |
| Behörden-Sites | Ja, schon vorher | BITV 2.0 (strenger als BFSG) |
<!-- 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>
| Verstoß | Sanktion | Zuständig |
|---|---|---|
| Fehlende Erklärung zur Barrierefreiheit | Bis 10.000 € | Marktüberwachungsbehörde Land |
| Produkt nicht konform (Online-Shop, App) | Bis 100.000 € | Marktüberwachungsbehörde Land |
| Wiederholte Verstöße | Marktverbot des Produkts | BAS / Länder |
| Verbraucherklagen | Abmahnung + Unterlassung | vzbv, Verbraucherschutzverbände |
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.
<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>
@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.{
"@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" }
}
knowsAbout, 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.{
"@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
}
}
hasMerchantReturnPolicy + shippingDetails sind seit 2023 bei Google Shopping de-facto Pflicht. gtin13/mpn matchen Produkte über Shop-Grenzen hinweg im Knowledge Graph.availability: InStock hardcoden, wenn Stock dynamisch — bei OOS-Klick verliert man Trust.// 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" }
}
}
validThrough ist Pflicht für Google for Jobs.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.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.
# 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
/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.# 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
| User-Agent | Zweck | Empfehlung |
|---|---|---|
| Googlebot | Google Search Index | Allow |
| Google-Extended | Gemini-Training | Disallow |
| GPTBot | OpenAI Training | Disallow |
| OAI-SearchBot | ChatGPT-Search Live | Allow |
| ChatGPT-User | User-getriggert | Allow |
| ClaudeBot | Anthropic Live | Allow |
| anthropic-ai | Anthropic Training | Disallow |
| PerplexityBot | Perplexity Live | Allow |
| CCBot | Common Crawl | Disallow |
| Applebot-Extended | Apple AI Training | Disallow |
| Bingbot | Bing + Copilot | Allow |
"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"
]
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.
<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>
"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
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.
// 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)}`]
}
};
}
@cloudflare/workers-types + workers-og-Lib. Astro: astro-og-canvas oder satori direkt im Build-Step.// 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'
}
});
# 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
International SEO — hreflang Edge Cases
Multi-Locale-Sites scheitern fast immer an inkonsistenten hreflang-Tags. Reciprocity, x-default, ccTLD vs Subdir, IP-Redirect-Falle.
| Strategie | Beispiel | Pro | Contra |
|---|---|---|---|
| ccTLD | example.de | Stärkstes Geo-Signal | Hoster, SSL pro TLD |
| Subdomain | de.example.com | Hosting-flexibel | Authority pro Subdomain |
| Subdirectory | example.com/de/ | Authority gebündelt | Geo-Signal schwächer |
| URL-Param | ?lang=de | — | Anti — Crawl-Probleme |
<!-- 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 -->
x-default ist die Fallback-URL für nicht-gematchte Locales — meistens die englische oder die Hauptsprache.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.// 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]
Accept-Language hart redirecten — VPN/Tor-Nutzer landen falsch. RICHTIG: Sprach-Switcher prominent + optional Banner-Hinweis mit Choice.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 | Budget | Hard Limit |
|---|---|---|
| HTML (initial) | ≤ 30 KB | 50 KB |
| CSS (kritisch, inline) | ≤ 14 KB | 20 KB |
| CSS (gesamt) | ≤ 50 KB | 100 KB |
| JS (initial bundle) | ≤ 100 KB | 170 KB |
| JS (gesamt route) | ≤ 200 KB | 350 KB |
| Fonts (gesamt) | ≤ 100 KB | 200 KB |
| LCP-Image | ≤ 100 KB AVIF | 200 KB |
| Total Page Weight | ≤ 500 KB | 1 MB |
| Requests | ≤ 30 | 50 |
| 3rd-Party-Domains | ≤ 4 | 8 |
// .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 }]
}
}
}
}
numberOfRuns: 5 ist Pflicht — einzelne Lighthouse-Runs streuen ±10%. Median aus 5 ist stabil.// 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);
sendBeacon 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 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"
Core Web Vitals — Performance
Google rankt schnelle Sites besser. INP hat seit März 2024 FID abgelöst. Die Targets:
<!-- 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">
width + 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.<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));
}
}
}
scheduler.yield() ist 2026 in Chromium verfügbar. Fallback: setTimeout(0).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>
<!-- 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>
sizes 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.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.// 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"
format=auto serviert AVIF an moderne Browser, WebP/JPEG an alte. quality=75 ist visuell verlustfrei für 95% der Cases.<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>
preload="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).muted autoplay playsinline. Anti: preload="auto" auf Listing-Seiten mit mehreren Videos — Megabyte-Schleuder.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.
<!-- 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
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 (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
# 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
"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.# 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
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 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>
autocomplete-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.<!-- 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)
aria-hidden + tabindex=-1 — Screenreader und Tastatur-Nav stolpern darüber.// 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');
}
});
aria-live — Screenreader sagen nichts, blinde User wissen nicht ob's geklappt hat.// 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 });
}
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.
<!-- 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"
}]
}
<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>
<!-- 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>
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).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")
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
// 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';
example.com funktioniert nicht auf app.example.com ohne explizite Konfiguration.// 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);
}
// 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
})
});
state-Parameter weglassen — CSRF-Lücke. Anti: redirect_uri nicht exakt validieren (Wildcard!) — Open-Redirect-Angriff.// 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');
}
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.
/* 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>
<!-- 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 -->
/* :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 */
: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.<!-- 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>
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.
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: '/' }
]
})
]
});
prefetch.viewport lädt Links erst beim Scroll ins Viewport. trailingSlash: 'never' + format: 'file' verhindert Redirect-Ketten und Canonical-Konflikte./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.// 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}` }
};
}
metadataBase macht alle relativen URLs (OG-Bilder, Canonical) automatisch absolut — Hauptgrund für kaputte OG-Previews. template: '%s | Brand' spart Repetition.next/head aus dem Pages Router ist im App Router obsolet — nie mischen.// 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'
};
}
// 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]);
});
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.
// 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)
// 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
}
}
}
// 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
// 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
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.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") -->
<!-- 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
| Hint | Was | Wann | Beispiel |
|---|---|---|---|
| preconnect | TCP+TLS-Handshake | 3rd-party Origins, die du bald brauchst | Fonts CDN, Analytics |
| dns-prefetch | Nur DNS-Lookup | Fallback für ältere Browser, sehr viele Origins | Tracking-Pixel |
| preload | Resource SOFORT laden | Critical Asset auf aktueller Seite (LCP-Image, Font) | Hero-Font |
| modulepreload | JS-Module SOFORT laden | Bekannte ES-Module-Imports | App-Entry |
| prefetch | Resource für NÄCHSTE Seite | Wahrscheinlich-nächste Navigation | "Next Article" |
| prerender | Komplette Seite vorrendern | Sehr sicher dass Nutzer dorthin geht | Pagination "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">
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).# 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"
// 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
}
// 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'
}
});
}
Access-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.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
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.
| Report | Was misst er | Wie oft prüfen |
|---|---|---|
| Performance | Klicks, Impressionen, CTR, Position pro Query/Page/Land/Device | Wöchentlich |
| Indexierung | Welche URLs sind im Index, welche nicht und warum | Wöchentlich |
| Core Web Vitals | Real-User LCP/INP/CLS aus CrUX, mobile + desktop | Monatlich |
| Manuelle Maßnahmen | Penalties durch Google-Reviewer | Wöchentlich (Alert!) |
| Sicherheitsprobleme | Malware, Hacking, Social Engineering | Wöchentlich (Alert!) |
# 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
# 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
# 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)
| Tool | DACH-Datenqualität | Preis/Mo | Stärke |
|---|---|---|---|
| Sistrix | Spitze (DE-Standard) | ~100 € | DACH-Sichtbarkeitsindex, historische Daten seit 2008 |
| SE Ranking | Solide | ~50 € | Bestes Preis/Leistung, gute UI |
| Mangools (KWFinder) | OK | ~30 € | Einsteiger, übersichtlich |
| Ahrefs | Top global | ~110 € | Backlinks, Content-Gap, Site-Audit |
| Semrush | Top global | ~130 € | All-in-one, PPC-Daten |
| Google Keyword Planner | Live-Daten | 0 € (mit Ads-Account) | Search Volume direkt von Google, gerundet |
| Google Trends | Live-Daten | 0 € | Saisonalität, Vergleich, Breakout-Topics |
| AnswerThePublic | OK | 0–11 € | Long-Tail-Fragen visualisiert |
| People-Also-Ask Scraping | Live | 0 € (Custom) | Echte AI-relevante Fragen |
# 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)
# 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
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
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.
| Tool | Hosting | Cookies | Consent | Use-Case |
|---|---|---|---|---|
| Plausible Cloud (EU) | EU (DE) | Nein | Nein¹ | SMB, Marketing |
| Plausible Self-Hosted | Eigene | Nein | Nein | Volle Kontrolle |
| Matomo Self-Hosted | Eigene | Optional | Nein (cookieless) | Tiefere Analyse |
| Umami Self-Hosted | Eigene | Nein | Nein | Minimal, FOSS |
| GA4 + Server-Side + Consent v2 | Hybrid | Ja | Ja | E-Commerce mit Ads |
| GA4 ohne Consent | — | — | — | Verboten in EU |
<!-- 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 } });
// 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"
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
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.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).
# 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
<!-- 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 -->
// 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
data_processing_options bei Meta).<!-- 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)
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.
// 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
debug in Production lassen — Disk-Full-Outage in 2 Wochen.// 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)
// 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)
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
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.
# 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)
# 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 (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
Pflicht ab 5000 Mails/Tag an Gmail oder Yahoo
- SPF + DKIM + DMARC alle drei korrekt (siehe Layer DNS)
- DMARC Policy mind.
p=none(besserquarantine) - 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
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).
| Hoster | Standort | Strom | Verifiziert |
|---|---|---|---|
| Hetzner | DE / FI | 100% Wasserkraft (FI), 100% Grün (DE seit 2017) | Green Web Foundation |
| Cloudflare | Global | 100% Renewables-Match seit 2019 | Cloudflare Sustainability Report |
| Vercel | Global (AWS-basiert) | AWS Region-abhängig | Teilweise |
| AWS Frankfurt (eu-central-1) | DE | 100% Renewables seit 2023 | AWS Sustainability |
| Netcup | DE | 100% Ökostrom | TÜV Süd zertifiziert |
| IONOS | DE | 100% Ökostrom | TÜV Süd |
| OVHcloud | FR / DE | Mix, 80% Renewables-Ziel 2025 | Eigene Reports |
| Webflow / Squarespace | USA | AWS / GCP — variiert | Teilweise |
# 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)
<!-- 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 -->
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.
| Stack | Stärke | Schwäche | Beispiel-Nutzer |
|---|---|---|---|
| Astro Starlight | FOSS, schnell, gut anpassbar, Markdown-first | Kein eingebauter Multi-Lang-Versions-Switcher | Bun, Cloudflare |
| Mintlify | Beste Editor-UX, AI-Search eingebaut, gehosted | Vendor-Lock, $$$ ab Skalierung | Anthropic, Cursor, Resend |
| Nextra | Next.js-Stack, MDX-stark | Update-Trägheit, weniger Innovation | SWR, Turbo |
| Docusaurus | FOSS, Versioning eingebaut, große Community | React-only, Bundle-Size hoch | React, Jest, Babel |
| VitePress | FOSS, Vite-fast, Vue-stack | Markdown-only (kein MDX nativ) | Vite, Vue, Vitest |
| Fumadocs | FOSS, modernes Next.js, Search/Search-AI eingebaut | Jung, kleine Community | Better Auth |
| GitBook | WYSIWYG, Editor-friendly | Vendor-Lock, Performance schwach | Klassische Knowledge-Base |
# 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');
llms-full.txt ist das einzelne File, das eine LLM mit allen Docs lädt — kein Crawling, kein RAG nötig.# 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
# 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 = 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