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

The Og Laravel Package

simonhamp/the-og

simonhamp/the-og is a lightweight PHP package for generating Open Graph images. Build customizable social share cards with text, colors, and images, then render them to PNG. Its simple API makes dynamic OG image generation easy to add to PHP and Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements (v0.8.0+): PHP 8.3+, Laravel 10/11/12, and the GD or Imagick PHP extension. The PHP 8.3 and Intervention Image v4 bump means you must upgrade those before upgrading this package.

composer require simonhamp/the-og
php artisan vendor:publish --tag=the-og-config   # publishes config/the-og.php

Where to look first:

  • config/the-og.php — default theme, fonts, image dimensions, and the base path used for saving/generating.
  • src/Themes/ — the shipped themes (Theme::Blog, Theme::Github, Theme::Terminal, etc.). These are the fastest way to see how layouts and boxes compose.
  • src/Interfaces/ — Layout, Box, Theme, Font. These are the contracts you'll implement for custom designs.
  • src/Image.php — the fluent builder you'll use in routes and jobs.

First use case — an OG route for a blog post:

use SimonHamp\TheOg\Image;
use SimonHamp\TheOg\Theme\Theme;

Route::get('/og/{post:slug}.png', function (Post $post) {
    return (new Image())
        ->width(1200)
        ->height(630)
        ->title($post->title)
        ->description($post->excerpt)
        ->url(route('posts.show', $post))
        ->theme(Theme::Blog)
        ->render()
        ->toPng();   // Intervention v4 encoder
})->middleware('cache.headers:public;max_age=3600');

Then wire it into your Blade layout:

<meta property="og:image" content="{{ route('og.post', $post) }}">

Implementation Patterns

Route + cache + CDN (the 90% pattern). Generating an image per request is expensive. Cache to disk keyed by model hash, then let your web server / CDN serve it:

Route::get('/og/{post:slug}.png', function (Post $post) {
    $path = storage_path("app/og/{$post->slug}.png");

    if (! file_exists($path)) {
        (new Image())
            ->title($post->title)
            ->description($post->excerpt)
            ->theme(Theme::Blog)
            ->save($path);
    }

    return response()->file($path);
});

Warm the cache on save. Hook into model events so images exist before social crawlers hit them:

static::saved(fn (Post $post) => GenerateOgImage::dispatch($post));

Custom themes. Build a Theme implementation that composes your fonts, layout, and box stack, then reference it by class instead of using the bundled enum. This is the cleanest extension point — you rarely need to touch Layout or Box unless you want pixel-level control.

Custom layouts/boxes (v0.8+ signatures). If you implement the contracts, they must match the new types:

use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Interfaces\PointInterface;

class MyLayout implements SimonHamp\TheOg\Interfaces\Layout
{
    public function render(): ImageInterface { /* ... */ }
}

class MyBox implements SimonHamp\TheOg\Interfaces\Box
{
    public function anchor(): PointInterface { /* ... */ }
}

Borders the v0.8 way. Since Border::color() and Image::border() now take ?string, pass hex/rgba/HTML names directly:

$image->border('#0f172a', 4);

Testing. Assert on the response type and dimensions rather than pixel bytes:

$this->get('/og/my-post.png')
    ->assertOk()
    ->assertHeader('Content-Type', 'image/png');

Multi-size output. Generate one image, then use Intervention v4 encoders via render() to emit square/portrait variants for different platforms without re-running layout.

Gotchas and Tips

The v0.7 → v0.8 breaking changes will bite you in this order:

  1. PHP 8.3 first. Composer will refuse to resolve until your composer.json ("php": "^8.3") and CI matrix are updated.
  2. Intervention Image v4. Anything in your app that touches the underlying image directly (encoders, ->toPng(), ->toJpeg(), resize/crop calls) needs the v4 API. The Image instance returned by render() is now ImageInterface, not the concrete Image — type-hints against Intervention\Image\Image will break.
  3. Image::border() signature. Anywhere you passed an Rgb\Color instance must now pass a string (hex/rgba/name). This is a silent-looking but type-fatal change — grep your codebase for ->border(.
  4. Border::color() signature. Same story: string in, ColorInterface out of the picture.
  5. Custom contracts. If you ship your own Box or Layout (common when building a reusable design system), update anchor() → PointInterface and render() → ImageInterface. Forgetting this is a hard fatal error at boot/first render.
  6. Laravel caching. If you've cached the old Intervention v3 Image objects (unlikely but possible via response caching), flush after upgrade.

Fonts and paths. Fonts are resolved relative to the package config path. In queue workers or Octane, base_path() is stable but public_path() may not be writable — always save to storage_path() and stream from a controller, never write directly into public/ from a job.

Memory and timeouts. A 1200×630 PNG with multiple layered backgrounds, images, and text can spike well over the default 128MB PHP limit. In jobs, set ini_set('memory_limit', '256M') and use a dedicated queue with a generous timeout. On Imagick, layer count matters more than resolution.

Text rendering quirks. Emoji and CJK glyphs need a font that actually contains them — the default fonts don't. Bundle a fallback font (e.g. Noto) and set it explicitly for user-generated titles. Long titles also silently overflow in most layouts; truncate with Str::limit() before passing them in.

Debugging renders. When an image looks wrong, call ->save(storage_path('debug.png')) and open it, then compare against the same input rendered through the bundled theme — this isolates whether the problem is your content or your custom layout.

Extension points, in order of usefulness: Theme → Layout → Box → Font. Start at Theme; reach for Layout only when the box stack can't express your design, and Box only when you need a genuinely new primitive.

Config quirks. The published config isn't merged — if you delete it, defaults apply but any custom theme/font registration you did there disappears. Commit it and treat it as app config, not vendor junk.

Caching headers. Social crawlers (Facebook, Twitter, Slack) cache aggressively. Serve with Cache-Control: public, max-age=3600 and consider a cache-busting query string in the og:image URL tied to updated_at, otherwise edited posts show stale previews for days.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle