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: Regulaer [Default] / Shorts / Alle), 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.

Der Video-Typ-Umschalter startet standardmäßig auf Regulär (nicht „Alle"). Grund: Geklumpte Shorts-Upload-Zeiten einzelner Ausreißer-Kanäle verzerren sonst Heatmap und Charts; die Standardansicht zeigt so das echte Publishing-Muster regulärer Videos. Ungültige mode-Werte fallen serverseitig (mount() / updatedMode()) auf regular zurück. Shorts und Alle bleiben umschaltbar, der gewählte Modus ist über den mode-URL-Parameter teilbar.

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, shorts → shorts_*-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).

Zählweise der veröffentlichten Zahlen​

Die hier gezeigten Benchmarks beruhen auf view_count aus der YouTube Data API. YouTube hat am 24.08.2026 geändert, was ein Aufruf ist — seitdem zählt er ab dem ersten Frame, ohne Mindestwiedergabezeit (Details: YouTube-Pipeline).

Warum das hier besonders zählt​

Zwei der drei Kernkennzahlen haben Aufrufe im Nenner:

  • engagement_rate_median = (Likes + Kommentare) / Aufrufe
  • like_to_view_ratio_median = Likes / Aufrufe

Der Nenner wächst ab dem Stichtag, der Zähler nicht — Likes und Kommentare ändern sich nicht. Sämtliche Engagement-Raten sinken deshalb strukturell, während die Views-pro-Follower-Ratio (vfr_median) gleichzeitig steigt. Am Nutzerverhalten hat sich dabei nichts geändert. Das ist der subtilste Effekt der Umstellung und der einzige, der ohne Kennzeichnung wie ein echter Trend aussieht.

Wie gekennzeichnet wird​

Die Zählweise ist ein Attribut der Daten, keine Fussnote in der Ansicht. Beide Aggregat-Tabellen tragen die Spalte view_counting_method, gesetzt beim Aggregieren:

TabelleZeitbezugPraktische Folge
youtube_publishing_monthly_snapshotsein Kalendermonatsauber trennbar — nur der August 2026 ist mixed
youtube_publishing_weekly_statsgleitendes Fenster von 365 Tagenvom 24.08.2026 bis 24.08.2027 durchgehend mixed

Der zweite Fall ist der, den man unterschätzt: Bei einem Jahresfenster ist „gemischt" kein Randfall, sondern der Dauerzustand für zwölf Monate.

Beide Seiten werden beschriftet, nicht nur die neue. Nur die neue zu kennzeichnen würde die ältere Hälfte stillschweigend zum Normalfall erklären — und der arglose Vergleich zwischen beiden ist genau der Schaden, den die Kennzeichnung verhindern soll.

Zeilen ohne Wert (aus der Zeit vor dieser Kennzeichnung) zeigen kein Etikett. Eine geratene Zählweise wäre schlechter als gar keine Angabe.

Was ausdrücklich nicht passiert​

Historische Werte werden nicht umgerechnet. Es gibt keinen belastbaren Faktor zwischen alter und neuer Zählweise — er hängt von Format, Kanalgrösse und Publikum ab. Ein aus einem Durchschnitt hochgerechneter Einzelwert sähe aus wie eine Messung und wäre keine. Die historischen Zahlen sind für ihren Zeitraum korrekt und bleiben unverändert stehen.

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