JavaScript-gerenderte Websites und Agent-Sichtbarkeit
Was heißt „JavaScript-gerendert"?
Eine Seite ist JavaScript-gerendert, wenn das HTML, das der Server sendet, im Wesentlichen leer ist — der eigentliche Inhalt (Überschriften, Text, Produktinformationen) erscheint erst, nachdem der Browser ein JavaScript-Bundle heruntergeladen und ausgeführt hat, das ein Framework wie React, Vue, Svelte oder Angular in ein leeres Root-Element mountet.
Eine typische JS-gerenderte Homepage sieht auf der Leitung so aus:
<body>
<div id="root"></div>
<script type="module" src="/assets/index-D9LVtTP6.js"></script>
</body>
Der sichtbare <body>-Inhalt ist null Bytes groß. Alles, was du im Browser siehst, wurde clientseitig aufgebaut, nachdem das JS gelaufen ist.
Warum das für Agenten wichtig ist
Die meisten KI-Crawler führen kein JavaScript aus. Wenn ChatGPT, Claude, Perplexity, Gemini-Fetches oder ein typisches Agenten-Framework deine URL abrufen, bekommen sie das rohe HTML — und deine Homepage ist leer. Deine Produktbeschreibung, Überschriften, Links und Texte sind für sie unsichtbar.
Das ist ein echter Schaden für Retrieval und Zusammenfassung. Ein LLM, das gefragt wird „Was macht dieses Unternehmen?" und dabei deine URL zitiert, liefert eine schlechte Antwort oder halluziniert, weil auf der Seite nichts zu lesen ist.
Was dadurch nicht kaputtgeht
Tags im statischen <head> deines HTML sind vom clientseitigen Rendering nicht betroffen. Selbst eine vollständig JS-gerenderte SPA kann diese Checks bestehen, wenn das Build-Tool den Head korrekt befüllt:
- Strukturierte JSON-LD-Daten (
<script type="application/ld+json">) - OpenGraph (
og:image,og:titleusw.) - Twitter-Card-Metadaten
- Canonical URL
- Favicons und
<link rel="alternate">-Tags
Wenn deine Website JS-gerendert ist und diese Checks ebenfalls fehlschlagen, sind das unabhängige Lücken — behebe sie, indem du die richtigen <head>-Tags in das HTML-Template deines Builds einfügst, unabhängig davon, ob du das Rendering selbst behebst.
Die gemeinsame Ursache: statisch gehostete SPA-Bundles
JS-gerenderte Websites werden meist als statisches Bundle ausgeliefert (eine index.html plus ein Ordner mit JS/CSS), deployt auf einen Host, der für jeden GET-Request die index.html ausliefert: Vercel static, Netlify, Cloudflare Pages, GitHub Pages, S3 + CloudFront, Fastly Frontend, Firebase Hosting.
Dieses Deployment-Muster hat überhaupt keine serverseitige Pro-Request-Logik. Dieselbe Konfiguration, die einen JS-gerenderten Body erzeugt, erzeugt auch:
- Dasselbe HTML für jeden
Accept-Header (keine Content Negotiation) - HTML 200 für unbekannte Pfade statt strukturierter JSON-404s
- Keine für Agenten abgestimmten
Vary-,Cache-Control- oder Rate-Limit-Header - Keine Möglichkeit, die Antwort nach User-Agent zu variieren
Ein einzelnes Symptom (z. B. Content Negotiation) zu beheben, ohne das Deployment-Modell anzugehen, ist meist unmöglich — du brauchst entweder serverseitige Logik oder eine Edge Function vor dem statischen Bundle.
Die Fix-Leiter
Wähle die günstigste Option, die deinen Fall löst.
1. Prerendering zur Build-Zeit
Wenn dein Homepage-Inhalt weitgehend statisch ist, prerendere ihn während des Builds. Der Browser hydriert weiterhin mit React/Vue/etc., aber das initiale HTML des Servers enthält bereits den gerenderten DOM. Crawler sehen echten Inhalt.
- Vite + react-snap oder vite-plugin-prerender
- Astro (
output: 'static') — JS-gerenderte Komponenten werden zur Build-Zeit serverseitig zu HTML gerendert - Next.js mit
output: 'export'odergetStaticProps - Nuxt mit
nuxt generate
Das ist der Fix mit dem geringsten Aufwand für Marketing-Seiten, Blogs und Dokumentation.
2. Vollständiges Server-Side Rendering (SSR)
Wenn die Homepage dynamische Daten braucht (Login-Status, A/B-Tests, frische Inhalte), nutze ein Framework, das bei jedem Request rendert:
- Next.js (Standard-App-Router mit React Server Components)
- Remix
- SvelteKit
- Nuxt im SSR-Modus
SSR braucht eine Runtime (Node, Bun, Deno oder eine Edge-Runtime) — das heißt, du verlässt reines Static Hosting.
3. Edge Function für Content Negotiation
Wenn die Migration zu SSR zu disruptiv ist, setze eine Edge Function vor dein statisches Bundle, die Requests abfängt und eine alternative Repräsentation ausliefert:
// Cloudflare Worker / Vercel Edge / Netlify Edge
export default async function (request) {
const accept = request.headers.get('accept') || '';
const ua = (request.headers.get('user-agent') || '').toLowerCase();
const isAgent = /claudebot|gptbot|chatgpt-user|perplexitybot|google-extended/.test(ua);
if (isAgent || accept.includes('text/markdown')) {
return new Response(await fetchMarkdownSummary(), {
headers: { 'Content-Type': 'text/markdown', 'Vary': 'Accept, User-Agent' },
});
}
return fetch(request); // pass through to static bundle
}
Das ist der richtige Fix, wenn die zugrunde liegende App clientseitig gerendert bleiben muss (z. B. eine stark interaktive App), Agenten aber trotzdem sinnvollen Inhalt lesen sollen.
4. <noscript>-Fallback (Minimallösung)
Nimm mindestens deine Kerninhalte in einen <noscript>-Block im statischen HTML auf:
<noscript>
<h1>Your Company</h1>
<p>What you do, in two sentences. Link to docs, pricing, API.</p>
<a href="/about">About</a> · <a href="/docs">Docs</a> · <a href="/pricing">Pricing</a>
</noscript>
Das ist kein Ersatz für Prerendering, verhindert aber, dass Crawler eine komplett leere Seite sehen. Die meisten JS-gerenderten Websites haben das nicht.
So prüfst du es
Rufe deine Website ohne JavaScript-Ausführung ab und prüfe, ob echter Inhalt im Body steht:
curl -s -A "ClaudeBot/1.0" https://yourdomain.com/ | \
python3 -c "import sys, re; t = sys.stdin.read(); body = re.search(r'<body[^>]*>(.*?)</body>', t, re.S); print(len(re.sub(r'<[^>]+>', '', body.group(1) if body else '').strip()))"
Wenn die ausgegebene Zahl unter ein paar hundert Zeichen liegt, sehen Agenten deine Inhalte nicht.