Zum Inhalt springen
Doctor SEO

SEO mit KI: KI als Arbeitswerkzeug

Claude Skills für SEO: installieren, nutzen, selbst schreiben

Eine fertige SKILL.md für ein Crawl-Audit, das Budget der gestuften Offenlegung und der Test, mit dem du einen fremden SEO-Skill vor dem Installieren prüfst.

Aktualisiert am 5 Oktober 2026 28 Min. Lesezeit

Am Ende dieser Lektion liegt bei dir ein Ordner mit drei Dateien: eine SKILL.md mit gültigem YAML-Kopf, eine Grenzwertdatei, die das Prüfskript selbst liest, und dieses Skript, das ohne eine einzige Fremdbibliothek läuft. Du gibst ihm einen Crawl-Export und bekommst priorisierte Befunde, in denen jede Zahl aus einer Zelle deiner eigenen Datei kommt.

Dass fremde Skills gerade das nicht zusichern, zeigt eine Inventarzahl deutlicher als jedes Token-Argument. Eine kostenlose SEO-Skill-Sammlung wird auf ihrer Seite mit 51 Skills in sieben Kategorien angekündigt, während das zugehörige Repository lhitches/claude-seo-skills weiter 29 nennt. Die Seite trägt kein Veröffentlichungsdatum, nur die Fußzeile „Copyright (c) 2026 Hawk Academy“. Welche Zahl stimmt, bleibt hier offen – interessant ist die Differenz, weil sie etwas über die Pflege dieser Grenzwerte sagt.

Worüber ein Skill nie entscheidet, ist, ob eine KI-Suchmaschine deine Seiten zitiert. Das gehört in Level 4 zu GEO und AIO; hier in Level 8 bleibt die KI ein Werkzeug in deiner Hand. Wo die Grenzen dieses Werkzeugs verlaufen, steht in Was Claude im SEO leisten kann – und was nicht.

Was du lernst

  • Welche Befunde ein Crawl-Export hergibt und welche Fragen er gar nicht beantworten kann.
  • Woran du vor dem Installieren erkennst, ob die Grenzwerte eines fremden SEO-Skills überhaupt nachprüfbar sind.
  • Warum die Zahl der installierten Skills eine Kostenentscheidung ist und keine der Bequemlichkeit.
  • Wie du ein Audit so verpackst, dass Skript und Mensch dieselben Grenzwerte lesen.
  • Welchen Exit-Code dein Prüfskript für welchen Fehlerfall braucht – und welchen es freihalten muss.
  • Wie du einen umstrittenen Check behandelst, ohne dich auf eine Seite zu schlagen.

Was in einem Crawl-Export steht und was nicht darin steht

Claude analysiert Crawl-Daten und crawlt keine Websites. Die Formulierung stammt von Lawrence Hitches in Claude for Technical SEO: Audits, Crawl Fixes & Schema vom 2. April 2026, in der Übersetzung: „Claude analysiert Crawl-Daten. Es crawlt keine Websites.“ Ein Praxisartikel ohne Stichprobengröße – und die Grenze, an der jeder Audit-Skill hängt.

Rechnen lässt sich alles, was als Zelle im Export steht: Statuscodes, Indexierbarkeit, doppelte Titles, fehlende H1, Canonicals, die woandershin zeigen, Seiten ohne eingehende interne Links, dünne Seiten. Der Rest fällt weg: der Skill ruft keine URL ab, rendert nichts und kennt kein Suchvolumen. Den Export erzeugst du mit deinem Crawler, und das ist Arbeit aus dem technischen SEO.

Wie verbreitet die Abgabe solcher Audits ist, hält State of AI and Automation in SEO von Keyword.com vom 1. Januar 2026 fest: 38 % nennen technische SEO-Audits unter den Aufgaben, für die sie KI einsetzen, und unter den 79 %, die angaben, es gebe automatisierbare Aufgaben, die sie bewusst nicht automatisieren, nannten 40 % technisches SEO. Beide Zahlen stammen aus einer Befragung mit n = 97 verwertbaren und selbst zusammengekommenen Antworten. Tragfähig ist daran der Kontrast innerhalb derselben Erhebung: technisches SEO steht auf beiden Listen. Repräsentativ für alle SEOs ist das nicht.

Für diese Zurückhaltung gibt es einen dokumentierten Grund. ContextBolt veröffentlichte am 23. Juni 2026 einen Selbstversuch über eine Woche, in dem das Modell Drittanbieter-Schätzungen wie gemessene Daten behandelte und an einem Keyword festhielt, dessen eigener Schwierigkeitswert es als nicht gewinnbar auswies. Eine Website, eine Woche, keine Kontrollgruppe, als Autor nur „David“ ohne Nachnamen: das beschreibt ein Fehlermuster und nicht dessen Häufigkeit. Dagegen hilft eine Abschlussprüfung, die für jede Zahl die Zelle verlangt, aus der sie stammt. Welchen Befund du zuerst anfasst, ist eine Frage von On-Page-SEO und Content.

Wer die SEO-Skills schreibt, die du installieren würdest

SEO-Skills kommen von Agenturen und Einzelpersonen, nicht von Anthropic. Das offizielle Repository anthropics/skills liefert die Spezifikation in ./spec/, Beispiele in ./skills/ und ein Startgerüst in ./template/: das Format also, nicht die SEO-Grenzwerte. Die schreibt jemand, dessen Begründungen du erst kennst, wenn du seine Dateien öffnest.

Zwei Sammlungen machen die Größenordnung greifbar. AgriciDaniel/claude-seo bündelt 25 Sub-Skills und 18 Sub-Agents unter MIT-Lizenz und deckt technisches SEO, E-E-A-T, Schema, GEO und AEO, Backlinks, Local SEO, semantisches Clustering, E-Commerce und internationales SEO ab; der Besitzername erscheint auf der Seite als „AgriciDaniel“, und die URL löst in beiden Schreibweisen auf. Hawk Academy, nach Angabe der Seite von StudioHawk erstellt, kündigt 51 kostenlose Skills in sieben Kategorien an, während das zugehörige Repository weiter 29 nennt. Weder das Repository noch die Seite trägt ein Veröffentlichungsdatum. Sterne- und Fork-Zahlen stehen hier nicht: sie sagen über Grenzwerte in einer Datei nichts.

Daraus wird ein Test von zwei Minuten. Öffne die Referenzdateien des fremden Skills und suche die Zahlen: Titellänge, Crawl-Tiefe, Wortzahl. Liegen sie in einer Datei, die du lesen, ändern und datieren kannst, installiere ihn. Liegen sie im Anweisungstext, schreib deinen eigenen – danach weißt du, was dein Audit beurteilt.

Wer den Prompt vergisst, arbeitet ohne seine Regeln

Eine Prompt-Liste verlangt Disziplin, und genau daran scheitert sie: den Artikel öffnen, den passenden Eintrag finden, 2.000 Wörter einfügen – und das bei jeder Unterhaltung neu. Wo das unterbleibt, arbeitet das Modell ohne deine Regeln, und auffallen wird es dir erst in der Korrektur. Eine SKILL.md verschiebt diese Last von dir in die Datei: nach der Anthropic-Plattformdokumentation, geprüft am 20. September 2026, liegen pro Skill rund 100 Token Metadaten dauerhaft im Kontext, der Rumpf wird geladen, sobald die Beschreibung zur Anfrage passt, und Referenzdateien erst, wenn jemand sie öffnet. Aufrechnen lässt sich das nicht: für deutschen Text veröffentlicht Anthropic keine Wort-zu-Token-Entsprechung. Die Ersparnis liegt ohnehin darin, dass niemand mehr etwas erinnern muss.

Gestufte Offenlegung – progressive disclosure in Anthropics eigener Benennung – heißt genau diese Aufteilung, und jede Stufe ist beziffert. Für diese Seiten liegt keine geprüfte URL vor, deshalb werden sie benannt und datiert, aber nicht verlinkt.

Stufe Wann sie lädt Erklärtes Budget Was darin steht
1. Metadaten immer, beim Start der Sitzung rund 100 Token pro Skill name und description aus dem YAML-Kopf
2. Anweisungen wenn der Skill greift unter 5.000 Token der Rumpf der SKILL.md
3. Ressourcen nur bei Zugriff nichts bis zum Öffnen Referenzdateien, Skripte, Daten

Die Kostenstelle ist Stufe 1, die einzige, die du nicht steuerst: sie fällt pro installiertem Skill in jeder Unterhaltung an, ob es um SEO geht oder nicht. Stufe 2 bezahlst du nur bei einem Treffer, Stufe 3 nur bei einem Zugriff: ein Skill, der nie greift, kostet dich nur seine Beschreibung, wie umfangreich seine Anweisungen auch sind. Teuer wird die Sammlung, nicht der einzelne Skill. Mein Kriterium dafür, nicht das von Anthropic: installiert bleibt, was im laufenden Monat wirklich aufgerufen wurde, alles andere liegt im Repository und ist von dort in einer Minute zurück.

Dieselbe Aufteilung ist der Grund, warum das Urteil dieses Audits in eigenen Dateien steht: was auf Stufe 3 liegt, kostet nichts, bis es geöffnet wird, also darf die Grenzwerttabelle so ausführlich sein, wie sie begründet sein muss, und ein Skript darf die immer gleichen Schritte übernehmen. Dateien in einem Ordner sind versionierbar, ein eingefügter Prompt ist es nicht.

Die Nachfrage hat die Form einer Datei angenommen

Wer ein SEO-Audit mit Claude sucht, sucht überwiegend ein Werkzeug und keine Erklärung. In der am 20. September 2026 für diesen Kurs erhobenen Google-Autocomplete-Stichprobe stand claude seo skill an der Spitze der zehn Vorschläge zum Stamm „claude seo“. Beim Stamm „claude seo audit“ waren von zehn Vorschlägen sechs Varianten mit skill, GitHub oder tool. Und „claude skills seo“ ergänzte unter anderem claude seo skill md – eine Anfrage mit Dateinamen.

Die Gegenprobe gehört daneben: „claude prompts for seo“ trug am selben Tag sieben Varianten, top 20 claude prompts for seo darunter. Beide Nachfragen bestehen gleichzeitig, weshalb Prompt-Muster nach Aufgabe ein eigenes Thema bleiben, das dieses Level getrennt behandelt. Abgelaufen ist kein guter Prompt, sondern ein Format: der Artikel mit 47 Einträgen zum Einfügen per Hand. Die Größe dieser Nachfrage gibt Autocomplete nicht her – es zeigt nur, dass getippt wird, was dort steht. Volumenzahlen stehen hier deshalb nicht, auch keine geschätzten.

Dein Konto ist nicht deine Organisation

Ein Skill, den du auf claude.ai hochlädst, gehört deinem Konto und nicht deiner Organisation: so steht es in der Anthropic-Plattformdokumentation, geprüft am 20. September 2026. In Teams tut diese Grenze zuerst weh, weil ein Audit nur dann einen Maßstab hat, wenn alle dieselbe Datei benutzen. Mein Schluss daraus, nicht der von Anthropic: für ein Team liegt der Skill dort, wo auch der Code liegt, damit die Änderungsgeschichte der Grenzwerte die Git-Historie ist.

Zwei weitere Grenzen derselben Seiten entscheiden mit, wie der Ordner aussehen darf. Über die API ausgeführte Skills haben keinen Netzzugriff – deshalb kommt das Prüfskript unten ohne Fremdbibliothek aus. Und eigene Skills wandern nicht von einer Oberfläche zur nächsten: wer auf claude.ai hochlädt, hat damit in Claude Code und über die API nichts installiert. Wie eine Oberfläche den Ordner übernimmt, steht in der Spezifikation im Verzeichnis ./spec/ von anthropics/skills. Diese Netzgrenze der Oberfläche deckt sich mit der des Modells, die Lawrence Hitches in Claude for Technical SEO am 2. April 2026 so formuliert hat, ohne Stichprobengröße: „Claude analysiert Crawl-Daten. Es crawlt keine Websites.“ Ein Skill verschiebt diese Grenzen nicht; wo er installiert ist, entscheidet nur, wer ihn benutzt.

Zwei Pflichtfelder, und eines wird nur verglichen

Der YAML-Kopf einer SKILL.md trägt zwei Pflichtfelder: name mit höchstens 64 Zeichen und description mit höchstens 1.024 Zeichen (Anthropic-Plattformdokumentation, geprüft am 20. September 2026). Die Beschreibung unten nutzt 258 dieser 1.024 Zeichen, und das ist Absicht: sie wird nicht gelesen, sondern mit deiner Anfrage verglichen. Sie braucht die Wörter, die du tatsächlich tippst – Crawl-CSV, doppelte Titles, Canonicals, verwaiste Seiten –, nicht möglichst viele.

---
name: seo-crawl-audit
description: Prüft einen Crawl-Export gegen dokumentierte Grenzwerte und
  liefert priorisierte On-Page-Befunde mit URL, Zellwert und angewandtem
  Grenzwert. Einsetzen, wenn ein Crawl-CSV angehängt ist oder Titles,
  Canonicals, H1 oder verwaiste Seiten geprüft werden sollen.
---

# On-Page-Audit aus einem Crawl-Export

## Voraussetzung

Dieser Skill crawlt nicht. Er rechnet über die Datei, die ihm übergeben wird.
Liegt keine Datei vor, nenne die Pflichtspalten und warte darauf. Schätze
keinen fehlenden Wert, ergänze keine URL, rekonstruiere keine Spalte.

## Eingaben

Pflicht: ein `internal_all`-Export aus Screaming Frog oder das Äquivalent
eines anderen Crawlers, dessen Spaltennamen in `CRAWL_SPALTEN` ergänzt sind.
Mindestens diese Spalten:

    Address, Status Code, Indexability, Title 1, Title 1 Length,
    Meta Description 1, H1-1, Canonical Link Element 1, Word Count, Inlinks

Optional: ein Seiten-Export aus der Search Console (`page`, `clicks`,
`impressions`). Er wird als dritter Parameter übergeben und über die exakte
URL verbunden. Ohne ihn bleibt `canonical_elsewhere` ungeprüft.

## Ablauf

1. Lauf starten:

       python3 scripts/audit_export.py CHECKS.md crawl.csv [search-console.csv]

   Exit 0 heißt: der Bericht auf stdout ist vollständig. Bei jedem anderen
   Code gibst du den Fehlertext aus und kein Audit-Ergebnis.

2. Gib den Kopf des Berichts vor den Befunden wieder: URLs in der Datei,
   Universum, ob eine Search-Console-Datei dabei war.

3. Lies `CHECKS.md` und übernimm die Spalte "Woher der Wert kommt" als
   Begründung jedes Befunds. Formuliere keine zusätzliche Begründung.

4. Übernimm den Block "Nicht geprüft" wörtlich. Ein Check ohne Spalte hat
   nichts gefunden, weil er nicht gelaufen ist, und das ist kein sauberes
   Ergebnis.

5. Was in keinen Check passt, kommt als Beobachtung ans Ende, ohne Priorität
   und ohne Empfehlung.

## Nicht Gegenstand dieses Skills

- Suchvolumen, Keyword-Difficulty, CPC. Steht nicht in einem Crawl.
- Keyword-Dichte. Dieser Skill rechnet sie nicht und bewertet sie nicht.
- Fremde Qualitäts-, Autoritäts- oder Toxizitätswerte.
- Jede Zahl, die nicht aus einer Zelle der Datei oder aus einer Rechnung
  über Zellen der Datei stammt.

## Vor der Abgabe

- [ ] Jeder Befund trägt die vollständige URL und den Zellwert aus dem Bericht
- [ ] Die URL-Zahl im Kopf stimmt mit der Eingabedatei überein
- [ ] Jeder angewandte Grenzwert steht in CHECKS.md
- [ ] Der Block "Nicht geprüft" ist übernommen, nicht weggelassen
- [ ] Keine Zahl stammt aus dem Modell
- [ ] Crawl-Datum und erzeugendes Werkzeug stehen im Kopf

## Dateien

- `CHECKS.md`: Grenzwerte, Prioritäten, Herkunft. Wird in Schritt 3 gelesen.
- `scripts/audit_export.py`: der deterministische Teil, nur Standardbibliothek.

Zwei Entscheidungen darin unterscheiden die Datei von einem langen Prompt. Im Rumpf steht kein einziger Grenzwert; die Zahlen liegen in CHECKS.md, also auf Stufe 3. Und die Voraussetzung steht ganz oben statt als Warnung am Ende, weil ein SEO-Skill fast immer an derselben Stelle scheitert: er füllt eine Lücke in den Daten mit etwas Plausiblem.

Der Ordner sieht damit so aus:

seo-crawl-audit/
  SKILL.md
  CHECKS.md
  scripts/
    audit_export.py

CHECKS.md: eine Datei, zwei Leser

Die Grenzwerte dieses Audits stehen weder im Rumpf der SKILL.md noch im Skript, sondern in CHECKS.md – und das Skript liest genau die Tabelle, die du liest. Das ist der Trick dieser Datei: die Zahl im Bericht und die Begründung, die du verteidigen musst, können nicht auseinanderlaufen, weil es für beide nur eine Quelle gibt.

# Checks und Grenzwerte

Jeder Check steht mit Grenzwert, Priorität und Herkunft hier und nirgends sonst.
`audit_export.py` liest die Spalten eins bis drei dieser Tabelle, du liest die
vierte. Eine Datei, zwei Leser: Bericht und Begründung können nicht
auseinanderlaufen.

| Check | Grenzwert | Prio | Woher der Wert kommt |
|---|---|---|---|
| status_not_200 | – | P1 | Ein interner Link zeigt auf etwas, das nicht antwortet |
| title_empty | – | P1 | Ohne Titel im Quelltext schreibt die Suchmaschine selbst einen |
| canonical_elsewhere | – | P1 | Eine URL mit gemessenen Klicks wird zurückgezogen |
| title_duplicate | 2 | P2 | Ab zwei gleichen Titeln ist die Vorlage geklont |
| h1_missing | – | P2 | Kein H1 im Export |
| orphan | – | P2 | Gefunden, aber intern nicht verlinkt |
| title_too_long | 60 | P3 | Hausgrenzwert, keine Google-Regel |
| thin_page | 300 | P3 | Hausgrenzwert, keine Google-Regel |
| metadesc_empty | – | P3 | Wird gezählt, nicht als Fehler gemeldet |

## Notizen

- Änderst du eine Zahl, schreib das Datum dazu. Sonst ist das Audit vom
  nächsten Monat mit diesem nicht vergleichbar.
- `orphan` zählt nur `inlinks`. In einem rein linkfolgenden Crawl kann
  `inlinks` = 0 nur der Start-URL passieren.
- Keine Zeile darf eine Kennung tragen, die der Code nicht kennt:
  `audit_export.py` endet dann mit 7 statt still weiterzulaufen.
- Ein Check, dessen Prämisse in der Branche umstritten ist, gehört nicht in
  diese Tabelle, sondern in die Beobachtungen am Ende des Berichts.

Aus dieser Kopplung fällt eine zweite Eigenschaft heraus. Trägt die Tabelle eine Kennung, die das Skript nicht kennt – ein Tippfehler, ein Check aus einer fremden Sammlung –, bricht der Lauf mit Exit-Code 7 ab statt die Zeile still zu überspringen. Ein Audit, das eine Prüfung unbemerkt auslässt, liest sich genauso gut wie eines, das sie bestanden hat, und ist das gefährlichere der beiden.

Die letzte Notiz der Datei ist eine Haltung: ein Check, dessen Prämisse in der Branche umstritten ist, gehört nicht in die Fehlerspalte, weil ein Grenzwert mit dem Etikett „Fehler“ Arbeit erzeugt, die niemand gebraucht hat. Welcher Check das hier betrifft, steht im letzten Abschnitt.

Jeder Fehlerfall bekommt seinen eigenen Code

Getrennte Exit-Codes trennen zwei Lagen, die gleich aussehen: null Zeilen im Bericht können heißen, dass der Export tabulatorgetrennt war, oder dass der Crawl wirklich leer ist. Das erste ist ein Bedienfehler, das zweite ein Befund über die Website. Das Prüfskript vergibt deshalb pro Fehlerfall einen eigenen Code, und Code 1 bleibt unbelegt.

Exit-Code Bedeutung
0 Bericht auf stdout, Prüfung vollständig durchgelaufen
2 falsche Anzahl Argumente
3 Eingabedatei fehlt, ist keine reguläre Datei oder lässt sich nicht öffnen
4 Datei ist nicht UTF-8, oder der CSV-Parser bricht ab
5 Header erkannt, aber keine Zeile mit URL
6 kein bekannter Spaltenname: Format und Trennzeichen prüfen
7 CHECKS.md nennt eine unbekannte Check-ID, einen unlesbaren Grenzwert oder eine fremde Priorität
8 unerwartete Ausnahme, es wird kein Prüfurteil ausgegeben
1 unbelegt

Code 1 bleibt frei, weil ein abgebrochener Schreibvorgang auf stdout – volle Platte, geschlossene Pipe – außerhalb jeder Fehlerbehandlung mit 1 endet. Deshalb berechnet das Skript den Bericht vollständig, bevor es irgendetwas schreibt: ein abgestürzter Lauf darf kein Urteil über deine Daten abgeben. Drei weitere Fallen sind ausdrücklich behandelt. UnicodeDecodeError erbt von ValueError und nicht von OSError, wird also eigens gefangen. bool ist eine Unterklasse von int, also weist die Zahlenprüfung True explizit ab. Und nirgends steht ein get(feld, ""): eine fehlende Spalte muss fehlen bleiben, sonst besteht ein kaputter Export die Prüfung mit null Befunden. Es sind rund 250 Zeilen, nur Standardbibliothek.

#!/usr/bin/env python3
"""Prüft einen Crawl-Export gegen die Grenzwerte in CHECKS.md.

    python3 audit_export.py CHECKS.md crawl.csv [search-console.csv]

Nur Standardbibliothek, damit dieselbe Datei in Claude Code und als Skill
über die API läuft, wo zur Laufzeit kein Paket installiert werden kann.

Exit-Codes:
  0  Bericht auf stdout, Prüfung vollständig durchgelaufen.
  2  Falsche Anzahl Argumente.
  3  Eine Eingabedatei fehlt, ist keine reguläre Datei oder lässt sich
     nicht öffnen.
  4  Eine Datei ist nicht UTF-8, oder der CSV-Parser bricht ab.
  5  Header erkannt, aber keine Zeile mit URL.
  6  Kein bekannter Spaltenname: Exportformat und Trennzeichen prüfen.
  7  CHECKS.md nennt eine unbekannte Check-ID, einen unlesbaren Grenzwert
     oder eine Priorität außerhalb von P1, P2, P3.
  8  Unerwartete Ausnahme. Es wird kein Prüfurteil ausgegeben.
  1  bleibt unbelegt.
"""
import csv
import os
import sys

CRAWL_SPALTEN = {
    "address": "url", "url": "url",
    "status code": "status", "http status code": "status",
    "indexability": "indexability",
    "title 1": "title", "page title": "title",
    "title 1 length": "title_len", "page title length": "title_len",
    "meta description 1": "metadesc",
    "h1-1": "h1", "h1": "h1",
    "canonical link element 1": "canonical", "canonical url": "canonical",
    "word count": "words", "words": "words",
    "inlinks": "inlinks",
}
GSC_SPALTEN = {
    "url": "url", "address": "url", "page": "url", "seite": "url",
    "top pages": "url", "häufigste seiten": "url",
    "clicks": "clicks", "klicks": "clicks",
    "impressions": "impressions", "impressionen": "impressions",
}
ZAHLENFELDER = {"status", "title_len", "words", "inlinks", "clicks",
                "impressions"}

# Jeder Check nennt die Felder, die er braucht, und ob er eine Zahl aus
# CHECKS.md liest. Grenzwert und Priorität stehen nicht hier, sondern in
# CHECKS.md. Fehlt ein Feld, gilt der Check als nicht geprüft und wird
# niemals als null Treffer gemeldet.
CHECKS = {
    "status_not_200": {"felder": ("status", "inlinks"), "zahl": False},
    "title_empty": {"felder": ("title",), "zahl": False},
    "canonical_elsewhere": {"felder": ("canonical", "clicks"), "zahl": False},
    "title_duplicate": {"felder": ("title",), "zahl": True},
    "h1_missing": {"felder": ("h1",), "zahl": False},
    "orphan": {"felder": ("inlinks", "status", "indexability"), "zahl": False},
    "title_too_long": {"felder": ("title_len",), "zahl": True},
    "thin_page": {"felder": ("words",), "zahl": True},
    "metadesc_empty": {"felder": ("metadesc",), "zahl": False},
}
PRIORITAETEN = ("P1", "P2", "P3")


def abbruch(code, text):
    sys.stderr.write(text + "\n")
    raise SystemExit(code)


def als_zahl(wert):
    """Gibt int oder None zurück. bool ist eine Unterklasse von int und wird
    ausdrücklich abgewiesen, damit True nicht als 1 durchgeht."""
    if isinstance(wert, bool):
        return None
    ziffern = "".join(z for z in str(wert) if z.isdigit())
    return int(ziffern) if ziffern else None


def lies_grenzwerte(pfad):
    """Liest die Markdown-Tabelle in CHECKS.md: Check-ID, Grenzwert, Priorität.
    Eine Datei, zwei Leser - Bericht und Begründung können nicht driften."""
    grenzen, prios = {}, {}
    with open(pfad, encoding="utf-8") as datei:
        for zeile in datei:
            if not zeile.lstrip().startswith("|"):
                continue
            zellen = [z.strip() for z in zeile.strip().strip("|").split("|")]
            if len(zellen) < 3:
                continue
            kennung = zellen[0].strip("`")
            if not kennung or not kennung[0].islower():
                continue
            if not all(z.isalnum() or z == "_" for z in kennung):
                continue
            if kennung not in CHECKS:
                abbruch(7, "unbekannte Check-ID in %s: %s" % (pfad, kennung))
            if CHECKS[kennung]["zahl"]:
                zahl = als_zahl(zellen[1])
                if zahl is None:
                    abbruch(7, "kein lesbarer Grenzwert für %s in %s"
                            % (kennung, pfad))
                grenzen[kennung] = zahl
            if zellen[2] not in PRIORITAETEN:
                abbruch(7, "unbekannte Priorität für %s in %s: %s"
                        % (kennung, pfad, zellen[2]))
            prios[kennung] = zellen[2]
    fehlend = sorted(k for k, v in CHECKS.items()
                     if k not in prios or (v["zahl"] and k not in grenzen))
    if fehlend:
        abbruch(7, "Zeile fehlt in %s: %s" % (pfad, ", ".join(fehlend)))
    return grenzen, prios


def lies_csv(pfad, spalten):
    """Gibt (Zeilen, erkannte Felder) zurück. Kein get(feld, "") irgendwo:
    eine fehlende Spalte muss fehlen bleiben, sonst besteht ein kaputter
    Export die Prüfung sauber."""
    zeilen, erkannt = [], set()
    with open(pfad, newline="", encoding="utf-8-sig") as datei:
        leser = csv.DictReader(datei)
        for spalte in leser.fieldnames or []:
            feld = spalten.get((spalte or "").strip().lower())
            if feld:
                erkannt.add(feld)
        for roh in leser:
            zeile = {}
            for spalte, wert in roh.items():
                feld = spalten.get((spalte or "").strip().lower())
                if feld is None:
                    continue
                zeile[feld] = als_zahl(wert) if feld in ZAHLENFELDER else wert
            if zeile.get("url"):
                zeilen.append(zeile)
    return zeilen, erkannt


def oeffne(pfad, spalten):
    try:
        return lies_csv(pfad, spalten)
    except UnicodeDecodeError as fehler:
        # UnicodeDecodeError erbt von ValueError, nicht von OSError: wer nur
        # OSError fängt, bekommt hier einen Traceback statt Code 4.
        abbruch(4, "nicht UTF-8: %s (%s)" % (pfad, fehler))
    except csv.Error as fehler:
        abbruch(4, "CSV nicht lesbar: %s (%s)" % (pfad, fehler))
    except OSError as fehler:
        abbruch(3, "nicht zu öffnen: %s (%s)" % (pfad, fehler))


def pruefe(zeilen, erkannt, grenzen, prios, mit_gsc):
    """Gibt (Treffer, nicht geprüft) zurück."""
    offen = {}
    for kennung, check in CHECKS.items():
        fehlt = [f for f in check["felder"] if f != "clicks" and f not in erkannt]
        if "clicks" in check["felder"] and not mit_gsc:
            fehlt.append("clicks")
        if fehlt:
            offen[kennung] = fehlt
    titel = {}
    for zeile in zeilen:
        text = (zeile.get("title") or "").strip()
        if text:
            titel[text] = titel.get(text, 0) + 1
    treffer = []

    def melde(kennung, url, wert):
        if kennung not in offen:
            treffer.append((prios[kennung], kennung, url, wert))

    for zeile in zeilen:
        url = zeile["url"]
        status = zeile.get("status")
        inlinks = zeile.get("inlinks")
        if status is not None and status != 200:
            if inlinks is not None and inlinks > 0:
                melde("status_not_200", url, "Status %d, %d interne Links"
                      % (status, inlinks))
            continue
        if (zeile.get("indexability") or "").strip() != "Indexable":
            continue
        text = (zeile.get("title") or "").strip()
        if not text:
            melde("title_empty", url, "(leer)")
        elif titel[text] >= grenzen["title_duplicate"]:
            melde("title_duplicate", url, "%s auf %d URLs" % (text, titel[text]))
        laenge = zeile.get("title_len")
        if laenge is not None and laenge > grenzen["title_too_long"]:
            melde("title_too_long", url, "%d Zeichen" % laenge)
        if not (zeile.get("h1") or "").strip():
            melde("h1_missing", url, "(leer)")
        if not (zeile.get("metadesc") or "").strip():
            melde("metadesc_empty", url, "(leer)")
        if inlinks == 0:
            melde("orphan", url, "0 interne Links")
        woerter = zeile.get("words")
        if woerter is not None and woerter < grenzen["thin_page"]:
            melde("thin_page", url, "%d Wörter" % woerter)
        kanonisch = (zeile.get("canonical") or "").strip()
        klicks = zeile.get("clicks")
        if kanonisch and kanonisch != url and klicks is not None and klicks > 0:
            melde("canonical_elsewhere", url, "zeigt auf %s, %d Klicks"
                  % (kanonisch, klicks))
    return sorted(treffer), offen


def main(argv):
    if not 3 <= len(argv) <= 4:
        abbruch(2, "Aufruf: audit_export.py CHECKS.md crawl.csv "
                   "[search-console.csv]")
    for pfad in argv[1:]:
        if not os.path.exists(pfad):
            abbruch(3, "Datei nicht gefunden: %s" % pfad)
        if not os.path.isfile(pfad):
            abbruch(3, "keine reguläre Datei: %s" % pfad)
    grenzen, prios = lies_grenzwerte(argv[1])
    zeilen, erkannt = oeffne(argv[2], CRAWL_SPALTEN)
    if not erkannt:
        abbruch(6, "kein bekannter Spaltenname in %s: Exportformat und "
                   "Trennzeichen prüfen" % argv[2])
    if not zeilen:
        abbruch(5, "keine Zeile mit URL in %s" % argv[2])
    gsc = {}
    if len(argv) == 4:
        gsc_zeilen = oeffne(argv[3], GSC_SPALTEN)[0]
        gsc = {z["url"]: z for z in gsc_zeilen}
    for zeile in zeilen:
        passend = gsc.get(zeile["url"], {})
        zeile["clicks"] = passend.get("clicks")
        zeile["impressions"] = passend.get("impressions")
    universum = [z for z in zeilen
                 if z.get("status") == 200
                 and (z.get("indexability") or "").strip() == "Indexable"]
    treffer, offen = pruefe(zeilen, erkannt, grenzen, prios, bool(gsc))
    ausgabe = ["Audit: %s" % argv[2],
                 "URLs in der Datei: %d" % len(zeilen),
                 "Universum (Status 200 und indexierbar): %d" % len(universum),
                 "Search-Console-Datei: %s" % ("ja" if gsc else "nein"),
                 "", "Treffer: %d" % len(treffer)]
    for prio, kennung, url, wert in treffer:
        ausgabe.append("%s  %-19s %s  %s" % (prio, kennung, url, wert))
    ausgabe.append("")
    ausgabe.append("Nicht geprüft: %d" % len(offen))
    for kennung in sorted(offen):
        ausgabe.append("    %-19s fehlt: %s"
                         % (kennung, ", ".join(sorted(offen[kennung]))))
    return "\n".join(ausgabe) + "\n"


if __name__ == "__main__":
    try:
        bericht = main(sys.argv)
    except SystemExit:
        raise
    except Exception as fehler:
        sys.stderr.write("interner Fehler: %r\n" % (fehler,))
        raise SystemExit(8)
    sys.stdout.write(bericht)

Zum Mitlaufen brauchst du zwei erfundene Eingabedateien ohne Messwerte einer echten Website. crawl.csv hat keine Spalte Meta Description 1, damit eine fehlende Spalte sichtbar wird, und sie ist eine URL-Liste und nicht die Ausgabe eines rein linkfolgenden Crawls – nur deshalb kann dort eine Seite ohne eingehende interne Links stehen.

Address,Status Code,Indexability,Title 1,Title 1 Length,H1-1,Canonical Link Element 1,Word Count,Inlinks
https://example.com/schuhe/,200,Indexable,Schuhe,6,Schuhe,https://example.com/schuhe/,820,34
https://example.com/schuhe/blau/,200,Indexable,Schuhe,6,,https://example.com/schuhe/,210,0
https://example.com/alt/,404,Non-Indexable,,0,,,0,7
https://example.com/ratgeber/,200,Indexable,Ein sehr langer Titel der die Hausgrenze von sechzig Zeichen deutlich reisst,76,Ratgeber,https://example.com/ratgeber/,1450,12

search-console.csv bringt die gemessenen Klicks mit, die einen Check erst auswertbar machen.

page,clicks,impressions
https://example.com/schuhe/,7,180
https://example.com/schuhe/blau/,3,64

Der erste Lauf kommt ohne die zweite Datei. Sieben Befunde, und zwei Checks sind nicht geprüft: canonical_elsewhere braucht clicks, metadesc_empty braucht die fehlende Spalte. Beide haben nichts gefunden, weil sie nicht gelaufen sind.

$ python3 scripts/audit_export.py CHECKS.md crawl.csv
Audit: crawl.csv
URLs in der Datei: 4
Universum (Status 200 und indexierbar): 3
Search-Console-Datei: nein

Treffer: 7
P1  status_not_200      https://example.com/alt/  Status 404, 7 interne Links
P2  h1_missing          https://example.com/schuhe/blau/  (leer)
P2  orphan              https://example.com/schuhe/blau/  0 interne Links
P2  title_duplicate     https://example.com/schuhe/  Schuhe auf 2 URLs
P2  title_duplicate     https://example.com/schuhe/blau/  Schuhe auf 2 URLs
P3  thin_page           https://example.com/schuhe/blau/  210 Wörter
P3  title_too_long      https://example.com/ratgeber/  76 Zeichen

Nicht geprüft: 2
    canonical_elsewhere fehlt: clicks
    metadesc_empty      fehlt: metadesc

Mit der zweiten Datei greift canonical_elsewhere: /schuhe/blau/ zeigt kanonisch auf /schuhe/ und hat drei gemessene Klicks, also wird eine URL zurückgezogen, die etwas einbringt. Sieben Befunde werden acht, zwei nicht geprüfte Checks einer.

