Skip to content

Reinstate get/set SEO data abilities - #23526

Draft
leonidasmi wants to merge 1 commit into
trunkfrom
1377-reinstate-getset-seo-data-abilities
Draft

Reinstate get/set SEO data abilities#23526
leonidasmi wants to merge 1 commit into
trunkfrom
1377-reinstate-getset-seo-data-abilities

Conversation

@leonidasmi

Copy link
Copy Markdown
Contributor

Context

Summary

This PR can be summarized in the following changelog entry:

  • Introduces two new Yoast SEO abilities to get and set SEO data for specified posts.

Relevant technical choices:

Test 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

Note for testers:
Throughout the testing process, to perform GET requests do the following:

  • Open a post in the Block editor
  • Open the browser's console and execute the following snippet:
pathToTest = '/wp-abilities/v1/abilities';
response = await wp.apiFetch( { path: pathToTest } );
console.log( response );
  • The example above performs a GET request to http://YOUR-BLOG-URL/wp-json/wp-abilities/v1/abilities; make sure to adapt the pathToTest value accordingly to what you're testing

  • Do a GET request to WP's http://example.com/wp-json/wp-abilities/v1/abilities endpoint and confirm that you see the yoast-seo/get-post-seo-data ability we added and that it looks like this:
    {
        "name": "yoast-seo/get-post-seo-data",
        "label": "Get Post SEO Data",
        "description": "Get the SEO data for a post. Identify the post by post_id, by permalink (URL), or by title keywords; the title may be a comma-separated list and returns the SEO data for every post matching any of the values, paginated most recently modified first (use the page parameter to reach older matches). At least one identifier is required. Only posts the current user is allowed to edit are returned.",
        "category": "yoast-seo",
        "input_schema": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
                "post_id": {
                    "type": "integer",
                    "description": "The ID of the post to retrieve.",
                    "minimum": 1
                },
                "permalink": {
                    "type": "string",
                    "description": "The permalink (URL) of the post to retrieve."
                },
                "title": {
                    "type": "string",
                    "description": "Keywords to search for in post titles. Provide a comma-separated list to search for several titles at once; each value is matched as a whole phrase against the post title, and a post matching any value is returned. At most 10 phrases are used per request; any beyond the first 10 are ignored. Results are paginated to 10 entities per page; see the page parameter."
                },
                "page": {
                    "type": "integer",
                    "description": "The page of title-search results to return, 1-based and defaulting to 1. Matches are ordered most recently modified first, so request a later page to reach older matches. An empty result means there are no further pages. Only applies to a title search.",
                    "minimum": 1,
                    "default": 1
                }
            }
        },
        "output_schema": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "post_id": {
                        "type": "integer"
                    },
                    "post_title": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "permalink": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "post_type": {
                        "type": "string"
                    },
                    "post_status": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "seo_title": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "seo_title_rendered": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The SEO title as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                    },
                    "meta_description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "meta_description_rendered": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The meta description as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                    },
                    "focus_keyphrase": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "canonical": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "canonical_rendered": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The canonical URL as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                    },
                    "is_cornerstone": {
                        "type": "boolean"
                    },
                    "noindex": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether search engines are told not to index this post. true means noindex (the post is excluded from search results); false means the post is forced to be indexed; null means no setting is stored and the post-type default applies."
                    },
                    "nofollow": {
                        "type": "boolean"
                    },
                    "noimageindex": {
                        "type": "boolean"
                    },
                    "noarchive": {
                        "type": "boolean"
                    },
                    "nosnippet": {
                        "type": "boolean"
                    },
                    "open_graph_title": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "open_graph_title_rendered": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The Open Graph title as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                    },
                    "open_graph_description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "open_graph_description_rendered": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The Open Graph description as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                    },
                    "twitter_title": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "twitter_title_rendered": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The Twitter title as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                    },
                    "twitter_description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "twitter_description_rendered": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The Twitter description as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                    },
                    "schema_page_type": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "schema_article_type": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "seo_score": {
                        "type": "string",
                        "enum": [
                            "na",
                            "bad",
                            "ok",
                            "good"
                        ],
                        "description": "The result of the SEO analysis that ran on the post when it was last saved."
                    },
                    "readability_score": {
                        "type": "string",
                        "enum": [
                            "na",
                            "bad",
                            "ok",
                            "good"
                        ],
                        "description": "The result of the readability analysis that ran on the post when it was last saved."
                    },
                    "inclusive_language_score": {
                        "type": "string",
                        "enum": [
                            "na",
                            "bad",
                            "ok",
                            "good"
                        ],
                        "description": "The result of the inclusive language analysis that ran on the post when it was last saved."
                    }
                }
            }
        },
        "meta": {
            "annotations": {
                "readonly": true,
                "destructive": false,
                "idempotent": true
            },
            "show_in_rest": true,
            "mcp": {
                "public": true
            }
        },
        "_links": {
            "self": [
                {
                    "href": "https://basic.wordpress.test/wp-json/wp-abilities/v1/abilities/yoast-seo/get-post-seo-data",
                    "targetHints": {
                        "allow": [
                            "GET"
                        ]
                    }
                }
            ],
            "collection": [
                {
                    "href": "https://basic.wordpress.test/wp-json/wp-abilities/v1/abilities"
                }
            ],
            "wp:action-run": [
                {
                    "href": "https://basic.wordpress.test/wp-json/wp-abilities/v1/abilities/yoast-seo/get-post-seo-data/run"
                }
            ]
        }
    }
