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

spatie/laravel-og-image

Generate Open Graph images for your Laravel app by defining OG image HTML inline in Blade views. The package screenshots them, serves them via a dedicated route, and caches them on disk, inheriting your existing CSS, fonts, and Vite assets.

View on GitHub
Deep Wiki
Context7

Getting Started

What it is, in one sentence: you write the OG image as a Blade template inside your page, and the package screenshots that HTML at 1200×630, stores the JPEG on your public disk, and rewrites your <head> meta tags to point at it — no external image API, no Figma exports, no hand-maintained og/*.jpg files.

Install and the three prerequisites people forget:

composer require spatie/laravel-og-image
php artisan vendor:publish --tag="og-image-config"   # optional but you'll want it

The package delegates rendering to spatie/laravel-screenshot, which wraps Browsershot. That means production needs Node.js, npm, and a Chrome/Chromium binary on the server. This is the single biggest adoption cost — check with your hosting setup before you build the feature, not after. Serverless/containerised environments without Chromium are the usual blocker; the config supports Cloudflare Browser Rendering as an alternative renderer if you can't ship a browser binary.

Where to look first: the config file after publishing. It's where dimensions, output format, disk, and the renderer live. Then the ?ogimage preview — it is your entire development loop.

First use case — a blog post page. Put the component in the post view, not the layout:

{{-- resources/views/posts/show.blade.php --}}
<x-og-image>
    <div class="w-full h-full bg-blue-900 text-white flex flex-col justify-center p-16">
        <h1 class="text-6xl font-bold leading-tight">{{ $post->title }}</h1>
        <p class="mt-6 text-2xl opacity-80">{{ $post->author->name }}</p>
    </div>
</x-og-image>

Then verify without opening Facebook's debugger:

https://your-app.test/posts/my-post?ogimage

That renders only the template content, with your page's real CSS, at 1200×630. If it looks right in the browser, the JPEG will look right. Iterate on the design there — it's a normal page render, so your Vite HMR still works.

What you actually ship: a hidden <template data-og-image> in the body plus <meta property="og:image" ...> tags pointing at /og-image/{md5}.jpeg. The middleware is auto-registered (no manual Kernel.php edit since 1.0.0), so the wiring is genuinely zero-config once the browser binary exists.

Sanity checklist before you trust it:

  1. ?ogimage renders the design.
  2. curl -I https://your-app.test/og-image/<hash>.jpeg returns 200 with a JPEG content type.
  3. The file exists on the configured disk.
  4. The page's raw HTML contains the og:image meta tag with the hashed URL.

Implementation Patterns

Treat the OG template as a component, not inline markup. The first thing that rots in this package is a 40-line Tailwind block copy-pasted into twelve views. Extract it:

{{-- resources/views/components/og/card.blade.php --}}
@props(['title', 'subtitle' => null, 'eyebrow' => null])

<x-og-image>
    <div class="w-full h-full bg-slate-900 text-white flex flex-col justify-between p-20 font-sans">
        <div class="text-2xl uppercase tracking-widest text-indigo-400">{{ $eyebrow }}</div>
        <h1 class="text-6xl font-black leading-none">{{ $title }}</h1>
        @if ($subtitle)
            <p class="text-3xl text-slate-400">{{ $subtitle }}</p>
        @endif
    </div>
</x-og-image>

Then <x-og.card :title="$post->title" ... /> per view. One file to redesign, one file to review in a PR.

Lean on your existing CSS — that's the whole selling point. Because the screenshot visits the real page URL in a real browser, Tailwind, custom fonts loaded via @font-face, and Vite-compiled assets all apply. The practical consequence: you must not put the component on a page that lacks the styles. If your OG card uses Tailwind classes, the page rendering it must load the Tailwind bundle. Don't attach it to a bare error page or a mail-preview route.

Use url= for anything you can't or won't screenshot. Since 1.2.0:

<x-og-image url="{{ asset('og/static-landing.jpg') }}" />

This is the right escape hatch for: marketing landing pages where you want a designed asset, pages behind auth where the screenshot crawler would get a 302 to login, and high-traffic pages where you don't want a Chromium launch in the request path. A good default policy: generate for the long tail of blog/docs/product pages, hardcode for the handful of flagship pages.

Prefer layout-level fallbacks and page-level overrides. The package supports a fallback for pages that don't declare a template. Register the site-wide default once (in the layout or config), then override on the pages that deserve bespoke art. This gives every shareable URL some image instead of a blank card on LinkedIn.

Watch the render path for authenticated pages. The generator visits your page URL server-side with ?ogimage appended. If the page requires a session, the internal request won't have one, and you'll screenshot a login redirect. Either use url= on those pages, or make sure the ?ogimage render path is reachable without auth. This is the #1 silent failure and it produces an image that "works" — it's just a screenshot of your login screen.

Sizing and format are config, not code. Keep the design at the documented default 1200×630 (that's the format every platform crops gracefully). If you change dimensions for a special case, verify with the platform debuggers — Twitter/X and LinkedIn crop differently, and a 1200×1200 image gets truncated in the timeline card.

Deployments: the generated JPEGs live on a disk. If your disk is ephemeral (typical container deploys), images regenerate on first request after each deploy. That's acceptable but adds latency to the first hit; if it isn't, point the disk at persistent storage or a CDN-backed bucket. Content-hashed filenames mean you can safely serve Cache-Control: immutable at the CDN — the package already sets cache headers, and the URL changes whenever the HTML changes.

Testing: assert on the meta tags your page emits, not on the JPEG bytes. The middleware and the og:image URL pattern are stable; the pixels are not, and a Browsershot dependency in CI will make your suite slow and flaky.

Gotchas and Tips

Cache invalidation is content-hash based — understand this before you panic. The URL is md5(html). Change the template, get a new hash, get a new image, no cache-busting needed. Change nothing, keep the old file forever. Two consequences: (a) you never see stale images on a page you edited, (b) your disk accumulates one JPEG per historical version of every template. Prune occasionally; there's no reason to keep a 2023 design.

Changing the design doesn't change the hash if the HTML is identical. If you tweak only CSS — a Tailwind class in a stylesheet, a font weight in app.css — the hash doesn't move, and the old JPEG keeps serving. This is the classic "why won't my redesign show up?" You must touch the template markup (or delete the stored file) to force regeneration. Keep a trivial change handy, or clear the og-image directory when you reskin.

?ogimage is a preview mode, not a public feature — but it is public. It renders your template on any URL. If your template renders user-supplied content, be conscious that it's reachable and that hitting it repeatedly can trigger Chromium launches. Rate-limit or gate the preview behind a signed URL / local env if that matters to you.

The Blade content in the component is not re-compiled (fixed in 1.3.2). User content containing {{ }}, @each, or similar no longer explodes. If you're on an older version and a post title with braces wrecks the page, that's this bug — upgrade.

Query strings. Pages served with existing query parameters (?page=2, UTM-tagged URLs) had broken image generation in early versions; fixed in 1.1.3/1.1.4 and correctly re-released as 1.3.1. Pin >=1.3.1 — the version-numbering hiccup (fixes published as 1.1.x below 1.3.0, so Composer kept resolving 1.3.0) means a loose constraint can silently give you the buggy build. Also note every distinct query string is a distinct page URL, so decide whether you want OG images on paginated/filtered URLs at all.

Fallback + non-HTML responses = cache poisoning (fixed in 1.1.1). If you use a fallback image and the middleware runs over an RSS/Atom feed or any response without </body>, older versions cached a bogus URL against the shared fallback hash — permanently. Symptoms look like "all my fallback images broke at once." Confirm your version and, if you serve feeds, be aware this was a real failure mode.

Middleware interference. The middleware rewrites response content. Release 1.1.2 fixed it destroying $response->original; if you have other middleware or tests inspecting the original response object, stay current.

Debugging order when an image is wrong or 404s:

  1. Append ?ogimage — does the design render? If not, it's CSS/Blade, not the package.
  2. Does the og:image meta tag have a hash in the URL? If it's missing entirely, the component isn't being rendered (wrong view, wrong section, @once collision).
  3. curl -I the /og-image/{hash}.jpeg URL — 404 means the file isn't on the disk; check the configured disk and storage permissions/symlink.
  4. tail the Laravel log — Browsershot failures (missing Chrome path, Node not on the web user's PATH, timeouts) throw loudly. The single most common production error is the web server user not being able to execute Node.

Browsershot is the operational risk, not the package. Chromium launch is ~200–800ms and memory-hungry. On a small VPS with traffic spikes, the first-hit-per-image generation can exhaust workers. Mitigations: url= for flagship pages, a queue-backed warmer that hits likely image URLs after deploy, and a memory ceiling on the FPM pool that accounts for Chromium.

Fonts must load synchronously enough for the screenshot. Remote font CDNs occasionally lose the race with the capture, producing a fallback-font image. Self-host critical fonts; the "it looks fine for me locally and wrong on the shared card" report is almost always a font-loading timing issue.

Extension point: the actions behind generation and storage are bound in the container and documented as overridable. Reach for them — rather than forking — when you need to change where files land (S3, CDN origin) or post-process the image. Similarly the renderer is swappable (Browsershot ↔ Cloudflare Browser Rendering) in config, which is the clean answer if your platform won't run Chromium.

If you use Laravel Boost, the package ships a skill (1.1.0) that teaches your assistant the correct component API — worth having on, since the API surface (component attributes, config keys) is small enough that a model will confidently invent method names otherwise.

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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log