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>
48 lines
1.5 KiB
PHP
48 lines
1.5 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Http\Controllers\VideoController;
|
||
use App\Models\Video;
|
||
use Illuminate\Console\Command;
|
||
|
||
/**
|
||
* Renders the 1200×630 VS composition PNG for a match video's social-share
|
||
* preview. Runs the same helper used by VideoController@ogImage but from a
|
||
* CLI process context, because Chromium reliably fails to launch under
|
||
* PHP-FPM ("Failed to launch the browser process: Code: null") on this
|
||
* host. Invoke detached via `nohup` from either the ogImage endpoint (on
|
||
* cache miss) or SportsMatchController on save.
|
||
*/
|
||
class RenderMatchOgImage extends Command
|
||
{
|
||
protected $signature = 'og:render-match {video}';
|
||
protected $description = 'Render the OG social-share PNG for a match video';
|
||
|
||
public function handle(): int
|
||
{
|
||
$video = Video::find($this->argument('video'));
|
||
if (! $video) {
|
||
$this->error("Video {$this->argument('video')} not found.");
|
||
return 1;
|
||
}
|
||
if ($video->type !== 'match' || ! $video->sportsMatch) {
|
||
$this->error("Video {$video->id} is not a match with a sports record.");
|
||
return 1;
|
||
}
|
||
|
||
$controller = new VideoController();
|
||
$r = new \ReflectionMethod($controller, 'renderMatchOgImage');
|
||
$r->setAccessible(true);
|
||
$png = $r->invoke($controller, $video);
|
||
|
||
if ($png === null) {
|
||
$this->error("Render failed. See laravel.log.");
|
||
return 1;
|
||
}
|
||
|
||
$this->info("Rendered " . strlen($png) . " bytes for video {$video->id}.");
|
||
return 0;
|
||
}
|
||
}
|