SEO, OG-Images & Storage Commands
Commands für SEO-Metriken, OG-Image-Generierung und Storage-Management (lokaler Storage, sharded — siehe Local-Filesystem-Sharding).
Sitemap
sitemap:generate {--dry-run} {--force}
XML Sitemaps fuer Search Engine Indexation generieren. Erstellt Sitemap-Index mit partitionierten Sub-Sitemaps in storage/app/sitemaps/ und verlinkt sie via Symlinks in public/.
# Standard: Generierung mit Dependency-Check
php artisan sitemap:generate
# Nur Statistik (keine Dateien)
php artisan sitemap:generate --dry-run
# Dependency-Check ueberspringen
php artisan sitemap:generate --force
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--dry-run | flag | false | Nur Statistik anzeigen, keine Dateien schreiben |
--force | flag | false | Dependency-Check auf public-explorer:refresh ueberspringen |
Interne Logik:
- Dependency-Check: Prueft ob
public-explorer:refreshheute gelaufen ist (Heartbeat) - Generierung: SitemapGenerator erstellt alle Sub-Sitemaps via XMLWriter (Streaming, memory-safe)
- Validierung: Jede Datei wird auf XML-Validitaet, Groesse (<50 MB) und URL-Count geprueft
- Metadata: Generierungsergebnis wird in persistenter JSON-Datei gespeichert
- Daily Snapshot:
SitemapDailySnapshotfuer 14-Tage-History (idempotent viaupdateOrCreate) - URL-Drop Check: Vergleich mit Vortag — bei >10% Rueckgang Admin-Notification
- robots.txt: Sitemap-URL in robots.txt aktualisieren
- Cron Heartbeat:
cron:heartbeat:sitemap_generate
Generierte Dateien:
| Datei | Inhalt |
|---|---|
sitemap.xml | Sitemap Index |
sitemap-static.xml | Homepage, Explorer Index, Newsletter, CMS-Seiten |
sitemap-categories.xml | Explorer Kategorie-Landing-Pages |
sitemap-tags-{n}.xml | Explorer Tag-Landing-Pages (partitioniert wie Profile) |
sitemap-profiles-{n}.xml | Explorer Profile (max. 10.000 <url>-Einträge pro Datei, d.h. 5.000 Profile × 2 hreflang-Varianten) |
Schedule: Taeglich 07:30 UTC (nach public-explorer:refresh)
Heartbeat: cron:heartbeat:sitemap_generate
Location: app/Console/Commands/GenerateSitemap.php
SEO & Web Vitals
seo:sync-search-console {--days=3} {--date=}
Google Search Console Metriken (Klicks, Impressionen, CTR, Position) via API synchronisieren.
# Standard: letzte 3 Tage (GSC hat ~2 Tage Daten-Verzögerung)
php artisan seo:sync-search-console
# Bestimmtes Datum
php artisan seo:sync-search-console --date=2026-02-20
# Mehr Tage nachholen
php artisan seo:sync-search-console --days=7
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--days | int | 3 | Anzahl Tage zum Syncen |
--date | string | — | Bestimmtes Datum (YYYY-MM-DD) |
Voraussetzung: SEARCH_CONSOLE_ENABLED=true + Service Account als Nutzer in der Search Console.
Schedule: Täglich 08:00 UTC
Location: app/Console/Commands/SyncSearchConsoleMetrics.php
seo:prune-metrics {--days=90} {--dry-run}
Search Console + Web Vitals Metriken älter als Retention löschen.
php artisan seo:prune-metrics # Standard: > 90 Tage
php artisan seo:prune-metrics --days=60 # Custom Retention
php artisan seo:prune-metrics --dry-run # Vorschau
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--days | int | 90 | Retention in Tagen |
--dry-run | flag | false | Vorschau ohne Löschung |
Schedule: Wöchentlich Sonntag 03:30 UTC
Location: app/Console/Commands/PruneSeoMetrics.php
seo:web-vitals-report
Wöchentlicher Core Web Vitals Report mit Degradation-Erkennung. Vergleicht aktuelle 7-Tage-Periode gegen vorherige 7 Tage und sendet Admin-Notification.
php artisan seo:web-vitals-report
Interne Logik:
- Lädt Zeitraum-Vergleich via
WebVitalsService::getComparison(7) - Prüft ob ≥10 Samples in aktueller Periode vorhanden (sonst Skip)
- Baut Report-Body mit p75-Werten, Ratings und Delta-Prozenten pro Metrik
- Erkennt Degradation: >10% Verschlechterung ODER Rating "poor"
- Sendet via
NotificationService::announceToAll('web_vitals_report', ..., adminOnly: true, expiryDays: 14) - Titel enthält Anzahl Verschlechterungen oder "Wochenbericht"
Schedule: Wöchentlich Montag 09:00 UTC
Location: app/Console/Commands/WebVitalsReport.php
OG-Image-Generierung
og-images:generate {--platform=} {--tier=} {--force} {--limit=0} {--chunk=100} {--missing-only}
OG-Images (1200x630px) für öffentliche Profile im Public Explorer via Browsershot/Chromium generieren. Dispatcht GenerateOgImageJob pro Profil auf die og-images Queue.
# Täglicher Lauf: nur fehlende OG-Images generieren
php artisan og-images:generate --missing-only
# Nur YouTube Mega-Tier
php artisan og-images:generate --platform=youtube --tier=mega
# Top 500 erzwungen neu generieren (ignoriert Hash)
php artisan og-images:generate --force --limit=500
# Alle Instagram-Profile mit Custom Chunk-Size
php artisan og-images:generate --platform=instagram --chunk=50
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--platform | string | alle | youtube oder instagram |
--tier | string | alle | micro, small, medium, large, mega |
--force | flag | false | Neu generieren auch wenn Hash unverändert |
--limit | int | 0 | Max. Profile (0 = alle) |
--chunk | int | 100 | DB-Batch-Größe |
--missing-only | flag | false | Nur Profile ohne og_image_path |
Interne Logik:
- Query:
published(),whereNull('deleted_at'), optional Platform/Tier-Filter - Sortierung: PRO first, dann Follower-Count absteigend
- Pre-Dispatch-Check: Hash vergleichen (außer
--force) - Dispatcht
GenerateOgImageJobpro Profil (Queue:og-images, Timeout 60s, 3 Tries,ShouldBeUniquemituniqueFor=600s)
Profilbild-Einbettung (Base64 Data URI): Profilbilder werden als Base64 Data URI direkt ins HTML eingebettet, damit headless Chrome keine Netzwerkzugriffe braucht. Fallback-Kette:
- Lokal gespeichertes Bild (R2/public Storage) — bevorzugt, nie ablaufend
- Externer CDN-Download (YouTube/Instagram URL, 5s Timeout) — Instagram-URLs koennen abgelaufen sein
- Postbox-Platzhalter (
public/assets/no-picture.jpg, gleiches Bild wie auf den Slidern viaSocialProfile::PLACEHOLDER_IMAGE) — wird einmal pro Process gecacht. Last-Resort 1x1 transparentes PNG falls die Platzhalter-Datei selbst fehlt.
Self-Healing: Wenn der Generator den Platzhalter verwenden musste (= kein lokales Profilbild + externer URL nicht abrufbar), setzt er OgImageGenerator->lastUsedPlaceholder = true. Der Job liest das und schreibt die Notiz "ohne Profilbild" in die note-Spalte des OgImageLog (Status bleibt success). Sobald social_profile_images.latestImage fuer das Profil gesynct wird, aendert sich der Profile-Hash (computeProfileHash() enthaelt path_thumbnail), und das OG-Image wird beim naechsten Lauf automatisch mit dem echten Bild regeneriert — kein manueller Force-Run noetig.
Admin-UI /admin/open-graph: Spalte "Hinweis" zeigt:
- bei
failed:error-Spalte (rot) - bei
success:note-Spalte (amber, z.B. "ohne Profilbild") - bei
skipped:noteodererror(grau, z.B. "Hash unveraendert")
Schedule: Täglich 08:00 UTC (mit --missing-only, nach public-explorer:refresh)
Location: app/Console/Commands/GenerateOgImages.php
og-images:cleanup {--dry-run}
Verwaiste OG-Images löschen, Dateien ohne DB-Eintrag bereinigen und alte Job-Logs prunen.
php artisan og-images:cleanup --dry-run # Vorschau
php artisan og-images:cleanup # Cleanup ausführen
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--dry-run | flag | false | Vorschau ohne Löschung |
Interne Logik:
- DB-Cleanup: Profile mit
og_image_pathprüfen, ob gelöscht/unveröffentlicht →og_image_path+ Datei bereinigen - Orphaned Files: Dateien im
og-images/Verzeichnis scannen, die keinem Profil zugeordnet sind → löschen - Log-Prune:
og_image_logsEinträge älter als 7 Tage löschen
Schedule: Wöchentlich Sonntag 04:00 UTC
Location: app/Console/Commands/CleanupOgImages.php
Cloudflare R2 Storage
storage:migrate-to-r2 {--path=} {--dry-run} {--resume} {--chunk=500} {--verify}
Migration lokaler Dateien nach Cloudflare R2. Liest vom local-public Disk, schreibt auf den public Disk (der bei PUBLIC_STORAGE_DRIVER=s3 auf R2 zeigt).
# Vorschau: was würde migriert?
php artisan storage:migrate-to-r2 --dry-run
# Migration starten
php artisan storage:migrate-to-r2
# Nur bestimmten Prefix migrieren
php artisan storage:migrate-to-r2 --path=og-images
# Fortsetzen nach Unterbrechung (DB-basiertes Progress-Tracking)
php artisan storage:migrate-to-r2 --resume
# Mit Dateigrößen-Verifikation nach Upload
php artisan storage:migrate-to-r2 --verify
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--path | string | — | Nur Dateien unter diesem Prefix migrieren |
--dry-run | flag | false | Vorschau ohne Upload |
--resume | flag | false | Bereits migrierte überspringen (DB-Tracking via r2_migration_tracking Tabelle) |
--chunk | int | 10 | GC-Intervall (Garbage Collection alle N Dateien) |
--refresh | int | 500 | S3-Disk alle N Uploads neu erstellen (Guzzle Memory-Leak-Mitigation) |
--verify | flag | false | Dateigrößen nach Upload vergleichen |
Schedule: Manuell (einmalig für Migration)
Location: app/Console/Commands/MigrateStorageToR2.php
storage:verify-r2 {--sample=100} {--path=} {--full}
Integrität der R2-Dateien gegen lokale Dateien prüfen.
# 100 zufällige Dateien prüfen
php artisan storage:verify-r2
# 500 zufällige Dateien
php artisan storage:verify-r2 --sample=500
# Alle Dateien (vollständiger Check)
php artisan storage:verify-r2 --full
# Nur OG-Images prüfen
php artisan storage:verify-r2 --path=og-images --full
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--sample | int | 100 | Anzahl zufälliger Dateien (ignoriert bei --full) |
--path | string | — | Nur Dateien unter diesem Prefix prüfen |
--full | flag | false | Alle Dateien prüfen |
Schedule: Manuell
Location: app/Console/Commands/VerifyR2Storage.php
storage:sync-r2-backup {--path=} {--dry-run} {--chunk=500}
Inkrementeller Sync vom Primary R2 Bucket zum Backup Bucket.
# Vorschau
php artisan storage:sync-r2-backup --dry-run
# Sync starten
php artisan storage:sync-r2-backup
# Nur OG-Images syncen
php artisan storage:sync-r2-backup --path=og-images
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--path | string | — | Nur Dateien unter diesem Prefix syncen |
--dry-run | flag | false | Vorschau ohne Kopie |
--chunk | int | 500 | Batch-Größe |
Schedule: Wöchentlich Sonntag 04:30 UTC
Location: app/Console/Commands/SyncR2Backup.php
cloudflare:fetch-r2-metrics {--force}
R2-Metriken via Cloudflare GraphQL/REST API abrufen und für das Admin-Dashboard cachen. Ersetzt den alten cloudflare:snapshot-metrics Command, der über S3 ListObjectsV2 alle Objekte scannen musste (820K+ Objekte, ~821 API-Calls, 5-10 Min).
# Standard: nutzt 30-Min-Cache
php artisan cloudflare:fetch-r2-metrics
# Cache ignorieren, Neuscan erzwingen
php artisan cloudflare:fetch-r2-metrics --force
| Option | Typ | Default | Beschreibung |
|---|---|---|---|
--force | flag | false | Neuer Abruf auch wenn Daten < 30 Min existieren |
Interne Logik (9 Schritte):
- REST
/r2/metrics— Account-Level Storage (1 API-Call, ~1s) - GraphQL
r2StorageAdaptiveGroups— Per-Bucket Storage Snapshot (1 API-Call, ~1-3s) - GraphQL
r2OperationsAdaptiveGroups— Operations nach actionType, 30 Tage (1 API-Call, ~1-3s) - Kostenberechnung mit Class A/B Mapping
- Backup-Bucket Storage via GraphQL
- Anomalie-Erkennung (>20% Abweichung → Admin-Notification)
- Metriken in Cache speichern (1h TTL) + persistenter JSON-Snapshot
- Täglicher DB-Snapshot in
r2_metric_snapshots(für Langzeit-Trends) - Heartbeat für Cron-Monitoring
EU-Jurisdiction: Buckets mit EU-Jurisdiction erfordern eu_-Prefix in GraphQL-Queries. Konfigurierbar via R2_JURISDICTION in .env.
Performance: 3 API-Calls statt 821 — ~3-5 Sekunden statt 5-10 Minuten, ~2-5 MB statt ~50-80 MB Memory.
Schedule: Stündlich
Timeout: 60s
Location: app/Console/Commands/CloudflareFetchR2Metrics.php