1132 create endpoint for sending the 2nd request to ai api#23154
Conversation
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.
Coverage Report for CI Build 93Coverage decreased (-0.7%) to 52.706%Details
Uncovered ChangesCoverage RegressionsNo coverage regressions found. Coverage Stats💛 - Coveralls |
| } 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 ); |
There was a problem hiding this comment.
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?
vraja-pro
left a comment
There was a problem hiding this comment.
Few comment about types and tests.
| * | ||
| * @return Section_List The list of outline sections. | ||
| */ | ||
| public function build_outline( Response $response ): Section_List { |
There was a problem hiding this comment.
build_outline is public solely for testability — Consider testing through handle() and making it private instead.
| * | ||
| * @return void | ||
| */ | ||
| protected function setUp(): void { |
There was a problem hiding this comment.
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
|
AC 🚧
Also add both of the endpoints and a nonce to the frontend via designated window object. |
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)? |
…sending-the-2nd-request-to-ai-api
…rdpress-seo into 1132-create-endpoint-for-sending-the-2nd-request-to-ai-api
thijsoo
left a comment
There was a problem hiding this comment.
couple of small remarks.
| $request->get_param( 'explanation' ), | ||
| $request->get_param( 'keyphrase' ), | ||
| $request->get_param( 'meta_description' ), | ||
| $category, |
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
I would swap these arround since the array is not optional but the subheading is.
| string $explanation, | ||
| string $keyphrase, | ||
| string $meta_description, | ||
| Category $category |
There was a problem hiding this comment.
I would pass the category values and make the object in the constructor.
Context
Summary
This PR can be summarized in the following changelog entry:
Relevant technical choices:
Content_Suggestion_Command_Handler::handleto pass$command->get_user()->IDtoToken_Manager::clear_tokens(it expects an int ID, not aWP_User).Test instructions
Test instructions for the acceptance test before the PR gets merged
This PR can be acceptance tested by following these steps:
Tools->Yoast Test, inDevelopment settingsflag theSwitch to AI staging APIcheckboxPOSTrequest to/wp-json/yoast/v1/ai_content_planner/get_outlinewith 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/categoryset from the response of theget_suggestionsendpoint added in #23125 and feed it straight into this call.{ "outline": [ { "subheading_text": "What is SEO?", "content_notes": [ "Define SEO and its importance.", "Explain how search engines work." ] } ] }wpseoContentPlannerand 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" } }CONSENT_REVOKEDerror).Relevant test scenarios
Test instructions for QA when the code is in the RC
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
[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 new images or SVGs.Innovation
innovationlabel.Fixes #