Skip to content

Feature/content planner#23205

Merged
vraja-pro merged 524 commits intotrunkfrom
feature/content-planner
Apr 28, 2026
Merged

Feature/content planner#23205
vraja-pro merged 524 commits intotrunkfrom
feature/content-planner

Conversation

@vraja-pro
Copy link
Copy Markdown
Contributor

@vraja-pro vraja-pro commented Apr 28, 2026

Context

Adds the Content Planner feature to Yoast SEO. This feature helps users discover and plan new content by analyzing their existing site and suggesting new post topics, complete with titles, focus keyphrases, meta descriptions, content outlines, and category suggestions — all powered by the Yoast AI.

Summary

This PR can be summarized in the following changelog entry:

  • Adds the Content Planner feature, which allows users to get AI-powered content suggestions and structured outlines for new posts directly from the block editor.
  • [search-metadata-previews other] Adds and exposes a helper to determine the color of meta description and SEO title progress bar.
  • [@yoast/ui-library other 0.1.0] Improves the tooltip arrow styling to avoid conflicting style sheets.
  • [@yoast/ui-library enhancement 0.1.0] Exposes the sparkles gradient icon and decouples the gradient border styling.

Relevant technical choices:

  • A new Gutenberg block (yoast/content-planner-banner) is inserted into new posts to serve as the entry point for the Content Planner. It is removed from the post content after the outline is applied.
  • Two new REST API endpoints are introduced: yoast/v1/ai_content_planner/get_suggestions (GET) and yoast/v1/ai_content_planner/get_outline (POST), which proxy requests to the Yoast AI staging/production API.
  • Content suggestions and outlines are cached in Redux store state to avoid redundant API calls when navigating back and forth between suggestions and outlines.
  • The feature is gated behind AI feature flags and reuses the existing AI consent flow. When AI features are disabled, the blocks gracefully degrade to non-rendered blocks without breaking the editor.
  • The spark usage counter is shared between the AI Generator and Content Planner features. Notifications are shown when the user approaches or reaches their free spark limit.

The following PRs were merged into this feature branch:

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

Prerequisites: Use a WordPress installation with Yoast SEO (and optionally Premium) active. Make sure your site has at least 5 posts with actual content in their titles and meta descriptions. Use the Yoast Test Helper plugin with the "Switch to AI staging API" option enabled under Tools → Yoast Test.


1. Inline banner — entry point

  • Go to Posts → Add New to create a new post.
  • Verify the inline banner appears below the first paragraph placeholder with the text "Stuck on what to write next?" and a "Get content suggestions" button.
  • If testing with a free (non-Premium) account, confirm the banner shows a "Using 1 spark" note. With Premium, confirm that note is absent.
  • Click the X button to dismiss the banner — confirm it disappears.
  • Save/publish the post and reload — confirm the banner does not reappear.
  • Create another new post, let the banner render without dismissing, save as draft, reload — confirm the banner is still visible.
  • Open an existing post (created before this feature) — confirm the banner does not appear.
  • Go to Pages → Add New — confirm the banner does not appear on pages.

2. Consent flow

  • Remove AI consent for your user (or use a fresh user without consent).
  • Open a new post, click "Get content suggestions" in the Yoast sidebar/metabox.
    • Verify the consent modal opens (instead of jumping straight to suggestions).
    • Grant consent — verify the consent modal closes and the content suggestions loading state begins.
  • Remove consent again. Click "Get content suggestions" from the inline banner.
    • Verify the consent modal opens.
    • Grant consent — verify the loading state begins.
  • Repeat both tests with Premium disabled.

3. Approve modal (sidebar/metabox entry point)

  • On a blank new post, click "Get content suggestions" in the Yoast sidebar or metabox.
    • Verify the modal follows the Empty canvas flow (title: "Looking for inspiration?", description about identifying content gaps).
  • Add some content to the post, then click "Get content suggestions" again.
    • Verify the modal follows the Existing content flow (title: "Get content suggestions", note that applying a suggestion will replace the current content and metadata).
  • Use all 10 free sparks (or simulate via wp.data.dispatch("yoast-seo/ai-generator").setUsageCount(10) in the console), then open a post with existing content and click "Get content suggestions".
    • Verify the modal follows the Limit reached / existing content flow.
  • Create a new post (blank canvas) and click "Get content suggestions" after exhausting sparks.
    • Verify the modal follows the Limit reached / blank canvas flow.
  • Without Premium, confirm the "Using 1 spark" note appears under the button (when sparks are not exhausted).

