Dokumentation durchblättern
Module entwickeln
Öffentliche Startseite & SEO
Eine öffentliche Startseite unter / ausliefern und einzelne Seiten gezielt für Suchmaschinen freigeben — über die PublicHomepageRegistry, mit default-deny als Grundzustand.
Eine Schneespur-Installation ist im Auslieferungszustand eine private Anwendung. Login, Admin, Kundenportal, Fahrer-App und Installer werden bewusst aus Suchmaschinen herausgehalten, um Kundendaten zu schützen. Dieser Schutz ist als default-deny angelegt: Was nicht ausdrücklich freigegeben wurde, bleibt privat — und er ist über drei Ebenen abgesichert, die alle denselben Grundzustand teilen.
Manche Betriebe — kleine Winterdienst-Unternehmen ohne eigene Website — möchten ihre Installation aber zugleich als öffentliche Website nutzen: Webspace mieten, Schneespur installieren, ein Frontpage-Modul aktivieren und so eine öffentliche Seite und die saubere Einsatz-Dokumentation an einem Ort haben. Damit das funktioniert, müssen die gewünschten öffentlichen Seiten für Suchmaschinen indexierbar sein.
Hinweis zum Status. Der hier beschriebene Erweiterungspunkt — die
PublicHomepageRegistryund die drei Indexierungs-Ebenen — existiert bereits. Das eigentliche Frontpage-Modul, das eine fertige öffentliche Startseite mitbringt, ist derzeit geplant und noch nicht verfügbar. Diese Seite beschreibt also den Mechanismus, auf dem ein solches kommendes Modul aufsetzt.
Diese Seite erklärt, wie ein Modul öffentliche Seiten ausliefert und sie gezielt für die Indexierung freigibt, ohne den Rest der Anwendung preiszugeben.
Eine Wahrheitsquelle. Alles Folgende läuft über eine einzige Registry, die
PublicHomepageRegistry. Ein Modul meldet seine öffentlichen Seiten einmal an; die robots.txt-Route, derX-Robots-Tag-Antwort-Header und das per-Seite gesetzte<meta name="robots">konsultieren dieselbe Registry zur Request-Zeit. Sie können daher nicht auseinanderdriften.
PublicHomepageRegistry
Klasse: App\Services\Extension\PublicHomepageRegistry (Singleton)
// Serve the public root URL "/" instead of redirecting to login.
// Registering a homepage automatically marks "/" crawlable.
register(callable $handler): void // handler returns Response|View|string
has(): bool
render(): mixed
// Declare ADDITIONAL public pages (leading slash optional). "/" is added by register().
allowCrawling(string ...$paths): void // e.g. allowCrawling('/leistungen', '/impressum')
crawlablePaths(): array // list<string>, normalized with leading slash
isCrawlable(string $path): bool // root matches exactly; sections match sub-paths
// Optional XML sitemap advertised in robots.txt (module must serve it itself).
setSitemapUrl(string $url): void
sitemapUrl(): ?string
Die Registry ist im Container als Singleton hinterlegt; Sie lösen sie wie jede andere
Erweiterungs-Registry über app(PublicHomepageRegistry::class) auf. register() setzt die
öffentliche Startseite und markiert dabei / automatisch als crawlbar — für alle weiteren
öffentlichen Pfade ist allowCrawling() zuständig. Ein optionaler Sitemap-Verweis lässt
sich über setSitemapUrl() ergänzen; die Sitemap selbst muss das Modul allerdings als eigene
Route ausliefern, die Registry verwaltet nur die URL.
Matching-Regeln von isCrawlable()
Welche angemeldeten Pfade eine konkrete Anfrage abdecken, entscheidet isCrawlable() nach
festen Regeln. Der Root-Pfad / matcht ausschließlich sich selbst; ein als Sektion
angemeldeter Pfad deckt zusätzlich seine Unterpfade ab; ein reiner Präfix, der nicht an einer
Pfadgrenze endet, matcht hingegen nicht.
| Angemeldet | Anfrage-Pfad | Crawlbar? |
|---|---|---|
/ | / | ✅ (Root matcht nur sich selbst) |
/ | /login | ❌ |
/leistungen | /leistungen | ✅ |
/leistungen | /leistungen/winterdienst | ✅ (Unterpfad) |
/leistungen | /leistungenX | ❌ (reiner Präfix, kein Unterpfad) |
Die letzte Zeile ist der Grund, warum das Matching nicht über ein simples
str_starts_with() läuft: /leistungenX beginnt zwar mit /leistungen, ist aber eine ganz
andere Seite. Nur ein echter Unterpfad (Trennung an /) gilt als abgedeckt.
Die Startseite ausliefern
In der ServiceProvider::boot() Ihres Moduls:
use App\Services\Extension\PublicHomepageRegistry;
app(PublicHomepageRegistry::class)->register(
fn () => view('frontpage::homepage')
);
Die Core-Route für / (in routes/web.php) fragt die Registry zur Request-Zeit ab.
Damit funktioniert die Auslieferung auch dann, wenn die Routen über route:cache
zwischengespeichert sind — die Registrierung wird nicht eingebacken, sondern bei jeder
Anfrage frisch konsultiert.
Die Präzedenz für / ist klar gestaffelt: Solange die Einrichtung noch nicht abgeschlossen
ist, gewinnt der Installer. Danach gewinnt eine registrierte Startseite. Ist keine
registriert, bleibt es beim unveränderten Standardverhalten — / leitet auf den Login
um.
Weitere öffentliche Seiten ergänzen
Eigene Routen registrieren Sie wie gewohnt (siehe Routen & APIs). Anschließend teilen Sie der Registry mit, welche davon öffentlich sind, damit sie gecrawlt und indexiert werden dürfen:
Route::middleware('web')->group(function () {
Route::get('/leistungen', [SiteController::class, 'services'])->name('frontpage.services');
Route::get('/impressum', [SiteController::class, 'imprint'])->name('frontpage.imprint');
});
app(PublicHomepageRegistry::class)->allowCrawling('/leistungen', '/impressum');
Alles, was Sie nicht an allowCrawling() übergeben, bleibt privat — das ist der
default-deny-Grundsatz in der Praxis.
Die drei Indexierungs-Ebenen
Die Freigabe für Suchmaschinen geschieht nicht an einer Stelle, sondern auf drei Ebenen, die unterschiedliche Aufgaben haben und alle vom Grundzustand „nicht indexieren” ausgehen.
| Ebene | Wo | Steuert | Standard | Öffentliche Freigabe |
|---|---|---|---|---|
| robots.txt | dynamische Route in routes/web.php | was gecrawlt werden darf | Disallow: / | Allow: je crawlablePaths() |
X-Robots-Tag | SecurityHeaders-Middleware | was indexiert werden darf (auch Nicht-HTML, z. B. PDFs) | noindex, nofollow auf jeder Antwort | Header entfällt für crawlbare Pfade |
<meta name="robots"> | die Blade-Layouts | was indexiert werden darf (nur HTML) | noindex, nofollow | @section('robots','index, follow') |
Warum drei Ebenen? Jede deckt eine Lücke der anderen ab:
- Die robots.txt stoppt das Crawling, bevor eine Anfrage überhaupt bei PHP ankommt.
- Der
X-Robots-Tag-Header ist das autoritative Index-Signal und greift auch für Antworten ganz ohne HTML — ein generierter PDF-Bericht hat kein<meta>-Tag, sehr wohl aber einen HTTP-Header. - Das
<meta name="robots">-Tag ist das sichtbare Signal auf HTML-Ebene und dient als zusätzliche Absicherung (defense-in-depth).
Wichtig ist, dass Middleware und Meta-Tag übereinstimmen: Beide werten dieselben registrierten Pfade aus, weichen also nicht voneinander ab.
Beispiel-Ausgaben der robots.txt
Wenn kein Frontpage-Modul aktiv ist (die übliche private Installation):
User-agent: *
Disallow: /
Wenn ein Modul / registriert, zusätzlich allowCrawling('/leistungen') aufgerufen und eine
Sitemap gesetzt hat:
User-agent: *
Disallow: /
Allow: /$
Allow: /leistungen
Allow: /build/
Allow: /favicon.ico
Allow: /favicon.svg
Sitemap: https://example.test/sitemap.xml
Allow: /$ verankert den Root mit dem Zeilenende-Anker, sodass nur / selbst crawlbar ist
und nicht etwa /login. Sektions-Pfade bleiben ohne Anker, damit ihre Unterseiten
mitabgedeckt sind. /build/ und die Favicons werden bei einer öffentlichen Seite immer
freigegeben, weil eine Suchmaschine CSS und JavaScript laden können muss, um die Seite korrekt
zu rendern.
Eine Seite indexierbar machen — Checkliste
-
Route registrieren (für Unterseiten) und über die
web-Middleware-Gruppe ausliefern, damitSecurityHeadersläuft. -
Öffentlich erklären:
allowCrawling('/ihr-pfad')— die Startseite/ist überregister()bereits automatisch enthalten. -
Das HTML freigeben. Wenn Ihre View ein Core-Layout erweitert, überschreiben Sie den robots-Abschnitt:
{{-- frontpage::homepage --}} @extends('layouts.guest') @section('robots', 'index, follow') @section('content') <h1>Winterdienst Mustermann</h1> ... @endsectionLiefern Sie eigenständiges HTML aus (also ohne ein Core-Layout zu erweitern), setzen Sie das
<meta name="robots" content="index, follow">einfach selbst. -
(Optional) Sitemap bekanntgeben:
setSitemapUrl('https://.../sitemap.xml')und die Route dafür selbst ausliefern.
Mehr ist nicht nötig: Die robots.txt-Route und der X-Robots-Tag-Header greifen den
registrierten Pfad automatisch auf.
Was ohne Zutun privat bleibt
Alles, was nicht angemeldet wurde, bleibt im default-deny-Zustand: /login und die übrige
Authentifizierung, /admin/*, /portal/*, /driver/*, /install/*, jede Admin-Seite eines
Moduls sowie alle Nicht-HTML-Antworten wie generierte PDF-Berichte. Sie behalten den
X-Robots-Tag: noindex, nofollow-Header und das voreingestellte noindex-Meta-Tag der
Layouts. Sie müssen dafür nichts tun — privat ist der Grundzustand, nicht das Ergebnis einer
Konfiguration.
Hinweise zum Deployment
- Die statische
public/robots.txtwurde entfernt, damit die dynamische Route greifen kann — eine statische Datei würde vom Webserver ausgeliefert, bevor PHP überhaupt läuft. Legen Sie sie nicht wieder an. - Der pauschale
X-Robots-Tag-Header wurde auspublic/.htaccessentfernt. Er war unbedingt gesetzt und konnte öffentliche nicht von privaten Seiten unterscheiden; dieSecurityHeaders-Middleware setzt ihn nun pro Anfrage. Beim Aktualisieren bestehender Installationen muss die neue.htaccessim Update-Paket mitkommen — sonst behält eine alte Apache-Installation den pauschalen Header, und die Startseite bleibt trotz aller anderen Freigaben aufnoindex. (nginx-Installationen hatten diesen Header nie und erhalten so erstmals das korrekte Verhalten.) - Statische Assets unter
/build/, Favicons und Ähnliches liefert der Webserver direkt aus; sie laufen nie durch die Middleware und bekommen daher auch nie einen noindex-Header — genau das, was eine Suchmaschine braucht, um öffentliche Seiten korrekt darzustellen.
Verwandte Themen
- Registries-Referenz — die
PublicHomepageRegistryim Registry-Katalog - Routen & APIs — Routen registrieren und Middleware
- Template-Slots — in bestehende Layouts einschieben, statt die Startseite zu ersetzen