Yoast indexables: why an SEO title you changed still shows the old one

Updated Published 10 min read

In short

Yoast SEO renders titles and meta descriptions from its own indexables table, not from post meta. Yoast rebuilds a post's indexable when the post is saved, or at the end of a request that changed its meta through WordPress's API. A write that bypasses that path leaves the indexable stale, and the page keeps serving the old value.

Key points

  • Yoast SEO's front end reads the wp_yoast_indexable table. Post meta is what the indexable is built from, not what the page is rendered from.
  • In Yoast SEO 28.7, a change to a _yoast_wpseo_ meta key made through update_post_meta() queues that post, and Yoast rebuilds its indexable when the request ends.
  • Direct SQL, imports that skip WordPress's meta functions, and writes made while Yoast is not loaded all leave a stale indexable, and nothing reports an error.
  • Plain wp yoast index builds only missing or outdated-version indexables; wp yoast index --reindex deletes and rebuilds every one.
  • Yoast does not save post indexables outside a production environment by default, so a stale indexable usually cannot be reproduced on staging.

The report always reads the same way. The SEO title was changed — by an import, a script, a bulk tool, a database edit — and the field in Yoast's meta box shows the new value, and the page still serves the old one. Nothing errored. Clearing the page cache did not help.

The cause is usually not a cache in the ordinary sense. It is that Yoast SEO keeps a second copy of your metadata, renders from that copy, and the copy was never told the first one changed.

What an indexable is

Since Yoast SEO 14.0, each public post, term and author archive, plus the homepage, gets a row in a table Yoast owns, wp_yoast_indexable (with your own table prefix). Yoast's functional specification calls it "an abstraction layer for interacting with post metadata relating to SEO", kept in "a dedicated database table", and says that when Yoast meets a URL it does not know, it creates a new record.

The SEO title and meta description you type into the Yoast box are still saved where they always were, in post meta under _yoast_wpseo_title and _yoast_wpseo_metadesc. Yoast kept them there deliberately — its own 14.0 post-mortem notes a site-wide re-index was possible "because we left all the data in wp_postmeta". The indexable is built from that post meta.

But the front end reads the indexable. In Yoast's post presentation class, the meta description is $this->model->description — the indexable row's column — falling back to the post type's template when it is empty. Post meta is not consulted on the way to the page. So the two copies can disagree, and when they do, the page shows the indexable's.

How Yoast keeps the two in sync

Yoast is not careless about this. In 28.7 it watches two things:

  • Saving a post. The post watcher hooks wp_insert_post and rebuilds the indexable from current data. This is why editing a post in the block editor always works.
  • Its own meta keys changing. The post meta watcher hooks added_post_meta, updated_post_meta and deleted_post_meta, keeps a list of every post whose changed key starts with _yoast_wpseo_, and rebuilds those indexables in a PHP shutdown function — once the request has finished its work.

The second watcher is the one most explanations of this problem leave out, and it changes the advice. A plugin that calls update_post_meta() on a Yoast key, in a request where Yoast is loaded, normally does reach the page — just not until that request ends.

When the indexable goes stale

Every way to end up with a stale row is a way of changing the data that neither watcher sees:

  • The write bypasses WordPress's meta functions. A direct UPDATE wp_postmeta, a SQL import, a database search-and-replace, a row edited in phpMyAdmin, an import tool that inserts meta rows itself for speed. No updated_post_meta action fires, so Yoast never hears about it.
  • Yoast is not loaded when the write happens. Yoast was deactivated during a migration and reactivated afterwards, a WP-CLI command ran with --skip-plugins, or a standalone script bootstrapped WordPress without it. The meta changes; the watcher that would have queued it was not there.
  • The write happens on a switched multisite blog. The post watcher's build_indexable() returns immediately when ms_is_switched() is true, so a network-level tool that calls switch_to_blog() and writes there leaves that site's indexable as it was.
  • Something reads the page before the request ends. The rebuild runs at shutdown. A tool that writes and then fetches the page within the same request sees the old value, every time — which is exactly how a "write, then check the page" test fails on a correct write.