4. Content suggestions modal

  • Click through the approve modal to open the content suggestions loading state:
    • Verify the loading text cycles through: "Analyzing your site content…" → "Composing your content suggestions…" → "Writing compelling headlines…"
    • Verify no notification appears during loading.
    • Verify the usage counter is also loading.
  • Once suggestions load:
    • Verify suggestion cards appear with intent badges: Informational = blue, Navigational = violet, Commercial = yellow, Transactional = green.
    • Hover over an intent badge — verify a tooltip appears above-right with a description.
    • Hover over a suggestion card — verify the background tints, border darkens, and the title becomes underlined.
    • Verify no browser console errors appear.
  • Click "Get content suggestions" from the inline banner — verify the modal opens directly to the loading/suggestions view (no approve modal in between).

5. Content outline modal

  • From the content suggestions modal, click on a suggestion card.
  • Verify the content outline modal opens with a loading state (skeleton placeholders for form values).
  • After loading completes, verify:
    • The correct intent badge is shown.
    • The "Why this content?" callout banner has background/border color matching the intent.
    • The Suggest category toggle, a suggested category badge, and form fields (Focus Keyphrase, Title, Meta description) are all visible and editable.
    • The blog post structure section shows draggable rows.
    • The "Content suggestions" back button and "Add outline to post" button are shown.
  • Test drag-and-drop: drag a structure row to a different position and verify it reorders.
  • Test keyboard reorder: focus a structure row and press Alt+ArrowUp / Alt+ArrowDown.
  • Edit the Title, Focus Keyphrase, and Meta description fields.
  • Click "← Content suggestions" to go back — verify the suggestions list appears instantly (no animation).
  • Click the same suggestion again — verify the outline modal shows your edits (cached) and does not make a new network request (check the Network tab — no new get_outline request).
  • Click a different suggestion — verify a new get_outline request is made.

6. Meta description progress bar

  • In the content outline modal, verify the meta description progress bar changes color as you type:
    • Empty → grey track
    • 1–120 chars → orange (too short)
    • 121–156 chars → green (correct)
    • 156 chars → orange (too long); for cornerstone content the threshold is different (shorter max = red when exceeded)

  • Apply the outline to the post, then go to Search Appearance in the Yoast metabox — verify the meta description progress bar length and color match what was shown inside the Content Planner modal.
  • Confirm the progress bar height inside the modal is 6px.

7. Applying the outline

Empty post (no confirmation modal):

  1. Create a new post (leave content empty).
  2. Go through the full flow and click "Add outline to post".
  3. Verify the outline is inserted without a confirmation modal.
  4. Verify the post content includes headings, paragraphs, content note blocks, and an FAQ block.
  5. Verify the post title, SEO title, meta description, focus keyphrase, and category are all set.

Post with existing content (confirmation modal):

  1. Open a post that has existing content.
  2. Go through the full flow and click "Add outline to post".
  3. Verify a confirmation modal appears with: red warning icon, title "Replace existing content with this outline?", Cancel and "Replace content" (red) buttons.
  4. Click Cancel — verify you return to the content outline view.
  5. Click "Add outline to post" again → "Replace content" — verify existing content is replaced and the modal closes.
  6. Click the Undo button in the Gutenberg editor — verify the content is reverted.

Applying the inline banner suggestion:

  • After applying an outline via the inline banner flow, verify the inline banner block is removed from the editor.
  • If the outline is not applied (modal closed), verify the banner remains visible.
  • Click the X on the banner to dismiss it — verify the block is removed.

8. Spark counter and notifications (free users)

  • Open the content suggestions modal and take note of your spark count.
  • Wait for suggestions to load — verify the count has increased by 1.
  • Reload the page and open the modal again — verify the count reflects the API value (persisted).
  • Keep generating suggestions until you reach 5 sparks remaining:
    • Verify an upsell notification appears with title "5 free sparks left!".
    • Dismiss the notification — verify the modal stays open.
    • Select a suggestion and open the outline modal — verify the notification is not shown there.
    • Go back to suggestions and generate again — verify the notification reappears with "4 free sparks left!".
  • Keep going until you reach the 10th spark:
    • Verify the notification title reads "You're out of free sparks!".
    • Try to generate more suggestions — verify an upsell modal appears.

