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.
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.comURL 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
| Requirement | Version |
|---|---|
| PHP | 8.3 or newer |
| Statamic | 6 |
html2img/html2img-php | Installed automatically as a dependency |
| API key | Free, from your dashboard |
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.
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_nameandsite_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 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/previewfor 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.
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:
| Key | Default | Purpose |
|---|---|---|
api_key | env('HTML2IMG_API_KEY') | The API key. The environment is canonical. |
base_uri | env('HTML2IMG_BASE_URI', 'https://app.html2img.com') | API base URI. |
default_template | og-images::default | The view rendered into the image. |
width / height | 1200 / 630 | Image dimensions in CSS pixels. |
dpi | 2 | Device pixel ratio, 1 to 4. |
format | png | Output format. |
collections | [] | Collections the addon is enabled for. |
overrides | [] | Per-collection template or dimension overrides. |
storage | cdn | cdn or asset; see storage modes. |
asset_container | null | Container used by asset storage. |
integration | standalone | standalone or seo-addon; see output modes. |
seo_field | og_image | Field written to in seo-addon mode. |
site_name | env('OG_SITE_NAME') | Passed to every template. |
site_logo | null | Absolute logo URL passed to every template. |
default_image | null | Fallback used by the tag when an entry has no image. |
queue | null | Queue 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:
| Field | Purpose |
|---|---|
og_image_headline | Overrides the text on the card without touching the title. |
og_image_subtitle | An optional second line. |
og_image_template | Renders this entry with a different view. |
og_image_custom | An image that bypasses generation entirely. |
og_image_disabled | Never 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 thei.html2img.comURL on the entry. Nothing to serve, and it works with static caching because the frontend outputs a plain static URL.assetdownloads 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.
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.
Related integrations
The same API and the same key across every stack. These are the neighbours of the Statamic integration.
Start rendering from Statamic
50 free credits, no card required. One credit renders one image or one PDF.