Wie KI deine Seite sieht: Inhalte, die der Crawler nicht findet
Eine Produktseite in einem Onlineshop: über dem Foto Name und Preis, darunter drei Tabs – „Beschreibung“, „Technische Daten“, „Versand“ – und weiter unten ein Bewertungsbereich, der nachgeladen wird, sobald der Nutzer dorthin scrollt. Die Beschreibung hat zweitausend Wörter, die technischen Daten dreißig Parameter, Bewertungen gibt es zweihundert. Alles, was man braucht, um die Frage „Eignet sich diese Lampe für ein kleines Büro?“ zu beantworten.
Jemand stellt genau diese Frage einem KI-Assistenten mit Websuche. Die Antwort zitiert deine Seite – und stützt sich ausschließlich auf zwei Sätze aus der Meta-Description und den Preis. Kein Wort über Maße, Lichtstärke oder Bewertungen, obwohl sie auf der Seite stehen.
Der erste Verdacht fällt meist auf CSS: „Die Tabs haben display: none, also sieht der Bot sie nicht.“ Das ist fast nie die Ursache. Um zu verstehen, wo der Inhalt wirklich verloren geht, muss man wissen, was ein Crawler überhaupt mit einer Seite macht.
Ein Crawler sieht sich die Seite nicht an, er liest HTML
Ein Browser lädt HTML, dann CSS und JavaScript, baut den DOM-Baum, führt Skripte aus, berechnet das Layout und zeichnet Pixel. Der Nutzer sieht das Ergebnis dieses ganzen Prozesses. Ein Crawler macht meist nur den ersten Schritt: Er lädt das HTML und zieht den Text daraus.
Der Googlebot ist hier die Ausnahme – er rendert Seiten seit Jahren in einem Browser, JavaScript inklusive, wenn auch mit Verzögerung. Die Crawler, die KI-Assistenten und deren Suche speisen, tun das aber größtenteils nicht. Analysen des Traffics solcher Bots, unter anderem von Vercel veröffentlicht, zeigten, dass sie JavaScript-Dateien herunterladen, aber nicht ausführen. In der Praxis bedeutet das eine einfache Regel, von der alles Weitere abhängt: Ein KI-Crawler sieht, was im vom Server gesendeten HTML steht – und sonst nichts.
Aus dieser Regel folgen zwei Dinge zugleich. Per Style versteckter, aber im HTML vorhandener Inhalt ist für den Crawler genauso sichtbar wie jeder andere. Und Inhalt, der erst im Browser entsteht – nach einem Klick, nach dem Scrollen, nach der Ausführung eines Skripts –, existiert für den Crawler nicht.
Die Falle: display: none ist nicht das Problem
Ein Crawler, der kein Layout berechnet, weiß nicht einmal, dass irgendeine CSS-Regel ein Element ausblendet. Für ihn ist ein Tab mit display: none, hidden oder visibility: hidden einfach ein weiteres Stück Text im HTML. Suchmaschinen gewichten solchen Inhalt womöglich anders, aber sie sehen ihn.
Das eigentliche Problem liegt einen Schritt früher – darin, ob der versteckte Inhalt überhaupt im HTML gelandet ist. Und das entscheidet nicht CSS, sondern die Art, wie die Komponente ihn ausblendet. Vergleiche zwei Umsetzungen derselben Tabs in React:
tsx
// „Bedingte“ Tabs: Im HTML steht nur das aktive Panelfunction ProductTabs({ tabs }: { tabs: Tab[] }) {const [active, setActive] = useState(tabs[0].id)const current = tabs.find((t) => t.id === active)!return (<div><TabList tabs={tabs} active={active} onChange={setActive} /><div role="tabpanel">{current.content}</div></div>)}
Sieht harmlos aus und funktioniert für den Nutzer gut. Aber der Server rendert nur den ersten Tab. Technische Daten und Versandinformationen entstehen im Browser erst nach einem Klick – und ein Crawler klickt nie. Für ihn hat die Produktseite eine Beschreibung und sonst nichts.
tsx
// Tabs per Attribut ausgeblendet: Im HTML stehen alle Panelsfunction ProductTabs({ tabs }: { tabs: Tab[] }) {const [active, setActive] = useState(tabs[0].id)return (<div><TabList tabs={tabs} active={active} onChange={setActive} />{tabs.map((t) => (<divkey={t.id}role="tabpanel"id={"panel-" + t.id}aria-labelledby={"tab-" + t.id}hidden={t.id !== active}>{t.content}</div>))}</div>)}
Dieselben Tabs für den Nutzer, derselbe Zustand, dieselbe Klickbehandlung. Es gibt nur einen Unterschied: Alle Panels werden gerendert, und die inaktiven blendet das Attribut hidden aus. Der Server schickt die ganze Beschreibung, alle technischen Daten und die Versandinformationen im HTML. Der Crawler liest alles.
Dasselbe gilt für FAQ-Akkordeons, aufklappbare Bereiche „Weitere Informationen“ und jede Komponente, die Inhalte auf Anforderung zeigt. Die Regel lautet: mit Style oder Attribut ausblenden, nicht mit einer Bedingung. {open && <Panel />} entfernt den Inhalt aus dem HTML, hidden={!open} versteckt ihn nur.
hidden="until-found": versteckt, aber auffindbar
Es gibt eine Variante, die noch einen Schritt weitergeht. Im Artikel über content-visibility habe ich hidden="until-found" beschrieben – ein Attribut, das ein Element wie hidden ausblendet, dem Browser aber erlaubt, es aufzudecken, wenn der Nutzer per Strg+F nach seinem Text sucht oder über einen Link mit Textfragment dort landet.
html
<div role="tabpanel" id="panel-spec" hidden="until-found"><table>…</table></div>
Für einen Crawler ist das genau dasselbe wie gewöhnliches hidden – der Text steht im HTML. Für den Nutzer löst es ein Problem, das Tabs schon immer hatten: Die Suche im Browser findet das Wort „Gewicht“ nicht, weil die Datentabelle in einem Tab steht, den niemand geöffnet hat. Mit until-found deckt der Browser das Panel selbst auf und löst am Element das Event beforematch aus, in dem du den aktiven Tab umschalten kannst, damit die Oberfläche stimmig bleibt. Ein Browser, der diesen Wert nicht kennt, behandelt ihn wie gewöhnliches hidden – du verlierst also nichts.
Denselben Effekt liefert das native <details> kostenlos: Der Inhalt eines geschlossenen Elements steht im HTML, und neuere Browser decken ihn bei der Suche auf.
Text, der nicht im HTML steht: CSS-content und Bilder
Die zweite Kategorie sind Inhalte, die auf der Seite sichtbar sind, aber gar kein Text im Dokument sind. Der heimtückischste Fall ist content in Pseudoelementen:
css
.price::after {content: " inkl. MwSt.";}.product-card--new::before {content: "Neu";}.shipping-info::before {content: "Kostenloser Versand ab 50 €";}
Der Nutzer sieht „Kostenloser Versand ab 50 €“. Der Crawler nicht, denn dieser Text steht nicht im HTML – er existiert nur im Stylesheet, und der Crawler wendet kein CSS an. Er steht auch nicht in textContent, lässt sich nicht kopieren, und ein in einen Assistenten eingefügter Seitenausschnitt enthält ihn nicht. Pseudoelemente sind gut für Dekoration – Icons, Pfeile, Anführungszeichen. Jede Information, die jemand zitieren könnte, sollte echter Text im HTML sein.
Dieselbe Regel gilt für Text in Bildern. Ein Banner „−30 % auf die ganze Kollektion bis Sonntag“, als ein einziges JPG exportiert, ist für einen Crawler nur sein alt – falls es eines gibt. Besser ist es, dasselbe mit echtem Text zu machen, der per CSS über das Foto gelegt wird: position: absolute für den Text, object-fit: cover für das Bild und darunter ein Verlauf für den Kontrast. Sieht genauso aus, skaliert mit dem Bildschirm, lässt sich übersetzen – und lesen.
Nachgeladene Inhalte: Bewertungen, „Mehr anzeigen“, Infinite Scroll
Die dritte Kategorie sind Inhalte, die erst nach einer Aktion des Nutzers oder nach dem Scrollen auf die Seite kommen. Im E-Commerce sind das meist Bewertungen – und das ist ein doppelter Verlust, denn genau nach Bewertungen fragen Menschen KI-Assistenten: „Taugt dieses Produkt etwas?“, „Worüber beschweren sich Käufer?“.
tsx
// Bewertungen, die geladen werden, wenn der Bereich ins Bild scrolltfunction Reviews({ productId }: { productId: string }) {const [reviews, setReviews] = useState<Review[]>([])const ref = useInView(() => {fetch("/api/reviews/" + productId).then((r) => r.json()).then(setReviews)})return <section ref={ref}>{reviews.map(renderReview)}</section>}
Für die Performance ist das vernünftig: Du lädst keine zweihundert Bewertungen, bevor jemand dorthin scrollt. Aber ein Crawler scrollt nicht, also ist der Bewertungsbereich für ihn leer. Der Kompromiss, der meist am besten funktioniert: die ersten paar Bewertungen und eine Zusammenfassung (Durchschnittsbewertung, Anzahl, häufigste Vor- und Nachteile) im HTML vom Server, den Rest beim Scrollen oder nach einem Klick auf „Mehr anzeigen“ nachladen. Für den Nutzer ist der Unterschied unmerklich, für den Crawler ist es der Unterschied zwischen „keine Bewertungen“ und konkreten Argumenten.
Dasselbe gilt für das Kürzen von Text. Eine per Style gekürzte Beschreibung – line-clamp, max-height mit Verlauf und einem Button „Weiterlesen“ – steht vollständig im HTML. Eine im Code gekürzte Beschreibung – description.slice(0, 300) mit Nachladen des Rests per Klick – hat im HTML dreihundert Zeichen.
css
/* Die ganze Beschreibung steht im HTML, sichtbar sind die ersten 4 Zeilen */.description:not(.is-expanded) {display: -webkit-box;-webkit-line-clamp: 4;-webkit-box-orient: vertical;overflow: hidden;}
Next.js: Was im HTML landet und was nicht
In Next.js mit dem App Router bekommst du vieles davon kostenlos, aber nicht alles. Vier Fälle sollte man kennen:
- Server Components werden auf dem Server gerendert, und ihr Ergebnis landet im HTML. In einer Server Component geladene Daten – Produktbeschreibung, technische Daten, die ersten Bewertungen – sind für jeden Crawler sichtbar.
- Client Components (
"use client") werden bei der ersten Anfrage ebenfalls auf dem Server gerendert. Ein häufiges Missverständnis:"use client"heißt nicht „nur im Browser“. Die Tabs aus dem Beispiel oben können eine Client-Komponente sein und schicken trotzdem alle Panels im HTML. - In
useEffectgeladene Daten oder solche aus einer Bibliothek, die nur im Browser läuft, landen nicht im HTML – Effekte laufen nicht auf dem Server. Dasselbe gilt für Komponenten, die perdynamic(..., { ssr: false })geladen werden. - Streaming mit Suspense – beschrieben im Artikel über
use()und Suspense – schickt Inhalte in derselben HTTP-Antwort, nur später. Ein Crawler, der die ganze Antwort lädt, bekommt auch den gestreamten Inhalt, auch wenn er am Ende des Dokuments steht und von einem kleinen Skript an seinen Platz verschoben wird. Der Text steht im HTML, also liest der Crawler ihn.
Das sicherste Muster für eine Produktseite: Daten in Server Components laden, Interaktivität in kleinen Client-Komponenten, die Inhalte als Props oder children bekommen, statt sie selbst zu laden.
Wie man prüft, was der Crawler sieht
Raten ist nicht nötig. Der einfachste Test ist, die Seite ohne Browser zu laden und darin nach einer bestimmten Phrase aus dem versteckten Inhalt zu suchen:
bash
# Steht eine Phrase aus den technischen Daten im vom Server gesendeten HTML?curl -s https://example.com/produkt/bueroleuchte | grep -c "Lichtstrom"# Dasselbe mit dem User-Agent-Header eines der Botscurl -s -A "GPTBot" https://example.com/produkt/bueroleuchte | grep -c "Lichtstrom"
0 bedeutet, dass der Crawler diese Phrase nicht sieht. Der zweite Weg braucht kein Terminal: Schalte in den DevTools JavaScript ab (in Chrome: Befehlspalette, „Disable JavaScript“) und lade neu. Was du dann siehst – oder nicht siehst –, kommt dem, was der Crawler bekommt, sehr nahe. Der dritte: „Seitenquelltext anzeigen“ statt des Tabs Elements. Elements zeigt den DOM nach der Ausführung der Skripte, der Quelltext das HTML, das der Server geschickt hat.
Fallen und Best Practices
- Ausblenden per Style schadet nicht, Fehlen im HTML schon.
display: noneundhiddenlassen den Text im Dokument.{open && <Panel />}entfernt ihn aus dem vom Server gesendeten HTML. - Tabs und Akkordeons vollständig rendern. Inaktive Panels mit dem Attribut
hiddenausblenden oder mithidden="until-found", das Inhalte zusätzlich per Strg+F auffindbar macht. - Informationen sind Text, kein CSS-
contentund kein Bild. Pseudoelemente der Dekoration überlassen, Bannertext per CSS über das Foto legen. - Die wichtigsten Bewertungen und eine Zusammenfassung im Server-HTML. Nachladen beim Scrollen ist gut für den Rest, aber ein Crawler scrollt nie.
- Text per Style kürzen, nicht im Code.
line-clamplässt die ganze Beschreibung im HTML,slice()nicht. "use client"schließt SSR nicht aus,useEffectundssr: falseschon. Inhalte in Server Components laden und an interaktive Komponenten übergeben.- Mit
curloder ohne JavaScript testen. Der Tab Elements in den DevTools zeigt die Seite nach den Skripten, der Crawler sieht sie davor.
Fazit
Die Crawler, die KI-Assistenten speisen, führen größtenteils kein JavaScript aus und wenden kein CSS an – sie lesen das vom Server gesendete HTML und ziehen den Text daraus. Deshalb ist das Ausblenden per Style, vor dem wir uns so fürchten, fast nie das Problem: Ein Tab mit display: none ist für einen Crawler genauso lesbar wie der Rest der Seite. Das Problem sind Inhalte, die in diesem HTML nicht stehen – bedingt gerenderte Panels, beim Scrollen geladene Bewertungen, im Code gekürzte Beschreibungen, per CSS-content eingefügte oder in ein Bild gebrannte Informationen. Die Lösungen sind einfach und verbessern meist auch Barrierefreiheit und die Suche im Browser: alles rendern und per Attribut ausblenden, die wichtigsten Inhalte auf dem Server laden, Informationen als Text schreiben. Und wenn du dir nicht sicher bist, sagen dir curl und ein einziges grep in einer Sekunde, was ein Crawler von deiner Seite sieht.