HTML to Image API for Craft CMS
The official Craft CMS plugin renders an Open Graph image for every entry in the sections you enable. The card is an ordinary Twig template in your own project, rendered against the entry by a queue job whenever it is saved.
composer require html2img/craft-og-images Requires: PHP 8.3 or newer, Craft CMS 5.10 or newer. Every account starts with 50 free credits, no card needed.
No canvas editor, no drag-and-drop card builder, no per-seat pricing. The Open Graph card is a Twig template in your own project, with your own fonts and CSS. When an entry is saved, a queue job renders that template against the entry, sends the HTML to the API and stores the finished PNG. Real Chrome does the rendering, so what you see in your browser is what lands in the image.
The plugin is built on the official PHP SDK, the same client documented there.
What it does
- Renders on entry save through the Craft queue, so saving stays fast.
- One template for the whole site, with per-section overrides where you need them.
- Editors can override the headline, the subtitle or the whole image per entry.
- Input hashing means routine saves that change nothing spend no credits.
- Stores the CDN URL the API returns, or downloads the PNG into an asset volume.
- Emits the meta tags itself, or hands the image to SEOmatic or Ether SEO.
- A live browser preview and a Generate button in the control panel, with no key needed to design.
Requirements
| Requirement | Version |
|---|---|
| Craft CMS | 5.10 or newer |
| PHP | 8.3 or newer |
html2img/html2img-php | Installed automatically as a dependency |
| API key | Free, from your dashboard |
The plugin generates images through the HTML to Image API, so it needs a key to render anything. Accounts are free and start with 50 credits, with no card needed. The pricing page covers paid volumes.
Installation
Install the plugin with Composer, then install it in Craft:
composer require html2img/craft-og-images
php craft plugin/install og-images
Add your API key to .env:
HTML2IMG_API_KEY="your-key-here"
The key travels in the X-API-Key header, as described in the
authentication docs.
Then open Settings → Auto Open Graph Images in the control panel, choose the sections to generate images for, and save. From that point every save of an entry in those sections queues a render.
Generation is a Craft queue job. On a site where the queue only runs on web
requests, an image appears shortly after the next front-end hit; with
runQueueAutomatically disabled you need a worker or a cron-driven
php craft queue/run. This is Craft’s normal queue setup, not anything specific
to the plugin.
Designing your card
The card is a normal Twig template in your project’s templates/ folder. Point
the Template setting at it, for example _og/card. Until then the bundled
default at og-images/_default is used: a self-contained 1200 by 630 card with
Google Fonts, an adaptive headline size, and your site name and date.
Templates receive:
| Variable | Value |
|---|---|
entry | The entry element. Every native and custom field works as normal. |
ogHeadline | The entry’s ogHeadline field if present and filled, otherwise the title. |
ogSubtitle | The entry’s ogSubtitle field if present and filled, otherwise null. |
siteName | The Site name setting, falling back to the Craft site name. |
siteLogo | The Site logo URL setting. |
Set the page size with width: 100vw; height: 100vh on the body and the card
fills whatever dimensions you configure, rather than being pinned to one size:
{# templates/_og/card.twig #}
<!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: 6vw;
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: 5.4vw; line-height: 1.1; margin: 0; font-weight: 800; letter-spacing: -0.02em }
.meta { font-size: 1.8vw; color: #aeb7c6 }
</style>
</head>
<body>
<h1>{{ ogHeadline }}</h1>
<div class="meta">
{% if ogSubtitle %}<span>{{ ogSubtitle }}</span> · {% endif %}
<span>{{ siteName }}</span>
{% if entry.postDate %} · <span>{{ entry.postDate|date('j M Y') }}</span>{% endif %}
</div>
</body>
</html>
Sizing in vw units throughout means the same template works at 1200x630 and at a
taller override without a second file. For layout ideas, the
template gallery collects ready-made cards, and the
Open Graph image template is a good starting point
to adapt.
Per-entry overrides
Editors override behaviour through plain fields on the entry type. The plugin looks for these handles by convention, and none of them are required:
| Field handle | Type | Effect |
|---|---|---|
ogHeadline | Plain text | Replaces the title in the card. |
ogSubtitle | Plain text | Passed to the template as ogSubtitle. |
ogDisabled | Lightswitch | Skips generation for the entry while on. |
There is one more, named by you rather than by convention: set Editor image field to the handle of an asset field, and whenever an editor picks an image there it wins over the generated one in the output.
Previewing
The control panel has a preview route at admin/og-images/preview that renders
your template at the configured dimensions with sample data, or against a real
entry with ?entryId=123. The settings page embeds it, so you can iterate on the
template and refresh. No API key is involved, because your browser renders the
same HTML the API’s Chrome does.
On each entry’s edit screen a sidebar panel shows the current generated image and a Generate button that performs a real API render immediately, ignoring the input hash. That is the parity check between the browser preview and the rendered PNG.
The API renders your HTML on its own servers, so every URL in the template must be
reachable from the public internet. An image at
https://my-site.ddev.site/logo.png renders fine in your browser preview and
comes out missing in the PNG. Host template assets somewhere public and reference
absolute URLs, inline small images as data URIs, or expose your dev site with a
tunnel such as ddev share while testing. Google Fonts always work, since they
load from Google’s CDN. The bundled default template is fully self-contained, so
first renders work on any dev site.
Configuration
Settings live in the control panel, and a config/og-images.php file overrides
them per environment:
| Setting | Default | What it does |
|---|---|---|
apiKey | $HTML2IMG_API_KEY | Your API key, normally an environment variable reference. |
sections | [] | Section handles that generate images on save. |
template | og-images/_default | The Twig template rendered for each card. |
width | 1200 | Viewport width in CSS pixels. |
height | 630 | Viewport height in CSS pixels. |
dpi | 2 | Device pixel ratio. 2 doubles the output pixels for crisp text. |
overrides | [] | Per-section overrides for template, width, height and dpi. |
storage | cdn | cdn stores the returned URL, asset downloads the PNG. |
volume | null | Volume handle used by asset storage. |
integration | standalone | standalone emits meta tags, seo-field fills an asset field. |
seoField | null | Asset field handle written to in seo-field mode. |
siteName | '' | Passed to templates. Falls back to the Craft site name. |
siteLogo | null | Absolute logo URL passed to templates. |
fallbackImage | null | Absolute URL used when an entry has no image at all. |
imageField | '' | Asset field handle an editor can use to override the output. |
regenerateOnResave | false | Also regenerate during bulk resave operations. |
A config file example with a per-section override:
<?php
return [
'sections' => ['blog', 'caseStudies'],
'template' => '_og/card',
'overrides' => [
'caseStudies' => [
'template' => '_og/case-study',
'height' => 675,
],
],
];
Images are generated per entry per site, keyed against the site the entry was saved in, so a multi-site Craft install gets one card per localisation rather than one shared across all of them.
Twig usage
Standalone
Drop the meta tag into your layout’s head:
{% if entry is defined and entry %}
{{ craft.ogImages.meta(entry) }}
{% endif %}
That emits og:image, og:image:width, og:image:height, og:image:alt,
og:image:type, twitter:card and twitter:image, resolving the cascade in
order: the editor’s chosen image if set, then the generated image, then the site
fallback image.
Need just the URL, for a JSON feed or a newsletter template?
{{ craft.ogImages.url(entry) }}
It returns null when there is no image at all, so guard it if the surrounding markup would otherwise render an empty attribute.
SEO plugin field
If SEOmatic or Ether SEO already owns your head, set Integration to
seo-field and name an asset field. The plugin writes each generated image into
that field and emits nothing itself. Point your SEO plugin’s social image at the
same field and it keeps full control of the markup. This mode always stores images
as assets.
Storage modes
cdn (the default) stores the i.html2img.com URL the API returns. The
frontend outputs a plain URL, which keeps pages static-cache friendly and adds no
weight to your server.
asset downloads each PNG into the volume you choose and stores the asset
instead, so the site has no runtime dependency on the API. Regenerating replaces
the existing asset’s file in place, keeping references to it valid.
On the free tier, asset storage is the safer choice: the image lives in your own
volume and keeps working after the CDN copy expires. On any paid plan CDN renders
are permanent, and upgrading makes earlier renders permanent too.
Queue jobs and bulk regeneration
Every save of an entry in an enabled section pushes a job onto the Craft queue, so the editor never waits on a render.
To regenerate across the site after changing a template:
php craft og-images/generate
php craft og-images/generate --section=blog
php craft og-images/generate --force
The command walks the enabled sections, prints a line per entry and finishes with
a summary. 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 re-renders everything regardless.
Bulk resaves such as resave/entries are ignored by default, for the same reason:
a maintenance resave should not spend a credit per entry. Turn on Regenerate on
bulk resaves if you want them included.
Asynchronous rendering
Synchronous renders have a 30 second budget, which a card template never
approaches, and the queue keeps even that away from editors. For very large
captures in your own code the API also supports webhook delivery through the
webhook_url parameter. This plugin does not use
it, but it is there if you outgrow synchronous renders in your own integrations.
Rendering anything else
The plugin is focused on Open Graph images for entries. The same account and key drive the whole API through the PHP SDK, which is installed as a dependency: screenshots of live URLs, invoices as vector PDFs, and named templates from a JSON payload are all one client call away.
Troubleshooting
Nothing renders when I save an entry. Check three things in order: the section
is enabled in Settings → Auto Open Graph Images, HTML2IMG_API_KEY is set in
the environment the queue runner sees, and the queue is actually running. On a
quiet site with runQueueAutomatically on, the job waits for the next front-end
request.
The preview looks right but the rendered PNG has missing images. The renderer
fetches assets over the public internet, so *.ddev.site, *.test and localhost
are invisible to it. Reference absolute public URLs, inline small images as data
URIs, or use a tunnel while testing.
Editor overrides do nothing. The plugin looks for the field handles
ogHeadline, ogSubtitle and ogDisabled by convention. A field named
og_headline or socialHeadline will not be picked up. The editor image field is
the exception: you name it yourself in the settings.
Every save spends a credit. It should not: the input hash skips unchanged cards. If it does, something in the card’s inputs is changing on every save, such as a date formatted down to the second.
Changing the template changed nothing. By design. Run
php craft og-images/generate --force, or use the Generate button on a single
entry to check the design before re-rendering a whole section.
A bulk resave did not regenerate anything. Also by design. Turn on
Regenerate on bulk resaves if you want resave/entries to include renders.
Multi-site entries share one image. They should not: images are keyed per entry per site. If two sites show the same card, check that the template is reading site-aware values rather than hard-coded ones.
FAQ
Which Craft version does it need? Craft CMS 5.10 or newer, on PHP 8.3 or newer.
What is the plugin handle?
og-images, so php craft plugin/install og-images and
php craft og-images/generate. The Composer package is
html2img/craft-og-images, and it is listed in the
Craft Plugin Store.
Can different sections use different designs?
Yes. overrides takes a per-section map with its own template, width,
height and dpi.
Does it work with SEOmatic or Ether SEO?
Yes. Set Integration to seo-field and name an asset field; the plugin fills
it and emits nothing itself, so your SEO plugin keeps full control of the head and
the tags are written once.
Does it work with Craft Commerce products or other element types? Generation is driven by entry saves in the sections you enable, so it covers entries. For other element types, call the PHP SDK directly from your own module.
What happens when I run out of credits? The render fails, the entry keeps whatever image it had, and the failure is recorded. Nothing on the front end breaks.
Can I preview without spending credits? Yes. The control panel preview is rendered by your own browser, not the API, so iterating on a design costs nothing. Only the Generate button and a real save spend a credit.
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 Craft CMS integration.
Start rendering from Craft CMS
50 free credits, no card required. One credit renders one image or one PDF.