Sports match share links now show the full VS composition (fighters,
flags, event, weight class, referee/court/match chips) in WhatsApp /
Telegram / Facebook / Twitter previews instead of the raw video frame.
- Browsershot + puppeteer-bundled Chromium screenshots a dedicated
1200x675 (true 16:9) render of the vs-mini composition. Chrome runs
through a small wrapper (bin/browsershot-node.sh) that raises the fd
limit and reads its full env, since PHP-FPM's stripped env otherwise
makes Chromium fail to launch.
- Render is JPEG @ q85 (~100 KB) instead of PNG (~700 KB) — WhatsApp
silently drops OG images above roughly 300 KB, which is why music
thumbnails always showed and match cards didn't.
- SportsMatchController@store/update renders synchronously so the
picture is guaranteed to exist before the user copies the share URL;
edits regenerate and old stamped variants are cleaned. The og:image
URL carries a ?v={updated_at} stamp so scrapers recache on edit.
- accessShare() (/s/{token}) now detects scraper user agents and serves
the video page HTML inline instead of a 302 — some crawlers don't
follow redirects when building link previews.
- og:description / twitter:description built from headerData(): flag
emoji + fighter names (blue vs red), event, weight, match#, court,
referee, view count, upload date.
- Fixed 3-dot menu on video cards being hidden behind the next card
(:has(.dropdown-menu.show) promotes the active card above siblings
and lifts the overflow:hidden clip on .yt-video-info).
- vs-mini: skip the entrance animation on touch devices and expose a
.vs-mini-static class so the OG render always captures the final
frame instead of a random mid-animation moment.
Also adds an admin gallery at /admin/og-previews for spot-checking the
generated preview JPEGs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SportsMatchController syncs Video.title with SportsMatch.title on
store/update so the "Match title" field drives the video's display
title everywhere.
- headerData(): "championship" now prefers event_name (event title)
and adds a dedicated "stage" key sourced from SportsMatch.title
(shown between the yellow lines above the weight class in both the
in-player VS overlay and the vs-mini gallery card).
- Video cards render match titles with flags + corner colours
("FINALS – 🇧🇭 Blue vs 🇧🇭 Red (-61 kg)"), matching the video
page header, and clip long titles to one line.
- Video gallery grid uses repeat(N, minmax(0, 1fr)) + grid-auto-rows:1fr
so every card thumbnail renders at exactly the same size; hover-preview
videos use object-fit:cover to stop portrait sources looking smaller.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds one chip per language currently present in public content —
counting both primary language and any secondary audio track, sorted
by video count, capped at 12. Clicking a chip lists every video whose
primary is that language or that has a matching secondary track; cards
surfaced only via a secondary track link straight to /videos/{key}?track=<id>
so the pinned playback wiring loads the right audio, get a small flag
badge on the thumbnail, and their hover-preview streams the track file
instead of the primary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The base card CSS (.yt-video-card/.yt-video-thumb/.yt-video-info) lived inside
video-card's @once <style> block, so it only emitted when a video card rendered.
Playlist cards reuse those classes, so on playlist-only pages (no video cards)
they rendered completely unstyled — a mess.
Extract the shared card CSS into resources/views/components/partials/card-styles
(kept @once) and @include it from BOTH video-card and playlist-card. Now the CSS
loads whenever either card renders, and @once still dedupes it to one copy on
pages that show both (home feed verified: emitted exactly once).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>