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_distributiondadurch auf die Verteilung eines einzelnen Channels (Monats-Heatmap fast leer). Deshalb MUSS zuerst pro(weekday, hour)summiert werden, bevorjsonb_object_agg()die bereits eindeutigen Paare zusammenfasst. Die gespeichertehour_distributionenthält damit die Summe über alle Channels; der Reader (heatmapFromSnapshots) teilt für "Ø Videos pro Channel" durch die in der Snapshot-Row gespeichertechannel_count(nicht durchCOUNT(*), 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:
| Bereich | Inhalt |
|---|---|
| Hero | Ueberschrift, Trust-Signal (Video-Anzahl), Data-Freshness |
| Filter-Bar | Kategorie (Dropdown), Video-Typ (Pill-Toggle: Regulaer [Default] / Shorts / Alle), Zeitraum (Monat-Dropdown) |
| KPI-Cards | Bester 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-Badges | Top-Stunden mit Count, formatiert in gewaehlter Zeitzone |
| Share-Section | Twitter/X, LinkedIn, WhatsApp, Copy-Link |
| FAQ-Accordion | 6 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 aufApexCharts-Global (CDN-Load), rendert Charts + HeatmaprenderCharts()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() / 60verschobenlivewire:navigatedEvent-Listener re-rendert nach SPA-NavigationformatHour(hour, mode)/tzLabel()fuer konsistente Stunden-Labels
URL-State & Caching
- URL-bound Properties (
#[Url]) fuercategory,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 nutztCACHE_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:
| Regular | Shorts (Parallel) |
|---|---|
hour_distribution (jsonb) | shorts_hour_distribution (jsonb) |
vfr_median | shorts_vfr_median |
vfr_p25 | shorts_vfr_p25 |
vfr_p75 | shorts_vfr_p75 |
engagement_rate_median | shorts_engagement_rate_median |
like_to_view_ratio_median | shorts_like_to_view_ratio_median |
best_hour_utc | shorts_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) / Aufrufelike_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:
| Tabelle | Zeitbezug | Praktische Folge |
|---|---|---|
youtube_publishing_monthly_snapshots | ein Kalendermonat | sauber trennbar — nur der August 2026 ist mixed |
youtube_publishing_weekly_stats | gleitendes Fenster von 365 Tagen | vom 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
deundenviawriteUrlSet()automatisch - Dynamische OG-Image (SVG) pro Filter-Kombination
- Strukturierte Daten (FAQ-Schema) im Template
Monitoring
| Heartbeat | Command | Schedule | Retry-Command |
|---|---|---|---|
youtube_publishing_stats | youtube:aggregate-publishing-stats | Mo 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-Komponenteresources/views/livewire/tools/youtube-publishing-analytics.blade.php— View + Alpine-Componentapp/Console/Commands/AggregateYouTubePublishingStats.php— Aggregation + Monatliche Snapshots + Cache-Invalidierungapp/Services/Sitemap/SitemapGenerator.php— Sitemap-Integrationroutes/console.php— Schedulerconfig/postbox.php— Feature-Flag + Heartbeat-Konfigurationlang/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