Skip to chapter content

Chapter 9 3 min read

Using the Data, Saving JSON to Post Meta

Decoding & using the data (save to post meta)

Chapter 8 ended with a real, decoded array of ideas, and then did nothing with it. The list printed to the page and vanished the moment you reloaded it. That’s the gap this chapter closes: taking the same decoded data and actually keeping it.

Nothing about the AI call changes here. Same schema shape (an object wrapping the array, per Chapter 8), same as_json_response(), same json_decode(). The only new step happens after the loop.

From array to post meta

Since this is now its own chapter rather than a continuation within the same file, create a new file, chapter-09-json-to-meta.php inside includes:

<?php
/**
* Chapter 9: Using the Data, Saving JSON to Post Meta
* Usage: add [ai_course_ch9] inside an actual post or page (it needs a
* real title to work from, via get_the_title() / get_the_ID()).
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // No direct access.
}
function ai_course_ch9_json_to_meta() {
$original_title = get_the_title();
if ( empty( $original_title ) ) {
return 'Add this shortcode inside an actual post or page so there is a title to work from.';
}
$schema = array(
'type' => 'object',
'properties' => array(
'suggestions' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'title' => array( 'type' => 'string' ),
'reason' => array( 'type' => 'string' ),
),
'required' => array( 'title', 'reason' ),
'additionalProperties' => false,
),
),
),
'required' => array( 'suggestions' ),
'additionalProperties' => false,
);
$prompt = sprintf(
'Suggest 3 alternate SEO-friendly titles for a WordPress post currently titled "%s". For each, give the title and a one-sentence reason it could perform better.',
$original_title
);
$json = wp_ai_client_prompt( $prompt )
->as_json_response( $schema )
->generate_text();
if ( is_wp_error( $json ) ) {
return 'Could not generate title suggestions: ' . esc_html( $json->get_error_message() );
}
$data = json_decode( $json, true );
if ( ! is_array( $data ) || empty( $data['suggestions'] ) || ! is_array( $data['suggestions'] ) ) {
return 'The AI did not return valid suggestions that time, try reloading.';
}
// Sanitize every suggestion before it goes anywhere near the database.
$sanitized_suggestions = array();
foreach ( $data['suggestions'] as $suggestion ) {
$sanitized_suggestions[] = array(
'title' => sanitize_text_field( $suggestion['title'] ),
'reason' => sanitize_text_field( $suggestion['reason'] ),
);
}
// This is the new part: all 3 suggestions get saved to the post, not
// just printed and forgotten on the next page load. WordPress serializes
// the array automatically, and unserializes it again when it's read back.
update_post_meta( get_the_ID(), '_ai_course_seo_title_suggestions', $sanitized_suggestions );
$saved_suggestions = get_post_meta( get_the_ID(), '_ai_course_seo_title_suggestions', true );
$output = '<p>' . count( $saved_suggestions ) . ' suggestions saved to this post\'s meta (<code>_ai_course_seo_title_suggestions</code>).</p>';
$output .= '<ol>';
foreach ( $saved_suggestions as $suggestion ) {
$output .= '<li><strong>' . esc_html( $suggestion['title'] ) . '</strong>: ' . esc_html( $suggestion['reason'] ) . '</li>';
}
$output .= '</ol>';
return $output;
}
add_shortcode( 'ai_course_ch9', 'ai_course_ch9_json_to_meta' );

Two things worth noticing before you run it.

First, the prompt is no longer a fixed string. sprintf() builds it from get_the_title(), so the suggestions are actually about the post you put the shortcode on, not a generic example.

Second, the whole $sanitized_suggestions array gets passed straight to update_post_meta(), not a single string. WordPress serializes arrays automatically when you save them to post meta, and unserializes them back into a proper PHP array when you call get_post_meta() again, no manual json_encode()/json_decode() juggling needed on your end.

The get_post_meta() call right after isn’t required for the save to work. It’s there so the output on the page proves the round trip actually happened, all 3 suggestions, not just the top one, rather than asking you to take it on faith.

A couple of details carried over from Chapter 8, not new here, worth a quick mention.

The is_array( $data ) || empty( $data['suggestions'] ) || ! is_array(...) check is the same double-check Chapter 8 explains in full: it catches both a malformed response and a technically-valid-but-empty list, which a plain isset() check would miss.

And the schema itself still needs the object-wrapping and additionalProperties: false pattern from Chapter 8. If this shortcode errors out with something mentioning additionalProperties or a root-type mismatch, that’s the exact issue Chapter 8’s warning covers, the fix is the same.

Add [ai_course_ch9] inside a real post or page (a fresh Post block, a page you’ve already written, anything with an actual title) and load it. You’ll see three suggested titles with reasons, and above them, confirmation that all three are now sitting in that post’s meta.

Why the meta key starts with an underscore

_ai_course_seo_title_suggestions starts with an underscore on purpose. WordPress treats underscore-prefixed meta keys as “protected”, meaning they’re hidden from the default Custom Fields UI in the block editor. That’s normal for meta your plugin manages programmatically and doesn’t want a user editing by hand through a generic textbox.

Try it yourself

Right now, every time this shortcode runs, it calls the AI again, generates three new suggestions, and overwrites whatever was saved before. That’s wasteful once the data is actually sitting in post meta.

Write a second, much simpler shortcode, [ai_course_ch9_view], that only reads _ai_course_seo_title_suggestions with get_post_meta() and displays it, no AI call at all. Add it to the same post after you’ve run [ai_course_ch9] at least once, and confirm you get the same three suggestions back instantly, no wait, no new API request, no cost. That’s the actual payoff of saving structured data instead of just displaying it: you get to use it again without regenerating it.

This book is created with Chapterwright