Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Og Image Laravel Package

backstage/laravel-og-image

Generate dynamic Open Graph images for your Laravel application. Create social share previews for pages, posts, and products so links stand out on Facebook, X, LinkedIn, and other platforms.

View on GitHub
Deep Wiki
Context7

Getting Started

The package wraps headless-Chrome rendering into a Laravel route so you can turn a Blade template into a 1200×630 PNG on demand. Treat it as "Blade in, image out."

Install and publish:

composer require backstage/laravel-og-image
php artisan vendor:publish --tag=og-image-config
php artisan vendor:publish --tag=og-image-views

Check that a browser binary is available (the package relies on a Puppeteer/Browsershot-style renderer; node and a Chromium build must exist on the host). In local dev, npx puppeteer browsers install chrome is usually enough.

Look here first, in this order:

  1. config/og-image.php — view path, width/height, browser binary path, cache settings, queue options.
  2. resources/views/vendor/og-image/ — the published default template you'll be editing.
  3. src/OgImageServiceProvider.php and any routes/ it registers — confirms whether a route is auto-registered or you wire your own.
  4. The README's "Usage" section for the facade/helper signature; that's the only API surface you'll touch day-to-day.

First use case — a blog post card:

// routes/web.php
use Backstage\OgImage\Facades\OgImage;

Route::get('/og/posts/{post:slug}', function (Post $post) {
    return OgImage::view('og.posts.show', [
        'title'  => $post->title,
        'author' => $post->author->name,
        'tag'    => $post->category,
    ]);
})->name('og.posts.show');
{{-- resources/views/og/posts/show.blade.php --}}
<div style="width:1200px;height:630px;box-sizing:border-box;padding:64px;
            background:#0f172a;color:#fff;font-family:Inter,sans-serif;
            display:flex;flex-direction:column;justify-content:space-between;">
    <span style="font-size:28px;opacity:.7;">{{ $tag }}</span>
    <h1 style="font-size:72px;line-height:1.1;margin:0;">{{ $title }}</h1>
    <span style="font-size:32px;opacity:.8;">{{ $author }}</span>
</div>

Then in your layout:

<meta property="og:image" content="{{ route('og.posts.show', $post) }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">

Visit /og/posts/my-post — you should get a Content-Type: image/png response. Then run php artisan og-image:warm (new in v1.4.0) to pre-render everything before real traffic hits the route.

Implementation Patterns

One template per content type. Don't try to make a universal card. Post cards, product cards, author cards, and category cards have different information density and different font scales. Keep them as separate Blade views and separate routes.

Generate at request time vs. pre-render. For a blog with hundreds of posts, on-demand is fine (cache headers do the work). For a storefront with 50k SKUs, pre-render on save:

// PostObserver
public function saved(Post $post): void
{
    dispatch(new GenerateOgImage($post))->onQueue('images');
}

The job calls the same OgImage::view(...) renderer, writes to storage/app/public/og/{slug}.png, and the route just serves the file. Keep a synchronous fallback route so a missing file regenerates rather than 404s.

Use og-image:warm for the bulk pass. v1.4.0 ships php artisan og-image:warm, which renders the images for all pages in advance. This replaces the hand-rolled "iterate every model and dispatch a job" command you'd have written before. Use it in three places:

# deploy hook: regenerate everything after a template/layout change
php artisan og-image:warm

# scheduled: catch content that slipped past observers
$schedule->command('og-image:warm')->weekly();

For genuinely large catalogs, still prefer the observer/job path per save (bounded work per write), and use warm as a reconciliation/sweeper. Check the command's options in src/Console/ before assuming it supports batching or queuing — the release note doesn't say, and the config is the contract.

Cache with a version token. Point your CDN at the OG route and bust the cache with a content hash:

route('og.posts.show', ['post' => $post->slug, 'v' => $post->updated_at->timestamp])

Also set Cache-Control: public, max-age=86400, s-maxage=604800 — but only if the URL is versioned, otherwise you'll serve stale cards after edits. Combine with stale-while-revalidate for the nice case where the user sees an old image while the new one renders.

Localization via query string. A single template that swaps strings based on ?locale=fr is cheaper than ten routes. Pass app()->setLocale($request->query('locale', 'en')) inside the closure before rendering.

Signed, rate-limited routes. OG endpoints are public by design and expensive to run. Sign them so nobody can hammer the renderer:

Route::get('/og/{post}', ...)->middleware(['signed'])->name('og.posts.show');
// and generate with URL::signedRoute('og.posts.show', $post)

og:image crawlers (Twitter, Slack, Facebook) follow signed URLs fine — the signature is just a query param.

Compose from Blade components. Fonts, logos, and layout primitives reused across templates belong in resources/views/components/og/…. The renderer doesn't care about component nesting, and it keeps font sizes consistent across card types.

Static analysis now works. v1.4.0 lets Larastan boot the package, so PHPStan/Larastan resolves the package's views instead of reporting unknown-view errors. Install Larastan in your app and run it over your OG views and the service-provider wiring — a broken view name or a missing variable passed to OgImage::view() is now catchable in CI instead of at render time. This is the cheapest way to catch "blank white PNG" bugs before deploy.

Testing. It's an HTTP route; test it like one:

public function test_og_image_renders(): void
{
    $post = Post::factory()->create();
    $this->get(route('og.posts.show', $post))
        ->assertOk()
        ->assertHeader('Content-Type', 'image/png');
}

Assert on the response body's PNG magic bytes (\x89PNG) to catch "HTML error page with 200 status" regressions, which happen more often than you'd expect. Add a smoke test that runs og-image:warm against a handful of records so the command path is covered too.

Gotchas and Tips

Chrome isn't on your production box. This is the #1 deployment surprise. Add Chromium to your Dockerfile or install puppeteer in a build step — Browsershot-style renderers fail with a cryptic spawn chrome ENOENT otherwise. Set OG_IMAGE_CHROME_PATH (check the config file for the exact key) to the binary and bake it into your image.

Fonts must exist on the server, not the CDN. If your template says font-family: Inter and Inter is loaded via Google Fonts, the renderer may screenshot before the webfont arrives, giving you Times New Roman. Either install the font in the OS image (apt-get install fonts-inter) or inline a base64 @font-face. Same for emoji — install fonts-noto-color-emoji or your 🚀 will be a tofu box.

Avoid @vite in the template. The renderer loads the view in a fresh browser context; asset URLs resolved relative to a non-existent host will 404. Inline all CSS in a <style> tag or a single @push. Tailwind users: compile a tiny "og" entry that only contains the utility classes your templates use, and inline that.

Cold starts are slow (1–3s). The first render after a deploy pays browser launch cost. v1.4.0's php artisan og-image:warm is the direct answer: run it as the last step of your deploy, after config:cache and route:cache, so the first real visitor gets a cached image instead of a cold render. Otherwise keep a warm browser process (if the package exposes a persistent instance), or accept it behind a cache. Never put this in the critical path of a page render — always link to the image URL, never generate inline.

og-image:warm will hammer your box. It renders every page sequentially in one process. On a small container this is exactly the workload that OOMs. Run it off-peak, check whether the command has a queue/batch/chunk option, and if it doesn't, wrap it: php artisan og-image:warm in a deploy hook that runs after traffic has drained, not inline in a rolling deploy where old and new instances both warm. Watch the deploy timeout too — thousands of renders × 2s each is a long deploy step.

Exact viewport sizing matters. 1200×630 is the OG standard; Twitter uses 1.91:1 so 1200×630 is fine, but a template with width:100% and unbounded content will overflow. Set explicit pixel dimensions on the root element, use box-sizing:border-box, and line-clamp long titles (-webkit-line-clamp: 3). Otherwise a 200-character headline renders as one clipped line.

Escape user content. Blade {{ }} escapes by default — don't reach for {!! !!} to allow bold in titles unless you sanitize first. A post title containing <script> won't execute in an image, but it can break layout or inject markup into the headless page.

config:cache in production. If your config file calls env(), it works, but if you call env('OG_IMAGE_...') from a service provider or template, it returns null after config:cache. Read from config('og-image.…') everywhere, and add the env key to config/og-image.php if it isn't already exposed.

Middleware kills crawlers. Don't put auth, verified, throttle:web, or CSRF on OG routes. Twitterbot and Slackbot have no session and will get a 302 to /login, and you'll get an image-less card. Keep these routes in a separate group with cache.headers only.

Debugging: dump to disk. When a render looks wrong, save it locally instead of staring at a broken URL in devtools:

OgImage::view('og.posts.show', $data)->save(storage_path('og-debug.png'));

Then open the PNG. If the PNG is blank white, the view failed to parse; if it has unstyled text, an asset failed to load; if it's truncated, viewport is wrong. Each symptom points to a specific cause, so it's faster than reading stack traces.

Memory limits. Chrome is memory-hungry. On a 512MB container, concurrent renders will OOM — and og-image:warm makes this a scheduled event rather than a random one. Either queue image generation or add a small semaphore (Cache::lock('og-render', 3)) around the render call.

Extension points. The package's config usually exposes the view path, dimensions, and browser options — those are your safe customization surface. Anything in src/ you modify will be overwritten on composer update. If you need custom behavior (e.g. a logo overlay post-process), wrap the generated image with intervention/image in your own controller rather than forking the package; you keep upgrade paths and the package stays a thin renderer.

Zero dependents means zero Stack Overflow. With only a handful of stars, expect to read the source. src/ is small enough that grep -r "function " src/ gives you the whole API in a minute — do that before assuming a feature exists. The README is likely the only documentation, so treat the config file as the contract. The v1.4.0 changelog is terse (two bullet points, no command options documented), so read src/Console/ for og-image:warm's actual signature — that's faster than guessing flags.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo