Skip to chapter content

Chapter 12 4 min read

Your First AI Image

Same pattern, new modality

Same builder, same is_wp_error() pattern you’ve written a dozen times by now, one new method. generate_image() instead of generate_text(), and what comes back is different enough to be worth slowing down for.

Note

One quick note before we get into it, since it applies to this chapter and the two after it. Image models vary a lot by provider and by which specific model your connector defaults to, some don’t support requesting more than one image per call, some are simply slow enough to time out under a fixed request window. Where that’s a known issue, the chapter says so directly. using_model_preference() (covered fully in Chapter 19) lets you request a specific model instead of whatever your connector defaults to, worth trying early if you get stuck, though it won’t fix everything in this module, a couple of the issues here turned out not to be fixable by switching models at all.

From generate_text() to generate_image()

$image_file = wp_ai_client_prompt( 'A futuristic WordPress logo in neon style' )
    ->generate_image();

if ( is_wp_error( $image_file ) ) {
    return;
}

echo '<img src="' . esc_url( $image_file->getDataUri(), array( 'data' ) ) . '" alt="">';

generate_image() doesn’t return a string. It returns a File object, and the way you get an actual image out of it is getDataUri(). That’s not a link to a hosted file somewhere, it’s the entire image, encoded as text, embedded directly into the src attribute. The browser decodes it right there without making a second request to fetch an image file. Convenient for a quick example like this one. Worth understanding the tradeoff before you build anything real with it, though, more on that in a moment.

Warning

esc_url() has a default whitelist of URL schemes it allows through, http, https, mailto, and a handful of others, and data: isn’t one of them. Left at its default, esc_url() silently strips a data URI down to nothing, which shows up as a broken image icon with an empty src, no error, no warning, just a picture that never loads. That’s why the line above passes array( 'data' ) as the second argument, telling esc_url() to explicitly allow that scheme through. Leave that second argument off and this exact example breaks.

Generating a real featured image concept

Create chapter-12-first-image.php inside includes:

<?php
/**
* Chapter 12: Your First AI Image
* Usage: add [ai_course_ch12] inside an actual post or page (it needs a
* real title to work from, via get_the_title()).
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // No direct access.
}
function ai_course_ch12_first_image() {
$post_title = get_the_title();
if ( empty( $post_title ) ) {
return 'Add this shortcode inside an actual post or page so there is a title to work from.';
}
$prompt = sprintf(
'A featured image concept for a blog post titled "%s". Editorial photography style, no text overlays.',
$post_title
);
$image_file = wp_ai_client_prompt( $prompt )->generate_image();
if ( is_wp_error( $image_file ) ) {
return 'Could not generate an image right now: ' . esc_html( $image_file->get_error_message() );
}
return '<img src="' . esc_url( $image_file->getDataUri(), array( 'data' ) ) . '" alt="AI-generated featured image concept" style="max-width: 100%; height: auto;">';
}
add_shortcode( 'ai_course_ch12', 'ai_course_ch12_first_image' );

Add [ai_course_ch12] inside a real post or page (the same requirement as Chapter 9’s SEO title example, it needs an actual title from get_the_title() to build a prompt from) and load it. You get an actual generated image, based on the specific title of that specific post, not a placeholder or a stock photo search result.

Warning

If you’re on OpenAI, if this call fails with something like “cURL error 28: Operation timed out after 30002 milliseconds with 0 bytes received,” that’s a genuine timeout, not a rejection, and it appears to be a real, current limitation rather than a one-off network blip (confirmed reproducible across multiple attempts).

Raising WordPress’s own http_request_timeout filter to allow more time didn’t fix it; the request still cuts off at almost exactly 30 seconds every time. That rules out the usual WordPress-side fix; whatever’s enforcing this limit isn’t something this book’s example code can control.

If you hit this, Gemini generated the same image without issue in testing. Switching providers for this chapter is the practical workaround for now; this kind of gap tends to get resolved in a future update to the AI Provider for OpenAI plugin rather than being permanent.

Why a data URI isn’t the whole story

getDataUri() is the fastest way to show an image on screen, which is why this chapter uses it. But nothing about it saves the image anywhere. Reload the page, and you get a brand new image, generated fresh, at a brand new cost, because there’s nothing on disk to reuse.

For a demo that’s fine. For a real featured-image feature, regenerating (and re-paying for) the same image on every single page load isn’t something you’d actually ship, you’d want to generate it once and save it to the Media Library, so the page just serves a normal, cached, already-paid-for file afterwards.

That’s more than this chapter needs to cover on its own, saving AI-generated files as real attachments comes up properly in Chapter 24. For now, know the distinction: what you’re looking at on screen right now exists for exactly one page load, and that’s a deliberate limitation of getDataUri(), not something broken.

Try it yourself

Add [ai_course_ch12] to a couple of your own actual posts, ones with real, specific titles, not test content, and see what kind of featured image concept comes back for each. Pay attention to how much the image quality depends on how specific the post title itself is: a title like “5 WordPress Security Tips for Small Business Owners” gives the model more to work with than “Blog Post 3.”

This book is created with Chapterwright