Skip to content

1132 create endpoint for sending the 2nd request to ai api#23154

Merged
thijsoo merged 18 commits intofeature/content-plannerfrom
1132-create-endpoint-for-sending-the-2nd-request-to-ai-api
Apr 16, 2026
Merged

1132 create endpoint for sending the 2nd request to ai api#23154
thijsoo merged 18 commits intofeature/content-plannerfrom
1132-create-endpoint-for-sending-the-2nd-request-to-ai-api

Conversation

@pls78
Copy link
Copy Markdown
Member

@pls78 pls78 commented Apr 10, 2026

Context

  • We need a second AI endpoint that, given a chosen content suggestion, returns a structured outline (a list of subheadings with content notes).

Summary

This PR can be summarized in the following changelog entry:

  • Adds an endpoint to request a content outline for a chosen content suggestion.

Relevant technical choices:

  • I have also included a small drive-by fix in Content_Suggestion_Command_Handler::handle to pass $command->get_user()->ID to Token_Manager::clear_tokens (it expects an int ID, not a WP_User).

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

  • If this PR is tested before the UI is done, run the following test actions through Postman. Otherwise test the UI connection PR since that tests this PR.
  • Make sure you can run API requests in Postman by copying your cookie and nonce, and have a live link / actual site ready to go so you can have an API callback.
  • Make sure you have Yoast Test Helper activated
    • Go to Tools -> Yoast Test, in Development settings flag the Switch to AI staging API checkbox
  • Open your site and make sure you have 5 posts that have some actual content in the title and meta description (you can copy/paste these from yoast.com posts). Optionally publish an "About" page too — the handler will include it in the request when present.
  • Send a POST request to /wp-json/yoast/v1/ai_content_planner/get_outline with a JSON body like:
{
  "post_type": "post",
  "language": "en-US",
  "editor": "classic",
  "title": "The Beginner's Guide to SEO: Strategies for Success",
  "intent": "informational",
  "explanation": "This post will serve as a foundational resource for newcomers to SEO, filling a gap for beginners looking for a systematic introduction.",
  "keyphrase": "beginner's guide to SEO",
  "meta_description": "Discover essential strategies for SEO success with our comprehensive beginner's guide.",
  "category": {
    "name": "SEO",
    "id": 1
  }
}

Tip: you can grab a realistic title / intent / explanation / keyphrase / meta_description / category set from the response of the get_suggestions endpoint added in #23125 and feed it straight into this call.

  • Make sure that in the response you see the following structure:
{
    "outline": [
        {
            "subheading_text": "What is SEO?",
            "content_notes": [
                "Define SEO and its importance.",
                "Explain how search engines work."
            ]
        }
    ]
}
  • Open a post in the Block editor
    • In the browser developers console type wpseoContentPlanner and make sure it returns an object shaped as follows:
{
  "endpoints": { 
    "contentPlanner": "yoast/v1/ai_content_planner/get_suggestions",
    "getOutline": "yoast/v1/ai_content_planner/get_outline" 
  }
}
  • Optionally verify the error paths: send the same request with an invalid/expired JWT (the route should retry once and ultimately surface a 401-style payload) and again after revoking AI consent (should return a CONSENT_REVOKED error).

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:

  • N/A

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.

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 new 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 #

pls78 added 4 commits April 10, 2026 16:08
Introduce the value objects representing the AI API outline response:
Section holds an optional subheading text and its content notes, and
Section_List wraps the collection in the {outline: [...]} array shape
expected by the front-end.
Add the Content_Outline_Command carrying the chosen suggestion's metadata
plus the user/post type/language/editor context, and the handler that
collects recent content, builds the request body for the AI API
/content-planner/next-post-outline endpoint, parses the response into a
Section_List, and applies the same unauthorized retry / forbidden consent
revocation flow as the suggestions handler.
Register POST /yoast/v1/ai_content_planner/get_outline, which validates
the chosen content suggestion fields, dispatches a Content_Outline_Command
to the handler, and returns the resulting outline. Mirrors the existing
Content_Planner_Route exception handling so payment-required and
too-many-requests errors keep surfacing missing licenses to the front-end.
…handler

Token_Manager::clear_tokens expects an int user ID, not a WP_User object.
Aligns the suggestions handler with the same call shape used by the new
outline handler.
@pls78 pls78 added this to the feature/content-planner milestone Apr 10, 2026
@coveralls
Copy link
Copy Markdown

coveralls commented Apr 10, 2026

Coverage Report for CI Build 93

