How to Update Yoast SEO Titles and Meta Descriptions via the WordPress REST API

Laptop on desk showing code editor — Yoast SEO WordPress REST API guide

When working with SEO, you’ll often end up with audits or gaps that reveal multiple pages or content types missing a meta title, meta description, or other meta information. Updating them in bulk is a tedious task, especially if you do it through the manual WordPress editor using something like Yoast.

In an ideal world you’d do it rapidly using an MCP server, with REST commands running behind the scenes — or just hit the REST API directly with an application password if you’re more comfortable that way. The problem: Yoast SEO doesn’t expose these fields to the REST API automatically.

So what do we need to do? We need to enable it. Let’s look at how, step by step, and explore the problem in more detail.

The Problem: Yoast’s Fields Are Not REST-Writable by Default

Yoast stores its two most-edited fields as standard WordPress post meta:

Field Meta key
SEO title _yoast_wpseo_title
Meta description _yoast_wpseo_metadesc

The catch is that Yoast registers these keys without show_in_rest. That has three knock-on effects:

  • A GET /wp/v2/pages/{id} request won’t include them in the meta object.
  • A POST that sets them in meta returns success, but WordPress silently drops the keys because they aren’t registered as REST-writable.
  • Yoast’s own /yoast/v1/ namespace only exposes the read-only yoast_head_json field. There’s no official write endpoint.

This has been raised with Yoast since 2019 (issue #14114 on GitHub), and the position hasn’t changed. If you want to write these fields through REST, you have to register them yourself.

The Fix: Register the Meta Keys Yourself

Add the following to your child theme’s functions.php (or a site-specific plugin). It tells WordPress that these two keys are REST-writable, with authentication and sanitisation built in.

function bb_register_yoast_rest_meta() {
    $post_types = array( 'post', 'page', 'product' );
    $keys       = array( '_yoast_wpseo_title', '_yoast_wpseo_metadesc' );
    foreach ( $post_types as $type ) {
        foreach ( $keys as $key ) {
            register_post_meta( $type, $key, array(
                'type'              => 'string',
                'single'            => true,
                'show_in_rest'      => true,
                'sanitize_callback' => 'sanitize_text_field',
                'auth_callback'     => function() {
                    return current_user_can( 'edit_posts' );
                },
            ) );
        }
    }
}
add_action( 'init', 'bb_register_yoast_rest_meta' );

After deploy, the two keys show up in the meta object on authenticated REST responses and accept updates through standard POST or PUT requests. The register_post_meta() reference is the canonical source for the options used here.

When This Earns Its Keep

If you’re only editing one or two pages, just use the Yoast sidebar in the editor. The REST route is worth setting up when you’re automating SEO with Claude and the WordPress MCP server (the wp_update_content tool goes through REST), migrating titles and descriptions across hundreds of pages or products from a CSV, mirroring SEO fields from another site (such as pulling a live Shopify store’s titles into a new WordPress build), keeping SEO content in a repo so changes are reviewable and re-applicable, or promoting SEO from staging to production without a full database import.

Using It

Reading the Fields

curl -s -u 'user:app_password' \
  'https://example.com/wp-json/wp/v2/pages/802?context=edit' \
  | jq '.meta'

Two things to note: you need ?context=edit, and you need to be authenticated (an application password is the easiest route). Without both, WordPress won’t return the meta object at all.

Writing the Fields

curl -s -u 'user:app_password' \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{
    "meta": {
      "_yoast_wpseo_title": "Your Brand | Product Category You Can Trust",
      "_yoast_wpseo_metadesc": "Short, benefit-led description that reads well in search results and stays under the 155-character cap."
    }
  }' \
  'https://example.com/wp-json/wp/v2/pages/802'

Writing via the WordPress MCP Server

If you’re using Claude with the WordPress MCP server, the same write looks like this:

wp_update_content(
  site = "your-site",
  type = "page",
  id = 802,
  updates = {},
  meta = {
    "_yoast_wpseo_title": "...",
    "_yoast_wpseo_metadesc": "..."
  }
)

Verify with wp_get_content and include_meta: true. Without the snippet above, the MCP call will look like it worked but leave Yoast untouched.

Extending to Other Post Types

The snippet covers post, page, and product. If you have a custom post type — say case-study or recipe — add its slug to the $post_types array. The CPT also needs show_in_rest => true in its own registration, which is standard for anything that uses the block editor.

Is It Safe?

Short answer: yes, it’s equivalent to the edit surface that already exists in the Yoast sidebar.

The snippet enforces three things. The auth_callback checks current_user_can( 'edit_posts' ), so anonymous requests get a 401. The sanitize_callback runs sanitize_text_field, which strips tags, null bytes, and excess whitespace before the value is saved. The type => string and single => true declarations make WordPress reject arrays or objects at the schema level.

What it doesn’t change: the same fields are already editable through the Yoast sidebar by anyone with edit_posts. You’re not granting a new capability, you’re exposing the same edit surface via a second interface. Yoast itself escapes these values when rendering them into <title> and <meta> tags, so sanitize_text_field on save is defence in depth, not the only line of defence. And REST writes still require a logged-in cookie/nonce, an application password, or OAuth — exactly what the admin already needs.

If you wanted to tighten things further, swap edit_posts for manage_options so only admins can write SEO via REST. That will also lock out legitimate editor-role users, which is usually too strict — but it’s there if you need it. For application-password hardening more broadly, the WordPress Advanced Administration Handbook on Application Passwords is the current canonical reference.

Troubleshooting

Update returns success but nothing changes. The snippet hasn’t deployed to that environment yet. Check functions.php on the server, then load any page once to trigger init.

GET still doesn’t show the keys. You’re probably missing ?context=edit, or you’re not authenticated, or the snippet isn’t loaded. WordPress only returns meta with edit context to authorised users.

You see the title save, but Yoast’s output in the HTML doesn’t change. Two usual causes. Either Yoast is falling back to its title template (%%sitename%% %%page%%) because the value you sent is empty after sanitisation — check what actually got through — or a page or CDN cache is serving a stale copy. Purge and re-fetch.

Final Thought

Yoast’s REST gap is mildly annoying but easy to close: eight lines of register_post_meta() in a child theme and the rest of your tooling starts working the way you’d expect. Once the fields are writable, bulk SEO work stops being a click-through-the-editor slog and turns into something you can script, review, and version.

References

Need expert WordPress support?

Whether it's custom development, performance issues, or ongoing maintenance—we've got you covered. Let's talk about keeping your WordPress site running at its best.