Official addon

HTML to Image API for Statamic

The official Statamic addon renders an Open Graph image for every entry in the collections you enable. You design the card as a normal Antlers or Blade view, the addon renders it against each entry on save in a queued job, and stores the resulting URL.

composer require html2img/statamic-og-images

Requires: PHP 8.3 or newer, Statamic 6. Every account starts with 50 free credits, no card needed.

See also Laravel PHP Craft CMS

Statamic already gives you a templating language, a Control Panel and a queue. This addon uses all three: the Open Graph card is an ordinary view in your project, the settings live under Tools in the CP, and generation happens in a queued job when an entry is saved.

It is built on the official PHP SDK, so the underlying client is the same one documented there. Because Statamic is a Laravel application, everything on the Laravel page applies to anything you want to render beyond Open Graph images.

What it does

  • Renders a developer-authored Antlers or Blade view into an Open Graph image on save, in a queued job, and stores the i.html2img.com URL on the entry.
  • Resolves settings and template choice through a cascade: a site default, then a per-collection override, then a per-entry override.
  • Skips regeneration when the render inputs are unchanged, so routine saves do not spend credits.
  • Outputs the social tags itself, or hands the URL to your existing SEO addon.
  • Adds a live preview and a Generate button to the entry’s publish screen.

Requirements

RequirementVersion
PHP8.3 or newer
Statamic6
html2img/html2img-phpInstalled automatically as a dependency
API keyFree, from your dashboard
An API key is required

The addon generates images through the HTML to Image API, so it needs a key to render anything. Accounts are free and include 50 credits, with no card needed. Free-tier renders are hosted for seven days; on any paid plan they are hosted permanently, including everything rendered before upgrading.

Installation

composer require html2img/statamic-og-images

Add your key to .env. This is the canonical source:

HTML2IMG_API_KEY=your-api-key

Publish the config and the default template if you want to customise them:

php artisan vendor:publish --tag=statamic-og-images-config
php artisan vendor:publish --tag=statamic-og-images-views

Then open Tools → Open Graph Images in the Control Panel, choose which collections are enabled, and save. From that point, saving an entry in one of those collections queues a render.

The queue has to be running

Generation is a queued job. With QUEUE_CONNECTION=sync it happens inline and the save waits for it; with a real connection you need a worker running, or nothing ever renders. This is Statamic’s usual queue setup, not anything specific to the addon.

Designing the card

The Open Graph card is a normal Statamic view. The bundled default lives at og-images::default; publish it to resources/views/vendor/og-images/default.antlers.html and edit it, or point the default_template setting at a view of your own, for example og_image.

Templates receive the entry’s augmented data, so you reference fields the normal way: {{ title }}, {{ date format="j M Y" }}, your own fields, and globals. The addon also passes:

  • og_headline: the headline override, falling back to the title.
  • og_subtitle: the subtitle override, if set.
  • site_name and site_logo: from the addon settings.

Treat author, date and an image as optional so one template suits both a blog post and a plain page:

{{ if author_name }}<span>{{ author_name }}</span>{{ /if }}
{{ if image }}<img src="{{ image }}" alt="">{{ /if }}
author is a reserved field

author is a reserved Statamic field, holding the entry’s author user. Name your own byline field something like author_name so the two do not collide in the template.

A card is a complete HTML document, so give the body the exact dimensions you have configured and inline everything it needs:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;800&display=swap">
    <style>
        * { box-sizing: border-box }
        body {
            margin: 0; width: 100vw; height: 100vh; padding: 80px;
            display: flex; flex-direction: column; justify-content: space-between;
            font-family: Inter, system-ui, sans-serif;
            background: linear-gradient(160deg, #0e1521, #16233a); color: #fff;
        }
        h1 { font-size: 64px; line-height: 1.1; margin: 0; font-weight: 800; letter-spacing: -0.02em }
        .meta { font-size: 22px; color: #aeb7c6 }
    </style>
</head>
<body>
    <h1>OG headline goes here</h1>
    <p class="meta">Site name goes here</p>
</body>
</html>

Sizing the body with 100vw and 100vh means one template works at whatever dimensions a collection is configured for, rather than being pinned to 1200x630.

Blade works too, if that is what your project uses. Anything a browser can render works, because a browser is what renders it.

The preview loop

Design in the browser. The addon ships a preview route that renders your template at the exact configured dimensions, with no API key required and no credits spent, because the browser renders the same HTML the API does:

  • Embedded in the Control Panel: the settings screen and each entry’s publish screen show a live preview.
  • Directly in a browser, for a tighter loop: /cp/og-images/preview for sample data, or /cp/og-images/preview?entry=<slug> for a real entry. The route is gated to authenticated Control Panel users.

Once the design looks right, the Generate button on the publish screen runs a real render through the API and shows the actual PNG. That is the parity check between the browser preview and the rendered image.

Local assets are invisible to the renderer

Renders happen on our servers in real Chrome, so every URL in your template must be reachable from the public internet. In production your asset and font URLs already are. On a development site an uploaded Statamic asset on *.ddev.site or *.test shows as missing in the rendered PNG even though the browser preview looks right. Reference publicly hosted assets, or expose the site with a tunnel (cloudflared tunnel --url ..., ddev share) and point APP_URL at it while you test. Web fonts from a public CDN such as Google Fonts always work.

Configuration

config/statamic-og-images.php:

KeyDefaultPurpose
api_keyenv('HTML2IMG_API_KEY')The API key. The environment is canonical.
base_urienv('HTML2IMG_BASE_URI', 'https://app.html2img.com')API base URI.
default_templateog-images::defaultThe view rendered into the image.
width / height1200 / 630Image dimensions in CSS pixels.
dpi2Device pixel ratio, 1 to 4.
formatpngOutput format.
collections[]Collections the addon is enabled for.
overrides[]Per-collection template or dimension overrides.
storagecdncdn or asset; see storage modes.
asset_containernullContainer used by asset storage.
integrationstandalonestandalone or seo-addon; see output modes.
seo_fieldog_imageField written to in seo-addon mode.
site_nameenv('OG_SITE_NAME')Passed to every template.
site_logonullAbsolute logo URL passed to every template.
default_imagenullFallback used by the tag when an entry has no image.
queuenullQueue connection for generation jobs; null uses the default.

Per-collection overrides are keyed by collection handle:

'overrides' => [
    'products' => ['template' => 'og_product', 'height' => 800],
],

The settings can also be managed from Tools → Open Graph Images in the Control Panel. A key entered there is stored outside version control, in resources/addons/; the environment variable remains the canonical source.

A custom HTTP client

The addon builds its client through the container, so binding a configured GuzzleHttp\ClientInterface as og-images.http replaces it, for retry middleware, a proxy or request logging:

// app/Providers/AppServiceProvider.php
use GuzzleHttp\Client;

public function register(): void
{
    $this->app->bind('og-images.http', fn () => new Client([
        'base_uri' => config('statamic-og-images.base_uri'),
        'timeout'  => 45,
    ]));
}

Entry fields

The addon injects its fields into the blueprints of enabled collections, so there is nothing to add by hand. They appear on the publish screen in an OG Images tab:

FieldPurpose
og_image_headlineOverrides the text on the card without touching the title.
og_image_subtitleAn optional second line.
og_image_templateRenders this entry with a different view.
og_image_customAn image that bypasses generation entirely.
og_image_disabledNever generate an image for this entry.

Two fields are managed by the pipeline rather than edited: og_image_url, which holds the result, and og_image_hash, the fingerprint of the render inputs. The hash is what makes a routine save free: when it matches, nothing is re-rendered.

Output modes

Standalone

Emit the tags yourself with the Antlers tag, typically in your head:

{{ og_image:meta }}

That outputs og:image, og:image:width, og:image:height, og:image:alt, og:image:type, twitter:card and twitter:image, resolving the cascade: the entry’s custom image, then the generated image, then the site fallback. When there is no image at all it outputs nothing rather than empty tags.

{{ og_image }} on its own returns just the resolved URL, for a feed, a JSON-LD block or a newsletter template:

<meta property="og:image" content="{{ og_image }}">

SEO addon integration

If you already run an SEO addon, set integration to seo-addon and seo_field to the field your addon reads for its social image. The addon writes the generated URL into that field and stays out of the meta business, so SEO Pro or Advanced SEO output it as usual. Tags are never written twice.

Storage modes

  • cdn (default) stores the i.html2img.com URL on the entry. Nothing to serve, and it works with static caching because the frontend outputs a plain static URL.
  • asset downloads the PNG into the configured Statamic asset container and stores that asset instead, for sites that do not want a runtime dependency on a third-party URL.
Free-tier renders expire after seven days

On the free tier, asset storage is the safer choice: the image lives in your own container and keeps working after the CDN copy expires. On any paid plan CDN renders are permanent, and upgrading makes earlier renders permanent too.

Bulk regeneration

After changing a template, regenerate across the enabled collections:

php please og:generate
php please og:generate --collection=blog
php please og:generate --collection=blog --force

Without --force, entries whose render inputs are unchanged are skipped thanks to the input hash, so a plain run only spends credits on entries that actually changed. --force ignores the hash and re-renders every entry, which spends a credit each.

This is the command to run after editing the card design: changing a template does not invalidate anything by itself, deliberately, so a design tweak never re-renders your whole site behind your back.

Regenerating a single entry

The Generate button on the publish screen renders that entry immediately, ignoring the input hash. It is the right tool for checking a design change against one real entry before running og:generate --force across a whole collection.

Rendering anything else

The addon is deliberately focused on Open Graph images. Statamic is a Laravel application, so anything else the API can do is available through the Laravel package or the PHP SDK, which is already installed as a dependency:

use Html2img\Html2imgClient;
use Html2img\Request\ScreenshotRequest;

$client = app(Html2imgClient::class); // the addon's configured client

// A screenshot of a live URL
$client->screenshot(new ScreenshotRequest(url: $entry->get('website'), fullpage: true));

Screenshots suit link previews and listing thumbnails; PDFs suit invoices and reports. Both are covered on the PHP page.

Asynchronous rendering

Synchronous renders have a 30 second budget, which a card template never approaches, and the queued job keeps even that away from editors. For very large captures in your own code, the API also supports webhook delivery.

Troubleshooting

Nothing renders when I save an entry. Three things to check, in order: the collection is enabled in Tools → Open Graph Images, HTML2IMG_API_KEY is set in the environment the queue worker sees, and a worker is actually running for the configured connection. A worker started before the key was added will not see it.

The preview looks right but the rendered PNG has missing images. The renderer fetches assets over the public internet. Anything on *.ddev.site, *.test or localhost is invisible to it. Point APP_URL at a public host or a tunnel while testing. Google Fonts always work.

The author’s name is empty in the card. author is a reserved Statamic field holding a user object, not a string. Use a separate field such as author_name.

Every save spends a credit. It should not: the input hash skips unchanged cards. If it does, something in the template’s inputs is changing on every save. The most common cause is a date formatted down to the second, or a random value in the design.

Changing the template changed nothing. By design. Run php please og:generate --collection=blog --force to re-render, or use the Generate button on one entry to check the design first.

Static caching serves a stale image. In cdn storage mode the frontend outputs a plain URL, so a regenerated image means a new URL and a cache that needs invalidating. Statamic’s usual static cache invalidation on save applies.

The addon does not appear in the CP. It registers under Tools. If it is missing, the package is probably installed but the service provider has not been discovered; composer dump-autoload and php please cache:clear usually settle it.

FAQ

Which Statamic version does it need? Statamic 6, on PHP 8.3 or newer.

Antlers or Blade? Either. The template is a normal Statamic view, so use whichever your project already uses.

Can different collections use different designs? Yes. Set overrides per collection handle, with its own template, width, height or dpi. An individual entry can also override the template through the og_image_template field.

Does it work with SEO Pro or Advanced SEO? Yes. Set integration to seo-addon and name the field your addon reads. The addon fills that field and outputs nothing itself, so the tags are written once.

What happens when I run out of credits? The render fails, the entry keeps whatever image it had, and the failure is logged. Nothing on the site breaks.

How much does a busy blog cost to run? One credit per render, and unchanged entries are not re-rendered, so the running cost is roughly one credit per meaningful edit. og:generate without --force tells you how many entries would actually render.

Can editors upload their own image instead? Yes. The og_image_custom field takes an image that bypasses generation entirely and wins in the cascade, and og_image_disabled opts an entry out completely.

How do I check my balance without spending a credit? Call GET /api/me.

Start rendering from Statamic

50 free credits, no card required. One credit renders one image or one PDF.