Wie diese Lösung integriert werden kann, wie sie genau funktioniert, was dahintersteckt und warum sie über den Standard hinausgeht, liest du hier.
Was ist llms.txt – und warum sollte es mich interessieren?
llmstxt.org beschreibt einen simplen Standard: eine Textdatei unter /llms.txt, die KI-Systemen und Sprachmodellen eine strukturierte, rauschfreie Übersicht einer Website liefert. Keine HTML-Struktur, kein Werbemüll, kein JavaScript – nur sauberer Markdown-Content, den ein LLM direkt verarbeiten kann.
Das Prinzip ist nicht neu. robots.txt sagt Crawlern, was sie dürfen. sitemap.xml listet auf, was es gibt. llms.txt sagt Sprachmodellen, was eine Seite ist und was sie enthält – strukturiert, ohne dass das Modell erst HTML parsen und bereinigen muss.
Der Standard ist seit Ende 2024 im Umlauf und noch informell – es gibt keine RFC, keine offizielle Spezifikation. Aber er wird von mehreren KI-Systemen bereits berücksichtigt, und die Richtung ist klar: Wer möchte, dass sein Content korrekt in KI-Antworten auftaucht, sollte ihn sauber und maschinenlesbar bereitstellen.
Für eine persönliche Website wie meine ist das kein kritisches Feature. Aber es ist eine saubere Sache – und technisch interessant genug, um es richtig umzusetzen.
Warum kein Plugin?
Es gibt bereits Plugins, die /llms.txt für WordPress generieren (siehe z.B. Website LLMs.txt oder LLMs.txt and LLMs-Full.txt Generator). Ich habe keines davon eingesetzt. Der Grund ist einfach: Ein Plugin für ~400 Zeilen PHP ist Overkill. Ich habe ein eigenes Theme, ich weiss wie WordPress-Rewrites funktionieren – warum eine Abhängigkeit einführen, die ich nicht brauche?
Die Theme-Integration hat ausserdem einen konkreten Vorteil: Ich kann die Logik direkt an mein Setup anpassen. Welche Metafelder für den Ausschluss genutzt werden, wie die Content-Bereinigung funktioniert, wie viele Posts im Vollindex erscheinen – alles kontrollierbar ohne Plugin-Einstellungsseite.
Was dabei herausgekommen ist
Statt einer einzelnen Datei biete ich vier dynamische, virtuelle Endpunkte an:
| URL | Inhalt |
|---|---|
/llms.txt | Vollständiger Index aller Seiten, Posts, Kategorien und Tags – als Liste mit Kurzbeschreibungen |
/llms-full.txt | Die 20 neuesten Posts + alle Seiten mit vollem Content |
/llms-full-category-{slug}.txt | Alle Posts einer Kategorie mit vollem Content |
/llms-full-tag-{slug}.txt | Alle Posts eines Tags mit vollem Content |
Der Standard von llmstxt.org definiert nur die Index-Datei. Die Vollinhalts-Endpunkte nach Kategorie und Tag sind eine eigene Erweiterung – und aus meiner Sicht der eigentliche Mehrwert. Ein LLM, das gezielt den Content einer bestimmten Kategorie verarbeiten soll, bekommt so alles auf einmal, ohne die Website crawlen zu müssen.
Technische Voraussetzungen
- WordPress mit aktivierten Permalinks (mod_rewrite resp. nginx-Proxymodus)
- PHP 8.0+ (Arrow Functions werden verwendet)
- Kein zusätzliches Plugin notwendig
Umsetzung Schritt für Schritt
Der Code verteilt sich auf fünf Bereiche, die nacheinander aufgebaut werden: Rewrite-Regeln, Query-Variablen, Header-Ausgabe, Ausschlusslogik, Content-Bereinigung und automatischer Flush. Jeder Abschnitt ist eigenständig und erklärbar – wer nur Teile übernehmen will, findet die Grenzen klar.
Virtuelle Endpunkte via WordPress Rewrite API
Hinweis: Im gesamten Code steht
yourtheme_als Platzhalter für deinen eigenen Theme-Präfix,YOURTHEME_für Konstanten. Ersetze beides konsequent durch deinen eigenen Namespace – z.B.mytheme_respektiveMYTHEME_.
Die Dateien existieren nicht physisch auf dem Server. WordPress liefert sie dynamisch – genau wie die eingebauten Sitemaps. Das passiert über die Rewrite API:
add_rewrite_rule( '^llms\.txt/?$', 'index.php?yourtheme_llms=1', 'top' );
add_rewrite_rule( '^llms-full\.txt/?$', 'index.php?yourtheme_llms_full=1', 'top' );
add_rewrite_rule( '^llms-full-category-([a-z0-9-]+)\.txt/?$', 'index.php?yourtheme_llms_full=1&yourtheme_llms_taxonomy=category&yourtheme_llms_term=$matches[1]', 'top' );
add_rewrite_rule( '^llms-full-tag-([a-z0-9-]+)\.txt/?$', 'index.php?yourtheme_llms_full=1&yourtheme_llms_taxonomy=post_tag&yourtheme_llms_term=$matches[1]', 'top' );
Das /?$ am Ende macht den Trailing Slash optional. WordPress macht bei /llms.txt per 301 einen Canonical Redirect auf /llms.txt/, aber beide URLs funktionieren korrekt.
Die Rewrite-Variablen müssen zusätzlich über den query_vars-Filter registriert werden, damit WordPress sie nicht ignoriert:
function yourtheme_llms_query_vars( $vars ) {
$vars[] = 'yourtheme_llms';
$vars[] = 'yourtheme_llms_full';
$vars[] = 'yourtheme_llms_taxonomy';
$vars[] = 'yourtheme_llms_term';
return $vars;
}
add_filter( 'query_vars', 'yourtheme_llms_query_vars' );
Nach dem Einbinden der Rewrite-Regeln müssen die Permalinks einmal geflusht werden – entweder manuell unter Einstellungen > Permalinks oder automatisch beim Theme-Aktivieren:
add_action( 'after_switch_theme', 'flush_rewrite_rules' );
add_action( 'switch_theme', 'flush_rewrite_rules' );
Content-Type und Robots-Header
Die Ausgabe erfolgt im template_redirect-Hook. Bevor der Content ausgegeben wird, setzen wir zwei Header:
header( 'Content-Type: text/plain; charset=utf-8' );
header( 'X-Robots-Tag: noindex' );
nocache_headers();
noindex ist wichtig: Die llms.txt soll nicht selbst im Suchmaschinen-Index landen. Ohne diesen Header würde eine Suchmaschine die Datei unter Umständen indexieren und damit einen sinnlosen Eintrag in den Suchergebnissen erzeugen.
Update 04.06.2026: Die llms.txt-Endpunkte setzen jetzt explizit Cache-Control: no-cache via WordPress‘ nocache_headers(). Ohne diesen Header liefert WordPress standardmässig einen Expires-Header von 30 Tagen – Browser und Proxies würden den Inhalt also einen Monat lang im Cache behalten, obwohl sich die llms.txt bei jedem neuen Beitrag ändert.
Inhalte ausschliessen
Posts und Seiten mit gesetztem _noindex-Meta werden automatisch aus allen Endpunkten ausgeschlossen. Das _noindex-Meta wird von Essential SEO gesetzt – meinem eigenen WordPress-SEO-Plugin.
if ( ! function_exists( 'yourtheme_llms_is_excluded' ) ) :
function yourtheme_llms_is_excluded( $post_id ) {
$excluded = '1' === get_post_meta( $post_id, '_noindex', true );
return (bool) apply_filters( 'llms_txt_post_excluded', $excluded, $post_id );
}
endif;
Der llms_txt_post_excluded-Filter macht die Funktion erweiterbar – andere SEO-Plugins können so ihre eigene Noindex-Logik einhängen. Wer z.B. Yoast SEO nutzt:
add_filter( 'llms_txt_post_excluded', function( $excluded, $post_id ) {
return $excluded || ( '1' === get_post_meta( $post_id, '_yoast_wpseo_meta-robots-noindex', true ) );
}, 10, 2 );
Update 06.06.2026: Passwortgeschützte Beiträge und Seiten werden jetzt ebenfalls aus allen Endpunkten ausgeschlossen. Der Passwortschutz steckt in WordPress nicht im Beitragsstatus, sondern im separaten Feld post_password — eine Abfrage nach post_status => publish liefert solche Beiträge also weiterhin mit. Ihr voller Inhalt wäre dadurch im Klartext über die llms.txt-Endpunkte abrufbar gewesen. Behoben mit has_password => false in allen Ausgabe-Queries.
Content bereinigen
Roher WordPress-Content enthält Block-Markup, Shortcodes, HTML-Entities und – je nach verwendeten Blöcken – UI-Artefakte. Die Bereinigungsfunktion:
function yourtheme_llms_clean_content( $content ) {
$content = strip_shortcodes( $content );
// Bilder zu ihrem Alt-Text – das ist der eigentliche Inhalt eines Bildes
$content = preg_replace_callback(
'#<img\b[^>]*>#i',
function( $matches ) {
if ( preg_match( '#(?<![\w-])alt\s*=\s*([\'"])(.*?)\1#i', $matches[0], $alt ) ) {
return trim( html_entity_decode( $alt[2], ENT_QUOTES, 'UTF-8' ) );
}
return '';
},
$content
);
// Links zu Markdown – URLs sind Inhalt
$content = preg_replace_callback(
'#<a\b([^>]*)>(.*?)</a>#is',
function( $matches ) {
$text = trim( wp_strip_all_tags( $matches[2] ) );
if ( ! preg_match( '#(?<![\w-])href\s*=\s*([\'"])(.*?)\1#i', $matches[1], $href ) ) {
return '';
}
$url = trim( html_entity_decode( $href[2], ENT_QUOTES, 'UTF-8' ) );
$safe = esc_url_raw( $url );
if ( '' === $safe || '#' === $url[0] ) {
return '';
}
if ( '' === $text ) {
return '';
}
return '[' . $text . '](' . $safe . ')';
},
$content
);
// Trenner einfügen, BEVOR Tags entfernt werden – sonst klebt wp_strip_all_tags() Inhalte zusammen
$content = preg_replace( '#</t[dh]>\s*<t[dh][^>]*>#i', ' | ', $content );
$content = preg_replace( '#<br\s*/?>#i', "\n", $content );
$content = preg_replace( '#</(li|tr|dt|dd)>#i', "\n", $content );
$content = preg_replace( '#</(p|div|h[1-6]|blockquote|figcaption|figure|pre|ul|ol|dl|table|section|article)>#i', "\n\n", $content );
$content = wp_strip_all_tags( $content );
$content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
$content = preg_replace( '/[ \t]*(?<!\+)\+[ \t]*$/m', '', $content );
$content = preg_replace( '/\n{3,}/', "\n\n", $content );
return trim( $content );
}
Ein nicht offensichtliches Detail steckt im preg_replace für das +: FAQ-Blöcke mit Accordion-Toggle speichern das +-Icon als Textinhalt eines <button>-Elements. Nach wp_strip_all_tags bleibt der Text übrig – direkt hinter der Frage: Was ist llms.txt?+. Das Regex entfernt ein einzelnes + am Zeilenende. Der Negativ-Lookbehind (?<!\+) sorgt dafür, dass doppelte Plus erhalten bleiben – «C++» oder «Notepad++» werden also nicht angetastet. Wer keine Accordion-Blöcke einsetzt, braucht diese Zeile nicht – sie schadet aber auch nicht.
Update 06.06.2026: Das Regex wurde verschärft. Die frühere Variante (/\s*\+\s*(?=\n|$)/m) entfernte jedes + am Zeilenende und machte aus «C++» fälschlich «C+». Der Negativ-Lookbehind behebt das.
Update 06.06.2026: Titel, Kategorie- und Tag-Namen, Beschreibungen und Excerpts laufen jetzt über eine eigene Inline-Bereinigung (wp_strip_all_tags() + html_entity_decode()) statt über esc_html(). Der Grund: esc_html() ist HTML-Escaping – in einer text/plain-Datei erzeugt es sichtbare Entities. WordPress texturiert Titel zudem vorab (aus & wird &), womit esc_html() doppelt kodieren und im Resultat &#038; stehen würde. Aus demselben Grund werden die Links neu mit esc_url_raw() statt esc_url() ausgegeben – esc_url() würde das & in URLs zu & kodieren, was in einer Textdatei nichts zu suchen hat.
Update 07.06.2026: clean_content() fügt jetzt Trenner ein, bevor die Tags entfernt werden. wp_strip_all_tags() entfernt Tags ohne Leerraum – dadurch verschmolzen Tabellenzellen zu einer einzigen Zeile und Absätze, die nur durch <br><br> getrennt waren, klebten zusammen. Neu werden Tabellenzellen zu « | », Tabellenzeilen und Listenpunkte zu Umbrüchen und Blockelemente zu Leerzeilen. Code in <pre> ist Entity-kodiert und bleibt unangetastet – die Einrückung bleibt erhalten.
Update 07.06.2026: Links und Bilder bleiben jetzt als Inhalt erhalten. Vor dem Strippen wird jedes <img> zu seinem Alt-Text – ein in einen Link verpacktes Bild (Galerie/Lightbox) wird damit zu [Alt-Text](URL), Bilder ohne Alt-Text fallen weg. <a href> wird zu [Text](URL), damit URLs erhalten bleiben (Belege, interne Links, Buttons/CTAs). Links ohne brauchbares Ziel werden entfernt: Seiten-Anker (#…), von esc_url_raw() abgelehnte Schemata (javascript:, data:) und <a> ohne href.
Automatischer Rewrite-Flush bei Theme-Updates
Rewrite-Regeln werden in der WordPress-Datenbank gecacht. Bei einer neuen Theme-Version können veraltete Regeln dazu führen, dass die Endpunkte nicht mehr funktionieren. Die Lösung: Beim init-Hook die Theme-Version mit der zuletzt gecachten vergleichen und bei Abweichung flushen:
add_action( 'init', function() {
$version = wp_get_theme()->get( 'Version' );
if ( get_option( 'yourtheme_llms_rewrite_version' ) !== $version ) {
flush_rewrite_rules();
update_option( 'yourtheme_llms_rewrite_version', $version );
}
}, 20 );
Der Hook läuft mit Priorität 20, damit die Rewrite-Regeln zuvor bereits registriert wurden (Standard-Priorität ist 10).
Wer keine automatische Versionsprüfung verwendet – oder feststellt, dass die Endpunkte nicht erreichbar sind (die llms.txt lässt sich nicht aufrufen): Ein manuelles Flushen unter Einstellungen > Permalinks > Speichern hat denselben Effekt und löst Rewrite-Probleme in der Regel zuverlässig.
Der vollständige Code
Der vollständige Code liegt in einem öffentlichen Repository auf GitHub – inklusive README, Changelog und Lizenz.
Integration ins Theme
Die Datei mit dem Code liegt dann z.B. unter inc/llms-txt.php und wird in der functions.php eingebunden:
require get_template_directory() . '/inc/llms-txt.php';
Keine weiteren Abhängigkeiten. Kein Plugin. Kein Build-Step.
Anpassen an dein Setup
Wer die Lösung in ein eigenes Theme integriert, muss folgendes anpassen:
- Den Funktionspräfix
yourtheme_durch den eigenen Theme-Präfix ersetzen – das betrifft Funktionsnamen, Query-Variablen und die Option für die Versions-Prüfung YOURTHEME_LLMS_FULL_POST_LIMIT(aktuell 20) kann beliebig angepasst werden- Der Ausschluss via
_noindex-Meta kann über den Filterllms_txt_post_excludedauf andere SEO-Plugins erweitert werden – Beispiel für Yoast SEO ist im Abschnitt weiter oben - Private Seiten und Entwürfe werden automatisch ausgeschlossen (
post_status => publish); passwortgeschützte Beiträge werden überhas_password => falseausgeschlossen - Die Zeile zur Accordion-Bereinigung (
preg_replacefür+) ist optional – wer keine Toggle-Blöcke einsetzt, kann sie weglassen
FAQ
Nein. Die Lösung setzt auf Classic Theme-Funktionen: direkte PHP-Integration in functions.php sowie die WordPress Rewrite API. Mit einem Block Theme funktioniert das nicht ohne Anpassungen.
Nein – sofern der X-Robots-Tag: noindex-Header korrekt gesetzt ist. Mehr dazu im Abschnitt Content-Type und Robots-Header.
In der Regel hilft ein manuelles Flushen der Permalinks unter Einstellungen > Permalinks > Speichern. Details und die automatische Alternative im Abschnitt Automatischer Rewrite-Flush.
Ja – über den Filter llms_txt_post_excluded. Damit lässt sich jede beliebige Ausschlusslogik einhängen, unabhängig davon welches SEO-Plugin du nutzt. Beispiel im Abschnitt Inhalte ausschliessen.
Der Standard ist noch jung und informell – eine offizielle Liste von Systemen, die ihn aktiv berücksichtigen, gibt es nicht. Unter llmstxt.org sind Directories und Integrationen dokumentiert.

Kommentare