fix(seo): Seitenstruktur von jeder Seite aus erreichbar, keine Duplikate mehr #95

Merged
Phil merged 5 commits from fix/indexierbare-navigation into main 2026-09-07 08:59:14 +00:00
Owner

Closes #82

Worum es geht

Google kennt 4 von 22 Seiten und keinen einzigen Blogartikel. Drei Ursachen,
alle behoben.

1. Die Navigation war auf 20 von 22 Seiten tot

„Über mich", „Branchen" und „Angebot" wurden als ./#angebot ausgeliefert. Auf
/blog löst das zu /blog#angebot auf, wo es kein solches Element gibt. Für
einen Crawler führte von jeder Unterseite kein Weg zurück in die Seitenstruktur.

Anker zeigen jetzt absolut auf die Startseite der jeweiligen Sprache. Nebenbei
warf der Sprung-Handler bei jedem Klick auf einen dieser Links einen Fehler, weil
er den href-String als CSS-Selektor benutzte. Auch das ist an der Wurzel behoben.

2. Die Paginierung war ein unendlicher Duplikat-Trichter

/blog/p2, /blog/p9 und /blog?page=2 lieferten alle 200 mit wortgleichem
Inhalt und einem Canonical auf sich selbst. Jetzt: 404 für Seitenzahlen, die es
nicht gibt, und ein Canonical, das aus der Site aufgelöst wird statt aus dem
Request.

Die Sperre steht vor dem Layout. Im Inhaltsblock hätte der Kopfbereich
bereits Cache-Control: public, max-age=3600 gesendet und der 404 wäre eine
Stunde lang öffentlich zwischengespeichert worden. Gemessen: jetzt no-store.

Zusätzlich hängt ether/seo unbedingt einen HTTP-Kopf Link: rel="canonical"
mit seinem eigenen, ungefilterten Wert an. Der widersprach dem HTML auf jeder
/pN-Adresse. Er wird jetzt überschrieben.

3. Es gab praktisch keine interne Verlinkung

Vier der sechs Artikel hingen an keiner Kategorie. Deshalb listete das Archiv
2 von 6, und der bereits vorhandene „Verwandte Artikel"-Block blieb bei ihnen
leer: er sucht über die Kategorie.

Die Anti-Slop-Reihe verlinkt sich jetzt selbst, über einen gemeinsamen Tag im
vorhandenen Feld. Kein neues Feld, künftige Reihen funktionieren ohne
Schema-Änderung.

Ein Fund nebenbei, der nicht im Issue stand

Die englische Sitemap existiert unter /en/sitemap.xml und listet alle sechs
englischen Artikel. Die robots.txt nannte nur die deutsche. Google erfuhr von
der englischen Seite also nie. Die Datei liegt jetzt im Repo statt in einem
Datenbankfeld.

Gemessen, nicht angenommen

Lokal gegen http://localhost:8090, nach clear-caches/all:

  • /blog/p2, /blog/p9, /blog/category/…/p2 → 404 mit Cache-Control: no-store
  • /blog?page=2 → 200, Canonical auf /blog, HTTP-Kopf identisch
  • /, /blog, /en/, /en/blog, Artikel, Kategorie → weiterhin 200
  • Header- und Footer-Links auf /blog bitidentisch mit denen auf /, keine
    doppelten Schrägstriche
  • Kategorie-Archiv listet jetzt alle sechs Artikel
  • Serien-Navigation erscheint auf den vier Anti-Slop-Artikeln in beiden Sprachen
    und verlinkt in der englischen Fassung die englischen Slugs; auf Artikeln ohne
    Reihe erscheint gar nichts
  • Migration idempotent, ids() liefert int, strikter Vergleich trägt

Ein unabhängiger Prüfagent mit ausschließlich lesenden Werkzeugen hat den Stand
gegengelesen und zwei Befunde gemeldet, beide oben behoben und nachgemessen.

Nicht geprüft: ob produktiv wirklich templates/404.twig erscheint statt der
Entwickler-Fehlerseite — lokal ist der Entwicklermodus an. Wird nach dem Deploy
live gemessen. Ebenso ungeprüft: das Aussehen der Serien-Navigation im Browser,
nur HTML und CSS sind belegt.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VyiQJ8BM3hcqH3E3sPvg1N