None of these produces an error. The meta value is right, the meta box shows it, and the row Yoast renders from still holds the previous one.

Why it works on staging

This is the detail that makes the problem so hard to pin down. Yoast saves post indexables only on a production site: should_index_indexables() returns true only when WordPress's environment type is production, unless a developer filters it. On a site set to staging or development, nothing is persisted, every request builds its presentation from current data, and there is no stale row to find.

So the import that leaves production showing old titles works perfectly when you rehearse it on staging. If a problem appears only in production, check wp_get_environment_type() on both before assuming anything else differs.

How to prove it is the indexable

Three reads of the same post, and the disagreement between them is the diagnosis. With WP-CLI, for post 123:

# 1. What is stored
wp post meta get 123 _yoast_wpseo_metadesc

# 2. What Yoast will render (adjust the wp_ prefix to yours)
wp db query "SELECT description FROM wp_yoast_indexable WHERE object_id = 123 AND object_type = 'post'"

# 3. What the page actually serves
curl -s https://example.com/?p=123 | grep -io '<meta name="description"[^>]*>'

If 1 is new and 2 and 3 are old, the indexable is stale. If 1 and 2 are new and 3 is old, it is a page cache. If all three are new and the search result is old, it is Google — which rewrites snippets on its own schedule anyway. Each answer points somewhere different, which is why it is worth the minute.

How to fix a stale indexable

  • One post: open it in the editor and update it. That fires wp_insert_post, and the post watcher rebuilds the row.
  • Many posts, or not sure which: wp yoast index --reindex. Yoast's documentation for the command describes --reindex as removing "all existing indexables" and then reindexing them. On a large site it takes a while, and it is the thorough option.

One thing that does not fix it: plain wp yoast index, or the "SEO data optimization" button in Yoast's tools. Both run the same indexation action, which selects posts with no indexable at the current builder version. A stale row is current-version and present, so it is skipped and the tool reports that everything is done. That is the right behaviour for the job those tools exist for — filling in a site that has never been indexed — and the wrong tool for this one.

Going forward, the durable fix is the boring one: write through update_post_meta(), with Yoast active, and tell Yoast directly when you cannot be sure its watcher will run.

How SnippetGrove writes to Yoast

SnippetGrove writes SEO titles and meta descriptions in bulk into whichever SEO plugin a site already runs, so Yoast's second copy is the first thing its Yoast adapter has to get right. All of it lives in a shared library, seo-hosts, that every write path goes through — generated runs, hand edits on the Missing metadata screen, and undo.

  • It writes where Yoast's own meta box writes. _yoast_wpseo_title and _yoast_wpseo_metadesc, through update_post_meta(), so Yoast's watcher sees every change as well.
  • Then it refreshes the indexable itself. After each batch — every 100 applied items in a run, or each save of hand edits — it asks Yoast's own indexable repository for the post's row and deletes it through Yoast's model, so Yoast rebuilds it from current post meta on the next request. If that route is unavailable it asks Yoast's builder to rebuild the row in place instead, and either way it clears WordPress's post cache.
  • It never touches Yoast's table with SQL. Yoast owns that schema and changes it between major versions; the adapter goes through Yoast's own classes or leaves the row alone.
  • It never re-saves the post to trigger a rebuild. wp_update_post() would work, and it would also bump post_modified on every post it touched — re-dating an entire catalogue after a metadata run, and misleading every feed and reader that trusts "last updated".

Why do both, when Yoast's watcher would usually catch the write? Because the adapter's refresh does not depend on Yoast's watcher being registered in that request, or on the request reaching its end before anything reads the page. It is idempotent with the watcher — deleting a row Yoast is about to rebuild costs one rebuild — and it closes every gap above that a well-behaved writer can close.

The proof is a check rather than an assertion. Under Settings → Check the SEO plugin connection, SnippetGrove writes a marked probe value to one published post, reads it back from Yoast's field, refreshes the indexable, fetches the page over HTTP, and passes only if the served <head> carries exactly one title and exactly one meta description with the probe in them. It then restores the original values and confirms post_modified did not move. When the check fails on Yoast, it also shows the indexable's description beside the post meta value, so a mismatch is visible as data rather than as a hunch. The documentation describes each step.

That round trip was run against Yoast SEO 28.1 on a live WooCommerce store on 2026-07-29, and against Yoast SEO 28.5 on WordPress 6.9.4 and 7.1.1 on 2026-09-19 — apply, undo and a manual-edit conflict, with the rendered page read before and after each and post_modified unchanged throughout. The coverage audit and the connection check are free and need no licence. Generation itself is hosted and enabled for activated licences; public licensing enrollment is not yet open.

If the stale value on your page is not Yoast's, the next most likely cause is a second SEO plugin rendering its own tags. That has a guide of its own: running two SEO plugins at once.

Sources

Technical claims in this guide are drawn from the primary documentation and specifications below, each confirmed live on the date shown.

  1. Indexables: Functional specification — Yoast developer portal. Accessed .
  2. Yoast SEO 14.0.x; or "Why you should never bypass wpdb" — Yoast developer blog. Accessed .
  3. Yoast SEO 28.7 source: src/presentations/indexable-post-type-presentation.php — WordPress.org plugin SVN (Yoast). Accessed .
  4. Yoast SEO 28.7 source: src/integrations/watchers/indexable-post-watcher.php — WordPress.org plugin SVN (Yoast). Accessed .
  5. Yoast SEO 28.7 source: src/integrations/watchers/indexable-post-meta-watcher.php — WordPress.org plugin SVN (Yoast). Accessed .
  6. Yoast SEO 28.7 source: src/helpers/indexable-helper.php — WordPress.org plugin SVN (Yoast). Accessed .
  7. Yoast SEO WP CLI: Reindex Indexables command — Yoast developer portal. Accessed .
  8. Yoast SEO 28.7 source: src/actions/indexing/indexable-post-indexation-action.php — WordPress.org plugin SVN (Yoast). Accessed .
  9. Control your snippets in Google Search results — Google Search Central. Accessed .

FAQ

What is a Yoast indexable?
An indexable is a row in Yoast SEO's own database table, wp_yoast_indexable, holding the SEO data for one URL: a post, a term, an author archive or the homepage. Yoast introduced indexables in version 14.0 and renders a page's title, meta description and social tags from that row rather than from post meta.
Why does my page show the old meta description after I changed it?
On a Yoast site the most common cause is a stale indexable: the value in post meta changed, but the copy in wp_yoast_indexable that Yoast renders from did not. That happens when the change bypassed WordPress's meta functions, was made while Yoast was not loaded, or was checked before the request that made it had finished.
Does updating _yoast_wpseo_metadesc with update_post_meta() update the page?
Normally, yes, as long as Yoast is active in that request. Yoast SEO 28.7 watches added, updated and deleted post meta for its own _yoast_wpseo_ keys and rebuilds the affected indexables in a shutdown function when the request ends. Anything that reads the page before then still sees the old value.
How do I rebuild Yoast indexables?
For one post, update it in the editor. For a whole site, run wp yoast index --reindex, which deletes every indexable and rebuilds them. Plain wp yoast index and the SEO data optimization tool only build indexables that are missing or from an older builder version, so they skip a row that exists but holds stale data.
Why can I not reproduce the problem on my staging site?
Yoast saves post indexables only when WordPress's environment type is production, unless a developer overrides it with a filter. On a staging or development site nothing is persisted, every request builds fresh from post meta, and a stale indexable cannot exist. A problem seen only in production often traces back to that difference.
Is it safe to delete rows from wp_yoast_indexable?
Yoast recreates a missing indexable the next time that URL is requested, which is what wp yoast index --reindex relies on. Deleting rows with raw SQL, though, couples you to a schema Yoast changes between major versions. Re-saving the post or running the reindex command does the same job through Yoast's own code.

Check your own titles and descriptions against everything in this guide — length, truncation, gaps and duplicates — a few thousand at a time.

Open the bulk checker