fix(seo): URLs aus der Site auflösen statt aus dem Request-Host #81

Merged
Phil merged 1 commit from fix/canonical-urls-not-request-host into main 2026-09-06 23:02:06 +00:00
Owner

P1 aus dem Live-Audit nach dem Deploy. Die Startseite hat sich selbst als http://localhost/ deklariert.

Refs #58

Befund

Live gemessen auf https://philflow.io/:

og:url        content="http://localhost/"
twitter:url   content="http://localhost/"
hreflang="de-DE"     href="http://localhost/"
hreflang="x-default" href="http://localhost/"
canonical                 https://philflow.io/     (war korrekt)

Auf /en/ dagegen alles richtig: hreflang="de-DE" href="https://philflow.io/".

Das Paar ist damit nicht reziprok — die eine Seite verweist auf einen Host, den die andere nicht kennt. Google verwirft in dem Fall nicht das einzelne Tag, sondern das komplette Sprach-Cluster der Startseite. Genau die Reparatur aus #62 war damit auf der wichtigsten Seite wirkungslos.

Die übrigen 21 Seiten waren sauber, deshalb ist es bei meiner Prüfung durchgerutscht: ich hatte hreflang auf /blog, /en/blog, /impressum und dem Kategorie-Archiv verifiziert — nicht auf /.

Ursache

og:url, twitter:url, die hreflang-Selbstreferenz, x-default und die canonicalUrl im JSON-LD stammten aus craft.app.request.absoluteUrl. Diese Variable spiegelt den Host-Header des jeweiligen Aufrufs. Ein interner Aufruf gegen localhost erzeugt damit eine Seite, die sich selbst http://localhost/ nennt — und Blitz speichert genau diese Fassung und liefert sie an alle echten Besucher aus.

Fix

Alle fünf Stellen lösen jetzt gegen die Site auf, nicht gegen den Request: seo.canonical für die Social-Tags, element.url beziehungsweise siteUrl() für hreflang. Die eigene Site wird damit genauso behandelt wie jede andere — der Sonderfall im Code entfällt ersatzlos.

Beleg

Lokal mit gefälschtem Host-Header:

curl -H "Host: evil.example.com" http://localhost:8090/
  og:url            http://localhost:8090/     (nicht evil.example.com)
  hreflang="de-DE"  http://localhost:8090/     (nicht evil.example.com)

Vorher hätte beides den fremden Host getragen. grep -rn absoluteUrl templates/ liefert keinen Treffer mehr.

Sofortmaßnahme, bereits erfolgt

Der Blitz-Cache wurde geleert und mit korrektem Host neu gewärmt, die Startseite liefert live wieder https://philflow.io/. Ohne diesen Fix kehrt der Zustand aber beim nächsten internen Aufruf zurück.

Empfehlung als Folge-Ticket

Zusätzlich allowedHosts in config/general.php setzen, damit Craft Aufrufe mit fremdem Host-Header von vornherein abweist. Das ist Härtung gegen Host-Header-Injection allgemein, nicht nur gegen dieses Symptom — braucht aber eine Prüfung, welche Hosts Healthchecks und Traefik intern verwenden, und gehört deshalb nicht in diesen Fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_017w4iMK5VNobPwXUWXmLKKh

P1 aus dem Live-Audit nach dem Deploy. Die Startseite hat sich selbst als `http://localhost/` deklariert. `Refs #58` ## Befund Live gemessen auf `https://philflow.io/`: ``` og:url content="http://localhost/" twitter:url content="http://localhost/" hreflang="de-DE" href="http://localhost/" hreflang="x-default" href="http://localhost/" canonical https://philflow.io/ (war korrekt) ``` Auf `/en/` dagegen alles richtig: `hreflang="de-DE" href="https://philflow.io/"`. Das Paar ist damit **nicht reziprok** — die eine Seite verweist auf einen Host, den die andere nicht kennt. Google verwirft in dem Fall nicht das einzelne Tag, sondern das **komplette Sprach-Cluster der Startseite**. Genau die Reparatur aus #62 war damit auf der wichtigsten Seite wirkungslos. Die übrigen 21 Seiten waren sauber, deshalb ist es bei meiner Prüfung durchgerutscht: ich hatte hreflang auf `/blog`, `/en/blog`, `/impressum` und dem Kategorie-Archiv verifiziert — nicht auf `/`. ## Ursache `og:url`, `twitter:url`, die hreflang-Selbstreferenz, `x-default` und die `canonicalUrl` im JSON-LD stammten aus `craft.app.request.absoluteUrl`. Diese Variable spiegelt den **Host-Header des jeweiligen Aufrufs**. Ein interner Aufruf gegen `localhost` erzeugt damit eine Seite, die sich selbst `http://localhost/` nennt — und Blitz speichert genau diese Fassung und liefert sie an alle echten Besucher aus. ## Fix Alle fünf Stellen lösen jetzt gegen die Site auf, nicht gegen den Request: `seo.canonical` für die Social-Tags, `element.url` beziehungsweise `siteUrl()` für hreflang. Die eigene Site wird damit genauso behandelt wie jede andere — der Sonderfall im Code entfällt ersatzlos. ## Beleg Lokal mit gefälschtem Host-Header: ``` curl -H "Host: evil.example.com" http://localhost:8090/ og:url http://localhost:8090/ (nicht evil.example.com) hreflang="de-DE" http://localhost:8090/ (nicht evil.example.com) ``` Vorher hätte beides den fremden Host getragen. `grep -rn absoluteUrl templates/` liefert keinen Treffer mehr. ## Sofortmaßnahme, bereits erfolgt Der Blitz-Cache wurde geleert und mit korrektem Host neu gewärmt, die Startseite liefert live wieder `https://philflow.io/`. Ohne diesen Fix kehrt der Zustand aber beim nächsten internen Aufruf zurück. ## Empfehlung als Folge-Ticket Zusätzlich `allowedHosts` in `config/general.php` setzen, damit Craft Aufrufe mit fremdem Host-Header von vornherein abweist. Das ist Härtung gegen Host-Header-Injection allgemein, nicht nur gegen dieses Symptom — braucht aber eine Prüfung, welche Hosts Healthchecks und Traefik intern verwenden, und gehört deshalb nicht in diesen Fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017w4iMK5VNobPwXUWXmLKKh
og:url, twitter:url, die hreflang-Selbstreferenz, x-default und die
JSON-LD-canonicalUrl stammten aus craft.app.request.absoluteUrl. Damit
schlaegt der Host-Header jedes beliebigen Aufrufs in die Seite durch —
und Blitz speichert das Ergebnis. Live stand deshalb auf der Startseite
`og:url = http://localhost/` und `hreflang="de-DE" href="http://localhost/"`,
waehrend /en/ korrekt auf https://philflow.io/ verwies. Ein nicht
reziprokes Paar verwirft Google als ganzes Sprach-Cluster.

Alle fuenf Stellen loesen jetzt gegen die Site auf: seo.canonical fuer
die Social-Tags, element.url bzw. siteUrl() fuer hreflang. Die eigene
Site wird damit genauso behandelt wie jede andere, der Sonderfall
entfaellt.

Belegt lokal: ein Aufruf mit `Host: evil.example.com` liefert
unveraendert die echte Site-URL statt des gefaelschten Hosts.

Refs #58

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w4iMK5VNobPwXUWXmLKKh
Phil merged commit e03c2eae5e into main 2026-09-06 23:02:06 +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!81
No description provided.