The SET SEO data ability
  • Do a GET request to WP's http://example.com/wp-json/wp-abilities/v1/abilities endpoint and confirm that you see the yoast-seo/update-post-seo-data ability we added and that it looks like this:
    {
        "name": "yoast-seo/update-post-seo-data",
        "label": "Update Post SEO Data",
        "description": "Update the SEO data for a single post. Identify the post by post_id or by permalink (URL). Only the fields you provide are changed; a provided empty value clears that field. Only posts the current user is allowed to edit can be updated.",
        "category": "yoast-seo",
        "input_schema": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
                "post_id": {
                    "type": "integer",
                    "description": "The ID of the post to update.",
                    "minimum": 1
                },
                "permalink": {
                    "type": "string",
                    "description": "The permalink (URL) of the post to update."
                },
                "canonical": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "schema_page_type": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "The Schema.org page type for the post. Must be one of the supported page types. Use null to clear it and fall back to the default.",
                    "enum": [
                        "WebPage",
                        "ItemPage",
                        "AboutPage",
                        "FAQPage",
                        "QAPage",
                        "ProfilePage",
                        "ContactPage",
                        "MedicalWebPage",
                        "CollectionPage",
                        "CheckoutPage",
                        "RealEstateListing",
                        "SearchResultsPage",
                        "",
                        null
                    ]
                },
                "schema_article_type": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "The Schema.org article type for the post. Must be one of the supported article types. Use null to clear it and fall back to the default.",
                    "enum": [
                        "Article",
                        "BlogPosting",
                        "SocialMediaPosting",
                        "NewsArticle",
                        "AdvertiserContentArticle",
                        "SatiricalArticle",
                        "ScholarlyArticle",
                        "TechArticle",
                        "Report",
                        "None",
                        "",
                        null
                    ]
                },
                "is_cornerstone": {
                    "type": "boolean"
                },
                "noindex": {
                    "type": [
                        "boolean",
                        "null"
                    ],
                    "description": "Whether search engines should be told not to index this post. true sets noindex (the post is excluded from search results); false forces the post to be indexed; null clears the setting and falls back to the post-type default."
                },
                "nofollow": {
                    "type": "boolean"
                },
                "noimageindex": {
                    "type": "boolean"
                },
                "noarchive": {
                    "type": "boolean"
                },
                "nosnippet": {
                    "type": "boolean"
                }
            }
        },
        "output_schema": {
            "type": "object",
            "properties": {
                "post_id": {
                    "type": "integer"
                },
                "post_title": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "permalink": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "post_type": {
                    "type": "string"
                },
                "post_status": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "seo_title": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "seo_title_rendered": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "The SEO title as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                },
                "meta_description": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "meta_description_rendered": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "The meta description as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                },
                "focus_keyphrase": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "canonical": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "canonical_rendered": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "The canonical URL as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                },
                "is_cornerstone": {
                    "type": "boolean"
                },
                "noindex": {
                    "type": [
                        "boolean",
                        "null"
                    ],
                    "description": "Whether search engines are told not to index this post. true means noindex (the post is excluded from search results); false means the post is forced to be indexed; null means no setting is stored and the post-type default applies."
                },
                "nofollow": {
                    "type": "boolean"
                },
                "noimageindex": {
                    "type": "boolean"
                },
                "noarchive": {
                    "type": "boolean"
                },
                "nosnippet": {
                    "type": "boolean"
                },
                "open_graph_title": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "open_graph_title_rendered": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "The Open Graph title as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                },
                "open_graph_description": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "open_graph_description_rendered": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "The Open Graph description as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                },
                "twitter_title": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "twitter_title_rendered": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "The Twitter title as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                },
                "twitter_description": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "twitter_description_rendered": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "The Twitter description as output on the front end: the global default template applied when no custom value is set, with replacement variables expanded. Null when nothing is output."
                },
                "schema_page_type": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "schema_article_type": {
                    "type": [
                        "string",
                        "null"
                    ]
                },
                "seo_score": {
                    "type": "string",
                    "enum": [
                        "na",
                        "bad",
                        "ok",
                        "good"
                    ],
                    "description": "The result of the SEO analysis that ran on the post when it was last saved."
                },
                "readability_score": {
                    "type": "string",
                    "enum": [
                        "na",
                        "bad",
                        "ok",
                        "good"
                    ],
                    "description": "The result of the readability analysis that ran on the post when it was last saved."
                },
                "inclusive_language_score": {
                    "type": "string",
                    "enum": [
                        "na",
                        "bad",
                        "ok",
                        "good"
                    ],
                    "description": "The result of the inclusive language analysis that ran on the post when it was last saved."
                }
            }
        },
        "meta": {
            "annotations": {
                "readonly": false,
                "destructive": false,
                "idempotent": true
            },
            "show_in_rest": true,
            "mcp": {
                "public": true
            }
        },
        "_links": {
            "self": [
                {
                    "href": "https://basic.wordpress.test/wp-json/wp-abilities/v1/abilities/yoast-seo/update-post-seo-data",
                    "targetHints": {
                        "allow": [
                            "GET"
                        ]
                    }
                }
            ],
            "collection": [
                {
                    "href": "https://basic.wordpress.test/wp-json/wp-abilities/v1/abilities"
                }
            ],
            "wp:action-run": [
                {
                    "href": "https://basic.wordpress.test/wp-json/wp-abilities/v1/abilities/yoast-seo/update-post-seo-data/run"
                }
            ]
        }
    }
  • Confirm that the above declarations and their descriptions make sense and help agents understand what's possible.
  • Confirm that you dont see seo_title, meta_description, open_graph_title, twitter_title, open_graph_description and twitter_description in the input schema, unless you have Premium active too
  • Activated Premium, repeat the request and confirm that you see seo_title, meta_description, open_graph_title, twitter_title, open_graph_description in the input schema now
  • Confirm that you dont see focus_keyphrase in the input schema, regardless whether you have Premium enabled or not
  • Now disable the indexables (using the add_filter( 'Yoast\WP\SEO\should_index_indexables', '__return_false' ); filter) and confirm that you can't find the above abilities

