abordage/laravel-og-images
A Laravel package for generating Open Graph images for social sharing. Define templates, render dynamic titles and metadata, and serve cached OG images for pages, posts, and products with minimal setup.
The package wraps Browsershot/Puppeteer so you can turn a Blade view into a PNG/JPEG/WebP — typically a 1200×630 Open Graph card — without hand-rolling Chrome invocation.
Minimal setup:
composer require abordage/laravel-og-imagesphp artisan vendor:publish --tag="og-images-config" (and the views tag if you want a starting template to copy).config/og-images.php (node/npm/chrome) or the matching env vars if your binaries live outside PATH.First use case to try: render a title/subtitle card from a controller and dump it to disk.
use Abordage\OgImages\Facades\OgImage;
OgImage::view('og-images.post')
->data(['title' => $post->title, 'author' => $post->author->name])
->save("og/{$post->slug}.png");
Then reference the absolute URL in your layout:
<meta property="og:image" content="{{ asset("og/{$post->slug}.png") }}">
Where to look first: the published config (binary paths, disk, route settings), the published Blade template (that's the whole design surface), and the OgImage facade for the fluent builder (view(), data(), save(), toResponse(), toHtml()).
Generate on write, not on read. The most common production shape is an observer or model event that dispatches a queued job after save. Rendering takes hundreds of milliseconds to a few seconds (Chrome cold start), which is too slow for a page request but trivial in a queue.
class PostObserver
{
public function saved(Post $post): void
{
GenerateOgImage::dispatch($post)->onQueue('images');
}
}
Store the resulting path (or a hash of the inputs) in a column so you can render the meta tag without touching the filesystem, and so you know when to regenerate.
Cache-bust with content hashes, not timestamps. Social platforms cache aggressively. Filenames like og/{slug}-{sha1($title.$subtitle)}.png mean a title edit produces a new URL and the crawler picks it up; a stable filename means you're stuck with the old card for days.
On-the-fly route + HTTP caching. If you enable the package route, the pattern is: signed or slug-addressed URL → render → cache the response with a long Cache-Control and an ETag. This is convenient for large sites where storing thousands of PNGs is annoying, but you must put a cache in front of it or every crawler hit spawns a Chrome process.
Template as a first-class artifact. Keep the OG Blade template dumb: inline CSS or a single compiled stylesheet, web-safe/system fonts or self-hosted fonts referenced by absolute path, no JS, no external network calls. Anything that depends on the request lifecycle (auth, session, locale) should be passed in explicitly via data().
Testing. Don't shell out to Chrome in CI unless you already have a Chrome-enabled runner. Assert on toHtml() for template correctness and use Bus::fake() to assert the job is dispatched. Cover the job itself with a single integration test tagged/skipped when the binary is missing.
Fallbacks matter. Always ship a static default OG image. If generation fails (missing font, OOM, Chrome crash), the meta tag should point at something valid rather than 404 — crawlers will happily cache the 404.
^1.1.0 and don't expect regressions to be caught for you. On Laravel 12+ this release is a no-op.--no-sandbox; Browsershot exposes this via its Chrome arguments. Symptoms are silent timeouts or "Failed to launch the browser process" — check the process stderr, not the Laravel log.og:image must be fully qualified. Use asset()/url() and set APP_URL correctly in every environment — a http://localhost OG image is the classic staging-leak bug.toHtml() on the builder and inspect the markup before blaming Chrome. If the HTML is right, the problem is binary paths, fonts, or sandbox flags — in that order.resources/views/og-images/), the config lets you swap disk/path/route prefix without touching code, and data() is the seam for injecting anything the template needs. Keep the template free of queries so it stays cacheable and testable.s3/object-store disk and let lifecycle rules handle it.How can I help you explore Laravel packages today?