Making-of: Mein SEO- und GEO-Tool

Wie aus 25 Jahren SEO-Praxis ein schnelles, kostenloses SEO/GEO-Analyse-Tool wurde — mit echten Code-Beispielen, wie Score und Checks tatsächlich rechnen.

von ·

Ich wollte schon immer ein SEO-Tool, das schnell und ohne Umwege das Wesentliche auf den Punkt bringt — keine Abo-Falle, kein Login, keine zwölf Dashboards, durch die ich mich klicke, um eine simple Frage zu beantworten: Ist diese Seite technisch sauber, und wird sie von klassischen Suchmaschinen und von KI-Systemen verstanden? Also habe ich es gebaut. Dieser Artikel ist das Making-of: die Beweggründe, die Architektur und vor allem die echte Rechen-Logik dahinter — mit Code, den du eins zu eins übernehmen kannst. Das fertige Werkzeug läuft kostenlos unter jpkc.com/tools/seo/.

Aktualisiert im Oktober 2026: Das Tool ist seit Juni gewachsen, und ein Abgleich mit dem aktuellen Code hat ein paar Stellen dieses Artikels überholt. Die SSRF-Prüfung zeigt jetzt die gehärtete Fassung mit IPv6-Auflösung, Port-Allowlist und gepinnter IP gegen DNS-Rebinding — die erste Fassung des Snippets hätte ich so nicht mehr zum Übernehmen empfohlen. Die AI-Crawler-Liste ist auf dem heutigen Stand von 16 Bots, die Lesbarkeitsformel nennt alle sechs Sprachvarianten, und „Was bewusst fehlt" und die FAQ berücksichtigen den lokalen Verlauf und die Sammelprüfungen im Expert Mode.

Warum überhaupt ein eigenes Tool

Nach gut 25 Jahren im Web kenne ich die SEO-Suiten alle. Sie können viel, kosten monatlich und verstecken die zwei, drei Zahlen, die wirklich zählen, hinter Funktionsbergen, die ich zu 90 % nie brauche. Für einen schnellen Blick auf eine einzelne URL — „passt das Fundament?" — ist das, als würde ich einen Bagger mieten, um ein Loch für eine Tomatenpflanze zu graben.

Was mir fehlte, war ein Werkzeug mit drei Eigenschaften:

  • Sofort und ohne Konto. URL rein, Analyse raus. Keine Registrierung, kein Tracking, kein „14 Tage gratis, dann 99 €/Monat".
  • Ehrlich gewichtet. Nicht 200 Mikro-Signale gleich laut, sondern die Dinge vorne, die ich aus Erfahrung als ranking- und zitierrelevant einschätze.
  • SEO und GEO. Klassische Suchmaschinen-Optimierung und Generative Engine Optimization in einem Durchlauf — denn 2026 entscheidet beides über Sichtbarkeit. Was GEO überhaupt ist, habe ich in Was ist GEO? ausführlich beschrieben.

Der entscheidende Vorteil von „selbst bauen": Ich lege offen, wie gerechnet wird. Bei den großen Tools ist der Score eine Blackbox. Hier ist jeder Punkt nachvollziehbar — und genau das zeige ich dir jetzt.

Die Architektur: Proxy plus Browser

Die erste Grundsatzentscheidung war technischer Natur. Eine SEO-Analyse muss den rohen HTTP-Verkehr einer Seite sehen: Status-Code, Redirect-Kette, Response-Header, SSL-Zertifikat, Timing. Aus dem Browser heraus geht das nicht — die Same-Origin-Policy und CORS verbieten es, fremde Seiten samt Header einfach zu laden. Also brauchte es einen serverseitigen Proxy in PHP, der die Zielseite holt und das Ergebnis an das Frontend zurückgibt.

Ein Proxy, der beliebige URLs abruft, ist allerdings ein klassisches SSRF-Risiko (Server-Side Request Forgery): Jemand könnte http://127.0.0.1/ oder interne Cloud-Metadaten-Endpunkte anfragen und so an Dinge kommen, die nie öffentlich sein sollten. Deshalb prüft der Proxy jede URL — auch jeden einzelnen Redirect-Hop —, bevor er sie anfasst, und gibt die geprüfte Zieladresse gleich mit zurück:

php
// Nur Web-Ports — der Proxy soll kein Port-Scanner für fremde Hosts sein
const PROXY_ALLOWED_PORTS = [ 80, 443, 3000, 5000, 8000, 8008, 8080, 8443 ];

function resolveProxyTarget( string $url, ?string &$error = null ): ?array {

    if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
        $error = 'Invalid URL format';
        return null;
    }

    // Nur http/https — kein file://, gopher://, dict:// etc.
    if ( ! str_starts_with( $url, 'http://' ) && ! str_starts_with( $url, 'https://' ) ) {
        $error = 'Only http/https URLs are allowed';
        return null;
    }

    $parsed = parse_url( $url );
    if ( empty( $parsed['host'] ) ) {
        $error = 'Missing host in URL';
        return null;
    }

    $port = $parsed['port'] ?? ( $parsed['scheme'] === 'https' ? 443 : 80 );
    if ( ! in_array( $port, PROXY_ALLOWED_PORTS, true ) ) {
        $error = 'Port ' . $port . ' is not allowed';
        return null;
    }

    $bareHost = trim( $parsed['host'], '[]' );   // IPv6 kommt in [Klammern]

    // IP-Literal: direkt prüfen, es gibt nichts aufzulösen
    if ( filter_var( $bareHost, FILTER_VALIDATE_IP ) !== false ) {
        if ( ! isPublicIpAddr( $bareHost ) ) {
            $error = 'IP address is not publicly routable';
            return null;
        }
        return [ 'host' => $bareHost, 'port' => $port, 'ip' => $bareHost, 'literal' => true ];
    }

    // Hostname: IPv4 (A) UND IPv6 (AAAA) auflösen
    $ips = @gethostbynamel( $bareHost ) ?: [];
    foreach ( @dns_get_record( $bareHost, DNS_AAAA ) ?: [] as $rec ) {
        if ( isset( $rec['ipv6'] ) ) {
            $ips[] = $rec['ipv6'];
        }
    }
    $ips = array_values( array_unique( $ips ) );

    if ( ! $ips ) {
        $error = 'Could not resolve host';
        return null;
    }

    // JEDE Adresse muss öffentlich sein — eine einzige private Antwort reicht für die Sperre
    foreach ( $ips as $ip ) {
        if ( ! isPublicIpAddr( $ip ) ) {       // blockt 127.0.0.0/8, 10/8, 192.168/16, ::1 …
            $error = 'Host resolves to a private/reserved address';
            return null;
        }
    }

    return [ 'host' => $bareHost, 'port' => $port, 'ip' => $ips[0], 'literal' => false ];
}

Der wichtige Teil ist nicht der Happy Path. Die Hostname-Auflösung passiert vor dem Request, und zwar für IPv4 und IPv6 — wer nur den A-Eintrag prüft, lässt einen Host durch, dessen AAAA-Eintrag auf ::1 zeigt. Geprüft werden alle aufgelösten IPs, sonst umgeht ein DNS-Eintrag, der zusätzlich auf 10.0.0.5 zeigt, die Sperre. isPublicIpAddr() nutzt PHPs Filter FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE und sperrt zusätzlich die Bereiche, die diese Flags übersehen, etwa CGNAT (100.64.0.0/10), NAT64 und 6to4.

Das allein reicht aber nicht, und das hat die erste Fassung dieses Artikels unterschlagen: Zwischen Prüfung und Verbindung löst cURL den Namen sonst ein zweites Mal selbst auf. Ein Angreifer mit eigenem DNS-Server antwortet bei der Prüfung mit einer öffentlichen IP und eine Sekunde später mit 127.0.0.1 — das klassische DNS-Rebinding. Deshalb bekommt cURL die geprüfte Adresse fest vorgegeben:

php
$target = resolveProxyTarget( $url, $error );
if ( $target === null ) {
    // abbrechen und $error melden
}

$ch = curl_init( $url );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, false );   // Redirects selbst prüfen, siehe unten

if ( ! $target['literal'] ) {
    // libcurl erwartet IPv6-Adressen in [Klammern]
    $ip = str_contains( $target['ip'], ':' ) ? '[' . $target['ip'] . ']' : $target['ip'];
    curl_setopt( $ch, CURLOPT_RESOLVE, [ $target['host'] . ':' . $target['port'] . ':' . $ip ] );
}