Closes #82 ## Worum es geht Google kennt 4 von 22 Seiten und keinen einzigen Blogartikel. Drei Ursachen, alle behoben. ## 1. Die Navigation war auf 20 von 22 Seiten tot „Über mich", „Branchen" und „Angebot" wurden als `./#angebot` ausgeliefert. Auf `/blog` löst das zu `/blog#angebot` auf, wo es kein solches Element gibt. Für einen Crawler führte von jeder Unterseite kein Weg zurück in die Seitenstruktur. Anker zeigen jetzt absolut auf die Startseite der jeweiligen Sprache. Nebenbei warf der Sprung-Handler bei jedem Klick auf einen dieser Links einen Fehler, weil er den href-String als CSS-Selektor benutzte. Auch das ist an der Wurzel behoben. ## 2. Die Paginierung war ein unendlicher Duplikat-Trichter `/blog/p2`, `/blog/p9` und `/blog?page=2` lieferten alle 200 mit wortgleichem Inhalt und einem Canonical auf sich selbst. Jetzt: 404 für Seitenzahlen, die es nicht gibt, und ein Canonical, das aus der Site aufgelöst wird statt aus dem Request. Die Sperre steht **vor** dem Layout. Im Inhaltsblock hätte der Kopfbereich bereits `Cache-Control: public, max-age=3600` gesendet und der 404 wäre eine Stunde lang öffentlich zwischengespeichert worden. Gemessen: jetzt `no-store`. Zusätzlich hängt `ether/seo` unbedingt einen HTTP-Kopf `Link: rel="canonical"` mit seinem eigenen, ungefilterten Wert an. Der widersprach dem HTML auf jeder `/pN`-Adresse. Er wird jetzt überschrieben. ## 3. Es gab praktisch keine interne Verlinkung Vier der sechs Artikel hingen an keiner Kategorie. Deshalb listete das Archiv 2 von 6, und der bereits vorhandene „Verwandte Artikel"-Block blieb bei ihnen leer: er sucht über die Kategorie. Die Anti-Slop-Reihe verlinkt sich jetzt selbst, über einen gemeinsamen Tag im vorhandenen Feld. Kein neues Feld, künftige Reihen funktionieren ohne Schema-Änderung. ## Ein Fund nebenbei, der nicht im Issue stand Die englische Sitemap existiert unter `/en/sitemap.xml` und listet alle sechs englischen Artikel. Die `robots.txt` nannte nur die deutsche. Google erfuhr von der englischen Seite also nie. Die Datei liegt jetzt im Repo statt in einem Datenbankfeld. ## Gemessen, nicht angenommen Lokal gegen `http://localhost:8090`, nach `clear-caches/all`: - `/blog/p2`, `/blog/p9`, `/blog/category/…/p2` → 404 mit `Cache-Control: no-store` - `/blog?page=2` → 200, Canonical auf `/blog`, HTTP-Kopf identisch - `/`, `/blog`, `/en/`, `/en/blog`, Artikel, Kategorie → weiterhin 200 - Header- und Footer-Links auf `/blog` bitidentisch mit denen auf `/`, keine doppelten Schrägstriche - Kategorie-Archiv listet jetzt alle sechs Artikel - Serien-Navigation erscheint auf den vier Anti-Slop-Artikeln in beiden Sprachen und verlinkt in der englischen Fassung die englischen Slugs; auf Artikeln ohne Reihe erscheint gar nichts - Migration idempotent, `ids()` liefert `int`, strikter Vergleich trägt Ein unabhängiger Prüfagent mit ausschließlich lesenden Werkzeugen hat den Stand gegengelesen und zwei Befunde gemeldet, beide oben behoben und nachgemessen. **Nicht geprüft:** ob produktiv wirklich `templates/404.twig` erscheint statt der Entwickler-Fehlerseite — lokal ist der Entwicklermodus an. Wird nach dem Deploy live gemessen. Ebenso ungeprüft: das Aussehen der Serien-Navigation im Browser, nur HTML und CSS sind belegt. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VyiQJ8BM3hcqH3E3sPvg1N
Header und Footer bauten "#angebot" zu "./#angebot" um. Auf /blog loest das
zu /blog#angebot auf, und dort gibt es kein Element mit dieser ID. Die drei
Navigationsziele funktionierten damit auf 2 von 22 Seiten. Fuer einen Crawler
heisst das: von jeder Unterseite fuehrt kein Weg zurueck in die Seitenstruktur.

Ein Makro loest reine Anker jetzt gegen die Startseite der aktuellen Site auf,
auf der EN-Site also gegen /en/. Externe und interne Nicht-Anker gehen
unveraendert durch.

Der Sprung-Handler nahm den href-String direkt als CSS-Selektor. Bei
"./#angebot" war das ungueltig und warf einen Fehler, den niemand abfing; mit
absoluten Adressen erst recht. Er loest den href jetzt gegen die aktuelle
Adresse auf und faengt nur noch echte Spruenge innerhalb derselben Seite ab.

Refs #82

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyiQJ8BM3hcqH3E3sPvg1N
/blog/p2, /blog/p9 und /blog?page=2 lieferten alle 200 mit wortgleichem Inhalt
und einem Canonical auf sich selbst. Bei sechs Artikeln und zwoelf pro Seite
gibt es keine zweite Seite; Google fand damit beliebig viele indexierbare
Kopien der Uebersicht.

Seitenzahlen jenseits des Vorhandenen liefern jetzt 404. Die Pruefung steht vor
dem Layout, nicht im Inhaltsblock: sonst haette der head bereits ein
"Cache-Control: public, max-age=3600" gesendet und der 404 waere eine Stunde
lang oeffentlich zwischengespeichert worden. pageInfo.currentPage taugt dafuer
nicht, Crafts Paginator klemmt den Wert auf die Gesamtzahl und meldet nie einen
Ueberlauf.

Das Canonical kommt nicht mehr von ether/seo, das den Query-String mit in die
Adresse nimmt, sondern aus der Basis-URL der Site. Zusaetzlich wird der
HTTP-Kopf "Link: rel=canonical" ueberschrieben, den das Plugin unbedingt
anhaengt: zwei widersprechende Angaben sind schlimmer als eine falsche.

Refs #82

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyiQJ8BM3hcqH3E3sPvg1N
Die vierteilige Anti-Slop-Reihe enthielt keinen einzigen Verweis auf ihre
anderen Teile. Vier der sechs Artikel hingen ausserdem an keiner Kategorie,
weshalb das Archiv 2 von 6 listete und der vorhandene "Verwandte
Artikel"-Block bei ihnen leer blieb: er sucht ueber die Kategorie.

Die Reihe traegt jetzt einen gemeinsamen Tag im vorhandenen articleTags-Feld,
eine neue Komponente listet daraus alle Teile in Erscheinungsreihenfolge und
hebt den aktuellen hervor. Kein neues Feld noetig, kuenftige Reihen
funktionieren ohne Schema-Aenderung.

Die Migration setzt Kategorie und Tag idempotent; beide Felder sind nicht
uebersetzbar, ein Speichern gilt fuer beide Sprachen.

Refs #82

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyiQJ8BM3hcqH3E3sPvg1N
Die englische Sitemap existiert unter /en/sitemap.xml und listet alle sechs
englischen Artikel. Die ausgelieferte robots.txt nannte nur die deutsche,
Google erfuhr von der englischen Fassung also nie.

Die Datei liegt ab jetzt im Repo statt im robots-Editor von ether/seo: nginx
liefert echte Dateien vor Craft aus, sie geht mit jedem Deploy mit und ist im
Diff sichtbar. Ein Feld in der Datenbank ist das alles nicht.

Refs #82

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyiQJ8BM3hcqH3E3sPvg1N
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyiQJ8BM3hcqH3E3sPvg1N
Phil merged commit 0dce79d51f into main 2026-09-07 08:59:14 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Phil/philflow.io!95
No description provided.