---
title: "HTML to Image API for Statamic | Open Graph Images Addon"
description: "Automatic Open Graph images for Statamic entries, rendered from your own Antlers or Blade view. Control Panel preview, queued generation, bulk regeneration."
url: "https://html2img.com/integrations/statamic/"
---

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

- **Install:** `composer require html2img/statamic-og-images`
- **Registry:** [Packagist](https://packagist.org/packages/html2img/statamic-og-images)
- **Source:** https://github.com/html2img/statamic-og-images
- **Licence:** MIT
- **Requires:** PHP 8.3 or newer, Statamic 6

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](https://html2img.com/integrations/php/), so the underlying
client is the same one documented there. Because Statamic is a Laravel
application, everything on the [Laravel page](https://html2img.com/integrations/laravel/) 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

| Requirement | Version |
| --- | --- |
| PHP | 8.3 or newer |
| Statamic | 6 |
| `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 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

```bash
composer require html2img/statamic-og-images
```

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

```dotenv
HTML2IMG_API_KEY=your-api-key
```

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

```bash
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.

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

```handlebars
{{ if author_name }}<span>{{ author_name }}</span>{{ /if }}
{{ if image }}<img src="{{ image }}" alt="">{{ /if }}
```

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

```html
<!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.

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

| 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](#storage-modes). |
| `asset_container` | `null` | Container used by `asset` storage. |
| `integration` | `standalone` | `standalone` or `seo-addon`; see [output modes](#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:

```php
'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:

```php
// 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`:

```handlebars
{{ 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:

```handlebars
<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.

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

```bash
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](https://html2img.com/integrations/laravel/) or the
[PHP SDK](https://html2img.com/integrations/php/), which is already installed as a dependency:

```php
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;
[HTML to PDF](https://html2img.com/html-to-pdf/) suits invoices and reports. Both are covered on the
[PHP page](https://html2img.com/integrations/php/).

## 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](https://html2img.com/docs/parameters/webhook-url/).

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