Mit CURLOPT_RESOLVE verbindet sich cURL genau mit der Adresse, die eben geprüft wurde. Die Redirect-Kette verfolgt der Proxy selbst, Hop für Hop, und jeder Hop durchläuft wieder dieselbe Prüfung samt Pin — denn eine harmlose öffentliche URL darf nicht über einen 302-Redirect auf etwas Internes zeigen.

Alles, was danach kommt — das HTML parsen, Überschriften zählen, Schema lesen — passiert dann wieder client-seitig im Browser über den nativen DOMParser. Das hält den Server schlank: Er holt nur Bytes, die eigentliche Analyse-Engine ist JavaScript. Diese Aufteilung — minimaler, abgesicherter PHP-Proxy plus dicke JS-Analyse — ist das Rückgrat des ganzen Tools.

Das Grundprinzip: ein Check ist nur vier Werte

Bevor ein einziger SEO-Wert berechnet wird, brauchte ich eine Form, in die jeder Check passt — vom HTTPS-Häkchen bis zur Lesbarkeit. Ich habe mich auf vier Werte festgelegt, und das ist vielleicht die wichtigste Designentscheidung im ganzen Tool, weil sie alles andere einfach hält:

// Ein Check produziert immer: erreichte Punkte, ein Status, ein Hinweis.
function scoreCheck( name, maxPoints, earned, status, hint ) {
	return { name, maxPoints, earned, status, hint: hint || "" };
}
// status ∈ { "pass", "partial", "fail" }

status ist bewusst dreiwertig, nicht binär. Die halbe Stufe partial ist der Grund, warum der Score sich „fair" anfühlt: Eine Seite mit einem 65 Zeichen langen Title hat nicht „versagt", sie ist nur nicht optimal — also gibt es Teilpunkte, nicht null. Genau das machen die meisten Pass/Fail-Checker falsch.

Mit diesem Baustein wird jeder einzelne Check zu einer kleinen, isolierten Funktion, die ich unabhängig verstehen und testen kann. Vier davon zeige ich dir jetzt im Detail.

SEO-Score: vier Beispiele aus der Praxis

Der SEO-Score besteht aus bis zu 36 solcher Checks in sieben Kategorien (Technical, On-Page, Crawling, Social, Performance, Accessibility, Content). Hier sind vier, die das Prinzip gut zeigen.

Title-Länge: Teilpunkte statt Schwarz-Weiß

Der Seitentitel ist eines der stärksten On-Page-Signale. Zu kurz verschenkt Kontext, zu lang wird in der Suche abgeschnitten. Ich bewerte in drei Stufen — optimal, akzeptabel, daneben:

js
function checkTitleLength( title ) {
	const len = title ? title.length : 0;

	const optimal    = len >= 50 && len <= 60;   // ideale Anzeigelänge
	const acceptable = len >= 30 && len <= 70;   // brauchbar, aber nicht ideal

	return scoreCheck(
		"Title (50–60 Zeichen)",
		8,
		optimal ? 8 : acceptable ? 4 : 0,
		optimal ? "pass" : acceptable ? "partial" : "fail",
		title ? `${len} Zeichen` : "Kein <title>-Tag gefunden"
	);
}
Title-Länge Status Punkte (von 8)
50–60 Zeichen pass 8
30–70 Zeichen partial 4
alles andere / fehlt fail 0

Acht Punkte für den Title, weil er real viel zur Klickrate beiträgt. Das Gewicht ist eine Erfahrungsentscheidung, keine universelle Wahrheit — aber genau solche Entscheidungen wollte ich selbst treffen statt sie einem fremden Algorithmus zu überlassen.

TTFB: gestufte Bewertung statt eines Schwellwerts

Bei der Time to First Byte zeigt sich derselbe Gedanke noch deutlicher. Ein einzelner Grenzwert wäre brutal — 199 ms voll, 201 ms nichts. Stattdessen gibt es zwei Stufen:

