Match videos: composed VS-card OG preview + richer share metadata

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) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-08-11 12:34:40 +03:00
1 parent 665bad76bf
commit 3e18bd2150
14 files changed
+3562 -21

No files matched your search

@@ -0,0 +1,40 @@
@extends('admin.layout')
@section('title', 'OG Previews')
@section('content')
<div style="padding: 24px;">
<h1 style="margin: 0 0 8px; color: #fff;">Generated VS Cards</h1>
<p style="color: #aaa; margin: 0 0 20px;">
These are the 1200×630 PNGs that WhatsApp, Facebook, and Twitter show
when a sports match video is shared. Generated on save; regenerated on
edit. Filename is <code>match-{video_id}-{updated_at}.png</code>.
</p>
@if($files->isEmpty())
<p style="color: #aaa;">No previews cached yet. Save a match video to generate one.</p>
@else
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 20px;">
@foreach($files as $f)
<div style="background: #1a1a1a; border: 1px solid #333; border-radius: 12px; overflow: hidden;">
<a href="{{ route('admin.og-previews.file', $f['name']) }}" target="_blank" style="display: block; background: #000;">
<img src="{{ route('admin.og-previews.file', $f['name']) }}"
alt="{{ $f['name'] }}"
style="display: block; width: 100%; height: auto;">
</a>
<div style="padding: 12px 14px; color: #ccc; font-size: 13px;">
<div style="font-weight: 600; color: #fff;">Video #{{ $f['id'] }}</div>
<div style="color: #888; margin-top: 4px; font-family: monospace; font-size: 11px; word-break: break-all;">
{{ $f['name'] }}
</div>
<div style="color: #888; margin-top: 4px;">
{{ number_format($f['size'] / 1024, 1) }} KB &middot;
{{ \Carbon\Carbon::createFromTimestamp($f['mtime'])->diffForHumans() }}
</div>
</div>
</div>
@endforeach
</div>
@endif
</div>
@endsection