Introduce two new abilities for getting and setting SEO data for given posts#23369
Introduce two new abilities for getting and setting SEO data for given posts#23369leonidasmi wants to merge 23 commits into
Conversation
Coverage Report for CI Build 4845Warning No base build found for commit Coverage: 50.021%Details
Uncovered Changes
Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
57d78fa to
824cc9a
Compare
af3c479 to
0fbb3ad
Compare
…es that we already have
There was a problem hiding this comment.
Pull request overview
This PR adds two new Yoast SEO WP Abilities to read and update a post’s SEO metadata, including support for resolving posts by ID/permalink and (for reads) by title keyword search with pagination. It introduces new application services for resolving post identifiers and mapping SEO fields between ability input/output and indexables, plus repository/query support and expanded unit/integration test coverage.
Changes:
- Add
yoast-seo/get-post-seo-dataandyoast-seo/update-post-seo-dataabilities with schemas and permission checks. - Implement title-keyword post lookup in
Indexable_Repository, and add services to resolve identifiers and map/apply SEO field patches. - Extend indexable→postmeta mapping to support per-column updates and “delete empty” semantics, with accompanying tests.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/WP/Repositories/Find_Posts_By_Title_Keywords_Test.php | WP-level integration tests for title-keyword post lookup behavior (OR phrases, pagination, caps, empty search). |
| tests/Unit/Repositories/Indexable_Repository_Test.php | Unit tests validating query construction for find_posts_by_title_keywords, including paging and caps. |
| tests/Unit/Helpers/Indexable_To_Postmeta_Helper_Test.php | Unit tests for new per-column postmeta mapping behavior and delete-empty semantics for added mappings. |
| tests/Unit/Abilities/User_Interface/Abilities_Integration_Test.php | Unit tests ensuring the two new abilities register correctly and permission callbacks behave as expected. |
| tests/Unit/Abilities/Application/Post_SEO_Field_Map_Test.php | Unit tests for SEO field mapping to output shape and patch-apply behavior onto indexables. |
| tests/Unit/Abilities/Application/Post_SEO_Data_Updater_Test.php | Unit tests for updating SEO data, cascading to postmeta, and rebuilding the indexable. |
| tests/Unit/Abilities/Application/Post_SEO_Data_Collector_Test.php | Unit tests for collecting SEO data for resolved posts and error propagation. |
| tests/Unit/Abilities/Application/Post_Identifier_Resolver_Test.php | Unit tests for resolving posts by ID/permalink/title keywords and error cases. |
| src/repositories/indexable-repository.php | Adds find_posts_by_title_keywords with phrase caps and pagination controls. |
| src/helpers/indexable-to-postmeta-helper.php | Adds per-column mapping and delete-empty support; extends mapped fields (cornerstone, schema types). |
| src/abilities/user-interface/abilities-integration.php | Registers the new abilities, schemas, and permissions; adds supporting dependencies. |
| src/abilities/application/post-seo-field-map.php | Implements the shared SEO field contract: ability input/output mapping and patch application. |
| src/abilities/application/post-seo-data-updater.php | Implements the write path: resolve → apply patch → map to postmeta → rebuild → return. |
| src/abilities/application/post-seo-data-collector.php | Implements the read path: resolve one/many → map to output array(s). |
| src/abilities/application/post-identifier-resolver.php | Adds shared resolver for post identification by ID/permalink/title keywords with standard errors. |
| composer.json | Updates YoastCS thresholds to reflect reduced CS errors. |
| public function map_column_to_postmeta( $indexable, $indexable_column, $delete_empty = false ) { | ||
| // The advanced-robots flags are persisted together under one meta key, so they cascade through it. | ||
| if ( \in_array( $indexable_column, [ 'is_robots_noimageindex', 'is_robots_noarchive', 'is_robots_nosnippet' ], true ) ) { | ||
| $indexable_column = 'meta_robots_adv'; | ||
| } | ||
|
|
||
| if ( ! isset( $this->yoast_to_postmeta[ $indexable_column ] ) ) { | ||
| return; | ||
| } | ||
|
|
||
| $map_info = $this->yoast_to_postmeta[ $indexable_column ]; | ||
| \call_user_func( [ $this, $map_info['map_method'] ], $indexable, $map_info['post_meta_key'], $indexable_column, $delete_empty ); | ||
| } |
| 'is_cornerstone' => (bool) $indexable->is_cornerstone, | ||
| 'noindex' => $indexable->is_robots_noindex, | ||
| 'nofollow' => (bool) $indexable->is_robots_nofollow, |
The additionalProperties => false addition to the post-identifier and update input schemas made the strict registration test's expected input_schema diverge from the actual wp_register_ability arguments, erroring the unit suite. Mirror additionalProperties => false into the two expected-schema helpers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
A merge conflict has been detected for the proposed code changes in this PR. Please resolve the conflict by either rebasing the PR or merging in changes from the base branch. |
Context
if indexables are enabledcondition for the pre-existing abilities as well, because they are also based on indexables.Summary
This PR can be summarized in the following changelog entry:
Relevant technical choices:
Title-based post lookup:
post_id,permalink, ortitle. The title value is a comma-separated list; each value is matched as a whole contiguous substring of the post's breadcrumb title, and a post is returned if it matches any value (OR/union). Example —hiking boots, trailcompiles to:Not super-DRY :
Abilities_Integration(where we register the SEO data a user can edit) andPost_SEO_Field_Map(where we register how the SEO data coming from the user are applied to indexable data)Abilities_Integrationside, where now the SEO data that we expect are glanceableTest instructions
Test instructions for the acceptance test before the PR gets merged
This PR can be acceptance tested by following these steps:
Declaration of the abilities
The GET SEO data ability
http://example.com/wp-json/wp-abilities/v1/abilitiesendpoint and confirm that you see theyoast-seo/get-post-seo-dataability we added and that it looks like this:The SET SEO data ability
http://example.com/wp-json/wp-abilities/v1/abilitiesendpoint and confirm that you see theyoast-seo/update-post-seo-dataability we added and that it looks like this:add_filter( 'Yoast\WP\SEO\should_index_indexables', '__return_false' );filter) and confirm that you can't find the above abilitiesUsage of the new abilities
Using the REST API
Using our `getting SEO data` ability
/wp-json/wp-abilities/v1/abilities/yoast-seo/get-post-seo-data/run?input[post_id]=<POST_ID><POST_ID>is the post ID/wp-json/wp-abilities/v1/abilities/yoast-seo/get-post-seo-data/run?input[permalink]=https://basic.wordpress.test/<POST_SLUG><POST_SLUG>is the post's slu<POST_TITLE_KEYWORDS>is keywords that exist in the post's title, separated by commas (we're gonna test this more extensively below).10 ways to backpack across northern Europe, you can use10 ways, backpack, northern Europeor any combination of that, to find your post_renderedattributes are the ones that are output in the frontend, with default settings and replacement variables expandedyoast_seo_invalid_<indentifier> - No post could be found for the given identifiererrorrest_ability_cannot_executeerrorpost_id, orpermalinkortitlein the parameters and confirm you get ayoast_seo_missing_identifiererrorUsing our `setting SEO data` ability
Run the following queries, to see the state of the postmeta and indexable data in the database for the post you're gonna edit (use the <POST_ID> accordingly):
Post meta:
Indexable:
Run a POST request to
/wp-json/wp-abilities/v1/abilities/yoast-seo/update-post-seo-data/run{"input":{"post_id":<POST_ID>,"seo_title":"new SEO title"}}{"input":{"permalink":<POST_PERMALINK>,"seo_title":"new SEO title"}}and instead of
seo_title, you can set any of the attributes that were listed in the ability declaration you saw in the above section. Some example that we like to test are belowFor all examples below, once you do the requests:
Canonical
{"input":{"post_id":<POST_ID>,"canonical":"https://basic.wordpress.test/my-canonical/"}}."canonical": "https://basic.wordpress.test/my-canonical/"and"canonical_rendered"matching it, and that the<link rel="canonical">on the front end points there._yoast_wpseo_canonicalrow with valuehttps://basic.wordpress.test/my-canonical/, and the indexablecanonicalcolumn holds the same value.{"input":{"post_id":<POST_ID>,"canonical":""}}."canonical"is back tonulland"canonical_rendered"reverted to the post's own permalink._yoast_wpseo_canonicalmeta row is gone, and the indexablecanonicalcolumn isNULL(it did not get the computed permalink baked in).Cornerstone
{"input":{"post_id":<POST_ID>,"is_cornerstone":true}}. Confirm the response shows"is_cornerstone": trueand the metabox toggle is on._yoast_wpseo_is_cornerstone=1, and the indexableis_cornerstonecolumn =1.{"input":{"post_id":<POST_ID>,"is_cornerstone":false}}. Confirm"is_cornerstone": falseand the toggle is off._yoast_wpseo_is_cornerstonemeta row is gone, and the indexableis_cornerstonecolumn =0.Schema
{"input":{"post_id":<POST_ID>,"schema_page_type":"ItemPage","schema_article_type":"NewsArticle"}}._yoast_wpseo_schema_page_type=ItemPageand_yoast_wpseo_schema_article_type=NewsArticle; the indexableschema_page_type/schema_article_typecolumns hold the same.{"input":{"post_id":<POST_ID>,"schema_page_type":"","schema_article_type":""}}and confirm both arenullin the response._yoast_wpseo_schema_*meta rows are gone, and both indexable schema columns areNULL.{"input":{"post_id":<POST_ID>,"schema_page_type":"FooPage"}}and confirm you get an error._yoast_wpseo_schema_page_typemeta row and the indexable column still hold their previous values (the rejected request wrote nothing).Advanced robots (noimageindex / noarchive / nosnippet)
_yoast_wpseo_meta-robots-advmeta key (as a comma-separated list), but each maps to its own indexable column — so this also tests that they merge correctly.{"input":{"post_id":<POST_ID>,"noimageindex":true,"noarchive":true}}."noimageindex": true,"noarchive": true,"nosnippet": false, and that the front-end robots meta tag includesnoimageindexandnoarchive._yoast_wpseo_meta-robots-adv=noimageindex,noarchive; the indexableis_robots_noimageindex=1,is_robots_noarchive=1,is_robots_nosnippet=0.{"input":{"post_id":<POST_ID>,"nosnippet":true}}.true(the previously-set flags were preserved, not wiped)._yoast_wpseo_meta-robots-adv=noimageindex,noarchive,nosnippet; all threeis_robots_*indexable columns =1.{"input":{"post_id":<POST_ID>,"noimageindex":false,"noarchive":false,"nosnippet":false}}.falsein the response and the front-end robots meta tag no longer lists any of them._yoast_wpseo_meta-robots-advmeta row is gone, and all threeis_robots_*indexable columns =0.At least one of the rest (robots + title)
{"input":{"post_id":<POST_ID>,"seo_title":"My custom title","noindex":true}}."seo_title"and"noindex": true, and the front-end title/robots meta reflect it._yoast_wpseo_title=My custom titleand_yoast_wpseo_meta-robots-noindex=1; the indexabletitlecolumn =My custom titleandis_robots_noindexcolumn =1.{"input":{"post_id":<POST_ID>,"noindex":null}}._yoast_wpseo_meta-robots-noindexmeta row is gone, and the indexableis_robots_noindexcolumn isNULL.{"input":{"post_id":<POST_ID>,"noindex":false}}._yoast_wpseo_meta-robots-noindexmeta row is equal to2, and the indexableis_robots_noindexcolumn is0.Repeat the test using invalid SEO data, for example use a schema page type that doesn't exist. eg.
{"permalink":<POST_PERMALINK>,"schema_page_type":"FooPage"}}. Confirm that you get an error.Repeat the test with a post ID or permalink that should yield no posts and confirm that you get a
yoast_seo_invalid_<indentifier> - No post could be found for the given identifiererrorRepeat the test with a user with no capabilities of editing that post and confirm that you get a
rest_ability_cannot_executeerrorUsing an AI agent
https://basic.wordpress.test/test-postpost"https://basic.wordpress.test/test-postpost"FooPage. Confirm that the agent wont do that.Test the post lookup via titles
Using REST API
//wp-json/wp-abilities/v1/abilities/yoast-seo/get-post-seo-data/run?input[title]=<KEYWORDS>, each time with a different title as :input[title]=hiking bootsshould yield the 2 first posts ("Hiking Boots Review" and "Best Hiking Boots 2026")input[title]=hiking boots, trailshould yield all posts ("Hiking Boots Review" and "Best Hiking Boots 2026" and "Trail Running Guide")input[title]=boots hikingshould yield no posts, so it will return ayoast_seo_invalid_<indentifier> - No post could be found for the given identifiererrorHiking Bootsin their title to test the pagination//wp-json/wp-abilities/v1/abilities/yoast-seo/get-post-seo-data/run?input[title]=<KEYWORDS>&input[page]=<NUMBER_OF_PAGINATION>, each time with a different title as :input[title]=hiking bootsandinput[page]=1should yield the 10 most recently modified postsinput[title]=hiking bootsandinput[page]=2should yield the next 10 most recently modified posts (if you followed the above instructions closely, this will be a single post with the "Hiking Boots Review" titleUsing AI Agent
Test permissions for agents
Relevant test scenarios
Test instructions for QA when the code is in the RC
Impact check
This PR affects the following parts of the plugin, which may require extra testing:
Other environments
[shopify-seo], added test instructions for Shopify and attached theShopifylabel to this PR.[yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached theGoogle Docs Add-onlabel to this PR.Documentation
Quality assurance
grunt build:imagesand committed the results, if my PR introduces or edits images or SVGs.Innovation
innovationlabel.Fixes #