AI Generator integration:

  • Use the AI Generator until you reach 5 sparks remaining — verify the same "5 free sparks left!" notification appears.
  • Continue until exhausted — verify "You're out of free sparks!" notification.

Premium (100 sparks/month limit):

  • With Premium active, run in the browser console:
    wp.data.dispatch("yoast-seo/ai-generator").setUsageCount(100)
    
  • Open the Content Planner — verify you do not see an upsell modal and suggestions are generated successfully.
  • Verify a notification reads "You've used 100 sparks this month."

9. Error handling

  • Open the browser console and paste the following interceptor to simulate API errors:
window._originalFetch = window.fetch;
window._testErrorCode = 500;
window._testErrorIdentifier = "";
window._testOutlineOnly = false;
window.fetch = async (...args) => {
  const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
  const opts = args[1] || {};
  if (url && url.includes('ai_content_planner')) {
    const isPost = opts.method === 'POST' || (typeof args[0] === 'object' && args[0]?.method === 'POST');
    if (window._testOutlineOnly && !isPost) {
      return new Response(JSON.stringify({
        suggestions: [ { intent: "informational", title: "Test Suggestion", explanation: "Test.", keyphrase: "test", meta_description: "Test.", category: { name: "SEO", id: 1 } } ]
      }), { status: 200, headers: { 'Content-Type': 'application/json' } });
    }
    return new Response(JSON.stringify({
      code: window._testErrorIdentifier || "error",
      message: "Simulated error",
      errorIdentifier: window._testErrorIdentifier || "",
      data: { status: window._testErrorCode }
    }), { status: window._testErrorCode, headers: { 'Content-Type': 'application/json' } });
  }
  return window._originalFetch(...args);
};

Suggestions errors (set window._testOutlineOnly = false, then click "Get content suggestions"):

Scenario Console trigger Expected message
Generic (500) window._testErrorCode = 500; window._testErrorIdentifier = ""; "Something went wrong" + help & support links
Rate limit (429) window._testErrorCode = 429; window._testErrorIdentifier = ""; "You've reached the Yoast AI rate limit"
Timeout (408) window._testErrorCode = 408; window._testErrorIdentifier = ""; "Connection timeout"
Site unreachable window._testErrorCode = 400; window._testErrorIdentifier = "SITE_UNREACHABLE"; "Yoast AI cannot reach your site"
WP HTTP error window._testErrorCode = 400; window._testErrorIdentifier = "WP_HTTP_REQUEST_ERROR"; "Something went wrong" + specific message

For each: verify the modal stays open, the correct error message is shown, UsageCounter is hidden, "Try again" re-triggers the fetch, and reopening the modal starts in a clean state.

Outline errors — same table but set window._testOutlineOnly = true before clicking a suggestion card. Verify the modal transitions from "Content suggestions" to "Content outline" and shows the error.

Cleanup: window.fetch = window._originalFetch;


10. Category handling

  • Create a post with categories assigned to existing posts.
  • Click "Get content suggestions" — verify suggestions are generated with existing categories (not empty).
  • To test edge cases, use the following filter in your WordPress install (replace 'EXISTING CATEGORY' and ID with a real category):
add_filter( 'http_response', function ( $response, $parsed_args, $url ) {
    if ( strpos( $url, 'content-planner/next-post-suggestions' ) === false ) {
        return $response;
    }
    $body = json_decode( wp_remote_retrieve_body( $response ) );
    if ( ! is_object( $body ) || ! isset( $body->choices ) || count( $body->choices ) < 3 ) {
        return $response;
    }
    // Case 1: existing category
    $body->choices[0]->category = (object) [ 'name' => 'EXISTING CATEGORY', 'id' => ID ];
    // Case 2: non-existent category
    $body->choices[1]->category = (object) [ 'name' => 'Yoast QA Missing Category', 'id' => 99999 ];
    // Case 3: empty-category sentinel
    $body->choices[2]->category = (object) [ 'name' => '', 'id' => -1 ];
    $response['body'] = wp_json_encode( $body );
    return $response;
}, 10, 3 );
  • Case 1 → verify your existing category is shown.
  • Cases 2 & 3 → verify the default category of your blog is shown as fallback.

11. AI features disabled

  • Disable Yoast AI from the Yoast settings.
  • Visit a post that previously had the inline banner block in its content.
    • Verify no JavaScript console errors appear.
    • Verify the block shows a graceful "your site doesn't include support for the 'yoast/content-planner-banner' block" message (or is invisible on the frontend).
  • Visit a post that previously had content suggestion blocks.
    • Verify no editor crash banner appears.
    • Verify the blocks can be converted to list blocks, which render correctly on the frontend.
  • Re-enable AI features and verify the inline banner and blocks work normally again.
  • Repeat with Premium enabled and disabled.

12. Elementor and Classic editor

  • Go to the Elementor editor and open the Yoast SEO tab in the sidebar.
    • Verify the Yoast logo and discovery copy are NOT shown (only the SEO analysis content).
  • Open the Classic editor and check the Yoast metabox.
    • Verify the logo and discovery copy are also NOT shown there.
  • Open the block editor — verify the logo and discovery copy ARE shown:
    • With AI enabled on a post: "Optimize your content for discovery or get new content suggestions"
    • With AI disabled or on a non-post: "Optimize your content for discovery"

Ads regression test (block editor only):

  • Deactivate Premium and trigger the webinar ad (set user meta _yoast_alerts_dismissed to a:1:{s:26:"webinar-promo-notification";b:0;}).
    • Verify the ad appears between the Yoast logo and copy.
  • Trigger the Black Friday ad (change the year in the second date in src/promotions/domain/black-friday-promotion.php to 2027).
    • Verify the ad appears in the editor intro. Revert the code change after testing.

13. Intent badge styling

  • In the content suggestions modal, verify the "Commercial" intent badge color matches the design (yellow/amber).
  • In the suggestions modal, hover over a badge — verify the cursor is a pointer (it's inside a clickable button).
  • In the outline modal, hover over a badge — verify the cursor is the default cursor (it's not clickable).
  • Verify the tooltip of badges in the suggestions modal does not overlap other elements.

14. Accessibility

  • With VoiceOver (Cmd+F5 on macOS):
    • Open the suggestions modal — verify it is announced as "Content suggestions, dialog".
    • Navigate with Tab — verify focus cycles through: Close (X) → suggestion cards → Close button.
    • Press Escape — verify the modal closes and focus returns to the triggering element.
  • With the error state active (use the interceptor from section 9):
    • Verify Tab cycles: Close (X) → Help (?) → error links → Close (footer) → Try again.
    • Verify Shift+Tab reverses the order and focus stays trapped inside the modal.

15. Spark notification position

  • Create a new post and click "Get content suggestions". Once suggestions are loaded, simulate reaching the notification threshold by running in the browser console:
    wp.data.dispatch("yoast-seo/ai-generator").setUsageCount(6)
    
  • Verify the spark notification appears in the bottom-left of the screen.
  • Add a focus keyphrase and use the AI Generator to generate a title or meta description. Once suggestions appear, set the usage count the same way and verify the notification is in the same bottom-left position.
  • Repeat both tests in mobile view (use browser DevTools to simulate a narrow viewport) — verify the notification appears at the bottom of the screen.

16. Approve modal design and spark limit states

  • Create a new post and click "Get content suggestions" in the Yoast sidebar.
    • Verify the modal matches the Empty canvas design flow.
  • Add some content to the post and click "Get content suggestions" again.
    • Verify the modal matches the Existing content design flow.
  • Exhaust all 10 free sparks (or simulate with wp.data.dispatch("yoast-seo/ai-generator").setUsageCount(10) in the console).
  • Open a post with existing content and click "Get content suggestions".
    • Verify the modal matches the Limit reached / existing content design flow (out-of-sparks content is shown).
  • Create a new post (blank canvas) and click "Get content suggestions".
    • Verify the modal matches the Limit reached / blank canvas design flow (out-of-sparks content is shown).

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.

Impact check

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

  • Block editor (Gutenberg) — new yoast/content-planner-banner block and yoast/content-planner-suggestion blocks are injected.
  • Yoast SEO sidebar and metabox — new "Get content suggestions" button and updated intro copy.
  • Classic editor and Elementor sidebar — Yoast logo/intro copy display logic changed.
  • AI Generator spark counter — shared state between AI Generator and Content Planner.
  • Spark notification position — affects both Content Planner and AI Generator flows on desktop and mobile.
  • Approve modal — design updated and spark limit states now display correctly.
  • REST API — two new endpoints (get_suggestions, get_outline).
  • search-metadata-previews package — new helper for progress bar color calculation.
  • @yoast/ui-library package — tooltip arrow styling and sparkles gradient icon.

Other environments

  • This PR also affects Shopify. ...
  • This PR also affects Yoast SEO for Google Docs. ...

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

JorPV and others added 30 commits April 16, 2026 11:48
…rate error management for suggestions and outline. Update SuggestionsPanel to accept error and retry props.
…api' into 1142-connect-content-planner-frontend-with-backend-endpoints-
…-the-2nd-request-to-ai-api' into 1142-connect-content-planner-frontend-with-backend-endpoints-
…-the-2nd-request-to-ai-api

1132 create endpoint for sending the 2nd request to ai api
…ntend-with-backend-endpoints-' into 1137-add-error-handling-content-planner

# Conflicts:
#	packages/js/src/ai-content-planner/components/content-outline-modal.js
#	packages/js/src/ai-content-planner/store/content-outline.js
…x-loading-state-discrepancies-in-content-planner-modals
…oints-' into fix-loading-state-discrepancies-in-content-planner-modals
vraja-pro and others added 20 commits April 27, 2026 17:01
After positioning the spark notification outside the panel.
Also fix a bug where the message related to being out of free sparks was never rendered
This is done to have one source of truth for the suggestion.
We want to keep the focus in the modal when going back to suggestions.
…gnment

Align the usage counter vertically
…ent-planner-route-and-content-planner-endpoint

Rename get suggestion endpoint classes
…test.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…e-and-meta-description-changes-the-in-the-content-suggestion

Update the suggestion in the content suggestion state
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…edly-on-premium

Refactor upsell logic to include premium subscription validation
@vraja-pro vraja-pro self-assigned this Apr 28, 2026
@vraja-pro vraja-pro requested a review from a team as a code owner April 28, 2026 12:13
@vraja-pro vraja-pro added the changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog label Apr 28, 2026
@coveralls
Copy link
Copy Markdown

coveralls commented Apr 28, 2026

Coverage Report for CI Build 4

Warning

No base build found for commit 777600d on trunk.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 56.588%

Details

  • Patch coverage: 263 uncovered changes across 37 files (344 of 607 lines covered, 56.67%).

Uncovered Changes

Top 10 Files by Coverage Impact Changed Covered %
packages/js/src/ai-content-planner/store/content-outline.js 51 13 25.49%
packages/js/src/ai-content-planner/blocks/banner-block.js 30 2 6.67%
packages/js/src/ai-content-planner/helpers/fetch.js 27 2 7.41%
packages/js/src/ai-content-planner/components/app.js 22 0 0.0%
packages/js/src/ai-content-planner/hooks/use-fetch-content-suggestions.js 20 1 5.0%
packages/js/src/ai-content-planner/blocks/content-suggestion-block.js 19 3 15.79%
packages/js/src/ai-content-planner/hooks/use-apply-outline.js 15 0 0.0%
packages/js/src/ai-content-planner/hooks/use-fetch-content-outline.js 12 0 0.0%
packages/js/src/ai-content-planner/containers/approve-modal.js 8 0 0.0%
packages/js/src/ai-content-planner/store/content-suggestions.js 42 34 80.95%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 26847
Covered Lines: 15583
Line Coverage: 58.04%
Relevant Branches: 16934
Covered Branches: 9192
Branch Coverage: 54.28%
Branches in Coverage %: Yes
Coverage Strength: 112041.06 hits per line

💛 - Coveralls

FAMarfuaty and others added 2 commits April 28, 2026 14:49
…tion

1175 fix spark notification position
…73-add-learn-more-link-to-approve-modal

# Conflicts:
#	packages/js/src/ai-content-planner/containers/approve-modal.js
@vraja-pro vraja-pro added this to the 27.6 milestone Apr 28, 2026
…ove-modal

1173 add learn more link to approve modal
@vraja-pro vraja-pro merged commit 6eb2a30 into trunk Apr 28, 2026
56 checks passed
@vraja-pro vraja-pro deleted the feature/content-planner branch April 28, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants