Zum Hauptinhalt springen

YouTube Publishing Analytics (oeffentlich)

Oeffentlich zugaengliche Tool-Seite mit datenbasierter Auswertung der besten Veroeffentlichungs-Zeitpunkte fuer YouTube-Videos. Zielt auf Long-Tail-SEO-Traffic fuer Anfragen wie "beste Uhrzeit YouTube Upload" oder "Wann Video veroeffentlichen".

Route

  • /{locale}/tools/youtube-publishing-analytics
  • Locales: de (primaer, Default), en
  • Feature-Flag: postbox.public_publishing_analytics.enabled (Default: true)
  • Rate-Limit: Named RateLimiter public-tools (30/min/IP)
  • Navigation: "Tools"-Dropdown zwischen Explorer und Dashboard (Desktop + Mobile)

Datenquellen

Wochentag-Aggregation (youtube_publishing_stats)

Taeglich aggregierter View auf Veroeffentlichungen des letzten Jahres. Enthaelt pro (category, mode)-Kombination:

  • Videos pro Wochentag
  • VFR-Median (Views/Follower Ratio) pro Wochentag
  • Engagement-Rate pro Wochentag ((likes + comments) / views) × 100 — Branchen-Standard für YouTube (Denominator ist views, nicht subscribers/followers: YouTube ist discovery-first, Views kommen aus Recommendations/Search/Shorts-Feed, nicht aus dem Subscriber-Inbox). 0-View-Videos werden vom Median ausgeschlossen. Benchmark-Orientierung: ~2% Schnitt, 3–5% gut, >5% exzellent (Mega-Channels >500k typ. 1–2%).
  • Like-to-View-Ratio pro Wochentag (likes / views) × 100
  • Shorts-Anteil (Share in %)
  • 7×24 Hour-Distribution (JSONB mit Wochentag→Stunde→Anzahl)

Cronjob: youtube:aggregate-publishing-stats laeuft woechentlich Montag 03:00 UTC (Heartbeat youtube_publishing_stats).

Monatliche Snapshots

Der Command youtube:aggregate-publishing-stats generiert immer automatisch einen Monats-Snapshot für den aktuellen Monat — kein separater Flag nötig. Die SQL-Aggregation summiert die Hour-Distributions serverseitig pro (weekday, hour) über alle Channels (jsonb_each_text()GROUP BY weekday, hour mit SUM) und baut daraus erst danach mit jsonb_object_agg() das Stunden-Objekt.

Wichtig (Fix 2026-05-31): jsonb_object_agg() dedupliziert in PostgreSQL doppelte Keys per Last-Value-Wins — es summiert NICHT. Die frühere Variante aggregierte direkt über alle Channels und kollabierte die Monats-hour_distribution dadurch auf die Verteilung eines einzelnen Channels (Monats-Heatmap fast leer). Deshalb MUSS zuerst pro (weekday, hour) summiert werden, bevor jsonb_object_agg() die bereits eindeutigen Paare zusammenfasst. Die gespeicherte hour_distribution enthält damit die Summe über alle Channels; der Reader (heatmapFromSnapshots) teilt für "Ø Videos pro Channel" durch die in der Snapshot-Row gespeicherte channel_count (nicht durch COUNT(*), das pro Snapshot-Row = 1 wäre).

Fuer historisches Backfilling:

# Snapshots fuer Januar bis April 2026 nachholen
php8.4 artisan youtube:aggregate-publishing-stats --from=2026-01-01 --to=2026-04-01

Iteriert ueber alle Kategorien plus einen NULL-Aggregate-Slot ("alle Kategorien").

Command-Uebersicht

# Alles berechnen (Weekly-Aggregation + Snapshot aktueller Monat)
php8.4 artisan youtube:aggregate-publishing-stats

# Nur ein bestimmtes Profil
php8.4 artisan youtube:aggregate-publishing-stats --profile=123

# Historische Monate nachholen (Aggregation + Snapshots fuer jeden Monat im Range)
php8.4 artisan youtube:aggregate-publishing-stats --from=2026-01-01 --to=2026-04-20

# Dry-Run (nur anzeigen, nichts schreiben)
php8.4 artisan youtube:aggregate-publishing-stats --dry-run

UI-Komponenten

Die Livewire-Komponente App\Livewire\Tools\YouTubePublishingAnalytics rendert:

BereichInhalt
HeroUeberschrift, Trust-Signal (Video-Anzahl), Data-Freshness
Filter-BarKategorie (Dropdown), Video-Typ (Pill-Toggle: Alle/Regulaer/Shorts), Zeitraum (Monat-Dropdown)
KPI-CardsBester Tag (VFR), Beste Stunde, Aktivster Tag, Shorts-Anteil
Chart-Grid (2×2)Videos/Wochentag, VFR-Median/Wochentag, Engagement-Rate/Wochentag, L2V-Ratio/Wochentag
Heatmap (7×24)Durchschnittliche Videos pro Channel, UTC↔Lokalzeit-Toggle
Best-Hours-BadgesTop-Stunden mit Count, formatiert in gewaehlter Zeitzone
Share-SectionTwitter/X, LinkedIn, WhatsApp, Copy-Link
FAQ-Accordion6 Fragen (VFR, Datenquelle, Update-Frequenz, Timezone, Kategorien, Shorts)
CTA (Gaeste)Register-Link fuer eigene Channel-Analyse

Alpine-Integration

ApexCharts-Rendering via Alpine-Component ytPubAnalytics(). Key-Verhalten:

  • init() wartet auf ApexCharts-Global (CDN-Load), rendert Charts + Heatmap
  • renderCharts() destroyed alte Chart-Instanzen vor Neuaufbau (bei Filter-Wechsel via Livewire re-render)
  • renderHeatmap() berechnet Zeitzonen-Shift clientseitig: UTC-Rohdaten werden per -new Date().getTimezoneOffset() / 60 verschoben
  • livewire:navigated Event-Listener re-rendert nach SPA-Navigation
  • formatHour(hour, mode) / tzLabel() fuer konsistente Stunden-Labels

URL-State & Caching

  • URL-bound Properties (#[Url]) fuer category, mode, month → teilbare Links
  • Server-Cache via Cache::remember('public:yt-pub:{category}:{mode}:{month}', 3600, …) pro Filter-Kombination
  • Cache-Invalidierung nach Aggregation via DELETE WHERE key LIKE 'public:yt-pub:%' auf der Database-Cache-Tabelle (Postbox nutzt CACHE_STORE=database)

Mode-Filter (regular/shorts/all) — Schema und Datenfluss

Der Mode-Filter wirkt seit 2026-05-25 ueberall (Counts, Heatmap, VFR/Engagement/L2V-Charts, KPI-Boxen). Vorher beruehrte er nur die Counts, weil der Aggregator Shorts via continue uebersprungen hat und keine parallelen Metrik-Spalten existierten.

Parallele Spalten in youtube_publishing_weekly_stats:

RegularShorts (Parallel)
hour_distribution (jsonb)shorts_hour_distribution (jsonb)
vfr_medianshorts_vfr_median
vfr_p25shorts_vfr_p25
vfr_p75shorts_vfr_p75
engagement_rate_medianshorts_engagement_rate_median
like_to_view_ratio_medianshorts_like_to_view_ratio_median
best_hour_utcshorts_best_hour_utc

youtube_publishing_monthly_snapshots hat dieselben Parallel-Spalten (ausser _p25/_p75, die monthly nicht aggregiert werden).

Aggregator-Logik: AggregateYouTubePublishingStats::processProfile() berechnet alle Metriken symmetrisch fuer beide Video-Typen. Die Per-Wochentag-Aggregation lebt in einem WeekdayAgg-DTO (app/Support/YouTubePublishing/WeekdayAgg.php) mit explizit getypten public Properties (regularCount, shortsCount, hours, vfr, vfrByHour, shortsHours, shortsVfr, shortsVfrByHour, etc.) — vorher als mixed-Array, das PHPStan in verschiedenen Versionen unterschiedlich narrow-getrackt hat. shorts_best_hour_utc wird per derselben VFR-by-Hour-Logik wie regulaere Videos ermittelt.

Monthly-Snapshot-Rollup: Zwei separate CTE-Stages (regular_hours + shorts_hours) — jede summiert in einer Subquery zuerst pro (weekday, hour_key) über alle Channels (GROUP BY weekday, hour_key mit SUM) und faltet das Ergebnis erst dann mit jsonb_object_agg(hour_key, hour_total) zu einem Stunden-Objekt mit eindeutigen Keys. Die CTEs werden per LEFT JOIN auf weekday zusammengeführt. (Diese SUM-vor-Aggregat-Reihenfolge ist zwingend — siehe Hinweis unter "Monatliche Snapshots".)

Livewire-Component: modeColumnMap() liefert je nach $this->mode die richtigen Spalten-Namen — regular/all → reguläre Spalten, shortsshorts_*-Spalten. mode=all wird als regular behandelt, weil ein Summieren der Veröffentlichungs-Stunden zwischen Regular und Shorts die unterschiedlichen Signal-Kanäle mischen würde (Standard-YT vs Kurzform haben inhärent andere Upload-Zeiten). heatmapFromSnapshots() teilt die gespeicherte Channel-Summe durch MAX(s.channel_count) (= Ø Videos pro Channel), konsistent mit dem Live-Pfad heatmapFromLive(), der direkt SUM(...)/COUNT(*) über die weekly_stats-Rows bildet.

OG-Image: YouTubePublishingAnalyticsOgRenderer::loadHeatmap() ist mode-aware — bei mode=shorts wird die shorts_hour_distribution-Spalte genutzt, sonst hour_distribution (konsistent mit dem modeBadge des Vorschaubilds).

SEO

  • Sitemap-Eintrag (static/sub-sitemap) wird automatisch gesetzt wenn Feature-Flag aktiv (SitemapGenerator::collectStaticUrls())
  • hreflang-Alternates zwischen de und en via writeUrlSet() automatisch
  • Dynamische OG-Image (SVG) pro Filter-Kombination
  • Strukturierte Daten (FAQ-Schema) im Template

Monitoring

HeartbeatCommandScheduleRetry-Command
youtube_publishing_statsyoutube:aggregate-publishing-statsMo 03:00 UTC (woechentlich)identisch

Heartbeat in config/postbox.php mit max_minutes, label und sla_weight eingetragen und in DailyPipelineStatus (Admin-UI) mit Retry-Command hinterlegt.

Dateien

  • app/Livewire/Tools/YouTubePublishingAnalytics.php — Livewire-Komponente
  • resources/views/livewire/tools/youtube-publishing-analytics.blade.php — View + Alpine-Component
  • app/Console/Commands/AggregateYouTubePublishingStats.php — Aggregation + Monatliche Snapshots + Cache-Invalidierung
  • app/Services/Sitemap/SitemapGenerator.php — Sitemap-Integration
  • routes/console.php — Scheduler
  • config/postbox.php — Feature-Flag + Heartbeat-Konfiguration
  • lang/de.json, lang/en.json — Uebersetzungen

Env

# Oeffentliche YouTube Publishing Analytics Seite aktivieren/deaktivieren.
# Bei false: Route 404, Sitemap-Eintrag entfaellt. Default: true.
POSTBOX_PUBLIC_PUBLISHING_ANALYTICS_ENABLED=true