Coverage decreased (-0.7%) to 52.706%

Details

  • Coverage decreased (-0.7%) from the base build.
  • Patch coverage: 241 uncovered changes across 8 files (0 of 241 lines covered, 0.0%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
src/ai/content-planner/user-interface/get-outline-route.php 112 0 0.0%
src/ai/content-planner/application/content-outline-command-handler.php 65 0 0.0%
src/ai/content-planner/application/content-outline-command.php 31 0 0.0%
src/ai/content-planner/domain/section.php 12 0 0.0%
src/ai/content-planner/infrastructure/endpoints/get-outline-endpoint.php 8 0 0.0%
src/ai/content-planner/domain/section-list.php 7 0 0.0%
src/ai/content-planner/infrastructure/wordpress-category-repository.php 5 0 0.0%
src/ai/content-planner/application/content-suggestion-command-handler.php 1 0 0.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 65050
Covered Lines: 34177
Line Coverage: 52.54%
Relevant Branches: 16294
Covered Branches: 8696
Branch Coverage: 53.37%
Branches in Coverage %: Yes
Coverage Strength: 46242.72 hits per line

💛 - Coveralls

@pls78 pls78 added the changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog label Apr 10, 2026
@pls78 pls78 marked this pull request as ready for review April 13, 2026 07:39
} catch ( Unauthorized_Exception $exception ) {
// Delete the stored JWT tokens, as they appear to be no longer valid.
$this->token_manager->clear_tokens( $command->get_user() );
$this->token_manager->clear_tokens( $command->get_user()->ID );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type mismatch in clear_tokens — both files

The drive-by fix in content-suggestion-command-handler.php and the new content-outline-command-handler.php both call:
$this->token_manager->clear_tokens( $command->get_user()->ID );
->ID returns int, but clear_tokens(string $user_id) expects a string. The fix intended to correct a type issue (passing WP_User instead of its ID) but still passes the wrong type.
Should be:
$this->token_manager->clear_tokens( (string) $command->get_user()->ID );
The unit tests mask this because they assert with ->with(1) (int), not ->with('1') (string).

Or we can add to the clear_token argument type int|string?

Copy link
Copy Markdown
Contributor

@vraja-pro vraja-pro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few comment about types and tests.

*
* @return Section_List The list of outline sections.
*/
public function build_outline( Response $response ): Section_List {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_outline is public solely for testability — Consider testing through handle() and making it private instead.

*
* @return void
*/
protected function setUp(): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent setUp vs set_up — Abstract_Section test uses setUp() (camelCase) while command handler tests use set_up() (snake_case). Minor, but inconsistent within the same PR.

I think in PHP we should use snake_case and in JS we should use camelCase

@vraja-pro
Copy link
Copy Markdown
Contributor

vraja-pro commented Apr 13, 2026

AC 🚧

  • Sending the request without the body gave me an error in the response which is good. I added the content to the body of the request.
  • Request before granting consent also result in an error in response which is good.
  • After granting consent, I get 404:
{
  "message": "not found",
  "errorIdentifier": "UNKNOWN"
}
  • I tested on tasteWP with a zip.

Also add both of the endpoints and a nonce to the frontend via designated window object.

@pls78
Copy link
Copy Markdown
Member Author

pls78 commented Apr 14, 2026

* After granting consent, I get 404:
{
  "message": "not found",
  "errorIdentifier": "UNKNOWN"
}
* I tested on tasteWP with a zip.

I think this is because the frontend is still not fully wired; can you try with a LiveLink (or Docker) local instance and Postman (or curl)?

Copy link
Copy Markdown
Contributor

@thijsoo thijsoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couple of small remarks.

$request->get_param( 'explanation' ),
$request->get_param( 'keyphrase' ),
$request->get_param( 'meta_description' ),
$category,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The category should be made inside the command not here in the DDD logic :)

* @param string|null $subheading_text The subheading text.
* @param array<string> $content_notes The content notes.
*/
public function __construct( ?string $subheading_text, array $content_notes ) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would swap these arround since the array is not optional but the subheading is.

string $explanation,
string $keyphrase,
string $meta_description,
Category $category
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would pass the category values and make the object in the constructor.

Copy link
Copy Markdown
Contributor

@thijsoo thijsoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CR + ACC 👍

@thijsoo thijsoo merged commit 78a8cff into feature/content-planner Apr 16, 2026
35 of 38 checks passed
@thijsoo thijsoo deleted the 1132-create-endpoint-for-sending-the-2nd-request-to-ai-api branch April 16, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants