HTML to Image API for WordPress
The official WordPress plugin gives every post and page its own share image, built from the title, author, excerpt, date and featured image, rendered in real Chrome and saved into your media library.
Requires: WordPress 6.2 or newer, PHP 7.4 or newer. Every account starts with 50 free credits, no card needed.
Links shared without an image get scrolled past. The plugin gives every post and page its own share image, built from the title, author, excerpt, date and featured image the moment you publish. No design work per post, no image editor, no page builder to fight with.
The official html2img WordPress plugin, Auto OG Images, is currently under review for inclusion in the WordPress plugin directory. Until that review completes it is installed from the source repository rather than from the Add Plugins screen. Everything on this page describes the plugin as it works today.
What the plugin does
On publish, it builds a card from the post title, author, excerpt, date and
featured image, renders it through POST /api/html in
real Chrome, and saves the PNG into your media library. The correct og:image and
twitter:image tags are then output on the post, or handed to Yoast SEO, Rank
Math, All in One SEO or SEOPress when one of those is active, so the tags are never
written twice.
Five designs ship with it (Classic, Split, Photo, Editorial and Minimal), each taking an accent colour, a background colour, an optional logo and toggles for the author name and site name. A custom template option accepts your own HTML with documented placeholders.
Because the rendering happens in a real browser, proper fonts, emoji in titles and photographic featured images all come out right. Nothing is drawn with GD or ImageMagick on your host, and no front-end JavaScript is added to your pages.
Availability today
| Route | Status |
|---|---|
| WordPress plugin directory | Under review |
| GitHub repository | html2img/wordpress, GPL-2.0-or-later |
| Current version | 1.1.0 |
Requirements
| Requirement | Version |
|---|---|
| WordPress | 6.2 or newer (tested up to 7.0) |
| PHP | 7.4 or newer |
| API key | Free, from your dashboard |
The plugin makes no front-end requests to the API and never exposes your key to
the browser: the key lives in the html2img_settings option and is used only from
the admin and from background tasks.
Installing
Until the directory listing is live, install from the source repository:
- Download the repository as a ZIP from
github.com/html2img/wordpress (Code
→ Download ZIP), or clone it into
wp-content/plugins/. - If you downloaded the ZIP, upload it under Plugins → Add New → Upload Plugin. WordPress identifies the plugin from its header, so the folder name does not matter.
- Activate it.
- Open Settings → OG Images and paste an API key from your dashboard.
- Pick a design, choose your post types, and save.
The plugin validates the key with a call to GET /api/me, which
costs nothing, and then shows your plan and remaining credits on the settings
screen.
New posts get an image on publish. Existing posts can be generated in bulk from Tools → OG Images.
A plugin installed from a ZIP does not receive automatic updates. Until the directory listing is live, watch the repository for changes and re-upload when you want to move to a newer version.
Settings
Everything lives in one option, html2img_settings, edited under Settings → OG
Images:
| Setting | Default | What it does |
|---|---|---|
| API key | empty | Validated against GET /api/me when saved. |
| Post types | post, page | Which public post types get images. Any public type can be enabled, products included. |
| Design | classic | One of Classic, Split, Photo, Editorial, Minimal, or your own template. |
| Accent colour | #6366f1 | Passed to the design as {{accent_color}}. |
| Background colour | #0b1220 | Passed to the design as {{background_color}}. |
| Logo | none | A media library image, passed to the design as {{logo}}. |
| Show author | on | Whether the author name appears on the card. |
| Show site name | on | Whether the site name appears on the card. |
| Custom template | empty | Your own HTML document, using the placeholders below. |
| Storage | media | media saves into the media library, cdn keeps the hosted URL. |
| Always override | off | Whether the generated image beats a social image set by hand in your SEO plugin. |
| Show in media library | off | Whether generated images appear in the media grid. |
| Delete on uninstall | off | Whether uninstalling removes the plugin’s data. |
The settings screen also carries a live design preview drawn by your own browser, not the API, so trying designs and colours costs nothing.
Post types
Any public post type can be enabled, not just posts and pages. Custom post types
appear in the list once registered as public, and WooCommerce products work the
same way: the bundled designs show the product title and its featured image.
Prices and other product fields are not included out of the box, but the
html2img_variables filter and a custom template can add
them.
Open Graph and Twitter tags
With no SEO plugin active, the plugin outputs the tags itself. With Yoast SEO, Rank Math, All in One SEO or SEOPress active, it feeds the generated image to that plugin through its own filters and outputs nothing, so the tags are never written twice. The settings screen states which plugin currently controls your social tags.
A social image chosen by hand in your SEO plugin wins by default, and the generated image fills the gap on every post that has none. Turning on Always override flips that round.
Generating and regenerating
On publish. An image is generated in the background when a post is published or updated. Publishing never waits on the API; the editor panel shows progress, and if background tasks are disabled on your host the panel offers a manual run button.
Per post. The Gutenberg sidebar panel and the classic editor metabox both show the current image, a regenerate button and a per-post off switch. The posts list adds a row action for a single post and a bulk action for a selection.
In bulk. Tools → OG Images counts how many images are out of date, shows what a full regeneration will cost in credits, and asks before spending anything. The run happens in batches, shows progress, can be resumed if interrupted, and stops cleanly if credits run out.
How generation is decided
Every render stores two hashes in post meta, and this is what keeps the plugin cheap to run:
- A content hash of the variable payload the design consumes: title, site name, author, excerpt, date and featured image identity. A save re-renders only when this hash changes, so saving a post without touching any of those costs nothing.
- A design fingerprint of the active design, its template HTML, the customisation settings and the render dimensions. Posts whose stored fingerprint differs from the current one are stale. The Tools screen counts them and offers a bulk regeneration with a credit estimate before anything runs.
A render also happens when the stored attachment has been deleted. Nothing regenerates silently when you change the design: you are always asked first.
Credits and the free plan
One render is one credit, the same as any API call. Free accounts get 50 credits, a one-time allowance rather than a monthly one, and paid plans are on the pricing page.
Renders made on the free plan stay on the CDN for seven days, but the plugin
stores every image in your media library by default, so nothing on your site
expires. If you switch storage to cdn on a free account, the plugin warns you
about the seven-day lifetime. Paid plan renders are kept permanently, and
upgrading makes every earlier render permanent too.
When an account runs out of credits, generation pauses. Existing images stay exactly as they are, posts published in the meantime fall back to whatever your SEO plugin or theme would do anyway, and a single admin notice tells you how many posts are waiting. They are generated when credits are available again, either on their next save or from the Tools screen.
The default setting downloads each render into wp-content/uploads and serves it
from your own domain. Your share images then keep working whatever happens to the
account, which is why it is the recommended option on every plan.
Custom templates
The custom template option takes a complete HTML document with placeholders. Text values are HTML-escaped before substitution.
| Placeholder | Value |
|---|---|
{{title}} | Post title, entity decoded |
{{title_class}} | title-l, title-m, title-s or title-xs by title length, for stepped font sizes |
{{site_name}} | Site name, empty when hidden in settings |
{{tagline}} | Site tagline |
{{author}} | Author display name, empty when hidden in settings |
{{excerpt}} | Manual excerpt, or the first 28 words of the content |
{{featured_image}} | Data URI or URL of the featured image, empty when there is none |
{{logo}} | Data URI or URL of the logo chosen in settings |
{{accent_color}} | Validated hex colour |
{{background_color}} | Validated hex colour |
Conditional sections show or hide markup based on whether a value is empty:
{{#author}}<span>{{author}}</span>{{/author}}
{{^featured_image}}<div class="fallback"></div>{{/featured_image}}
Templates must be complete HTML documents, self-contained apart from Google Fonts
loaded with link tags. Size the page with width: 100vw; height: 100vh and use
vw units throughout, and one template works at any configured dimensions:
<!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: {{background_color}}; color: #fff;
}
h1 { margin: 0; font-weight: 800; line-height: 1.1; letter-spacing: -0.02em }
.title-l { font-size: 5.4vw } .title-m { font-size: 4.6vw }
.title-s { font-size: 3.8vw } .title-xs { font-size: 3.2vw }
.rule { height: 0.5vw; width: 8vw; background: {{accent_color}} }
.meta { font-size: 1.7vw; color: #aeb7c6 }
</style>
</head>
<body>
<div class="rule"></div>
<h1 class="{{title_class}}">{{title}}</h1>
<div class="meta">
{{#author}}<span>{{author}}</span> · {{/author}}
<span>{{site_name}}</span>
</div>
</body>
</html>
The API renders your HTML on its own servers, so every URL in a custom template
must be reachable from the public internet. A logo on a staging site behind HTTP
auth, or on localhost, comes out missing. Featured images and the settings logo
are inlined as data URIs by the plugin, which is why they work; anything else you
reference must be public. Google Fonts always work.
Hooks for developers
Filters
// Change or extend the variables a design receives. Values added here
// join the content hash, so changes to them trigger re-renders exactly
// like core fields.
add_filter( 'html2img_variables', function ( array $variables, int $post_id ): array {
$variables['price'] = get_post_meta( $post_id, '_price', true );
return $variables;
}, 10, 2 );
// Final say on whether a post gets an image.
add_filter( 'html2img_should_generate', function ( bool $should, int $post_id, WP_Post $post ): bool {
return $should && ! has_term( 'no-og', 'category', $post );
}, 10, 3 );
// Adjust the render arguments sent to the API: width, height, dpi, html.
add_filter( 'html2img_render_args', function ( array $args, int $post_id ): array {
$args['dpi'] = 1;
return $args;
}, 10, 2 );
// Register or replace designs. Each entry maps a slug to a name and the
// absolute path of a template file.
add_filter( 'html2img_designs', function ( array $designs ): array {
$designs['brand'] = [
'name' => 'Brand',
'file' => get_stylesheet_directory() . '/og-designs/brand.html',
];
return $designs;
} );
// Change the render dimensions globally.
add_filter( 'html2img_dimensions', function ( array $dimensions ): array {
return [ 'width' => 1200, 'height' => 630, 'dpi' => 1 ];
} );
// Cap for inlining images as data URIs, in bytes of the source file.
add_filter( 'html2img_inline_image_max_bytes', fn () => 2000000 );
// Provide the API key from configuration instead of the database.
add_filter( 'html2img_api_key', fn () => defined( 'HTML2IMG_API_KEY' ) ? HTML2IMG_API_KEY : '' );
The last one is worth knowing about on a managed host: define HTML2IMG_API_KEY
in wp-config.php and the key never touches the database or a settings export.
Actions
// After an image was generated and stored.
add_action( 'html2img_after_generate', function ( int $post_id, int $attachment_id, array $response ): void {
// $attachment_id is 0 in CDN storage mode.
// $response is the full API response body.
}, 10, 3 );
Post meta
All keys are prefixed _html2img_ and hidden from custom fields: image_id,
cdn_url, render_id, expires_at, content_hash, fingerprint,
generated_at, status, error, disabled and queued_at. Generated
attachments carry _html2img_generated.
Rolling your own
If you would rather not run a plugin, the API is one HTTP request from any hook. This is the plugin’s whole idea in twenty lines:
add_action( 'transition_post_status', function ( $new, $old, $post ) {
if ( 'publish' !== $new || 'publish' === $old ) {
return;
}
$html = sprintf(
'<!doctype html><html><body style="margin:0;width:1200px;height:630px;padding:80px;
box-sizing:border-box;display:flex;align-items:center;font:800 64px/1.1 system-ui;
background:#0f172a;color:#fff">%s</body></html>',
esc_html( get_the_title( $post ) )
);
$response = wp_remote_post( 'https://app.html2img.com/api/html', [
'timeout' => 35,
'headers' => [
'Content-Type' => 'application/json',
'X-API-Key' => HTML2IMG_API_KEY,
],
'body' => wp_json_encode( [ 'html' => $html, 'width' => 1200, 'height' => 630 ] ),
] );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
update_post_meta( $post->ID, '_og_image_url', $body['url'] );
}, 10, 3 );
add_action( 'wp_head', function () {
if ( ! is_singular() ) {
return;
}
$url = get_post_meta( get_the_ID(), '_og_image_url', true );
if ( $url ) {
printf( '<meta property="og:image" content="%s">', esc_url( $url ) );
}
} );
That covers the happy path. What the plugin adds on top: change detection so unchanged saves cost nothing, background generation so publishing never waits, media library storage, five designs, SEO plugin hand-off, bulk regeneration with a credit estimate, and clean behaviour when credits run out. The PHP integration page covers the underlying API in full, including screenshots, PDFs and named templates.
Troubleshooting
The key is rejected on the settings screen. The plugin validates it against
GET /api/me. A rejected key is usually a copy-paste with whitespace, or a key
from a different account. Re-copy it from your
dashboard.
No image appears after publishing. Generation runs in the background through
WP-Cron. On a site where cron is disabled or unreliable, the editor panel offers a
manual run button; use that to confirm the pipeline works, then look at whether
DISABLE_WP_CRON is set without a real cron job replacing it.
The image is there but the tags are not. An active SEO plugin owns the head. Check the settings screen, which names whichever plugin currently controls your social tags, and look for a social image set by hand on that post: it wins unless Always override is on.
A custom template renders with missing images. Anything you reference has to be publicly reachable. Featured images and the settings logo are inlined as data URIs by the plugin, which is why they always work; a logo referenced by URL from a staging site will not.
Every save spends a credit. It should not: the content hash skips unchanged
posts. If it does, something in the payload changes on every save, often a
html2img_variables filter adding a timestamp or a random value.
Changing the design did not regenerate anything. By design. Go to Tools → OG Images, which counts the stale posts and tells you what a regeneration will cost before it starts.
Bulk regeneration stopped part way. It stops cleanly when credits run out and can be resumed from the same screen once you have more. Progress is saved, so nothing is re-rendered twice.
Multisite. The plugin works per site on a network, each site with its own settings and API key. There is no network-level configuration yet.
FAQ
Is the plugin on WordPress.org yet? Not yet. It is under review for the plugin directory. Until that completes, install it from the repository as described above.
Does it work with Yoast SEO or Rank Math? Yes, and also with All in One SEO and SEOPress. The plugin feeds the generated image to whichever is active through its own filters and outputs nothing itself, so tags are never written twice.
What if I already set a social image on a post by hand? It is left alone. A manually chosen social image wins by default; the generated image only fills the gap on posts without one. Always override changes that.
Do the images expire? Not the ones on your site. Images are saved into your media library by default and stay there. The copies on the CDN expire after seven days on the free plan, which only matters if you switch on CDN storage. Paid plan renders never expire.
Can I customise the design?
Each bundled design takes an accent colour, a background colour, a logo and toggles
for the author name and site name. Beyond that, the custom template option accepts
your own HTML with the placeholders listed above, and the html2img_designs filter
registers a design file from your theme.
What data is sent to the API? Only what the image shows: the post title, the excerpt, the author display name, the site name and tagline, the date, the featured image and your design settings including the logo. Nothing is sent for post types you have not enabled or posts you have switched off, and nothing is sent from your site’s public pages. The privacy policy and terms cover the service itself.
Does publishing wait for the image? No. The render runs in the background and typically finishes within seconds.
Does it slow down my public pages? No. It adds a handful of meta tags and no front-end JavaScript. Everything else happens in the admin.
What happens if I deactivate it? Existing images stay in the media library. Uninstalling only removes the plugin’s data if you have turned on the Delete on uninstall setting.
Related integrations
The same API and the same key across every stack. These are the neighbours of the WordPress integration.
Start rendering from WordPress
50 free credits, no card required. One credit renders one image or one PDF.