Usage of the new abilities

  • Create a post with meaningful content and set its SEO data (title, meta description, social title, robots, SEO/readability score, etc.)
Using the REST API
Using our `getting SEO data` ability
  • You can smoke test the respective steps in the original PR to ensure no regressions, they should be working exactly the same
Using our `setting SEO data` ability
  • You can smoke test the respective steps in the original PR to ensure no regressions, but there are some differences listed below:
  • With Premium not active: trying to set either one of seo_title, meta_description, open_graph_title, twitter_title, open_graph_description and twitter_description and focus_keyphrase, you will get a 400 - ability_invalid_input error code
    • Make sure that indeed the relevant indexable data did NOT change after that
    • For any other field, you should get the same behavior described in the original PR
  • With Premium active: trying to set either one of seo_title, meta_description, open_graph_title, twitter_title, open_graph_description and twitter_description, now works like seamlessly. Not for focus_keyphrase though, because this is not editable at all now.
    • Make sure that indeed the relevant indexable data DID change after that
Using an AI agent
  • Follow the documentation on how to test WP Abilities
  • To use the GET SEO data ability,
  • You can smoke test the respective steps in the original PR to ensure no regressions, they should be working exactly the same
  • To use the SET SEO data ability,
  • You can smoke test the respective steps in the original PR to ensure no regressions, but there should be some differences:
    • With Premium not active: telling the agent to set either one of SEO Title, Meta description, OG title/description or Twitter title/description, it will not be possible for it to do it
      • Make sure that indeed the relevant indexable data did NOT change after that
      • For any other data, you should get the same behavior described in the original PR
    • With Premium active: telling the agent to set either one of SEO Title, Meta description, OG title/description or Twitter title/description, it will now be possible for it to do it
      • Make sure that indeed the relevant indexable data DID change after that

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

QA can test this PR by following these steps:

Impact check

This PR affects the following parts of the plugin, which may require extra testing:

Other environments

  • This PR also affects Shopify. I have added a changelog entry starting with [shopify-seo], added test instructions for Shopify and attached the Shopify label to this PR.
  • This PR also affects Yoast SEO for Google Docs. I have added a changelog entry starting with [yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached the Google Docs Add-on label to this PR.

Documentation

  • I have written documentation for this change. For example, comments in the Relevant technical choices, comments in the code, documentation on Confluence / shared Google Drive / Yoast developer portal, or other.

Quality assurance

  • I have tested this code to the best of my abilities.
  • During testing, I had activated all plugins that Yoast SEO provides integrations for.
  • I have added unit tests to verify the code works as intended.
  • If any part of the code is behind a feature flag, my test instructions also cover cases where the feature flag is switched off.
  • I have written this PR in accordance with my team's definition of done.
  • I have checked that the base branch is correctly set.
  • I have run grunt build:images and committed the results, if my PR introduces or edits images or SVGs.

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label.
  • I have added my hours to the WBSO document.

Fixes #

@coveralls-official

Copy link
Copy Markdown

Coverage Report for CI Build 2146

Coverage increased (+0.2%) to 55.234%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 2 uncovered changes across 2 files (68 of 70 lines covered, 97.14%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
src/abilities/infrastructure/post-seo-field-map.php 44 43 97.73%
src/abilities/user-interface/abilities-integration.php 26 25 96.15%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 70415
Covered Lines: 38844
Line Coverage: 55.16%
Relevant Branches: 17314
Covered Branches: 9612
Branch Coverage: 55.52%
Branches in Coverage %: Yes
Coverage Strength: 42740.26 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant