Zwei Dateien entscheiden, ob der Client des Brave Web Discovery Project eine deiner URLs behält oder wegwirft: die Antwort-Header und der Quelltext, beides holst du selbst mit curl. Diese Lektion liefert das Skript dazu und die Dateien, an denen du jede Zeile seiner Ausgabe nachstellst.
Überraschend ist, woran entschieden wird. Nicht der Besuch eines Programmteilnehmers wird geprüft, sondern eine zweite Anfrage, die der Client danach selbst stellt – nach einer zufälligen Wartezeit von 1 bis 20 Minuten. Wer im Suchergebnis eine URL stehen hat, die mit einer Weiterleitung antwortet, verliert sie dort, und der Besuch hilft ihm nicht.
Alles, was diese Seite über diesen Client sagt, hat eine einzige Herkunft: eine Auswertung des offenen Clients am Commit f25eb3d, die MERJ im Juni 2026 veröffentlicht hat. Ein Artefakt, ein Commit, ein Analyst, keine unabhängige Wiederholung und keine Stichprobengröße. Was das belegt, steht weiter unten in einem eigenen Abschnitt.
Die Lektion gehört zu Level 8, SEO mit KI, in dem die Modelle Werkzeuge in deiner Hand sind. Wie KI-Suchmaschinen auswählen, wen sie zitieren, gehört zu Level 4 zu GEO und AIO. Diese Seite beschreibt eine Bedingung aus dem technischen SEO und keinen GEO-Rat: betroffen ist der Bau einer Seite, nicht ihr Text.
Was du lernst
- Welche zwei Dateien du brauchst, bevor über eine URL entschieden werden kann.
- Welche fünf Urteile eine Prüfzeile tragen kann – und warum eines davon kein Bestehen ist.
- An welcher Stelle im Team jede der sieben Bedingungen repariert wird.
- Warum ein Dokument von 167 Bytes jede Bedingung von MERJ erfüllt.
- Welcher der beiden Entdeckungswege für eine kleine Website überhaupt in Frage kommt.
- Warum zu diesen Grenzwerten keine Stichprobengröße gehört – und woran du merken würdest, dass einer von ihnen nicht mehr gilt.
Geprüft wird die zweite Anfrage, nicht der Besuch
Niemand bei Brave trägt deine URL ein. In das Web Discovery Project gelangt sie allein über die Browser der Programmteilnehmer, und dafür nennt MERJs Auswertung des Commits f25eb3d (Juni 2026) zwei Mechanismen. Der eine zählt Besuche zusammen: lesbar wird eine Meldung erst, wenn sie von rund 20 Teilnehmern aus verschiedenen Netzen auf derselben Seite zusammenkommt – MERJ nennt dafür das Verschlüsselungsverfahren STAR, dessen Schwelle bei 20 Anteilen liegt. Diese Zahl bekommt eine kleine Website für eine neue Seite nicht zusammen.
Der andere Mechanismus kommt ohne Masse aus. Sieht ein einzelner Teilnehmer deine Seite in einer Trefferliste von Google, Bing, Yahoo oder DuckDuckGo, holt der Client die URL anschließend selbst – mit einem zufälligen Abstand von 1 bis 20 Minuten. Die Filter sehen deshalb nie den Seitenaufruf des Teilnehmers, sondern diesen zweiten Abruf: eine Antwort, die dein Server Minuten später ausliefert.
Damit setzt der günstige Mechanismus voraus, was er belohnen soll: eine Trefferliste, auf der deine Seite schon steht. Fehlt sie dort, läuft er nie an, und eine Brave-eigene Optimierung, die diesen Schritt ersetzt, ist in den Quellen dieser Lektion nicht dokumentiert – es bleibt die Arbeit aus Level 2 zu technischem SEO. In deiner Hand liegt die andere Hälfte: die Gründe, aus denen dieser zweite Abruf weggeworfen wird.
Sieben Filter, geordnet nach dem Preis der Reparatur
Der Client des Web Discovery Project verwirft eine URL, bevor er etwas sendet, sobald eine der sieben Bedingungen aus MERJs Auswertung des Commits f25eb3d (Juni 2026) nicht erfüllt ist. Die Tabelle nennt alle sieben mit ihrem Grenzwert, sortiert nach dem Preis der Reparatur: oben steht, was ein Nachmittag erledigt, unten, wofür ein Budget nötig ist. Diese Spalte stammt von doctor-seo.net.
| Filter | Grenzwert aus der Auswertung | Was ihn in der Praxis reißt | Wo die Reparatur sitzt |
|---|---|---|---|
| Weiterleitungen | keine, der Client folgt keinem Sprung | Die verteilte URL steht ohne Schrägstrich, mit www oder auf http |
Wo Links entstehen: Newsletter, Profile, Anzeigen |
| Statuscode | nur HTTP 200 | Eine URL antwortet mit 404 und wird weiter verlinkt | Redaktion und interne Verlinkung |
| Query-String | über 22 Zeichen verworfen, außer das HTML nennt ein Canonical auf denselben Host | Jeder Kampagnenparameter – gefährlich nur in Vorlagen ohne Canonical | Template-Entwicklung |
noindex und Canonical |
zählen ausschließlich im HTML | Eine Direktive, die es nur als HTTP-Header gibt | Server-Konfiguration und CDN |
| Dokumentgröße | 2 MB | Eingebettete Daten im Quelltext: Kataloge als JSON, Bilder als base64 | Template-Entwicklung |
| Antwortzeit | 10 Sekunden | Erster Aufruf ohne Cache zur Lastspitze | Hosting und Caching |
| JavaScript | wird nie ausgeführt | Inhalt, der erst im Browser entsteht | Architektur, kein Ticket |
Die vierte Spalte ist der eigentliche Befund. Sechs der sieben Zeilen beschreiben Mängel, die überall etwas kosten und hier alles. Anders gebaut ist allein die vierte: wer noindex nur als Header setzt, hat die Direktive dort hinterlegt, wo dieser Client sie nicht liest, und bleibt aufnahmefähig, obwohl er das Gegenteil wollte.
Alle sieben Werte hängen an demselben Commit: wer die Liste ohne das Datum weitergibt, macht aus einer Momentaufnahme eine Regel. Und wer alle sieben besteht, hat damit noch keine Brave-Indexierung erreicht, sondern ist nur nicht vorzeitig ausgeschlossen.
Der Index gehört Brave: kein Eintrag dort, kein Zitat bei Claude
Claude erreicht das offene Web über Brave Search, und diese Abhängigkeit trägt ein Datum: als Subunternehmer steht Brave Search seit dem 19. März 2025 in Anthropics Trust Center, turbopuffer seit dem 6. Mai 2026. Zusammengetragen hat die Auflistung Xponent21 und am 2. Juli 2026 nachgeprüft; eine bestätigte URL liegt im Quellenbestand dieses Kurses nicht vor, sie steht hier also benannt und datiert ohne Link.
Fehlt eine URL in diesem Index, hilft kein Satzbau weiter, und eine Oberfläche, in der Publisher den Zustand ihrer Seiten nachsehen könnten, nennen die Quellen dieser Lektion nicht. Deshalb lohnt eine Filterliste aus dem Quellcode: sie ist der Teil der Kette, an dem du etwas tun kannst.
Zwei Dinge gehen hier regelmäßig durcheinander. Das erste ist die Frage, wie stark Claudes Zitate den Ergebnislisten von Brave folgen: dazu gibt es eigene Erhebungen mit eigenen Stichprobengrößen, und auf dieser Seite steht absichtlich keine Überschneidungszahl – zitiert zu werden ist das Thema von Level 4 zu GEO und AIO.
Das zweite ist das Zugriffsprotokoll des eigenen Servers. Anthropic dokumentiert in seinem Hilfeartikel zum eigenen Crawling drei getrennte Agenten – ClaudeBot, Claude-User und Claude-SearchBot –, und die Seite zeigt nur eine relative Zeitangabe, trägt also kein absolutes Datum. Ob diese drei bei dir auftauchen, sagt nichts darüber, ob Brave deine Seite gespeichert hat: Abrufe benannter Agenten und Entdeckung über fremdes Surfverhalten sind zwei Mechanismen nebeneinander.
Das Prüfskript: drei Phasen, zehn Zeilen, zehn belegte Exit-Codes
Das folgende Skript beurteilt eine URL gegen die Filter des Web-Discovery-Project-Clients. Es greift nicht auf das Netz zu, rechnet über zwei selbst geholte Dateien und braucht nur die Standardbibliothek. Zuerst die Antwort speichern, mit allen Sprüngen und der Zeit:
curl -sSL -D kopfzeilen.txt -o dokument.html -w '%{time_total}\n' \
'https://example.com/werkstatt/bremsbelaege-wechseln/'
Dann die Prüfung starten. Die Sekundenzahl ist der vierte Parameter und optional, Schalter nimmt das Skript keine an:
python3 brave-index-check.py 'https://example.com/werkstatt/bremsbelaege-wechseln/' \
kopfzeilen.txt dokument.html 1.07
Die Ausgabe hat drei Phasen und zehn Zeilen, davon tragen acht MERJs Grenzwerte und zwei sind eigene Zeilen von doctor-seo.net. Die acht sind nicht die sieben Bedingungen in anderer Verpackung: Statuscode und Weiterleitung stehen getrennt, weil ihre Reparatur an zwei Stellen sitzt, die Direktiven-Regel zerfällt in drei Zeilen für das meta-Element, den X-Robots-Tag-Header und die Quelle des Canonical, und für das Nie-Ausführen von JavaScript gibt es keine Zeile: es beschreibt, wie der Client liest, und ist keine Bedingung, an der eine URL durchfällt.
Diese Lücke füllt Phase 3, in der Ausgabe ein eigener Block mit eigener Überschrift: dort prüft das Skript den ausgelieferten Inhaltstyp und einen Mindestwert von 500 Zeichen sichtbarem Text als Ersatzmaß dafür, dass kein JavaScript läuft. MERJ nennt keinen Textwert, die 500 sind meine Hausmarke, und MIN_TEXT passt du an deine Vorlagen an. Beides in einer Liste würde einem eigenen Kriterium die Autorität der Quelle leihen.
Jede Zeile trägt einen von fünf Zuständen. OK ist bestanden, SPERRE heißt, dass der Client die URL hier wegwirft, ABWEICHUNG, dass sie aufnahmefähig bleibt, während eine gesetzte Direktive ungelesen bleibt. OFFEN heißt, dass nichts zu vergleichen war, und ENTFÄLLT steht, wo der Client das Dokument nie anfordert. Dazu kommen zehn Exit-Codes: kein Bedienfehler teilt sich einen Code mit einem Urteil über die URL.
| Exit-Code | Bedeutung |
|---|---|
0 |
behalten: alle Sperrzeilen bestanden, keine Abweichung |
2 |
verworfen: mindestens eine Sperrzeile fällt aus |
3 |
behalten, aber eine gesetzte Direktive bleibt ungelesen |
4 |
falsche Anzahl Parameter |
5 |
Eingabedatei fehlt oder lässt sich nicht öffnen |
6 |
eine Eingabedatei ist nicht UTF-8 |
7 |
Kopfdatei unbrauchbar: keine HTTP-Statuszeile darin |
8 |
Dokumentdatei unbrauchbar: leer, Header-Abzug oder ohne Tag |
9 |
der vierte Parameter ist keine positive Zahl |
10 |
unerwartete Ausnahme, es wird kein Urteil ausgegeben |
1 |
bleibt unbelegt |
Dass 1 frei bleibt, steht im Kopf des Skripts: einen unbehandelten Traceback beendet Python selbst mit 1, und trüge 1 auch VERWORFEN, wäre ein Absturz von einem Urteil nicht zu unterscheiden.
Drei Fallen sind an benannten Stellen abgeräumt. Dekodiert wird streng statt mit errors='replace', weil ein ersetztes Zeichen in einer Canonical-URL den Hostvergleich verfälschen würde; der Fall hat einen eigenen Zweig, denn UnicodeDecodeError stammt von ValueError ab und nicht von OSError. Der vierte Parameter geht durch eine Funktion, die True zurückweist, weil Wahrheitswerte in Python Ganzzahlen sind. Und es gibt kein get(name, "") auf einen Header: ein fehlender und ein leer gesendeter sind zwei Befunde.
#!/usr/bin/env python3
"""Entscheidet, ob der Client des Brave Web Discovery Project eine URL behält
oder sie vor dem Senden verwirft.
python3 brave-index-check.py <URL> <kopfzeilen.txt> <dokument.html> [Sekunden]
Die Prüfung läuft in drei Phasen. Phase 1 beurteilt die Antwort auf die URL,
die geprüft wird, Phase 2 das Dokument dahinter, Phase 3 zwei eigene Zeilen von
doctor-seo.net, die MERJ nicht veröffentlicht. Jeder Grenzwert der ersten beiden
Phasen stammt aus MERJs Auswertung des offenen Clients am Commit f25eb3d, Juni 2026.
Kein Netzzugriff, nur Standardbibliothek. Die beiden Dateien holst du selbst:
curl -sSL -D kopfzeilen.txt -o dokument.html -w '%{time_total}\\n' \\
'https://example.com/eine-url/'
Zustände einer Zeile:
OK bestanden
SPERRE der Client verwirft die URL an dieser Zeile
ABWEICHUNG die URL bleibt geeignet, aber eine gesetzte Direktive wird
nicht gelesen
OFFEN es gab nichts zu vergleichen - kein Bestehen
ENTFÄLLT dieses Dokument wird nie angefordert
Exit-Codes. Jeder Code benennt genau einen Fall, und kein Bedienfehler teilt
sich einen Code mit einem Urteil über die URL:
0 behalten: alle Sperrzeilen bestanden, keine Abweichung
2 verworfen: mindestens eine Sperrzeile fällt aus
3 behalten, aber mit Abweichung: eine Direktive bleibt ungelesen
4 falsche Anzahl Parameter
5 eine Eingabedatei fehlt, ist keine reguläre Datei oder lässt sich nicht
öffnen
6 eine Eingabedatei ist nicht UTF-8
7 die Kopfdatei ist unbrauchbar: keine HTTP-Statuszeile darin
8 die Dokumentdatei ist unbrauchbar: leer, ein Header-Abzug oder ohne ein
einziges Tag
9 der vierte Parameter ist keine positive Zahl
10 unerwartete Ausnahme, es wird kein Urteil ausgegeben
1 bleibt unbelegt, weil Python einen unbehandelten Traceback selbst mit 1
beendet
"""
import os
import re
import sys
from urllib.parse import urlparse
GRENZE_BYTES = 2 * 1024 * 1024 # MERJ: 2 MB
GRENZE_SEKUNDEN = 10.0 # MERJ: 10 Sekunden
GRENZE_QUERY = 22 # MERJ: längere Query-Strings werden verworfen
MIN_TEXT = 500 # EIGENER Wert, nicht von MERJ. An deine Vorlagen anpassen.
AUFRUF = ("Aufruf: brave-index-check.py <URL> <kopfzeilen.txt> <dokument.html> [Sekunden]\n"
" kopfzeilen.txt Antwort-Header, mit curl -D geschrieben, alle Sprünge\n"
" dokument.html der Antwortkörper derselben Anfrage\n"
" Sekunden optional, die Gesamtzeit aus curl -w '%{time_total}'\n"
" weitere Parameter und Schalter nimmt das Skript nicht an")
ANFRAGE, DOKUMENT, EIGENE = "anfrage", "dokument", "eigene"
zeilen = []
def notiere(phase, name, zustand, hinweis):
zeilen.append((phase, name, zustand, hinweis))
def ende(code, text):
sys.stderr.write(text.rstrip() + "\n")
raise SystemExit(code)
def lade(pfad):
if not os.path.exists(pfad):
ende(5, "Datei nicht gefunden: %s" % pfad)
if not os.path.isfile(pfad):
ende(5, "keine reguläre Datei: %s" % pfad)
try:
with open(pfad, "rb") as datei:
return datei.read()
except OSError as fehler:
ende(5, "nicht zu öffnen: %s (%s)" % (pfad, fehler.strerror))
def entschlüsselt(rohdaten, pfad):
"""Dekodiert streng. UnicodeDecodeError erbt von ValueError und nicht von
OSError und braucht deshalb einen eigenen Zweig; mit errors='replace'
würde ein ersetztes Zeichen still eine Canonical-URL verändern."""
try:
return rohdaten.decode("utf-8")
except UnicodeDecodeError as fehler:
ende(6, "nicht UTF-8: %s (%s)" % (pfad, fehler))
def komma(zahl, stellen=2):
"""Formatiert eine Zahl mit Dezimalkomma nach DIN 5008. Prozentzeichen
kommen in dieser Ausgabe nicht vor: ein Byte-Wert gegen seine Grenze ist
nachrechenbar, ein Anteil daran nicht."""
return ("%.*f" % (stellen, zahl)).replace(".", ",")
def positive_zahl(wert):
"""Liefert eine Sekundenzahl oder None. bool ist in Python eine Unterklasse
von int: ohne die erste Abfrage käme True als eine Sekunde durch, sobald
diese Funktion aus einem anderen Skript mit einem Schalter aufgerufen wird."""
if isinstance(wert, bool):
return None
try:
sekunden = float(wert)
except (TypeError, ValueError):
return None
return sekunden if sekunden > 0 else None
def sprünge(kopftext):
"""Zerlegt einen curl-Kopfabzug in Sprünge. Leere Liste, wenn keine
Statuszeile darin steht."""
liste, aktuell = [], None
for zeile in kopftext.splitlines():
if re.match(r"^HTTP/\d", zeile):
aktuell = {"status": zeile.strip(), "kopf": {}}
liste.append(aktuell)
elif aktuell is not None and ":" in zeile:
name, wert = zeile.split(":", 1)
aktuell["kopf"].setdefault(name.strip().lower(), []).append(wert.strip())
return liste
def statuscode(sprung):
treffer = re.match(r"^HTTP/\S+\s+(\d{3})", sprung["status"])
return int(treffer.group(1)) if treffer else 0
def kopfwert(kopf, name):
"""Gibt (gesendet, Wert) zurück. Nirgends steht ein kopf.get(name, ''):
ein nicht gesendeter und ein leer gesendeter Header sind zwei verschiedene
Befunde, und nur der erste darf als "nichts zu vergleichen" durchgehen."""
if name not in kopf:
return False, None
gefüllt = [wert for wert in kopf[name] if wert.strip()]
return True, (gefüllt[0] if gefüllt else "")
def sichtbarer_text(markup):
ohne = re.sub(r"(?is)<(script|style|template|noscript|title)\b.*?</\1>", " ", markup)
ohne = re.sub(r"(?s)<!--.*?-->", " ", ohne)
ohne = re.sub(r"(?s)<[^>]+>", " ", ohne)
return re.sub(r"\s+", " ", ohne).strip()
def canonical_im_html(markup):
tag = re.search(r"(?is)<link[^>]+rel=['\"]?canonical['\"]?[^>]*>", markup)
if not tag:
return None
adresse = re.search(r"""(?is)href=['"]?([^'"\s>]+)""", tag.group(0))
return adresse.group(1) if adresse else None
def canonical_im_header(wert):
adresse = re.search(r"<([^>]+)>", wert)
return adresse.group(1).strip() if adresse else wert.strip()
def beurteile(url, kopfpfad, dokumentpfad, sekunden):
sprungliste = sprünge(entschlüsselt(lade(kopfpfad), kopfpfad))
if not sprungliste:
ende(7, "Kopfdatei unbrauchbar: keine HTTP-Statuszeile in %s - falsche Datei, "
"oder curl hat den Körper hierhin geschrieben" % kopfpfad)
rohdokument = lade(dokumentpfad)
if not rohdokument.strip():
ende(8, "Dokumentdatei unbrauchbar: %s ist leer" % dokumentpfad)
dokument = entschlüsselt(rohdokument, dokumentpfad)
if re.match(r"^\s*HTTP/\d", dokument):
ende(8, "Dokumentdatei unbrauchbar: %s beginnt mit einer Statuszeile und ist "
"damit ein Header-Abzug" % dokumentpfad)
if "<" not in dokument:
ende(8, "Dokumentdatei unbrauchbar: kein einziges Tag in %s" % dokumentpfad)
erster = sprungliste[0]
code = statuscode(erster)
weiterleitungen = [statuscode(s) for s in sprungliste if 300 <= statuscode(s) < 400]
# Zeile 1 (MERJ): Der Client sieht die erste Antwort auf die URL, die der
# Programmnutzer vor sich hatte. Angenommen wird nur 200.
if code == 200:
notiere(ANFRAGE, "Antwort 200", "OK", "die geprüfte URL antwortet mit 200")
else:
notiere(ANFRAGE, "Antwort 200", "SPERRE",
"erste Antwort %d, angenommen wird nur 200" % code)
# Zeile 2 (MERJ): Weiterleitungen werden nicht verfolgt.
if weiterleitungen:
ziel = kopfwert(erster["kopf"], "location")[1] or "?"
notiere(ANFRAGE, "keine Weiterleitung", "SPERRE",
"%d nach %s, der Client folgt nicht" % (weiterleitungen[0], ziel))
else:
notiere(ANFRAGE, "keine Weiterleitung", "OK", "eine Antwort, kein Zwischenziel")
# Nach einem Sprung fordert der Client dieses Dokument nie an. Alles, was an
# ihm oder an der Gesamtzeit der Kette gemessen wird, beschreibt dann eine
# andere URL als die geprüfte.
abbruch = code != 200 or bool(weiterleitungen)
def dokumentzeile(phase, name, zustand, hinweis):
if abbruch:
notiere(phase, name, "ENTFÄLLT", "dieses Dokument wird nie angefordert")
else:
notiere(phase, name, zustand, hinweis)
# Zeile 3 (MERJ): 10 Sekunden.
if sekunden is None:
dokumentzeile(ANFRAGE, "Antwortzeit <= 10 s", "OFFEN",
"nicht gemessen: vierter Parameter aus curl -w '%{time_total}'")
else:
dokumentzeile(ANFRAGE, "Antwortzeit <= 10 s",
"OK" if sekunden <= GRENZE_SEKUNDEN else "SPERRE",
"%s s gegen %d s" % (komma(sekunden), int(GRENZE_SEKUNDEN)))
# Zeile 4 (MERJ): 2 MB.
größe = len(rohdokument)
dokumentzeile(DOKUMENT, "Dokument <= 2 MB",
"OK" if größe <= GRENZE_BYTES else "SPERRE",
"%d von %d Bytes" % (größe, GRENZE_BYTES))
# Zeile 5 (MERJ): noindex zählt nur im HTML.
metas = re.findall(r"(?is)<meta[^>]+name=['\"]?robots['\"]?[^>]*>", dokument)
metatext = " ".join(metas).lower()
dokumentzeile(DOKUMENT, "kein noindex im HTML",
"OK" if "noindex" not in metatext else "SPERRE",
metas[0][:88] if metas else "im HTML steht kein meta-robots-Element")
# Zeile 6 (MERJ): ein Query-String über 22 Zeichen nur mit Canonical auf
# denselben Host.
query = urlparse(url).query
canonical_html = canonical_im_html(dokument)
gleicher_host = (bool(canonical_html)
and urlparse(canonical_html).netloc in ("", urlparse(url).netloc))
if len(query) <= GRENZE_QUERY:
dokumentzeile(DOKUMENT, "Query-String <= 22", "OK",
"%d Zeichen Query-String, Grenze 22" % len(query))
elif gleicher_host:
dokumentzeile(DOKUMENT, "Query-String <= 22", "OK",
"%d Zeichen, aber das HTML nennt ein Canonical auf denselben Host: %s"
% (len(query), canonical_html))
else:
dokumentzeile(DOKUMENT, "Query-String <= 22", "SPERRE",
"%d Zeichen und kein Canonical auf denselben Host im HTML" % len(query))
letzter = sprungliste[-1]["kopf"]
# Zeile 7 (MERJ): ein Robots-Header wird nicht gelesen.
gesendet, xrt = kopfwert(letzter, "x-robots-tag")
if not gesendet:
dokumentzeile(DOKUMENT, "X-Robots-Tag", "OFFEN", "der Header X-Robots-Tag fehlt")
elif xrt == "":
dokumentzeile(DOKUMENT, "X-Robots-Tag", "OFFEN",
"X-Robots-Tag leer gesendet, nichts zu vergleichen")
elif "noindex" in xrt.lower() and "noindex" not in metatext:
dokumentzeile(DOKUMENT, "X-Robots-Tag", "ABWEICHUNG",
"noindex nur im Header (%s), und Header liest dieser Client nicht"
% xrt)
else:
dokumentzeile(DOKUMENT, "X-Robots-Tag", "OK",
"Header gesendet, deckungsgleich mit dem HTML: %s" % xrt)
# Zeile 8 (MERJ): ein Canonical im Link-Header wird ebenso nicht gelesen.
gesendet, link = kopfwert(letzter, "link")
canonical_header = (canonical_im_header(link)
if gesendet and link and "canonical" in link.lower() else None)
if canonical_header and not canonical_html:
dokumentzeile(DOKUMENT, "Canonical-Quelle", "ABWEICHUNG",
"Canonical nur im Link-Header (%s), gelesen wird allein das "
"link-Element im HTML" % canonical_header)
elif canonical_header and canonical_header.rstrip("/") != canonical_html.rstrip("/"):
dokumentzeile(DOKUMENT, "Canonical-Quelle", "ABWEICHUNG",
"Link-Header nennt %s, das HTML nennt %s - gelesen wird das HTML"
% (canonical_header, canonical_html))
elif canonical_html:
dokumentzeile(DOKUMENT, "Canonical-Quelle", "OK", canonical_html)
else:
dokumentzeile(DOKUMENT, "Canonical-Quelle", "OFFEN", "im HTML steht kein Canonical")
# --- ab hier zwei eigene Zeilen von doctor-seo.net, nicht von MERJ ---
gesendet, inhaltstyp = kopfwert(letzter, "content-type")
if not gesendet:
dokumentzeile(EIGENE, "Content-Type", "SPERRE", "kein Content-Type gesendet")
elif inhaltstyp == "":
dokumentzeile(EIGENE, "Content-Type", "SPERRE", "Content-Type leer gesendet")
else:
dokumentzeile(EIGENE, "Content-Type",
"OK" if "html" in inhaltstyp.lower() else "SPERRE", inhaltstyp)
hülle = re.search(r"(?is)<div[^>]+id=['\"]?(root|app|__next|__nuxt)['\"]?[^>]*>\s*</div>",
dokument)
text = sichtbarer_text(dokument)
if hülle:
dokumentzeile(EIGENE, "Text ohne JavaScript", "SPERRE",
"Platzhalter ohne Inhalt: %s" % hülle.group(0)[:56])
else:
dokumentzeile(EIGENE, "Text ohne JavaScript",
"OK" if len(text) >= MIN_TEXT else "SPERRE",
"%d Zeichen Text im HTML, eigener Mindestwert %d" % (len(text), MIN_TEXT))
ÜBERSCHRIFTEN = [
(ANFRAGE, "Phase 1 - die Antwort auf die geprüfte URL. Grenzwerte aus MERJs\n"
"Auswertung des WDP-Clients, Commit f25eb3d, Juni 2026."),
(DOKUMENT, "Phase 2 - das Dokument dahinter. Dieselbe Quelle, dasselbe Datum."),
(EIGENE, "Phase 3 - zwei Zeilen von doctor-seo.net und NICHT von MERJ. Geprüft\n"
"werden der Inhaltstyp der Antwort und ein Mindestwert von %d Zeichen\n"
"Text; für Text nennt MERJ keinen Wert, dieser ist eine Hausmarke."
% MIN_TEXT),
]
def bericht(url):
stücke = ["", "URL: %s" % url, ""]
for phase, überschrift in ÜBERSCHRIFTEN:
stücke.append(überschrift)
for eintrag in [z for z in zeilen if z[0] == phase]:
stücke.append(" [%-10s] %-22s %s" % (eintrag[2], eintrag[1], eintrag[3]))
stücke.append("")
sperren = sum(1 for z in zeilen if z[2] == "SPERRE")
abweichungen = sum(1 for z in zeilen if z[2] == "ABWEICHUNG")
if sperren:
stücke.append("VERWORFEN: %d Sperrzeile(n) nicht bestanden, der Client verwirft\n"
"diese URL." % sperren)
schluss = 2
elif abweichungen:
stücke.append("BEHALTEN, MIT %d ABWEICHUNG(EN): aufnahmefähig. Eine gesetzte Direktive\n"
"bleibt dabei ungelesen, die beabsichtigte Ausnahme greift hier also nicht."
% abweichungen)
schluss = 3
else:
stücke.append("BEHALTEN: keine Sperrzeile fällt aus, die URL ist aufnahmefähig.\n"
"Gespeichert wird sie erst, wenn einer der beiden Entdeckungswege eintritt.")
schluss = 0
return "\n".join(stücke) + "\n", schluss
def main(argv):
if not 4 <= len(argv) <= 5:
ende(4, "%d Parameter übergeben, erwartet werden drei oder vier.\n%s"
% (len(argv) - 1, AUFRUF))
sekunden = None
if len(argv) == 5:
sekunden = positive_zahl(argv[4])
if sekunden is None:
ende(9, "vierter Parameter %r ist keine positive Zahl.\n%s" % (argv[4], AUFRUF))
beurteile(argv[1], argv[2], argv[3], sekunden)
return bericht(argv[1])
if __name__ == "__main__":
try:
ausgabe, abschluss = main(sys.argv)
except SystemExit:
raise
except Exception as fehler:
sys.stderr.write("interner Fehler, kein Urteil: %r\n" % (fehler,))
raise SystemExit(10)
sys.stdout.write(ausgabe)
sys.exit(abschluss)
Fünf erfundene Eingabedateien und vier Läufe
Die folgenden fünf Dateien sind erfunden und enthalten keine Messwerte einer echten Website. Sie stehen vollständig hier, jede in einem eigenen Block ohne Beschriftungszeile, weil nur so jede Zahl der vier Läufe nachprüfbar ist. Gespeichert werden sie mit dem Zeilenumbruch am Ende, den ein Editor beim Sichern ohnehin schreibt – die Bytezahlen unten setzen ihn voraus. Zuerst die Header einer sauberen Seite, kopfzeilen-200.txt:
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=600
vary: Accept-Encoding
Dann die Fassung ohne Schrägstrich, mit zwei Sprüngen – kopfzeilen-301.txt:
HTTP/2 301
location: https://example.com/werkstatt/bremsbelaege-wechseln/
content-length: 0
HTTP/2 200
content-type: text/html; charset=utf-8
Die dritte trägt zwei Direktiven außerhalb des HTML, kopfzeilen-abweichung.txt:
HTTP/2 200
content-type: text/html; charset=utf-8
x-robots-tag: noindex, nofollow
link: <https://example.com/werkstatt/>; rel="canonical"
Dazu eine Ratgeberseite mit Canonical im HTML, dokument.html:
<!doctype html>
<html lang="de"><head>
<meta charset="utf-8">
<title>Bremsbeläge wechseln: Werkzeug, Reihenfolge, Drehmomente</title>
<link rel="canonical" href="https://example.com/werkstatt/bremsbelaege-wechseln/">
</head><body>
<h1>Bremsbeläge wechseln: Werkzeug, Reihenfolge, Drehmomente</h1>
<p>Der Wechsel der vorderen Bremsbeläge dauert an einem Rad etwa vierzig Minuten,
wenn das Werkzeug vollständig daliegt. Fehlt eine Rückstellzange, wird daraus ein
halber Tag, weil der Kolben sonst nicht in den Bremssattel zurückgeht.</p>
<p>Die Reihenfolge ist immer dieselbe: Fahrzeug sichern, Rad abnehmen, Führungsbolzen
lösen, Sattel aushängen und am Federbein abstützen, alte Beläge entnehmen, Sitzflächen
reinigen, Kolben zurückstellen, neue Beläge einlegen, Sattel anschrauben. Jeder Schritt
davor oder danach kostet Zeit, kein Schritt lässt sich überspringen.</p>
<p>Die Drehmomente stehen im Reparaturleitfaden des Herstellers und nirgends sonst
verbindlich. Wer sie schätzt, reißt ein Gewinde oder fährt mit einem lockeren Sattel.
Das Entlüften entfällt bei einem reinen Belagwechsel, solange keine Leitung geöffnet
wurde, und die ersten Bremsungen danach gehören auf eine leere Straße.</p>
</body></html>
Und eine leere Anwendungshülle, dokument-huelle.html:
<!doctype html>
<html lang="de"><head><meta charset="utf-8"><title>Werkstatt</title></head>
<body><div id="app"></div><script src="/bundle.js"></script></body></html>
Der erste Lauf ist der gute Fall: neun Zeilen OK, eine OFFEN ohne X-Robots-Tag, Exit-Code 0.
$ python3 brave-index-check.py 'https://example.com/werkstatt/bremsbelaege-wechseln/' \
kopfzeilen-200.txt dokument.html 1.07
URL: https://example.com/werkstatt/bremsbelaege-wechseln/
Phase 1 - die Antwort auf die geprüfte URL. Grenzwerte aus MERJs
Auswertung des WDP-Clients, Commit f25eb3d, Juni 2026.
[OK ] Antwort 200 die geprüfte URL antwortet mit 200
[OK ] keine Weiterleitung eine Antwort, kein Zwischenziel
[OK ] Antwortzeit <= 10 s 1,07 s gegen 10 s
Phase 2 - das Dokument dahinter. Dieselbe Quelle, dasselbe Datum.
[OK ] Dokument <= 2 MB 1238 von 2097152 Bytes
[OK ] kein noindex im HTML im HTML steht kein meta-robots-Element
[OK ] Query-String <= 22 0 Zeichen Query-String, Grenze 22
[OFFEN ] X-Robots-Tag der Header X-Robots-Tag fehlt
[OK ] Canonical-Quelle https://example.com/werkstatt/bremsbelaege-wechseln/
Phase 3 - zwei Zeilen von doctor-seo.net und NICHT von MERJ. Geprüft
werden der Inhaltstyp der Antwort und ein Mindestwert von 500 Zeichen
Text; für Text nennt MERJ keinen Wert, dieser ist eine Hausmarke.
[OK ] Content-Type text/html; charset=utf-8
[OK ] Text ohne JavaScript 937 Zeichen Text im HTML, eigener Mindestwert 500
BEHALTEN: keine Sperrzeile fällt aus, die URL ist aufnahmefähig.
Gespeichert wird sie erst, wenn einer der beiden Entdeckungswege eintritt.
# exit-code 0
Der zweite Lauf ist der lehrreiche. Dieselbe Dokumentdatei, nur die URL ohne Schrägstrich: zwei Sperrzeilen in Phase 1, und die übrigen acht drucken ENTFÄLLT statt einer Zahl. Was curl gespeichert hat, beschreibt hier das Ziel der Weiterleitung und keinen Körper, den der Client je anfordert.
$ python3 brave-index-check.py 'https://example.com/werkstatt/bremsbelaege-wechseln' \
kopfzeilen-301.txt dokument.html 1.07
URL: https://example.com/werkstatt/bremsbelaege-wechseln
Phase 1 - die Antwort auf die geprüfte URL. Grenzwerte aus MERJs
Auswertung des WDP-Clients, Commit f25eb3d, Juni 2026.
[SPERRE ] Antwort 200 erste Antwort 301, angenommen wird nur 200
[SPERRE ] keine Weiterleitung 301 nach https://example.com/werkstatt/bremsbelaege-wechseln/, der Client folgt nicht
[ENTFÄLLT ] Antwortzeit <= 10 s dieses Dokument wird nie angefordert
Phase 2 - das Dokument dahinter. Dieselbe Quelle, dasselbe Datum.
[ENTFÄLLT ] Dokument <= 2 MB dieses Dokument wird nie angefordert
[ENTFÄLLT ] kein noindex im HTML dieses Dokument wird nie angefordert
[ENTFÄLLT ] Query-String <= 22 dieses Dokument wird nie angefordert
[ENTFÄLLT ] X-Robots-Tag dieses Dokument wird nie angefordert
[ENTFÄLLT ] Canonical-Quelle dieses Dokument wird nie angefordert
Phase 3 - zwei Zeilen von doctor-seo.net und NICHT von MERJ. Geprüft
werden der Inhaltstyp der Antwort und ein Mindestwert von 500 Zeichen
Text; für Text nennt MERJ keinen Wert, dieser ist eine Hausmarke.
[ENTFÄLLT ] Content-Type dieses Dokument wird nie angefordert
[ENTFÄLLT ] Text ohne JavaScript dieses Dokument wird nie angefordert
VERWORFEN: 2 Sperrzeile(n) nicht bestanden, der Client verwirft
diese URL.
# exit-code 2
Der dritte Lauf nimmt eine Kampagnen-URL mit 58 Zeichen Query-String und besteht trotzdem, weil das HTML ein Canonical auf denselben Host nennt – die dokumentierte Ausnahme steht hier als Zeile in einer Ausgabe und nicht als Behauptung. Die beiden Header-Direktiven erzeugen zwei ABWEICHUNG-Zeilen und Exit-Code 3: aufnahmefähig, obwohl mit noindex ausgenommen.
$ python3 brave-index-check.py 'https://example.com/werkstatt/bremsbelaege-wechseln/?utm_source=newsletter&utm_medium=mail&utm_campaign=bremsen' \
kopfzeilen-abweichung.txt dokument.html 1.07
URL: https://example.com/werkstatt/bremsbelaege-wechseln/?utm_source=newsletter&utm_medium=mail&utm_campaign=bremsen
Phase 1 - die Antwort auf die geprüfte URL. Grenzwerte aus MERJs
Auswertung des WDP-Clients, Commit f25eb3d, Juni 2026.
[OK ] Antwort 200 die geprüfte URL antwortet mit 200
[OK ] keine Weiterleitung eine Antwort, kein Zwischenziel
[OK ] Antwortzeit <= 10 s 1,07 s gegen 10 s
Phase 2 - das Dokument dahinter. Dieselbe Quelle, dasselbe Datum.
[OK ] Dokument <= 2 MB 1238 von 2097152 Bytes
[OK ] kein noindex im HTML im HTML steht kein meta-robots-Element
[OK ] Query-String <= 22 58 Zeichen, aber das HTML nennt ein Canonical auf denselben Host: https://example.com/werkstatt/bremsbelaege-wechseln/
[ABWEICHUNG] X-Robots-Tag noindex nur im Header (noindex, nofollow), und Header liest dieser Client nicht
[ABWEICHUNG] Canonical-Quelle Link-Header nennt https://example.com/werkstatt/, das HTML nennt https://example.com/werkstatt/bremsbelaege-wechseln/ - gelesen wird das HTML
Phase 3 - zwei Zeilen von doctor-seo.net und NICHT von MERJ. Geprüft
werden der Inhaltstyp der Antwort und ein Mindestwert von 500 Zeichen
Text; für Text nennt MERJ keinen Wert, dieser ist eine Hausmarke.
[OK ] Content-Type text/html; charset=utf-8
[OK ] Text ohne JavaScript 937 Zeichen Text im HTML, eigener Mindestwert 500
BEHALTEN, MIT 2 ABWEICHUNG(EN): aufnahmefähig. Eine gesetzte Direktive
bleibt dabei ungelesen, die beabsichtigte Ausnahme greift hier also nicht.
# exit-code 3
Der vierte Lauf begründet Phase 3. Ein Dokument von 167 Bytes mit <div id="app"></div> und einem Skript-Tag besteht alle acht MERJ-Zeilen – sechsmal OK, zweimal OFFEN, keine Sperre. Erst die eigene Textzeile hält es auf.
$ python3 brave-index-check.py 'https://example.com/konfigurator/' \
kopfzeilen-200.txt dokument-huelle.html 1.07
URL: https://example.com/konfigurator/
Phase 1 - die Antwort auf die geprüfte URL. Grenzwerte aus MERJs
Auswertung des WDP-Clients, Commit f25eb3d, Juni 2026.
[OK ] Antwort 200 die geprüfte URL antwortet mit 200
[OK ] keine Weiterleitung eine Antwort, kein Zwischenziel
[OK ] Antwortzeit <= 10 s 1,07 s gegen 10 s
Phase 2 - das Dokument dahinter. Dieselbe Quelle, dasselbe Datum.
[OK ] Dokument <= 2 MB 167 von 2097152 Bytes
[OK ] kein noindex im HTML im HTML steht kein meta-robots-Element
[OK ] Query-String <= 22 0 Zeichen Query-String, Grenze 22
[OFFEN ] X-Robots-Tag der Header X-Robots-Tag fehlt
[OFFEN ] Canonical-Quelle im HTML steht kein Canonical
Phase 3 - zwei Zeilen von doctor-seo.net und NICHT von MERJ. Geprüft
werden der Inhaltstyp der Antwort und ein Mindestwert von 500 Zeichen
Text; für Text nennt MERJ keinen Wert, dieser ist eine Hausmarke.
[OK ] Content-Type text/html; charset=utf-8
[SPERRE ] Text ohne JavaScript Platzhalter ohne Inhalt: <div id="app"></div>
VERWORFEN: 1 Sperrzeile(n) nicht bestanden, der Client verwirft
diese URL.
# exit-code 2
Die Bedienfehler sind einzeln ausgeführt: zwei und fünf Parameter enden mit 4, ein fehlender Dateiname und ein Verzeichnis mit 5, eine Kopfdatei mit einem Byte außerhalb von UTF-8 mit 6, das Dokument an der Stelle der Kopfdatei mit 7, eine leere Datei mit 8 und schnell als vierter Parameter mit 9. Code 10 ist das Netz darunter und fiel in keinem Lauf. Ein abgebrochener Download wird nicht erkannt; dafür gibt es die Bytezahl in Phase 2. Eine ganze URL-Liste durch diese zehn Zeilen zu schicken ist Skriptarbeit mit Claude Code für technisches SEO.
Drei URLs dieser Website, am 25. September 2026 gemessen
Drei echte URLs sind ebenfalls durch die Aufnahmefilter des Web-Discovery-Project-Clients gelaufen, gemessen am 25. September 2026 auf doctor-seo.net – dem einzigen Host, über den dieser Kurs Zahlen aus erster Hand veröffentlichen kann. Die Stichprobe ist genau das: drei URLs auf einem Host, und eine Studie ist sie nicht.
| Geprüfte URL | Ergebnis | Die Zahl dazu |
|---|---|---|
| Die Level-8-Übersicht, mit Schrägstrich am Ende | besteht jeden Grenzwert | 200 ohne Sprung, 56.962 Bytes, 1,52 Sekunden, Canonical im HTML |
| Dieselbe URL ohne Schrägstrich am Ende | verworfen an der ersten Zeile | 301 auf die Fassung mit Schrägstrich |
| Das Kursverzeichnis mit zwei Kampagnenparametern | besteht trotz Überlänge | 35 Zeichen Query-String, gerettet vom Canonical auf denselben Host |
Der mittlere Fall ist der, den die meisten Websites vorfinden. Eine Normalisierung auf die Fassung mit Schrägstrich ist überall gute Praxis und kostet nur hier etwas – nicht, weil sie falsch wäre, sondern weil dieser Client ihr nicht folgt.
Die beiden anderen Grenzwerte sind weit gefasst: 56.962 Bytes sind weniger als ein Dreißigstel von 2 MB, und 1,52 Sekunden stehen gegen 10. Reißen kann beides eine Vorlage, die Daten in den Quelltext einbettet.
Was im Quellcode steht, und was deshalb noch nicht gemessen ist
Hinter jedem Grenzwert dieser Seite steht ein einziges Dokument: MERJs Auswertung des offenen Clients am Commit f25eb3d, veröffentlicht im Juni 2026. Eine Stichprobengröße steht dort nicht, und sie kann dort nicht stehen: erhoben wurde nichts, gelesen wurde ein Stand. Nachvollziehen kann das jeder, der denselben Commit öffnet – und nötig ist der Hinweis, weil dieselben Zahlen herumgehen, ohne dass jemand das tut.
Was dieser Stand des Clients tut, lässt sich aufzählen und im Commit nachschlagen: er nimmt allein eine 200er-Antwort an, folgt keinem Sprung, bricht bei 2 MB und bei 10 Sekunden ab, verwirft Query-Strings über 22 Zeichen ohne Canonical auf denselben Host, führt kein JavaScript aus und sucht noindex und Canonical allein im HTML. Nicht gesagt ist alles Übrige: wie viele echte URLs an welcher Bedingung scheitern, ob dieselben Bedingungen in Braves eigener Verarbeitung erneut auftauchen und welcher Stand in den Browsern der Teilnehmer installiert ist.
Was fehlt, ist die Gegenprobe. Niemand hat veröffentlicht, wie sich diese Bedingungen auf URLs auswirken, die tatsächlich in Braves Index stehen – und genau dort liegt die offene Frage dieses Themas. Ein einziger Fund würde sie bewegen: eine Seite, die in Brave auftaucht, obwohl sie an einer Zeile scheitert – in den Quellen dieser Lektion kommt so ein Fall nicht vor. Wie man das über Monate festhält, gehört zu Level 5 zu SEO-Analytics und Messung.
Für die eigene Dokumentation folgt daraus eine Regel: jeder übernommene Grenzwert bekommt den Commit und den Monat in dieselbe Zeile. Steht die Herkunft nicht daneben, wandert die Zahl weiter und die Bedingung, unter der sie galt, bleibt zurück.
Der Ratschlag, der keine einzige Zeile nennt
Im Netz steht zu diesem Thema in vielen Varianten derselbe Ratschlag – Claude nutzt Brave, also ranke in Brave –, und als Arbeitsanweisung ist er leer: er benennt keine Bedingung, an der eine URL tatsächlich scheitert. Diese Lektion benennt acht und sagt bei zwei weiteren, dass die Zahl dahinter von doctor-seo.net stammt.
Die Branche ist sich nicht einig, ob mit Generative Engine Optimization ein Fachgebiet eigener Art gemeint ist oder nur ein neues Wort für dieselbe Arbeit. Eine Antwort darauf steht hier nicht, und zwar nicht aus Vorsicht: diese Seite taugt für keine der beiden Lesarten als Beleg. Wer GEO als eigenes Fachgebiet führt, kann auf den Abrufweg zeigen, den sie beschreibt – ein eigener Index, eine eigene Software, eigene Grenzwerte. Wer darin nur ein Etikett sieht, zeigt auf die vierte Spalte der Filtertabelle: Statuscodes, Weiterleitungen, Header und Vorlagen sind seit Jahren dieselbe Arbeit, nur mit weniger Spielraum. Auf einen Durchgang, der verschiedene Antwortoberflächen nach demselben Verfahren prüft, kann keines der beiden Lager verweisen.
Hier steht am Ende nur, ob ein fertiges Dokument abgeholt, gelesen und gespeichert werden kann. Was ein Modell von der Arbeit davor übernehmen kann, behandelt Was Claude im SEO leisten kann – und was nicht.
Häufige Fehler
- Das Zugriffsprotokoll als Beleg für Brave lesen. ClaudeBot, Claude-User und Claude-SearchBot in deinen Logs belegen Abrufe von Anthropic und nicht, dass Brave deine Seite gespeichert hat. Die Korrektur: die zwei Mechanismen getrennt führen.
- Die Kampagnen-URL verteilen, bevor die Vorlage ein Canonical ausliefert. Jeder realistische Parametersatz reißt die 22 Zeichen, und gerettet wird er allein durch ein Canonical auf denselben Host im HTML. Die Korrektur: zuerst die Vorlagen finden, die keines ausgeben, dann den Newsletter verschicken.
- Ein Dokument freigeben, weil keine Sperre ausfällt. Der vierte Lauf oben bringt 167 Bytes ohne einen Satz Text durch alle acht MERJ-Zeilen. Die Korrektur: das Dokument danach selbst öffnen und den Absatz suchen, den ein Leser auf dem Bildschirm sieht.
- Die sieben Werte wie eine Schnittstellenzusage behandeln. Eine Zusage von Brave liegt in den Quellen dieser Lektion nicht vor; die Werte stehen in einem Stand des offenen Clients, den MERJ im Juni 2026 ausgewertet hat (
f25eb3d). Die Korrektur: sie wie den Messwert eines fremden Systems führen – mit Herkunft, mit Datum und mit einer Frist, nach der du nachsiehst.
Kurzfassung
- Geprüft wird nicht der Besuch eines Programmteilnehmers, sondern eine zweite Anfrage, die der Client des Web Discovery Project nach 1 bis 20 Minuten selbst stellt (MERJ, Commit
f25eb3d, Juni 2026). - Zwei Entdeckungswege: rund 20 Teilnehmer aus verschiedenen Netzen auf derselben Seite, Schwelle 20 Anteile im Verfahren STAR – oder ein einzelner Teilnehmer, der die Seite in einer Trefferliste von Google, Bing, Yahoo oder DuckDuckGo sieht.
- Sieben Bedingungen, nach dem Preis der Reparatur geordnet: keine Weiterleitung, nur HTTP 200, Query-Strings über 22 Zeichen nur mit Canonical auf denselben Host,
noindexund Canonical nur im HTML, 2 MB, 10 Sekunden, kein JavaScript. - Alle sieben stehen in einem Stand des offenen Clients, den MERJ ausgewertet hat; zugesagt hat Brave sie in keiner der hier verwendeten Quellen. Eine Stichprobengröße gibt es nicht, weil nichts erhoben wurde, und wie sich die Bedingungen auf tatsächlich indexierte URLs auswirken, hat niemand veröffentlicht.
- Die Aufnahmefilter setzen eine Mindestbedingung und keinen Maßstab: ein Dokument von 167 Bytes ohne Text besteht alle acht MERJ-Zeilen.
- Eine Direktive, die nur im HTTP-Header steht, kehrt die Richtung des Fehlers um – die URL bleibt aufnahmefähig, obwohl sie ausgenommen werden sollte.
- Eigene Messung vom 25. September 2026 über drei URLs von doctor-seo.net: die Level-8-Übersicht besteht mit 56.962 Bytes und 1,52 Sekunden, dieselbe URL ohne Schrägstrich fällt an einem 301 aus, und 35 Zeichen Query-String bestehen dank Canonical.
- Die sieben Bedingungen verteilen sich über fünf Zuständigkeiten – Linkpflege, Redaktion, Templates, Server-Konfiguration, Hosting –, und eine ist keine Reparatur, sondern eine Architekturfrage.
Häufige Fragen
Fällt eine Weiterleitung von http auf https auch darunter?
Nach MERJs Auswertung des Commits f25eb3d (Juni 2026) folgt der Client des Web Discovery Project keinem Sprung, unabhängig vom Grund: beurteilt wird die Antwort auf die URL, die der Teilnehmer im Suchergebnis gesehen hat. Eine Normalisierung von http auf https, von www auf die nackte Domain oder auf die Fassung mit Schrägstrich bleibt überall richtig und kostet nur hier etwas. Repariert wird sie dort, wo Links ausgespielt werden.
Muss ich für Brave etwas in meine robots.txt schreiben?
MERJs Auswertung des Clients sagt dazu nichts, und diese Lektion ergänzt es nicht. Wichtig ist die Unterscheidung: das Web Discovery Project entdeckt URLs über das Surfverhalten freiwillig teilnehmender Nutzer, während eine robots.txt-Regel einen über seinen User-Agent benannten Abrufer adressiert. Ob Websites KI-Crawler sperren sollten, ist in der Branche umstritten: die eine Seite hält das Zulassen für die einzige Möglichkeit, in Antwortoberflächen aufzutauchen, die andere sieht in Antworten ohne Besuche eine Wertübertragung. Eine Messung, die das entscheidet, liegt nicht vor, und dieser Kurs gibt dazu keine Empfehlung.
Wie oft muss ich diese Prüfung wiederholen?
Zu zwei Anlässen. Erstens nach jedem Eingriff an Headern, Weiterleitungen oder Vorlagen – ein Website-Relaunch, ein neues Caching, ein Wechsel der URL-Fassung –, denn dort entstehen die Befunde aus Phase 1. Zweitens, wenn eine Entscheidung über Templates oder Rendering an einem dieser Werte hängt: dann sieh im Commit nach, statt diese Seite zu zitieren. Wie häufig sich dieser Client ändert, ist nicht veröffentlicht; bekannt ist ein Stand, der von f25eb3d im Juni 2026.
Was bedeutet OFFEN, und warum ist das kein Bestehen?
OFFEN heißt, dass es nichts zu vergleichen gab. Fehlt der X-Robots-Tag-Header, findet diese Zeile keine Abweichung – bestätigt hat sie aber auch nichts, und deshalb trägt sie ein eigenes Wort und nicht OK. Dasselbe gilt für einen leer gesendeten Header, den das Skript im Hinweistext unterscheidet: ein leerer zeigt auf eine kaputte Konfiguration, ein fehlender auf eine Entscheidung. Im Bericht an andere trennst du deshalb beides: so viele Zeilen bestanden, so viele hatten keinen Vergleichswert.
Quellen
Einträge mit (ohne Link) haben im Quellenbestand dieses Kurses keine bestätigte URL und stehen mit Urheber und Datum.
- MERJ, Auswertung des offenen Clients des Brave Web Discovery Project, Commit
f25eb3d– Juni 2026. Ohne Stichprobengröße: ein Artefakt, ein Commit, ein Analyst, keine unabhängige Wiederholung. (ohne Link) - Anthropic, Trust Center: Brave Search als Subunternehmer seit dem 19. März 2025, turbopuffer seit dem 6. Mai 2026 – erfasst von Xponent21, erneut geprüft am 2. Juli 2026. (ohne Link)
- Anthropic, Hilfeartikel zum eigenen Crawling – ClaudeBot, Claude-User und Claude-SearchBot. Die Seite zeigt nur eine relative Zeitangabe, deshalb steht hier kein absolutes Datum.
- Eigene Messung auf doctor-seo.net – 25. September 2026, drei URLs, Header und Quelltext mit
curlgeholt. Veröffentlicht in der spanischen und der englischen Ausgabe dieser Lektion; über Sprachgrenzen hinweg verlinkt dieser Kurs nicht. brave-index-check.py, die fünf erfundenen Eingabedateien und die vier Läufe stehen vollständig oben: nur Standardbibliothek, kein Netzzugriff, zehn belegte Exit-Codes. Ein Datum tragen sie nicht, weil die Ausgabe nur an den Dateien hängt.
Weiter im Kurs
Dieses Level erscheint Lektion für Lektion, und die Übersicht zeigt, was schon online ist. Vorher erschienen sind Was Claude im SEO leisten kann – und was nicht, Claude Skills für SEO: installieren, nutzen, selbst schreiben und Claude Code für technisches SEO.
- Level: SEO mit KI: KI als Arbeitswerkzeug
- Kursverzeichnis: Der kostenlose SEO-Kurs von doctor-seo.net