---
title: "HTML to Image API for Craft CMS | Open Graph Images Plugin"
description: "Automatic Open Graph images for Craft CMS entries, designed as a Twig template. Control panel settings, queued generation, SEOmatic and Ether SEO hand-off."
url: "https://html2img.com/integrations/craft/"
---

# 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.

- **Install:** `composer require html2img/craft-og-images`
- **Registry:** [Craft Plugin Store](https://plugins.craftcms.com/og-images)
- **Source:** https://github.com/html2img/html2img-craft
- **Licence:** MIT
- **Requires:** PHP 8.3 or newer, Craft CMS 5.10 or newer

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](https://html2img.com/integrations/php/), 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](https://app.html2img.com/register) |

> **Note: 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](https://html2img.com/pricing/) covers paid volumes.

## Installation

Install the plugin with Composer, then install it in Craft:

```bash
composer require html2img/craft-og-images
php craft plugin/install og-images
```

Add your API key to `.env`:

```bash
HTML2IMG_API_KEY="your-key-here"
```

The key travels in the `X-API-Key` header, as described in the
[authentication docs](https://html2img.com/docs/authentication/).

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.

> **Note: 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:

| 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:

```twig
{# 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](https://html2img.com/templates/) collects ready-made cards, and the
[Open Graph image template](https://html2img.com/templates/open-graph-image/) 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.

> **Warning: 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:

| 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
<?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`:

```twig
{% 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?

```twig
{{ 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.

> **Tip: 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:

```bash
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](https://html2img.com/docs/parameters/webhook-url/). 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](https://html2img.com/integrations/php/), which is installed
as a dependency: screenshots of live URLs, [HTML to PDF](https://html2img.com/html-to-pdf/) for
invoices, 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](https://plugins.craftcms.com/og-images).

**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](https://html2img.com/integrations/php/) 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`](https://html2img.com/docs/account/).
