ghassan 3e18bd2150 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) <noreply@anthropic.com>
2026-08-11 12:34:40 +03:00

81 lines
3.5 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{-- ══════════════════════════════════════════════════════════════════════
VS OG 1200×630 standalone page consumed by Browsershot for the
social-share preview image. Renders the exact same .vs-mini
composition used on video cards, but at fixed 1200×630 with
animations disabled (via .vs-mini-static) so the screenshot always
captures the final composed frame.
Public but noindex only invoked by the internal ogImage()
controller through a loopback HTTP request from Browsershot.
════════════════════════════════════════════════════════════════════════ --}}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OG Preview</title>
<meta name="robots" content="noindex,nofollow">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Anton&family=Barlow+Condensed:wght@400;600;700;800&display=swap">
{{-- Force http:// on the flag CSS URL Laravel's asset() caches the
scheme on first use (https from AppServiceProvider::forceScheme) and
URL::forceScheme('http') in the controller can't override that
cache. Chromium hits this endpoint via loopback where https isn't
reachable, so we build the URL ourselves. --}}
<link rel="stylesheet" href="http://{{ request()->getHost() }}/vendor/flag-icons/css/flag-icons.min.css">
<style>
/* Canvas is a true 16:9 at 1200×675 so the 1920×1080 vs-mini stage
maps cleanly with scale 0.625 (no overflow on either axis). WhatsApp/
Facebook accept any aspect ratio for og:image; 1200×675 is inside
their recommended range and preserves the full VS composition. */
html, body {
margin: 0; padding: 0;
width: 1200px; height: 675px;
background: #050507;
overflow: hidden;
}
.og-frame {
position: relative;
width: 1200px; height: 675px;
overflow: hidden;
}
/* Same wrapper the vs-mini expects — .yt-video-thumb is what the mini's
`position: absolute; inset: 0` anchors to, and its aspect drives fit(). */
.og-frame .yt-video-thumb {
position: relative;
width: 100%; height: 100%;
overflow: hidden;
background: #050507;
}
</style>
</head>
<body>
<div class="og-frame">
<div class="yt-video-thumb">
{{-- vs-mini reads $video->sportsMatch->headerData() to compose. --}}
@include('components.partials.vs-mini', ['video' => $video])
</div>
</div>
<script>
// Force the mini into its final composed state — no animation-delay
// window, no ResizeObserver races. We also inline the correct scale for
// a 1200×630 frame (1200/1920 = 0.625) so we don't depend on the fit()
// callback firing before Browsershot captures.
(function () {
const el = document.querySelector('.vs-mini');
if (!el) return;
el.classList.add('vs-mini-static', 'vs-mini-fit');
el.style.setProperty('--vs-mini-scale', String(1200 / 1920));
// Signal readiness for Browsershot's waitUntilNetworkIdle to observe
// once webfonts are done rendering.
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => { window.__vsOgReady = true; });
} else {
window.__vsOgReady = true;
}
})();
</script>
</body>
</html>