# Was Crawler und KI von htmx-Inhalten sehen: eine Messung

> Ich habe Prüfmarken in eine htmx-Demo gesetzt und gemessen, was curl, ein Headless-Browser und ein KI-Assistent davon sehen. Was sichtbar bleibt und was nicht.

Source: https://www.jpkc.com/db/blog/htmx-crawler-geo/

In [Teil 1](https://www.jpkc.com/db/blog/htmx-einordnung/) dieser Reihe habe ich einen Veranstaltungskalender mit htmx, mit Unpoly und ohne Bibliothek gebaut. Im Browser sehen alle Fassungen gleich aus. Die Frage dieses Teils ist, ob das auch für die Programme gilt, die deine Seite nicht mit einem Browser ansehen: Suchmaschinen-Crawler, KI-Crawler und KI-Assistenten, die eine URL auf Zuruf abrufen.

Für klassisches SEO ist die Frage alt. Für GEO, also die Sichtbarkeit in KI-Antworten, ist sie neu und schärfer. Ein Inhalt, den ein KI-System beim Abruf nicht sieht, kann es auch nicht zitieren. Ich wollte deshalb nicht nachlesen, was Crawler angeblich können, sondern selbst messen, was bei ihnen ankommt.

> **Methodik-Hinweis:** Alle Messungen stammen vom 29.09.2026 und liefen gegen die live ausgelieferte [Demo](https://www.jpkc.com/db/demo/veranstaltungskalender/). Aussagen über Google, Bing und die KI-Anbieter stützen sich auf deren Dokumentation und auf veröffentlichte Studien. Wo eine Aussage nur von Mitarbeitern stammt oder nur beobachtet ist, sage ich das dazu.

## Wie Crawler heute rendern

**Google** führt JavaScript aus. Laut [Dokumentation zu JavaScript-SEO](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics?hl=de) nutzt die Google Suche eine aktuelle Chromium-Version. Der Ablauf hat zwei Schritte: Googlebot holt zuerst das HTML, die Seite wandert dann in eine Warteschlange, und erst später rendert ein Headless-Chromium sie und führt das JavaScript aus. Das kann Sekunden dauern, aber auch länger. Eine Einschränkung ist für diese Demo wichtig: Bei Seiten mit `noindex` kann Google das Rendern ganz überspringen. Die Demo ist `noindex`, sie kann also nicht zeigen, was in Googles Index landet. Sie zeigt, was ein Crawler grundsätzlich sehen *kann*.

Google interagiert beim Rendern nicht mit der Seite. Die Doku zu [Lazy Loading](https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading?hl=de) sagt, dass Googlebot weder scrollt noch klickt. Inhalte, die erst ein Klick auf einen Button lädt, sieht Google nicht. Wie Google Inhalte weiter unten trotzdem erfasst, steht dort nicht. Die bekannte Antwort stammt von Google-Mitarbeitern: John Mueller sprach [2017](https://www.seroundtable.com/googlebot-9000px-high-viewport-24727.html) von einem sehr hohen Viewport, mit dem Googlebot rendert, und Martin Splitt sagte [2020](https://www.searchenginejournal.com/google-infinite-scroll-lazy-loading/363184/) knapp: „It doesn't scroll."

Links erkennt Google nur als `<a>` mit `href`-Attribut. Die Doku zu [crawlbaren Links](https://developers.google.com/search/docs/crawling-indexing/links-crawlable?hl=de) sagt ausdrücklich, dass Google URLs nicht zuverlässig aus anderen Elementen extrahieren kann, die über Skriptereignisse als Links fungieren. `hx-get` nennt die Doku nicht, fällt aber unter dieselbe Regel. Das ist meine Schlussfolgerung, keine wörtliche Aussage von Google.

**Bing** hat im Oktober 2019 angekündigt, Bingbot schrittweise auf ein aktuelles, Chromium-basiertes Edge umzustellen und es laufend zu aktualisieren, so die [Ankündigung des „evergreen Bingbot"](https://blogs.bing.com/webmaster/october-2019/The-new-evergreen-Bingbot-simplifying-SEO-by-leveraging-Microsoft-Edge). Eine aktuelle, ausdrückliche Empfehlung von Bing zu JavaScript-lastigen Seiten konnte ich nicht belegen.

**KI-Crawler** sind der eigentliche Grund für diesen Artikel. Die Analyse von Vercel und MERJ vom Dezember 2024, [„The rise of the AI crawler"](https://vercel.com/blog/the-rise-of-the-ai-crawler), kommt zu einem klaren Ergebnis: Die Crawler von OpenAI (GPTBot, OAI-SearchBot, ChatGPT-User) und Anthropic (ClaudeBot) holen JavaScript-Dateien zwar ab, führen sie aber nicht aus. Rendern können laut Studie nur Googles Infrastruktur, die auch Gemini nutzt, und Applebot. Eine [Fallstudie von Glenn Gabe](https://www.gsqi.com/marketing-blog/ai-search-javascript-rendering/) vom August 2025 bestätigt das Bild: ChatGPT, Perplexity und Claude konnten clientseitig gerenderte Inhalte nicht lesen, Googles AI Overviews und Bing Copilot schon. Zwei Einschränkungen gehören dazu. Die Anbieter selbst dokumentieren das Nicht-Rendern nicht ausdrücklich, und agentische Browser, die im Auftrag eines Nutzers surfen, führen JavaScript sehr wohl aus. Das sind dann aber Browser, keine Crawler.

## Der Versuchsaufbau

Jeder Inhaltsblock der Demo trägt eine sichtbare Prüfmarke, ein eindeutiges Wort wie `CANARY-LISTE-HTMX-DE-R-…`. Der vordere Teil sagt, welcher Block es ist, der hintere ist ein Hash. Den kann kein Programm erraten, ohne die Seite gesehen zu haben. Die Markierungen robuster und fragiler Blöcke unterscheiden sich. In diesem Artikel kürze ich sie ab, damit sie nicht selbst auf einer indexierten Seite stehen und die Messung verfälschen.

Dann habe ich dieselben URLs auf drei Wegen abgerufen:

1. **curl**: ein einfacher HTTP-Abruf ohne JavaScript, so wie ihn ein Crawler ohne Rendering sieht (`curl -sL`, Version 8.5.0).
2. **Headless-Browser**: ein Chromium ohne Fenster (Playwright, `HeadlessChrome/155`), Viewport 1280 × 720. Je URL: laden, ans Seitenende scrollen, drei Sekunden warten, dann das fertige DOM speichern. Das ist eine großzügige Sicht: Googlebot scrollt nicht, gleicht das aber mit einem sehr hohen Viewport teilweise aus (siehe unten).
3. **KI-Assistent**: Claude über das Werkzeug WebFetch von Claude Code, mit der Bitte, jede Zeichenkette zurückzugeben, die mit `CANARY-` beginnt. Laut [Dokumentation](https://code.claude.com/docs/de/tools-reference) holt WebFetch die Seite, wandelt das HTML in Markdown um und lässt ein kleines Modell die Frage darauf beantworten. Dass dabei kein JavaScript läuft, steht dort nicht ausdrücklich. Ich habe es beobachtet: Eine reine JavaScript-Anwendung kam bei WebFetch als leere Hülle an.

Ein kleines Skript liest die gespeicherten Antworten und prüft für jede Prüfmarke, ob sie vorkommt.

## Das Ergebnis

| Seite · Block | curl | Headless | Claude |
|---|---|---|---|
| htmx · Liste (im HTML) | ✓ | ✓ | ✓ |
| htmx · Nächste Termine (im HTML) | ✓ | ✓ | ✓ |
| Unpoly · Liste und Nächste Termine | ✓ | ✓ | ✓ |
| ohne Bibliothek · Liste, Nächste Termine, Kurzinfo-Dialog | ✓ | ✓ | ✓ |
| htmx · Tab-Ziel als eigene URL (`/htmx/lesungen/`) | ✓ | ✓ | ✓ |
| htmx · Seite 2 als eigene URL (`/htmx/seite/2/`) | ✓ | ✓ | ✓ |
| fragil · Liste (im HTML) | ✓ | ✓ | ✓ |
| fragil · Nächste Termine (`hx-trigger="load"`) | ✗ | ✓ | ✗ |
| fragil · Mehr laden (`hx-trigger="revealed"`) | ✗ | ✓ | ✗ |
| fragil · Tab (Button mit `hx-get`) | ✗ | ✗ | ✗ |
| fragil · Kurzinfo (Button mit `hx-get`) | ✗ | ✗ | ✗ |

Die englische Fassung der Demo liefert per curl und im Headless-Browser exakt dasselbe Muster. Die KI-Sicht habe ich nur für die deutschen URLs gemessen.

Drei Dinge lese ich aus der Tabelle.

**Was im HTML steht, sieht jeder.** Alle robusten Blöcke kommen in allen drei Sichten an, bei htmx genauso wie bei Unpoly und ohne Bibliothek. Die Bibliothek ist für die Sichtbarkeit egal, entscheidend ist, ob der Inhalt in der Serverantwort steht.

**Was nachgeladen wird, sieht nur, wer JavaScript ausführt.** Die beiden fragilen Blöcke hinter `load` und `revealed` fehlen bei curl und bei Claude. Nach der Studienlage zu KI-Crawlern gilt dasselbe für GPTBot, ClaudeBot und PerplexityBot. Der Headless-Browser sieht sie, Googlebot vermutlich auch, weil er rendert.

**Was hinter einem Klick liegt, sieht niemand.** Tab und Kurzinfo in der fragilen Fassung fehlen in allen drei Sichten, auch im Headless-Browser. Das deckt sich mit Googles Aussage, dass Googlebot nicht klickt. Und `hx-get` ist kein Link, dem ein Crawler folgen könnte.

## `revealed` hängt an der Höhe des Fensters

Beim Endlos-Scrollen per `hx-trigger="revealed"` wollte ich es genauer wissen. htmx lädt den nächsten Schwung, sobald das Platzhalter-Element am Listenende in den sichtbaren Bereich kommt. Also habe ich dieselbe fragile Seite im Headless-Browser ohne jedes Scrollen geladen, einmal mit 720 Pixel Fensterhöhe und einmal mit 4000:

| Fensterhöhe, ohne Scrollen | Termine | Block „Mehr laden" |
|---|---|---|
| 1280 × 720 | 6 | fehlt |
| 1280 × 4000 | 12 | da |

Ob ein Renderer den nachgeladenen Inhalt sieht, entscheidet also nicht htmx, sondern die Fensterhöhe des Renderers. Mit Muellers „sehr hohem Viewport" würde Googlebot diese kurze Liste wohl vollständig sehen. Bei einer langen Liste mit vielen Nachlade-Stufen wäre ich mir nicht sicher, und belegen lässt es sich mit einer `noindex`-Demo nicht. Für Inhalte, die gefunden werden sollen, ist das eine zu wacklige Grundlage.

## Warum die robuste Fassung robust ist

Die robuste htmx-Fassung erreicht dieselbe Bedienung, ohne dass Inhalte vom JavaScript abhängen. Das Muster dahinter ist schlicht: **Jeder Zustand hat eine eigene, echte URL, und diese URL liefert eine vollständige Seite.**

- Der Tab „Lesung" ist ein Link auf `/htmx/lesungen/`. Diese Seite existiert, und ein Crawler folgt dem Link wie jedem anderen. htmx holt sich nur den Ausschnitt `#katalog` aus derselben Seite (`hx-select`) und schreibt die Adresse per `hx-push-url` in den Verlauf.
- „Mehr laden" ist ein Link auf `/htmx/seite/2/`, eine gewöhnliche Seite 2. htmx hängt ihre Einträge an, statt die Seite zu wechseln.
- „Nächste Termine" steht einfach im HTML. Für einen Kasten mit drei Terminen gibt es keinen Grund, ihn nachzuladen.

Die fragile Fassung verletzt diese Regel an genau den Stellen, die in der Tabelle ein ✗ tragen: Ihre Fragmente haben keine eigene Seite.

Ein Detail hat mich beim Nachmessen überrascht. Ruft jemand ein Fragment direkt auf, zum Beispiel weil ein Crawler die URL aus dem Quelltext fischt, bekommt er ein nacktes HTML-Stück ohne `<html>`, ohne `<head>`, ohne Navigation. Mein Server lieferte es mit `200` und korrektem `charset=utf-8` aus, aber ohne jeden Hinweis, dass es nicht in einen Index gehört. Ein `<meta name="robots">` kann ein Fragment nicht tragen, weil es keinen `<head>` hat. Ich habe deshalb für den ganzen Demo-Bereich den Header `X-Robots-Tag: noindex, nofollow` in der `.htaccess` ergänzt. Bei einem Fragment ohne `<head>` ist dieser Header der Weg, einen Index-Ausschluss mitzuschicken. Ein Eintrag in der `robots.txt` würde dagegen nur das Crawlen untersagen, nicht die Aufnahme einer anderswo gefundenen URL.

### Wenn der Server Fragmente liefert

Meine Demo ist statisch und braucht deshalb getrennte Fragment-Dateien. In einer echten Anwendung würde der Server anhand des Headers `HX-Request: true` entscheiden, ob er die ganze Seite oder nur ein Fragment schickt. Dann gelten zwei Regeln aus der [htmx-Dokumentation](https://htmx.org/docs/#caching):

1. Der Server muss `Vary: HX-Request` senden. Sonst liefert ein Cache womöglich das Fragment an jemanden aus, der die ganze Seite wollte, oder umgekehrt.
2. Findet htmx einen Verlaufs-Eintrag nicht in seinem Zwischenspeicher, holt es die Seite neu und schickt dabei ebenfalls `HX-Request: true` mit, erwartet aber die ganze Seite. Seit htmx 2.0.5 steuert das die Option `historyRestoreAsHxRequest`, standardmäßig an. Wer anhand von `HX-Request` Fragmente ausliefert, muss den Header `HX-History-Restore-Request` prüfen oder die Option abschalten.

Und eine URL, die per `hx-push-url` in der Adresszeile landet, muss beim Neuladen eine vollständige Seite liefern. Das Neuladen ist ein ganz normaler Seitenaufruf ohne htmx.

## Der Bogen zu diesem Blog

Jeder Artikel hier hat einen Markdown-Zwilling unter derselben Adresse mit `.md` am Ende. Dazu kommt eine [`llms.txt`](https://www.jpkc.com/db/llms.txt), die auf genau diese Zwillinge verweist. Das ist das Experiment dieser Seite: Inhalte so auszuliefern, dass KI-Systeme sie ohne HTML-Rauschen lesen können. Die Messung zeigt einen zweiten Vorteil. Markdown braucht kein JavaScript, und damit gibt es in den Zwillingen nichts, was ein nicht-rendernder Crawler übersehen könnte.

Wie gut das zusammenpasst, zeigt ein Detail aus der Recherche. WebFetch schickt bei jedem Abruf einen `Accept`-Header, der Markdown bevorzugt. Mein Server beantwortet solche Anfragen per Content-Negotiation mit dem `.md`-Zwilling. Die Demo hat keine Zwillinge, dort kommt deshalb HTML zurück. Das habe ich mit einem Abruf per `Accept: text/markdown` nachgeprüft. Bei den Artikeln antwortet der Server auf dieselbe Anfrage mit dem Zwilling. Ob WebFetch ihn dann tatsächlich bekommt, sehe ich von außen allerdings nicht, weil das Werkzeug HTML ohnehin in Markdown umwandelt.

## Was ich daraus mitnehme

- **Inhalte, die gefunden oder zitiert werden sollen, gehören in die erste Serverantwort.** Nachladen per `load` oder `revealed` ist für KI-Crawler nach heutigem Stand unsichtbar. Das gilt unabhängig von der Bibliothek.
- **Jeder Zustand braucht eine URL, die eine ganze Seite liefert.** Dann ist htmx nur noch Beschleunigung, und Crawler folgen gewöhnlichen Links.
- **Buttons mit `hx-get` sind für Crawler Sackgassen.** Für Bedienelemente ist das in Ordnung, für Inhalte nicht.
- **Fragment-URLs gehören per `X-Robots-Tag` aus dem Index**, weil sie kein eigenes `<head>` haben.

Die Bibliothek entscheidet also nicht über die Sichtbarkeit. Sichtbar ist, was du ohne JavaScript auslieferst, und htmx macht es leicht, beides zu haben. Im [dritten Teil](https://www.jpkc.com/db/blog/htmx-barrierefreiheit/) geht es um eine andere Gruppe, die deine Seite nicht mit den Augen liest: Menschen mit Screenreader oder Tastatur. Dort ist die Bilanz für htmx weniger freundlich.

