Official plugin

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.

See also PHP Statamic WordPress

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

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

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.

The queue has to run

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:

VariableValue
entryThe entry element. Every native and custom field works as normal.
ogHeadlineThe entry’s ogHeadline field if present and filled, otherwise the title.
ogSubtitleThe entry’s ogSubtitle field if present and filled, otherwise null.
siteNameThe Site name setting, falling back to the Craft site name.
siteLogoThe 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> &middot; {% endif %}
        <span>{{ siteName }}</span>
        {% if entry.postDate %} &middot; <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 handleTypeEffect
ogHeadlinePlain textReplaces the title in the card.
ogSubtitlePlain textPassed to the template as ogSubtitle.
ogDisabledLightswitchSkips 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.

Local URLs are invisible to the renderer

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:

SettingDefaultWhat it does
apiKey$HTML2IMG_API_KEYYour API key, normally an environment variable reference.
sections[]Section handles that generate images on save.
templateog-images/_defaultThe Twig template rendered for each card.
width1200Viewport width in CSS pixels.
height630Viewport height in CSS pixels.
dpi2Device pixel ratio. 2 doubles the output pixels for crisp text.
overrides[]Per-section overrides for template, width, height and dpi.
storagecdncdn stores the returned URL, asset downloads the PNG.
volumenullVolume handle used by asset storage.
integrationstandalonestandalone emits meta tags, seo-field fills an asset field.
seoFieldnullAsset field handle written to in seo-field mode.
siteName''Passed to templates. Falls back to the Craft site name.
siteLogonullAbsolute logo URL passed to templates.
fallbackImagenullAbsolute URL used when an entry has no image at all.
imageField''Asset field handle an editor can use to override the output.
regenerateOnResavefalseAlso 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.

Free-tier renders expire after seven days

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.

Start rendering from Craft CMS

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