js
function checkTtfb( ttfbMs ) {
	if ( ttfbMs === null ) {
		return scoreCheck( "TTFB < 500 ms", 4, 0, "fail", "Keine Timing-Daten" );
	}

	const earned = ttfbMs < 200 ? 4 : ttfbMs < 500 ? 2 : 0;
	const status = ttfbMs < 200 ? "pass" : ttfbMs < 500 ? "partial" : "fail";

	return scoreCheck( "TTFB < 500 ms", 4, earned, status, `${ttfbMs} ms` );
}

Unter 200 ms ist exzellent (volle 4 Punkte), unter 500 ms noch in Ordnung (2 Punkte), darüber zu langsam. Warum TTFB überhaupt zählt und wie er mit den Core Web Vitals zusammenhängt, steht in Core Web Vitals & Performance.

Security-Header: zählen statt einzeln prüfen

Manche Checks sind eine Mengenfrage. Bei den Security-Headern interessiert weniger, welcher genau fehlt, sondern ob die Seite das Thema grundsätzlich ernst nimmt. Also zähle ich, wie viele aus einer Liste relevanter Header gesetzt sind:

js
const SECURITY_HEADERS = [
	"Strict-Transport-Security", "Content-Security-Policy",
	"X-Frame-Options", "X-Content-Type-Options",
	"Referrer-Policy", "Permissions-Policy",
	"Cross-Origin-Opener-Policy", "Cross-Origin-Resource-Policy",
];

function checkSecurityHeaders( headers ) {
	const present = SECURITY_HEADERS.filter( h => Boolean( headers[ h ] ) ).length;

	const earned = present >= 4 ? 3 : present >= 2 ? 1 : 0;
	const status = present >= 4 ? "pass" : present >= 2 ? "partial" : "fail";

	return scoreCheck( "Security-Header ≥ 4", 3, earned, status,
		`${present}/8 Security-Header vorhanden` );
}

Vier oder mehr von acht: voller Punkt. Zwei bis drei: halbe Anerkennung. Darunter: Nachholbedarf. Diese „zähl die guten Signale"-Logik taucht im Tool an mehreren Stellen auf — bei Open-Graph-Tags, bei semantischen HTML-Elementen, bei den AI-Crawlern.

Lesbarkeit: die sprachabhängige Flesch-Formel

Das ist mein liebster Teil, weil hier echte Sprachwissenschaft im Code steckt. Lesbarkeit lässt sich messen — über den Flesch Reading Ease. Die Formel setzt die durchschnittliche Satzlänge und die durchschnittliche Silbenzahl pro Wort ins Verhältnis. Der Haken: Die Konstanten gelten nur fürs Englische. Deutsche Wörter sind länger und silbenreicher; mit der englischen Formel sähe jeder deutsche Text künstlich „schwer" aus.

Deshalb erkennt das Tool die Sprache aus <html lang> und rechnet mit der passenden Variante. Fürs Deutsche ist das die Amstad-Formel:

js
// Durchschnittliche Satzlänge (ASL) und Silben pro Wort (ASW)
const asl = wordCount / Math.max( 1, sentenceCount );
const asw = syllableCount / Math.max( 1, wordCount );

let flesch;
if ( lang === "de" ) {
	flesch = 180 - asl - 58.5 * asw;            // Amstad (Deutsch)
} else if ( lang === "fr" ) {
	flesch = 207 - 1.015 * asl - 73.6 * asw;    // Kandel-Moles (Französisch)
} else if ( lang === "es" ) {
	flesch = 206.84 - 1.02 * asl - 60.0 * asw;  // Fernández Huerta (Spanisch)
} else if ( lang === "it" ) {
	flesch = 217 - 1.3 * asl - 60.0 * asw;      // Franchina-Vacca (Italienisch)
} else if ( lang === "nl" ) {
	flesch = 206.835 - 0.93 * asl - 77.0 * asw; // Douma (Niederländisch)
} else {
	flesch = 206.835 - 1.015 * asl - 84.6 * asw; // Original-Flesch (Englisch und alle übrigen)
}

const score = Math.round( Math.min( 100, Math.max( 0, flesch ) ) );

Das Silbenzählen ist seinerseits sprachabhängig. Für deutsche Wörter werden Umlaute und ß erst phonetisch ersetzt, dann Vokalgruppen gezählt:

