- The plan
- Step 1: Build the card from the post
- Step 2: Render and store the image when a post is saved
- Step 3: Output the Open Graph tags
- Backfilling existing posts
Share a WordPress post on LinkedIn, Slack or X and the preview is usually a letdown. It pulls your featured image at the wrong aspect ratio, falls back to your site logo or shows nothing at all. A featured image is not an Open Graph image. It is rarely 1200x630, its focal point gets cropped in the wrong place and plenty of posts have none set. The fix is to generate a proper share card for every post automatically, from the post's own title and details, and serve it as the og:image. This article wires that up in a small plugin or your theme's functions.php.
The plan
You need three things: a card design, a way to render it to an image when a post is saved, and the meta tags that point social platforms at it. We keep the design as HTML, render it through the Open Graph image endpoint at a real 1200x630, store the resulting URL in post meta, and output it in the document head. Nothing runs on the front end, so there is no performance cost to readers.
If you want the background on why featured images fall short and how the rendered-card approach holds up across platforms, our deeper write-up on dynamic Open Graph images in WordPress covers the edge cases. Here we will build the working version.
Step 1: Build the card from the post
Start with a function that turns a post into a branded 1200x630 card. Inline every style so the render does not depend on your theme stylesheet.
function ai_og_card_html( WP_Post $post ): string {
$title = esc_html( get_the_title( $post ) );
$site = esc_html( get_bloginfo( 'name' ) );
$cat = esc_html( get_the_category( $post->ID )[0]->name ?? 'Article' );
return <<<HTML
<div style="width:1200px;height:630px;box-sizing:border-box;padding:80px;
display:flex;flex-direction:column;justify-content:space-between;
font-family:Arial,sans-serif;color:#fff;
background:linear-gradient(135deg,#1e3a8a,#4f46e5);">
<div style="font-size:24px;letter-spacing:.1em;text-transform:uppercase;opacity:.8;">{$cat}</div>
<h1 style="font-size:64px;line-height:1.1;margin:0;">{$title}</h1>
<div style="font-size:28px;font-weight:bold;">{$site}</div>
</div>
HTML;
}
Step 2: Render and store the image when a post is saved
Hook save_post. When a published post is saved, render the card and stash the returned URL in post meta. Guard against autosaves and revisions so you are not firing on every keystroke.
add_action( 'save_post', 'ai_generate_og_image', 20, 2 );
function ai_generate_og_image( int $post_id, WP_Post $post ): void {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( $post->post_status !== 'publish' || $post->post_type !== 'post' ) {
return;
}
$response = wp_remote_post( 'https://app.html2img.com/api/html', [
'headers' => [
'X-API-Key' => HTML2IMG_API_KEY,
'Content-Type' => 'application/json',
],
'timeout' => 20,
'body' => wp_json_encode( [
'html' => ai_og_card_html( $post ),
'width' => 1200,
'height' => 630,
] ),
] );
if ( is_wp_error( $response ) ) {
return;
}
$url = json_decode( wp_remote_retrieve_body( $response ), true )['url'] ?? null;
if ( $url ) {
update_post_meta( $post_id, '_og_image', esc_url_raw( $url ) );
}
}
Define HTML2IMG_API_KEY in wp-config.php rather than hard-coding it in the theme. Because the work happens on save, the reader never waits for it, and the URL is ready long before anyone shares the post.
Step 3: Output the Open Graph tags
Now print the stored image in the head, with the dimensions that let platforms render it without re-fetching. Fall back to the featured image only if no card has been generated yet.
add_action( 'wp_head', 'ai_output_og_tags' );
function ai_output_og_tags(): void {
if ( ! is_singular( 'post' ) ) {
return;
}
$post_id = get_queried_object_id();
$image = get_post_meta( $post_id, '_og_image', true )
?: get_the_post_thumbnail_url( $post_id, 'full' );
if ( ! $image ) {
return;
}
printf( '<meta property="og:image" content="%s" />' . "\n", esc_url( $image ) );
printf( '<meta property="og:image:width" content="1200" />' . "\n" );
printf( '<meta property="og:image:height" content="630" />' . "\n" );
printf( '<meta name="twitter:card" content="summary_large_image" />' . "\n" );
}
If an SEO plugin such as Yoast already prints og:image, filter its output instead of adding a second tag. Yoast exposes wpseo_opengraph_image, so you can return your stored URL there and let the plugin handle the rest of the markup.
add_filter( 'wpseo_opengraph_image', function ( $image ) {
$stored = get_post_meta( get_queried_object_id(), '_og_image', true );
return $stored ?: $image;
} );
Backfilling existing posts
New posts are covered on save. For the archive, run a one-off WP-CLI command that loops your published posts and triggers the same generation, so the whole site gets cards in one pass.
WP_CLI::add_command( 'og:backfill', function () {
foreach ( get_posts( [ 'numberposts' => -1, 'post_status' => 'publish' ] ) as $post ) {
ai_generate_og_image( $post->ID, $post );
WP_CLI::log( "Generated card for: {$post->post_title}" );
}
} );
That is the full loop. Every post now gets a clean, on-brand 1200x630 card built from its own title and category, generated the moment it is saved and served as the og:image. Your shares stop pulling badly cropped featured images, and you never open a design tool to make a single one.
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