Zum Hauptinhalt springen

SEO, OG-Images & Storage Commands

Commands für SEO-Metriken, OG-Image-Generierung und Cloudflare R2 Storage-Management.


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
OptionTypDefaultBeschreibung
--dry-runflagfalseNur Statistik anzeigen, keine Dateien schreiben
--forceflagfalseDependency-Check auf public-explorer:refresh ueberspringen

Interne Logik:

  1. Dependency-Check: Prueft ob public-explorer:refresh heute gelaufen ist (Heartbeat)
  2. Generierung: SitemapGenerator erstellt alle Sub-Sitemaps via XMLWriter (Streaming, memory-safe)
  3. Validierung: Jede Datei wird auf XML-Validitaet, Groesse (<50 MB) und URL-Count geprueft
  4. Metadata: Generierungsergebnis wird in persistenter JSON-Datei gespeichert
  5. Daily Snapshot: SitemapDailySnapshot fuer 14-Tage-History (idempotent via updateOrCreate)
  6. URL-Drop Check: Vergleich mit Vortag — bei >10% Rueckgang Admin-Notification
  7. robots.txt: Sitemap-URL in robots.txt aktualisieren
  8. Cron Heartbeat: cron:heartbeat:sitemap_generate

Generierte Dateien:

DateiInhalt
sitemap.xmlSitemap Index
sitemap-static.xmlHomepage, Explorer Index, Newsletter, CMS-Seiten
sitemap-categories.xmlExplorer Kategorie-Landing-Pages
sitemap-tags-{n}.xmlExplorer Tag-Landing-Pages (partitioniert wie Profile)
sitemap-profiles-{n}.xmlExplorer 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
OptionTypDefaultBeschreibung
--daysint3Anzahl Tage zum Syncen
--datestringBestimmtes 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
OptionTypDefaultBeschreibung
--daysint90Retention in Tagen
--dry-runflagfalseVorschau 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:

  1. Lädt Zeitraum-Vergleich via WebVitalsService::getComparison(7)
  2. Prüft ob ≥10 Samples in aktueller Periode vorhanden (sonst Skip)
  3. Baut Report-Body mit p75-Werten, Ratings und Delta-Prozenten pro Metrik
  4. Erkennt Degradation: >10% Verschlechterung ODER Rating "poor"
  5. Sendet via NotificationService::announceToAll('web_vitals_report', ..., adminOnly: true, expiryDays: 14)
  6. 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
OptionTypDefaultBeschreibung
--platformstringalleyoutube oder instagram
--tierstringallemicro, small, medium, large, mega
--forceflagfalseNeu generieren auch wenn Hash unverändert
--limitint0Max. Profile (0 = alle)
--chunkint100DB-Batch-Größe
--missing-onlyflagfalseNur Profile ohne og_image_path

Interne Logik:

  1. Query: published(), whereNull('deleted_at'), optional Platform/Tier-Filter
  2. Sortierung: PRO first, dann Follower-Count absteigend
  3. Pre-Dispatch-Check: Hash vergleichen (außer --force)
  4. Dispatcht GenerateOgImageJob pro Profil (Queue: og-images, Timeout 60s, 3 Tries, ShouldBeUnique mit uniqueFor=600s)

Profilbild-Einbettung (Base64 Data URI): Profilbilder werden als Base64 Data URI direkt ins HTML eingebettet, damit headless Chrome keine Netzwerkzugriffe braucht. Fallback-Kette:

  1. Lokal gespeichertes Bild (R2/public Storage) — bevorzugt, nie ablaufend
  2. Externer CDN-Download (YouTube/Instagram URL, 5s Timeout) — Instagram-URLs koennen abgelaufen sein
  3. Grauer Platzhalter-Kreis im Template

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
OptionTypDefaultBeschreibung
--dry-runflagfalseVorschau ohne Löschung

Interne Logik:

  1. DB-Cleanup: Profile mit og_image_path prüfen, ob gelöscht/unveröffentlicht → og_image_path + Datei bereinigen
  2. Orphaned Files: Dateien im og-images/ Verzeichnis scannen, die keinem Profil zugeordnet sind → löschen
  3. Log-Prune: og_image_logs Einträ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
OptionTypDefaultBeschreibung
--pathstringNur Dateien unter diesem Prefix migrieren
--dry-runflagfalseVorschau ohne Upload
--resumeflagfalseBereits migrierte überspringen (DB-Tracking via r2_migration_tracking Tabelle)
--chunkint10GC-Intervall (Garbage Collection alle N Dateien)
--refreshint500S3-Disk alle N Uploads neu erstellen (Guzzle Memory-Leak-Mitigation)
--verifyflagfalseDateigröß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
OptionTypDefaultBeschreibung
--sampleint100Anzahl zufälliger Dateien (ignoriert bei --full)
--pathstringNur Dateien unter diesem Prefix prüfen
--fullflagfalseAlle 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
OptionTypDefaultBeschreibung
--pathstringNur Dateien unter diesem Prefix syncen
--dry-runflagfalseVorschau ohne Kopie
--chunkint500Batch-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
OptionTypDefaultBeschreibung
--forceflagfalseNeuer Abruf auch wenn Daten < 30 Min existieren

Interne Logik (9 Schritte):

  1. REST /r2/metrics — Account-Level Storage (1 API-Call, ~1s)
  2. GraphQL r2StorageAdaptiveGroups — Per-Bucket Storage Snapshot (1 API-Call, ~1-3s)
  3. GraphQL r2OperationsAdaptiveGroups — Operations nach actionType, 30 Tage (1 API-Call, ~1-3s)
  4. Kostenberechnung mit Class A/B Mapping
  5. Backup-Bucket Storage via GraphQL
  6. Anomalie-Erkennung (>20% Abweichung → Admin-Notification)
  7. Metriken in Cache speichern (1h TTL) + persistenter JSON-Snapshot
  8. Täglicher DB-Snapshot in r2_metric_snapshots (für Langzeit-Trends)
  9. 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