- Why a fallback beats nagging editors
- Render the card from HTML
- Set it as the real featured image on publish
- The lighter option: generate at render time
- A note on Open Graph images
Some of your authors forget to set a featured image, and every theme you will ever use assumes they never do. The result is a broken hole in your archive grid, an empty thumbnail next to related posts, and a bare social card. This article shows how to generate a branded featured image from the post title automatically, so a missing thumbnail can never reach the front end again.
Why a fallback beats nagging editors
You can add an editorial checklist, a "please add a featured image" notice, or a plugin that blocks publishing. People still forget, and blocking publishing annoys the people you least want to annoy. A generated fallback fixes the problem at the layer where it actually matters: the moment a post goes live without a thumbnail, one gets made.
It also standardises the look. Instead of a mix of stock photos, screenshots and nothing, posts without a chosen image get a clean, on-brand card built from the title. Your archive stops looking half-finished.
There are two ways to do this, and they suit different sites. You can generate a real attachment and set it as the featured image, which is the better choice for most sites because the image then behaves like any other upload. Or you can generate the image at render time and never touch the media library, which is lighter but more limited. We will build the first properly and cover the second briefly.
Render the card from HTML
Whichever route you take, you need an image. Building it as HTML and rendering it to a PNG keeps the design in code where you can version and preview it, rather than drawing on a canvas by hand.
Send the markup to a rendering API and store the URL it returns. HTML to Image has a blog hero template built for exactly this shape, so you can POST a small JSON payload and skip the markup entirely, or send your own HTML when the design has to match your brand precisely.
function accr_render_featured_image( string $title, string $category ): ?string {
$html = accr_featured_template( $title, $category );
$response = wp_remote_post( 'https://app.html2img.com/api/html', array(
'timeout' => 20,
'headers' => array(
'X-API-Key' => ACCR_HTML2IMG_KEY,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array(
'html' => $html,
'width' => 1200,
'height' => 630,
) ),
) );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
return $data['url'] ?? null;
}
Keep the API key out of the code with a constant defined in wp-config.php, never committed to the repository:
// wp-config.php
define( 'ACCR_HTML2IMG_KEY', 'your-key-here' );
The template itself is ordinary HTML and CSS, so use whatever layout you like. Because a real browser does the rendering, display: grid, web fonts and gradients all work.
function accr_featured_template( string $title, string $category ): string {
$title = esc_html( $title );
$category = esc_html( strtoupper( $category ) );
$site = esc_html( get_bloginfo( 'name' ) );
return <<<HTML
<!doctype html>
<html>
<head><meta charset="utf-8">
<style>
* { margin: 0; box-sizing: border-box; }
body {
width: 1200px; height: 630px; padding: 80px;
font-family: system-ui, sans-serif; color: #fff;
background: linear-gradient(135deg, #0f172a, #334155);
display: grid; grid-template-rows: auto 1fr auto;
}
.cat { font-size: 24px; letter-spacing: .1em; color: #38bdf8; font-weight: 700; }
.head { align-self: center; font-size: 68px; font-weight: 800; line-height: 1.1; }
.site { font-size: 26px; color: #94a3b8; }
</style>
</head>
<body>
<div class="cat">{$category}</div>
<div class="head">{$title}</div>
<div class="site">{$site}</div>
</body>
</html>
HTML;
}
Set it as the real featured image on publish
Now hook the generation into the publish flow. The right hook is transition_post_status, which fires when a post moves to publish. Check for an existing thumbnail first so you never overwrite an image an author chose.
add_action( 'transition_post_status', 'accr_maybe_generate_thumbnail', 10, 3 );
function accr_maybe_generate_thumbnail( string $new, string $old, WP_Post $post ): void {
if ( 'publish' !== $new || 'post' !== $post->post_type ) {
return;
}
if ( has_post_thumbnail( $post ) ) {
return;
}
$terms = get_the_terms( $post, 'category' );
$category = ( $terms && ! is_wp_error( $terms ) ) ? $terms[0]->name : 'Article';
$url = accr_render_featured_image( get_the_title( $post ), $category );
if ( ! $url ) {
return;
}
accr_sideload_as_thumbnail( $url, $post->ID );
}
The last step turns the remote PNG into a proper attachment and attaches it. WordPress ships helpers for this, but they only load in admin context, so require the files before using them.
function accr_sideload_as_thumbnail( string $url, int $post_id ): void {
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$attachment_id = media_sideload_image( $url, $post_id, null, 'id' );
if ( ! is_wp_error( $attachment_id ) ) {
set_post_thumbnail( $post_id, $attachment_id );
}
}
That is the whole loop. A post is published without a thumbnail, an on-brand card is rendered from its title, WordPress downloads it into the media library, and it becomes the featured image. Every theme template that calls the_post_thumbnail() now has something to show.
Because the image is a normal attachment, WordPress generates its responsive sizes automatically, the same as any upload. If you serve those sizes yourself, image srcsets in WordPress without a plugin covers how the markup is built.
The lighter option: generate at render time
If you would rather not add rows to the media library, filter the thumbnail markup instead and return a generated image only when one is missing. Cache the URL in post meta so you render once per post rather than once per page view.
add_filter( 'post_thumbnail_html', 'accr_fallback_thumbnail_html', 10, 5 );
function accr_fallback_thumbnail_html( string $html, int $post_id ): string {
if ( '' !== $html ) {
return $html; // a real thumbnail exists
}
$url = get_post_meta( $post_id, '_accr_fallback_image', true );
if ( ! $url ) {
$post = get_post( $post_id );
$url = accr_render_featured_image( $post->post_title, 'Article' );
if ( $url ) {
update_post_meta( $post_id, '_accr_fallback_image', $url );
}
}
return $url ? '<img src="' . esc_url( $url ) . '" alt="" />' : $html;
}
The trade-off is that this image is not a media item, so it will not appear in the media picker and will not get WordPress's generated sizes. It is fine for a purely visual fallback and avoids cluttering the library.
A note on Open Graph images
A featured image and an Open Graph image are not the same thing. The featured image is the thumbnail your theme shows on the site. The Open Graph image is the card that appears when the post is shared on social platforms, set through meta tags. They often use the same picture, but not always, and the theme does not set the social one for you. If you want both handled automatically, pair this with auto-generating Open Graph images for every WordPress post, and see the HTML to Image docs for webhook delivery if you would rather be notified when a render finishes than wait on the request.
With the publish hook in place, the rule holds on its own: no post reaches your archive without an image again, and you never had to ask an author to remember.
Interested in proving your knowledge of this topic? Take the WordPress Development certification.
WordPress Development
Covering all aspects of WordPress web development, from theme development, plugin development, server set up and configuration and optimisation.
$99