$ python3 scripts/audit_export.py CHECKS.md crawl.csv search-console.csv
Audit: crawl.csv
URLs in der Datei: 4
Universum (Status 200 und indexierbar): 3
Search-Console-Datei: ja

Treffer: 8
P1  canonical_elsewhere https://example.com/schuhe/blau/  zeigt auf https://example.com/schuhe/, 3 Klicks
P1  status_not_200      https://example.com/alt/  Status 404, 7 interne Links
P2  h1_missing          https://example.com/schuhe/blau/  (leer)
P2  orphan              https://example.com/schuhe/blau/  0 interne Links
P2  title_duplicate     https://example.com/schuhe/  Schuhe auf 2 URLs
P2  title_duplicate     https://example.com/schuhe/blau/  Schuhe auf 2 URLs
P3  thin_page           https://example.com/schuhe/blau/  210 Wörter
P3  title_too_long      https://example.com/ratgeber/  76 Zeichen

Nicht geprüft: 1
    metadesc_empty      fehlt: metadesc

Der dritte Aufruf zeigt den Tippfehler in der Grenzwertdatei. Zu sehen sind 3, 6 und 7; die übrigen stehen im Kopf des Skripts.

$ python3 scripts/audit_export.py CHECKS.md fehlt.csv
Datei nicht gefunden: fehlt.csv
$ echo $?
3
$ sed 's/,/	/g' crawl.csv > crawl.tsv
$ python3 scripts/audit_export.py CHECKS.md crawl.tsv
kein bekannter Spaltenname in crawl.tsv: Exportformat und Trennzeichen prüfen
$ echo $?
6
$ sed 's/^| title_too_long/| title_to_long/' CHECKS.md > CHECKS-tippfehler.md
$ python3 scripts/audit_export.py CHECKS-tippfehler.md crawl.csv
unbekannte Check-ID in CHECKS-tippfehler.md: title_to_long
$ echo $?
7

Wo die Branche streitet: eigene Disziplin oder umbenanntes SEO

Mehrere veröffentlichte SEO-Skills bringen einen Check mit, der prüft, ob eine Seite „für KI-Suche bereit“ ist – und für diesen Check gibt es keinen vertretbaren Grenzwert, weil schon seine Prämisse umstritten ist. Ob GEO eine eigene Disziplin oder umbenanntes SEO ist, ist in der Branche offen, und dieser Kurs entscheidet die Frage nicht. Wie schnell hier etwas erfunden wird, hat ContextBolt am 23. Juni 2026 festgehalten: im einwöchigen Selbstversuch auf einer Website, ohne Kontrollgruppe, schlug das Modell ein „AI-ranking markup“ vor, ein Markup für das Ranking in KI, das es nicht gibt.

Das eine Lager liest die Belege zu Googles AI Overviews so: eine Antwortfläche, die auf gewöhnlichen Suchergebnissen aufsetzt, belohnt im Wesentlichen das, was organisches SEO ohnehin belohnt – dieselbe Arbeit mit Feinschliff auf Passagenebene. Das andere hält die Belege zu ChatGPT dagegen, das einen eigenen Index und damit einen eigenen Abrufweg betreibt – also eine andere Arbeit. Beide Lesarten stützen sich auf Erhebungen zu je einer Oberfläche.

Was die Daten nicht entscheiden, lässt sich genau benennen: es gibt keine Messung, die beide Oberflächen unter einer Methode nebeneinanderstellt, und keine, die zeigt, dass ein „GEO-Check“ in einem Skill irgendeine Sichtbarkeit verändert. Für die Grenzwertdatei folgt daraus eine Konsequenz: ein solcher Check kommt in die Beobachtungen und nicht in die Fehlerspalte, denn eine Seite durchfallen zu lassen behauptet ein Urteil, das die Belege nicht tragen. Dieses Level behandelt die Modelle als Werkzeug; zitiert zu werden gehört zu GEO und AIO in Level 4.

Häufige Fehler

  1. Dreißig Skills „für den Fall“ installieren. Jeder kostet rund 100 Token Metadaten in jeder Unterhaltung, benutzt oder nicht (Anthropic-Plattformdokumentation, geprüft am 20. September 2026): dreißig schlafende Skills liegen in der Größenordnung von 3.000 Token vor der ersten Frage. Die Lösung: die Sammlung monatlich durchgehen.
  2. Eine generische Beschreibung schreiben. „Hilft bei SEO-Aufgaben“ greift nie, weil genau dieser Satz mit deiner Anfrage verglichen wird. Die Lösung: dritte Person, was der Skill tut und wann er einzusetzen ist, in den Wörtern, die du wirklich tippst.
  3. Einem Diagnose-Skill Schreibrechte geben. Will Scott beschrieb am 28. August 2026 in Search Engine Land einen Fall, in dem Claude eine Startseite in zwei neue URLs klonte und nur die Title-Tags änderte: null Impressionen und null Klicks auf beiden, die Startseite blieb auf Position 9 oder schlechter. Dokumentiert ist das auf zwei Websites, was der Autor als systematisches Verhalten liest und nicht als Einzelfall; eine Stichprobe ist es nicht. Die Lösung: Lesen und Schreiben in getrennte Läufe, Veröffentlichen von Hand.
  4. Zahlen durchlassen, die nicht in der Datei stehen. ContextBolt dokumentierte am 23. Juni 2026, dass Drittanbieter-Schätzungen wie gemessene Daten behandelt wurden. Die Lösung: jede Zahl im Bericht trägt die Zelle, aus der sie kommt, oder sie fällt weg.
  5. Einen nicht geprüften Check als sauberen melden. Ein Check, dessen Spalte fehlt oder dem die Search-Console-Datei fehlt, hat nichts gefunden, weil er nicht gelaufen ist. Die Lösung: den Block „Nicht geprüft“ aus dem Bericht übernehmen, nicht weglassen.

Kurzfassung

  • Ein Crawl-Export beantwortet nur Fragen über seine Zellen: Status, Titles, H1, Canonicals, interne Links, Wortzahl. Alles andere nicht, und ein Audit-Skill darf die Lücke nicht füllen – Claude analysiert Crawl-Daten und crawlt keine Websites (Lawrence Hitches, 2. April 2026).
  • 38 % setzen KI für technische Audits ein, und unter den 79 % mit bewusst nicht automatisierten Aufgaben nennen 40 % technisches SEO (Keyword.com, 1. Januar 2026, n = 97, selbst zusammengekommen). Dieselbe Aufgabe steht also auf beiden Listen.
  • Der YAML-Kopf braucht name bis 64 Zeichen und description bis 1.024 Zeichen (Anthropic-Plattformdokumentation, geprüft am 20. September 2026). Die Beschreibung wird nicht gelesen, sondern mit der Anfrage verglichen.
  • Stufe 1 der gestuften Offenlegung kostet rund 100 Token pro installiertem Skill in jeder Unterhaltung, Stufe 2 unter 5.000 Token beim Greifen, Stufe 3 nichts bis zum Öffnen. Teuer wird die Sammlung, nicht der einzelne Skill.
  • Grenzwerte gehören in eine Datei, die Skript und Mensch lesen. Eine Kennung, die der Code nicht kennt, beendet den Lauf statt still eine Prüfung auszulassen.
  • Jeder Fehlerfall braucht seinen eigenen Exit-Code, und Code 1 bleibt unbelegt: ein abgestürzter Lauf darf kein Urteil über deine Daten abgeben.
  • Eine Sammlung wird auf ihrer Seite mit 51 Skills angekündigt, während das zugehörige Repository 29 nennt. Prüfe vor dem Installieren, ob du die Grenzwerte lesen, ändern und datieren kannst.

Häufige Fragen

Was mache ich, wenn das Prüfskript einen anderen Code als 0 liefert?

Du gibst den Fehlertext aus und kein Audit-Ergebnis. Bei 3 fehlt eine Eingabedatei, bei 6 passt das Exportformat nicht, bei 7 nennt CHECKS.md eine Kennung, die das Skript nicht kennt – alle drei sind schnell behoben. Code 8 meldet eine unerwartete Ausnahme und gibt nichts aus; Code 1 trifft einen Lauf, der mitten im Schreiben abbricht, sodass höchstens ein Teil des Berichts ankommt. Ein unvollständiger Bericht ist kein Urteil, und aus einem leeren auf eine saubere Website zu schließen wäre der Fehler.

Kann ein Claude Skill meine Website crawlen?

Von sich aus nicht. Ein Skill ist eine Anweisungsdatei samt Ressourcen und bringt keinen Crawler mit; über die API hat er nicht einmal Netzzugriff. Lawrence Hitches fasste die Grenze am 2. April 2026 zusammen: Claude analysiert Crawl-Daten, es crawlt keine Websites. Der realistische Ablauf ist crawlen, exportieren, den Export an den Skill geben. Soll Claude den Crawl selbst starten, brauchst du einen MCP-Server für dein Crawling-Werkzeug, und das behandelt dieses Level getrennt.

Lohnt sich ein eigener Skill, wenn es fertige Sammlungen gibt?

Das hängt an einer einzigen Frage: kannst du die Grenzwerte des fremden Skills lesen? Ein SEO-Skill von außen bringt strittige Entscheidungen als Fehler verkleidet mit – welche Titellänge ein Problem ist, welche Crawl-Tiefe zu viel ist, ob eine Seite einen KI-Check besteht, über den die Branche streitet. Liegen die Zahlen in einer Referenzdatei, die du öffnen und ändern kannst, nimm die Sammlung.

Quellen

  • Format, Grenzen und Budget: Anthropic-Plattformdokumentation, geprüft am 20. September 2026 – SKILL.md mit YAML-Kopf, name bis 64 und description bis 1.024 Zeichen, die drei Budgets der gestuften Offenlegung und die Grenzen über die Oberflächen hinweg. Ohne Link: für diese Seiten liegt keine geprüfte URL vor.
  • Die Spezifikation: anthropics/skills, Verzeichnis ./spec/, dazu ./skills/ und ./template/. Repository ohne Veröffentlichungsdatum.
  • Die harte Grenze eines Audits: Lawrence Hitches, Claude for Technical SEO: Audits, Crawl Fixes & Schema, 2. April 2026. Praxisartikel ohne Stichprobengröße.
  • Wie oft Audits abgegeben werden: Keyword.com, State of AI and Automation in SEO, 1. Januar 2026, n = 97 verwertbare, selbst zusammengekommene Antworten. 38 % setzen KI für technische Audits ein; unter den 79 % mit bewusst nicht automatisierten Aufgaben nennen 40 % technisches SEO.
  • Ein dokumentiertes Fehlermuster: ContextBolt, Claude SEO Experiment: A Week of Running My Real SEO, 23. Juni 2026. Eine Woche, eine Website, keine Kontrollgruppe; als Autor nur „David“ ohne Nachnamen.
  • Der Fall zu Schreibrechten: Will Scott, Use Claude for SEO. Don’t let Claude do SEO., Search Engine Land, 28. August 2026. Zwei Websites dokumentiert, keine Stichprobe; der Autor liest das Verhalten als systematisch und nicht als Einzelfall. Ohne Link: für diese Seite liegt keine geprüfte URL vor.
  • Die Skill-Sammlungen: AgriciDaniel/claude-seo, 25 Sub-Skills und 18 Sub-Agents, MIT-Lizenz; und Hawk Academy, nach Angabe der Seite von StudioHawk erstellt, 51 kostenlose Skills in sieben Kategorien, ohne Veröffentlichungsdatum und mit der Fußzeile „Copyright (c) 2026 Hawk Academy“, während das zugehörige Repository lhitches/claude-seo-skills weiter 29 nennt. Keine Sterne- oder Fork-Zahlen.
  • Die Nachfrage: Google Autocomplete, für diesen Kurs am 20. September 2026 erhoben. Es zeigt, dass ein Begriff getippt wird, nicht wie oft; eine Volumenzahl steht in dieser Lektion nicht.

Weiter im Kurs