Zum Hauptinhalt springen
Schneespur
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 PublicHomepageRegistry und 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, der X-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.

AngemeldetAnfrage-PfadCrawlbar?
//✅ (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.

EbeneWoSteuertStandardÖffentliche Freigabe
robots.txtdynamische Route in routes/web.phpwas gecrawlt werden darfDisallow: /Allow: je crawlablePaths()
X-Robots-TagSecurityHeaders-Middlewarewas indexiert werden darf (auch Nicht-HTML, z. B. PDFs)noindex, nofollow auf jeder AntwortHeader entfällt für crawlbare Pfade
<meta name="robots">die Blade-Layoutswas 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

  1. Route registrieren (für Unterseiten) und über die web-Middleware-Gruppe ausliefern, damit SecurityHeaders läuft.

  2. Öffentlich erklären: allowCrawling('/ihr-pfad') — die Startseite / ist über register() bereits automatisch enthalten.

  3. 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>
        ...
    @endsection

    Liefern Sie eigenständiges HTML aus (also ohne ein Core-Layout zu erweitern), setzen Sie das <meta name="robots" content="index, follow"> einfach selbst.

  4. (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.txt wurde 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 aus public/.htaccess entfernt. Er war unbedingt gesetzt und konnte öffentliche nicht von privaten Seiten unterscheiden; die SecurityHeaders-Middleware setzt ihn nun pro Anfrage. Beim Aktualisieren bestehender Installationen muss die neue .htaccess im Update-Paket mitkommen — sonst behält eine alte Apache-Installation den pauschalen Header, und die Startseite bleibt trotz aller anderen Freigaben auf noindex. (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