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>
81 lines
3.5 KiB
PHP
81 lines
3.5 KiB
PHP
{{-- ══════════════════════════════════════════════════════════════════════
|
||
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>
|