js
function countSyllablesDE( word ) {
	word = word.toLowerCase();
	if ( word.length <= 3 ) return 1;

	// Umlaute/ß phonetisch auflösen, damit Vokalgruppen stimmen
	word = word.replace( /ä/g, "ae" ).replace( /ö/g, "oe" )
		.replace( /ü/g, "ue" ).replace( /ß/g, "ss" )
		.replace( /[^a-z]/g, "" );

	const groups = word.match( /[aeiouy]{1,2}/g );   // zusammenhängende Vokale = grob eine Silbe
	return groups ? Math.max( 1, groups.length ) : 1;
}

Das ist eine Heuristik, kein perfekter Silbentrenner — aber für eine relative Lesbarkeits-Einordnung über einen ganzen Text völlig ausreichend. Der resultierende Wert wird in Stufen übersetzt (90+ „Sehr leicht", 60–69 „Standard", unter 30 „Sehr schwer") und fließt mit zwei Punkten in den Content-Score ein. Warum verständliches Schreiben gerade für KI-Systeme zählt, vertiefe ich in Schreiben für KI.

Im selben Schritt entstehen nebenbei noch die Top-Keywords mit Dichte — nach Abzug von Stoppwörtern in mehreren Sprachen:

js
const freq = {};
for ( const w of words ) {
	const lower = w.toLowerCase();
	if ( lower.length >= 3 && ! STOPWORDS[ lower ] ) {
		freq[ lower ] = ( freq[ lower ] || 0 ) + 1;
	}
}

const topKeywords = Object.keys( freq )
	.sort( ( a, b ) => freq[ b ] - freq[ a ] )
	.map( word => ( {
		word,
		count: freq[ word ],
		density: Math.round( freq[ word ] / wordCount * 1000 ) / 10,   // Prozent, 1 Nachkommastelle
	} ) );

Vom Check zum Score: die Summe

Sind alle Checks gelaufen, ist der Gesamt-Score trivial — und das ist der Punkt. Weil jeder Check seine maxPoints und earned selbst kennt, ist die Aggregation eine einzige Schleife:

js
function totalScore( checks ) {
	let earned = 0, max = 0;
	for ( const c of checks ) {
		earned += c.earned;
		max    += c.maxPoints;
	}
	return Math.round( earned / max * 100 );   // 0–100
}

Daraus wird zuletzt ein Schulnoten-Grade: A ab 90, B ab 80, C ab 60, D ab 40, sonst F. Keine Magie, keine geheime Gewichtung im Hintergrund — die ganze „Intelligenz" steckt in den einzelnen, lesbaren Check-Funktionen. Genau so wollte ich es: nachvollziehbar bis auf den einzelnen Punkt.

GEO-Score: warum ein zweiter, unabhängiger Wert

Hier wird es interessant, denn das ist der eigentliche Grund, warum ich das Tool 2026 noch einmal angefasst habe. Klassisches SEO sagt dir, ob Google deine Seite mag. Aber ob Claude, ChatGPT oder Perplexity deinen Inhalt sauber lesen, zuordnen und zitieren können, ist eine andere Frage — mit anderen Signalen. Deshalb gibt es einen zweiten, völlig eigenständigen GEO-Score, der den SEO-Score nie beeinflusst und umgekehrt. Eine Seite kann technisch top und KI-blind sein, oder umgekehrt.

Der GEO-Score nutzt denselben scoreCheck-Baustein, prüft aber andere Dinge.

Schema-Vielfalt: rekursiv durch den @graph

Strukturierte Daten sind für KI-Zitate Gold (Hintergrund: Structured Data & Technical GEO). Es reicht aber nicht, ob JSON-LD da ist — interessant ist, wie viele verschiedene Typen ausgezeichnet sind. Moderne Seiten verschachteln das in einem @graph, deshalb sammle ich die @type-Werte rekursiv ein:

js
function collectSchemaTypes( node, types = new Set() ) {
	if ( ! node || typeof node !== "object" ) return types;

	const t = node[ "@type" ];
	if ( Array.isArray( t ) ) t.forEach( x => types.add( x ) );
	else if ( t ) types.add( t );

	for ( const key of Object.keys( node ) ) {
		if ( key === "@context" ) continue;
		const val = node[ key ];
		if ( val && typeof val === "object" ) collectSchemaTypes( val, types );  // tiefer graben
	}
	return types;
}

function checkSchemaVariety( jsonLdBlocks ) {
	const types = new Set();
	jsonLdBlocks.forEach( block => collectSchemaTypes( block, types ) );
	const n = types.size;

	return scoreCheck( "Schema-Vielfalt", 3,
		n >= 3 ? 3 : n >= 2 ? 2 : n >= 1 ? 1 : 0,
		n >= 3 ? "pass" : n >= 1 ? "partial" : "fail",
		n > 0 ? `${n} Typ(en): ${[ ...types ].slice( 0, 5 ).join( ", " )}` : "Keine Schema-Typen" );
}

Drei oder mehr Typen — etwa Article plus Person plus BreadcrumbList — sprechen für eine bewusst maschinenlesbar ausgezeichnete Seite. Genau das honoriert der Check.

llms.txt und die AI-Crawler

Zwei GEO-Checks drehen sich um die Frage, ob du KI-Systeme überhaupt hereinlässt. Das Tool holt automatisch die llms.txt aus dem Domain-Root (die Markdown-Datei, die einer KI Zweck und Struktur der Seite erklärt) und prüft die robots.txt gezielt gegen 16 bekannte KI-Crawler, und zwar für jeden einzeln — User-agent: * greift nur für einen Bot, der keine eigene Gruppe hat:

js
const AI_CRAWLERS = [
	// Training
	"GPTBot", "ClaudeBot", "Google-Extended", "Applebot-Extended",
	"meta-externalagent", "CCBot", "Bytespider", "Amazonbot",
	// KI-Suche
	"OAI-SearchBot", "Claude-SearchBot", "PerplexityBot", "DuckAssistBot",
	// Abruf im Auftrag eines Nutzers
	"ChatGPT-User", "Claude-User", "Perplexity-User", "MistralAI-User",
];

function checkAiCrawlers( robotsTxt, pageUrl ) {
	const blocked = AI_CRAWLERS.filter(
		bot => ! isAllowedForAgent( robotsTxt, pageUrl, bot )
	);
	const n = blocked.length;

	return scoreCheck( "AI-Crawler erlaubt", 3,
		n === 0 ? 3 : n <= 3 ? 2 : n <= 6 ? 1 : 0,
		n === 0 ? "pass" : n <= 3 ? "partial" : "fail",
		n ? `${n}/${AI_CRAWLERS.length} blockiert: ${blocked.slice( 0, 3 ).join( ", " )}${n > 3 ? "…" : ""}`
			: `Alle ${AI_CRAWLERS.length} großen AI-Crawler erlaubt` );
}

In der ersten Fassung dieses Artikels standen hier acht Bots. Inzwischen trennt die Liste nach Zweck, anthropic-ai ist nicht mehr dabei, und Anthropic kommt dafür mit drei Bots vor: ClaudeBot fürs Training, Claude-SearchBot für die Suche und Claude-User für Abrufe im Auftrag eines Nutzers. Die Punkt-Schwellen sind gleich geblieben, wirken bei 16 Bots aber strenger. Korrigiert habe ich auch den Status: Ab vier blockierten Bots meldet der Check „fail", obwohl es bis sechs noch einen Punkt gibt — das alte Snippet zeigte hier fälschlich „partial". Und der Zähler fragt nicht nach dem Zweck: Wer nur die acht Trainings-Crawler aussperrt und Suche und Nutzer-Abrufe zulässt, landet trotzdem bei 0 von 3 Punkten.

Das ist ein bewusst neutraler Check: Er bewertet das Zulassen positiv, weil das Tool aus der Perspektive „ich will in KI-Antworten vorkommen" gebaut ist. Wer Inhalte vor dem Training schützen will, liest hier trotzdem schwarz auf weiß, welche Bots er gerade aussperrt — und das ist genauso wertvoll.

Kurze Absätze: was LLMs lieber lesen

Ein letztes, kleines, aber sprechendes Beispiel: KI-Systeme extrahieren aus kurzen, fokussierten Absätzen zuverlässiger als aus Textwänden. Also misst das Tool die durchschnittliche Absatzlänge:

js
function checkParagraphLength( wordCount, paragraphCount ) {
	if ( ! paragraphCount ) {
		return scoreCheck( "Kurze Absätze (Ø < 150 Wörter)", 2, 0, "fail", "Keine Absätze erkannt" );
	}
	const avg = Math.round( wordCount / paragraphCount );

	return scoreCheck( "Kurze Absätze (Ø < 150 Wörter)", 2,
		avg < 150 ? 2 : avg < 250 ? 1 : 0,
		avg < 150 ? "pass" : avg < 250 ? "partial" : "fail",
		`${paragraphCount} Absätze, Ø ${avg} Wörter` );
}

Unter 150 Wörtern pro Absatz: voller Punkt. Das ist dieselbe Empfehlung, die ich in Schreiben für KI gebe — hier nur als messbare Zahl gegossen.

Was bewusst fehlt

Genauso wichtig wie das, was drin ist, ist das, was ich weggelassen habe. Es gibt kein Konto, kein Tracking, kein dauerhaftes Crawl-Monitoring über tausende URLs, keine Rank-Tracking-Historie. Das ist Absicht, nicht Faulheit. Das Tool beantwortet eine Frage — „ist diese Seite sauber für Suche und KI?" — und zwar in Sekunden. Im Expert Mode prüft es auf Wunsch inzwischen auch den Status aller URLs aus den Sitemaps und die strukturierten Daten der intern verlinkten Seiten. Das sind aber einmalige Läufe auf Knopfdruck, kein Monitoring. Alles, was diese Frage nicht direkt bedient, hätte es nur langsamer, komplizierter und erklärungsbedürftiger gemacht. YAGNI als Feature: Der Verzicht ist das Produkt.

FAQ

Kostet das Tool etwas?

Nein. Der SEO- & GEO-Analyzer ist kostenlos und ohne Konto nutzbar. Es gibt keine Registrierung, kein Abo und kein Tracking. URL eingeben, Analyse ablesen — das war von Anfang an der ganze Sinn der Übung.

Was ist der Unterschied zwischen dem SEO- und dem GEO-Score?

Der SEO-Score bewertet die klassische Suchmaschinen-Tauglichkeit: HTTPS, Title, Meta-Description, Überschriften, Ladezeit, Security-Header und Ähnliches. Der GEO-Score bewertet die KI-Tauglichkeit: strukturierte Daten, llms.txt, ob KI-Crawler zugelassen sind, saubere Heading-Hierarchie und kurze Absätze. Beide Werte sind unabhängig — eine Seite kann beim einen glänzen und beim anderen durchfallen.

Wie genau ist der Score?

Jeder Punkt ist nachvollziehbar an eine konkrete, geprüfte Regel gebunden — keine Blackbox. Schwellen wie „Title 50–60 Zeichen" oder „TTFB < 200 ms" sind etablierte Best Practices, die Gewichtung beruht auf 25 Jahren Praxis. Der Score ist eine fundierte Einordnung, kein Naturgesetz: Er zeigt zuverlässig, wo eine Seite steht und was als Nächstes dran ist — er ersetzt aber kein redaktionelles Urteil über den Inhalt selbst.

Warum kein Login und kein Verlauf auf dem Server?

Weil der Sinn des Tools Geschwindigkeit und Ehrlichkeit ist. Ein Login wäre eine Hürde vor der eigentlichen Aufgabe, und ein Verlauf auf dem Server hätte Tracking und Datenhaltung bedeutet — beides wollte ich ausdrücklich nicht. Was es gibt, bleibt in deinem Browser: Das URL-Feld merkt sich die letzten zehn analysierten URLs im localStorage, Clear history löscht sie wieder. Wer eine Analyse behalten will, exportiert sie als JSON oder als HTML-Bericht.

Weiterlesen

Den Rahmen für die KI-Seite liefert Was ist GEO?; die technische Tiefe dazu steht in Structured Data & Technical GEO. Wie du Inhalte schreibst, die Suche und KI gleichermaßen mögen, klärt Schreiben für KI, und das Performance-Fundament behandelt Core Web Vitals & Performance. Und am schnellsten verstehst du das Tool, indem du eine eigene URL durchlaufen lässt: jpkc.com/tools/seo/.