Content Projects

Project: Meta Description and Alt-Text Generator

⏱ 14 min

This project builds an ability that, given a post ID, generates a meta description for the post and alt text for every image attached to it, in one call. It’s built to run across many posts at once, since meta descriptions and alt text are exactly the kind of small, repetitive task that piles up across hundreds of older posts.

What you'll learn in this lesson
How to generate two different outputs from one ability call
A meta description for the post and alt text for each attached image.
How to write alt text that serves accessibility first
Describing what an image conveys, not stuffing it with keywords.
How to make an ability bulk-capable
Accepting an array of post IDs instead of just one.
Where to store the generated output
Post meta for the meta description, the attachment's own alt text field for images.
Prerequisites

Course 5 for wp_ai_client_prompt(). Test posts should have at least one attached image with post_parent set to the post, so there’s something real for the alt-text half to work on.

Step 1: Register a bulk-capable ability

input_schema accepts an array of post IDs, not a single one, so one call can cover many posts.

// File: seo-metadata/seo-metadata.php
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'seo-metadata/generate',
		array(
			'label'               => __( 'Generate meta descriptions and alt text', 'seo-metadata' ),
			'description'         => __( 'Generates a meta description and image alt text for one or more posts. Alt text describes what each image conveys, for accessibility, not for keyword stuffing.', 'seo-metadata' ),
			'category'            => 'content',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'post_ids' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ) ),
				),
				'required'   => array( 'post_ids' ),
			),
			'output_schema'       => array(
				'type'  => 'array',
				'items' => array(
					'type'       => 'object',
					'properties' => array(
						'post_id'         => array( 'type' => 'integer' ),
						'meta_description' => array( 'type' => 'string' ),
						'images_updated'  => array( 'type' => 'integer' ),
					),
				),
			),
			'permission_callback' => function ( $input ) {
				foreach ( $input['post_ids'] as $id ) {
					if ( ! current_user_can( 'edit_post', $id ) ) {
						return false;
					}
				}
				return true;
			},
			'execute_callback'    => 'seo_metadata_generate_for_posts',
			'meta' => array(
				'annotations' => array( 'destructive' => false, 'idempotent' => true ),
				'mcp'         => array( 'public' => true ),
			),
		)
	);
} );

permission_callback checks every post ID in the batch individually, not just whether the caller can edit posts in general, so a bulk call can’t sneak through edits on content the caller doesn’t actually have rights to.

Step 2: Generate for each post, using the AI Client SDK

// File: seo-metadata/seo-metadata.php
function seo_metadata_generate_for_posts( $input ) {
	$results = array();

	foreach ( $input['post_ids'] as $post_id ) {
		$post = get_post( $post_id );
		if ( ! $post ) {
			continue;
		}

		$body = wp_strip_all_tags( $post->post_content );

		$meta_prompt = "Write an SEO meta description for this post, under 155 characters, accurately summarizing the content, no clickbait:\n\nTitle: {$post->post_title}\nContent: {$body}";
		$meta_description = wp_ai_client_prompt( $meta_prompt )->generate_text();

		if ( ! empty( $meta_description ) ) {
			update_post_meta( $post_id, '_seo_meta_description', sanitize_text_field( trim( $meta_description ) ) );
		}

		$images_updated = seo_metadata_generate_alt_text_for_images( $post_id, $post->post_title );

		$results[] = array(
			'post_id'          => $post_id,
			'meta_description' => sanitize_text_field( trim( $meta_description ) ),
			'images_updated'   => $images_updated,
		);
	}

	return $results;
}

function seo_metadata_generate_alt_text_for_images( $post_id, $post_title ) {
	$images = get_posts( array(
		'post_type'      => 'attachment',
		'post_parent'    => $post_id,
		'post_mime_type' => 'image',
		'posts_per_page' => -1,
	) );

	$updated = 0;

	foreach ( $images as $image ) {
		$existing_alt = get_post_meta( $image->ID, '_wp_attachment_image_alt', true );
		if ( ! empty( $existing_alt ) ) {
			continue; // Never overwrite alt text a human already wrote.
		}

		$image_url = wp_get_attachment_url( $image->ID );
		$prompt    = "This image appears in a post titled \"{$post_title}\". Based on its filename and context, write concise, descriptive alt text (under 125 characters) that conveys what the image shows, for a screen reader user, not for search engines. Filename: {$image->post_title}";

		$alt_text = wp_ai_client_prompt( $prompt )->generate_text();

		if ( ! empty( $alt_text ) ) {
			update_post_meta( $image->ID, '_wp_attachment_image_alt', sanitize_text_field( trim( $alt_text ) ) );
			$updated++;
		}
	}

	return $updated;
}

Step 3: Why alt text is framed as accessibility first, not SEO first

The prompt explicitly says “for a screen reader user, not for search engines.” This matters: alt text that’s optimized purely for keyword density tends to be stuffed and unnatural, exactly the opposite of what a screen reader user actually needs, a plain, accurate description of what the image shows. Good alt text tends to help SEO as a side effect of being genuinely useful, but writing it backward, for search engines first, produces worse accessibility and often worse SEO outcomes too. Also note the code never overwrites alt text a human already wrote, get_post_meta() is checked first and the loop skips any image that already has one.

Test it

Run the ability against one or more real post IDs with attached images:

wp-env run cli wp eval '
$result = wp_get_ability( "seo-metadata/generate" )->execute( array(
	"post_ids" => array( 42, 43 ),
) );
print_r( $result );
'
wp-env run cli wp post meta get 42 _seo_meta_description

Confirm the meta description is under 155 characters and accurately reflects the post, then check one of the post’s images in wp-admin’s media library to confirm alt text was filled in.

Running this across your entire media library at once

Passing hundreds of post IDs in one call means hundreds of sequential AI provider calls (one per meta description, one per image), which can run long enough to hit a PHP execution time limit or an API rate limit. For a real bulk run across an entire site, process in smaller batches (a WP-CLI command looping in chunks of 20 to 50) rather than one enormous ability call.

Recap

One ability call generates both a meta description and alt text for every image on a post, or a batch of posts, using wp_ai_client_prompt() for each piece of text and writing results to post meta and the attachment’s own alt-text field. Existing alt text is never overwritten, and the alt-text prompt is deliberately framed around what a screen reader user needs, not around keyword density.

Resources & further reading

← Project: Site-Content Chatbot Project: Bulk-Editing Assistant →