Skip to content

feat(bulk-editor): add "needs improvement" filter options to the filters dropdown#23438

Merged
vraja-pro merged 33 commits into
feature/bulk-editor-ph2from
1346-bulk-editor-add-needs-improvement-filter-option-to-filter-dropdown
Jul 24, 2026
Merged

feat(bulk-editor): add "needs improvement" filter options to the filters dropdown#23438
vraja-pro merged 33 commits into
feature/bulk-editor-ph2from
1346-bulk-editor-add-needs-improvement-filter-option-to-filter-dropdown

Conversation

@FAMarfuaty

@FAMarfuaty FAMarfuaty commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Context

The bulk editor's filter dropdown had no way to surface posts whose search-appearance or social fields need attention. This PR adds "needs improvement" filter options for the SEO title, meta description, social title and social description. It is part of bulk editor phase 2 (base branch feature/bulk-editor-ph2). The companion Premium PR Yoast/wordpress-seo-premium#5027 adds morphology so the bulk editor's scores match the post editor on Premium sites.

Summary

This PR can be summarized in the following changelog entry:

  • Adds "needs improvement" filter options to the bulk editor filter dropdown, matching posts with an empty or weakly-scored SEO title or meta description.

Relevant technical choices:

  • "Needs improvement" for the SEO title / meta description means the field is empty or its persisted per-field score is in the bad/ok range (1–70). Social fields have no assessors, so they match on emptiness only.
  • A missing or 0 score is the "never scored" sentinel, deliberately outside the range, so unscored posts match only through the empty check. Backfilling existing posts is out of scope — scores populate as posts are opened in the editor or edited in the bulk editor.
  • Parity with the existing scores: no-backfill matches how the SEO score (linkdexprimary_focus_keyword_score) and readability score (content_scorereadability_score) already behave. Indexable_Post_Builder populates all four purely by copying post meta — nothing is scored server-side (scoring is browser-JS only). Historical posts carry linkdex/content_score only because that meta accumulated from editor saves; a post never opened in the editor has no SEO/readability score either. The "SEO data optimization" step in the first-time configuration is just the indexation process (it copies existing meta into the indexable table — "it won't change any content"), so it does not compute or backfill the new per-field scores. Existing posts therefore show empty per-field scores until re-saved in the editor or bulk-edited.
  • How the score is computed: each Search field gets one 0–100 score — the average of its two assessments normalized like Yoast's overall SEO score (SEO title = SEO title width + Keyphrase in SEO title; meta description = Keyphrase in meta description + Meta description length). A field is tagged "needs improvement" when this combined score is 1–70 (the red bad + orange ok bands, i.e. below a green 71+), not when each assessment individually shows red — e.g. a green SEO title width with a red Keyphrase in SEO title still averages into the 1–70 band and is tagged.
  • The score half of the filter is ignored when SEO analysis is disabled site-wide, falling back to the empty check.
  • Known limitation (out of scope, tracked separately pending a product decision): a post that relies on a content-type template for its SEO title / meta description (the Search Appearance defaults) has an empty stored value, so the empty check flags it as "needs improvement" even though the editor analyses and scores the template-resolved value. The likely fix is a template-aware empty check (empty only when both the per-post value and the content-type template are blank).
  • Per-field scores are persisted the same way as content_score (post meta → indexable columns via the meta watcher); a migration adds two nullable int columns.
  • Bulk-editor re-scoring runs the two field assessors (SeoTitleAssessor / MetaDescriptionAssessor) directly on the main thread against the server-rendered field values, instead of booting the analysis worker and running a full analysis, to keep the bulk-editor page light. A manual edit re-scores from the rendered response; an applied AI suggestion (Premium) re-scores just that one field from its literal value.
  • Premium augments the main-thread researcher with morphology through a @wordpress/hooks filter seam (yoast.bulkEditor.analysis.configureResearcher), implemented in Yoast/wordpress-seo-premium#5027.

Important

For more detailed documentation on the approach and technical choices, please refer to this doc below 👇🏽
bulk-editor-needs-improvement-filter.md

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps.

Setup and migration

  1. Build the plugin (yarn build / grunt build) and activate it. On activation the AddSeoTitleAndMetaDescriptionScores migration runs, adding the seo_title_score and meta_description_score columns to the indexable table (wp_yoast_indexable). Confirm both columns exist. (On a dev checkout the migration usually needs to be run manually — see Running the migration manually below.)
  2. Make sure SEO analysis is enabled: Yoast SEO → General → Features → SEO analysis (on by default).

Running the migration manually

On a dev checkout the plugin version usually already matches the stored version, so the migration runner's version gate skips it and the two columns are never added. Force the pending migrations with WP-CLI (in a Docker / wp-env setup, run it inside the container, e.g. docker exec --user www-data <container> wp ...):

wp eval '$runner = YoastSEO()->classes->get( Yoast\WP\SEO\Initializers\Migration_Runner::class ); $runner->run_migrations( "free", "99.0.0" );'

The "99.0.0" target only has to exceed the stored version so the gate lets the run proceed; the runner then applies every not-yet-run migration, including AddSeoTitleAndMetaDescriptionScores. Verify the columns were added:

wp eval 'global $wpdb; $t = $wpdb->prefix . "yoast_indexable"; var_export( $wpdb->get_col( "SHOW COLUMNS FROM {$t}" ) );'

Look for seo_title_score and meta_description_score in the output. The migration's down() removes both again on a rollback.

Filter dropdown appearance (match the Figma design)

Open the Filters dropdown on the Search appearance tab and compare it against the Figma design. Each "needs improvement" option ("SEO titles", "Meta descriptions") should show a round red score bullet (#dc3232, ~12px) before its label, the section should be separated from the status filters by a divider, and there should be no visible "Needs improvement" heading (it exists only for screen readers). Confirm the bullet's size, colour and spacing match the design.

The filter — empty fields

  1. Open the bulk editor. On the Search appearance tab, apply "SEO title needs improvement": posts with an empty SEO title appear. Repeat for "Meta description needs improvement".
  2. On the Social appearance tab, apply "Social title / description needs improvement": only posts with an empty social title / description match (social fields are matched on emptiness only).

The filter — with indexables disabled (post-meta fallback)

The bulk editor reads from post meta instead of the indexable table when indexables are turned off. Disable them with a snippet (mu-plugin or Code Snippets):

add_filter( 'Yoast\WP\SEO\should_index_indexables', '__return_false' );

Make sure you have (a) a post that has never had an SEO title (so it has no _yoast_wpseo_title meta row at all) and (b) a post whose SEO title was set and then cleared to empty. On the Search appearance tab, apply "SEO title needs improvement": both posts appear. Repeat for "Meta description needs improvement", and for the social fields on the Social appearance tab. With SEO analysis enabled, a filled title scored 1–70 also appears; a filled title scored 71+ does not.

The filter — weak scores (search tab only)

Each field's score is the average of its two assessments (exact per-assessment thresholds: packages/yoastseo/src/scoring/assessments/SCORING SEO.md), and a field is flagged when that average lands in the 1–70 band. A normal-length title/description already scores green on width/length, so the keyphrase's presence is the easiest lever.

Craft a post that NEEDS improvement (non-empty, so it exercises the score path — not the empty check). On a post, set a focus keyphrase (e.g. banana bread), then:

  • SEO title = a normal-length title that does not contain the keyphrase, e.g. Delicious homemade loafSEO title width green (9) + Keyphrase in SEO title red (2) → score ≈ 61.
  • Meta description = ~130 characters (the green 121–156 band) that do not contain the keyphrase → Keyphrase in meta description red (3) + Meta description length green (9) → score ≈ 67.

Open/save the post so the scores persist. In the bulk editor, both "SEO title needs improvement" and "Meta description needs improvement" now list it.

Craft a post that does NOT need improvement. Same keyphrase, then:

  • SEO title beginning with the exact keyphrase, e.g. Banana bread: easy homemade recipe → both title assessments green → score 100.
  • Meta description of ~130 characters that contains the keyphrase → both description assessments green → score 100.

Neither filter lists this post. Also confirm: a good-scored (71+) filled title does not appear, and a filled field that was never scored does not match the score band (it only matches the empty check — a documented gap for template-defaulted fields).

Updating the score from a manual edit

  1. Edit an SEO title inline in the bulk editor and save it. The Network tab shows a POST yoast/v1/bulk_editor/update_scores; refresh/refetch and confirm the row leaves or joins the filtered set according to its new score. Editing the focus keyphrase re-scores both the SEO title and the meta description.

Updating the score from AI generation (Premium)

  1. With Premium active and AI generation available, generate a new SEO title (and/or meta description) for a row and apply it — both a single "Apply" and "Apply all". Confirm an update_scores POST fires for the applied field and, on refetch, the row leaves/joins the filtered set exactly like a manual edit. (Applying an AI suggestion re-scores only that field.)

When SEO analysis is disabled — Yoast settings (site-wide)

  1. Turn SEO analysis off in Yoast SEO → General → Features. The filter's score half is ignored: only empty fields match ("needs improvement" behaves as the empty-only filter). The bulk editor sends no update_scores request, and opening/saving a post in the editor clears its stored score.

When SEO analysis is disabled — user profile (per-user)

  1. Re-enable SEO analysis site-wide, then disable "SEO analysis" on your own user profile. In the post editor the analysis and its score fields are off for you, and saving a post clears that post's score. The bulk editor filter and re-scoring are unaffected — they follow the site-wide setting — so other posts still filter by score.

When Premium is not activated

  1. On a Free-only site, view the bulk editor page source and confirm the morphology script is not enqueued (wpseoPremiumBulkEditorMorphology is absent). The filter and the manual-edit re-score still work; scores are computed with base word forms. (AI generation is a Premium feature and is not present.)

When Premium is activated — morphology parity

  1. With a valid Premium subscription and a morphology-supported site language (e.g. English), set a focus keyphrase and an SEO title using a different word form of the keyphrase (e.g. plural vs. singular); note the SEO title score in the editor. Editing that title in the bulk editor recomputes a score that matches the editor's (morphology word forms are counted). Blocking the morphology request degrades to base word forms without a console error.

Right-to-left (RTL)

Switch to an RTL language (Settings → General → Site Language, or the user profile language — e.g. Arabic or Hebrew) and reopen the Filters dropdown. The score bullet uses logical spacing, so confirm the layout mirrors correctly: the red bullet sits before its label on the right-hand side, the checkbox / bullet / label order reads right-to-left, and the dot is neither clipped nor misplaced.

Editors

  1. The per-field scores are written by the post editor, so verify the hidden score inputs fill and save in the Default Block/Gutenberg editor, the Classic editor and Elementor.

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
  • Console: watch for errors while a score is computed in the editor and while the bulk editor fires the update_scores request (manual and AI apply).
  • Content types: the filter and score persistence should be checked on posts, pages and a custom post type (the bulk editor is post-based, not taxonomy-based).
  • Editors: the per-field scores are written by the editor — verify the hidden inputs fill and save in Default Block/Gutenberg, Classic and Elementor.
  • Browsers: the scoring and the fetch run client-side — verify in Chrome and Firefox.
  • Multisite: the Premium morphology fetch uses the network-safe home URL — verify the fetch and the subscription gating on a multisite install.
  • RTL: the score bullet uses logical margins — verify the mirrored layout on an RTL site language (see the Right-to-left section above).

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above, as part of the bulk editor phase 2 feature.

QA can test this PR by following these steps:

  • See the acceptance steps above.

Impact check

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

  • The bulk editor (filter dropdown + inline editing + Premium AI apply).
  • Post/term save in every editor (a new hidden field is written and triggers an indexable rebuild).
  • WPSEO_Meta (two new general fields) and the indexable schema (migration adding two columns).

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 Yoast/reserved-tasks#1346

@coveralls

coveralls commented Jul 8, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 713

Coverage increased (+0.2%) to 55.559%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 53 uncovered changes across 11 files (394 of 447 lines covered, 88.14%).
  • 3 coverage regressions across 3 files.

Uncovered Changes

Top 10 Files by Coverage Impact Changed Covered %
src/config/migrations/20260709144332_AddSeoTitleAndMetaDescriptionScores.php 26 0 0.0%
src/bulk-editor/infrastructure/posts/post-meta-posts-collector.php 40 32 80.0%
src/bulk-editor/infrastructure/updates/field-renderer.php 5 0 0.0%
packages/js/src/bulk-editor/hooks/use-inline-edit.js 65 61 93.85%
packages/js/src/bulk-editor/services/field-scores.js 56 53 94.64%
src/bulk-editor/infrastructure/posts/indexable-posts-collector.php 22 20 90.91%
admin/metabox/class-metabox.php 1 0 0.0%
packages/js/src/analysis/refreshAnalysis.js 1 0 0.0%
packages/js/src/initializers/analysis.js 1 0 0.0%
src/bulk-editor/application/updates/score-updater.php 31 30 96.77%
Total (28 files) 447 394 88.14%

Coverage Regressions

3 previously-covered lines in 3 files lost coverage.

File Lines Losing Coverage Coverage
src/bulk-editor/infrastructure/posts/post-meta-posts-collector.php 1 36.7%
inc/class-wpseo-meta.php 1 21.83%
packages/js/src/bulk-editor/hooks/use-inline-edit.js 1 90.83%

Coverage Stats

Coverage Status
Relevant Lines: 72915
Covered Lines: 40414
Line Coverage: 55.43%
Relevant Branches: 18901
Covered Branches: 10598
Branch Coverage: 56.07%
Branches in Coverage %: Yes
Coverage Strength: 41275.49 hits per line

💛 - Coveralls

FAMarfuaty and others added 10 commits July 9, 2026 17:36
…cores

Adds seo_title_score and meta_description_score post meta (0-100, mirroring content_score) plus matching indexable columns and a migration, so the assessor-based needs-improvement filter has a stored value to query. A default of 0 doubles as the never-scored sentinel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derives the SEO title and meta description scores from the existing SEO analysis results and writes them to their hidden fields, so they save as post meta alongside the overall score. A not-derivable score of 0 is never written, preserving any existing score.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt filter

Extends both posts collectors so the search-tab needs-improvement filter also matches posts whose stored SEO title or meta description score is in the bad/ok range (1-70), on top of the empty-field check. The social tab stays empty-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onse

The search update response now carries the SEO title and meta description with replacement variables resolved, so the bulk editor can re-score them on the value users actually see. Both fields are always returned so a focus keyphrase edit can be re-scored too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a POST /bulk_editor/update_scores route (and its endpoint) that persists the SEO title and meta description scores the bulk editor recomputes after a save, reusing the post access checks. Writing the meta rebuilds the indexable score columns through the normal watcher flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cores

Adds seo_title_score and meta_description_score post meta (0-100, mirroring content_score) plus matching indexable columns and a migration, so the assessor-based needs-improvement filter has a stored value to query. A default of 0 doubles as the never-scored sentinel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derives the SEO title and meta description scores from the existing SEO analysis results and writes them to their hidden fields, so they save as post meta alongside the overall score. A not-derivable score of 0 is never written, preserving any existing score.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt filter

Extends both posts collectors so the search-tab needs-improvement filter also matches posts whose stored SEO title or meta description score is in the bad/ok range (1-70), on top of the empty-field check. The social tab stays empty-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onse

The search update response now carries the SEO title and meta description with replacement variables resolved, so the bulk editor can re-score them on the value users actually see. Both fields are always returned so a focus keyphrase edit can be re-scored too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a POST /bulk_editor/update_scores route (and its endpoint) that persists the SEO title and meta description scores the bulk editor recomputes after a save, reusing the post access checks. Writing the meta rebuilds the indexable score columns through the normal watcher flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

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.

…-filter-dropdown' of github.com:Yoast/wordpress-seo into 1346-bulk-editor-add-needs-improvement-filter-option-to-filter-dropdown
…-editor-add-needs-improvement-filter-option-to-filter-dropdown
@FAMarfuaty
FAMarfuaty changed the base branch from trunk to feature/bulk-editor-ph2 July 15, 2026 08:49
FAMarfuaty and others added 7 commits July 15, 2026 15:52
After a search-appearance save the bulk editor re-runs the analysis worker on the rendered fields, derives the SEO title and meta description scores, and persists them, so the needs-improvement filter reflects edits without reopening the post editor. Enqueues the worker script data on the bulk editor page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ad of the worker

Re-scoring after an edit now runs only the SEO title and meta description assessors on the main thread, instead of spinning up the analysis worker and running the full page analysis to read four results. A JS filter (yoast.bulkEditor.analysis.researcher) lets Premium return a researcher with morphology data; Free scores with base word forms, matching the Free editor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lysis is disabled

The needs-improvement filter only matches on the stored per-field score while SEO analysis is enabled; when it is off the score is no longer maintained, so the search fields fall back to the empty-field check (like the social fields).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After a search-appearance save the bulk editor re-scores the SEO title and meta description on the rendered field values and persists the result, so the needs-improvement filter reflects edits without reopening the post editor. Scoring runs the two field assessors directly on the main thread (no analysis worker, no full-page analysis); a JS filter (yoast.bulkEditor.analysis.configureResearcher) lets Premium augment the researcher with morphology data, while Free scores with base word forms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lysis is disabled

The needs-improvement filter only matches on the stored per-field score while SEO analysis is enabled; when it is off the score is no longer maintained, so the search fields fall back to the empty-field check (like the social fields).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-filter-dropdown' of github.com:Yoast/wordpress-seo into 1346-bulk-editor-add-needs-improvement-filter-option-to-filter-dropdown
@FAMarfuaty FAMarfuaty added changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog innovation Innovative issue. Relating to performance, memory or data-flow. labels Jul 20, 2026
@FAMarfuaty
FAMarfuaty marked this pull request as ready for review July 20, 2026 11:22
FAMarfuaty and others added 2 commits July 20, 2026 16:02
Applying an AI-generated SEO title or meta description now recomputes just
that field's score via createSingleFieldScorer, wired through onFieldApplied,
so the needs-improvement filter reflects AI edits the same as manual ones.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…core dot

Relabels the options to the plain field names and adds a decorative red
(#dc3232) score dot; a visually-hidden group legend keeps the "needs
improvement" meaning for assistive tech.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FAMarfuaty
FAMarfuaty force-pushed the 1346-bulk-editor-add-needs-improvement-filter-option-to-filter-dropdown branch from 76359bb to 569e812 Compare July 20, 2026 14:03

Copilot AI left a comment

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.

Pull request overview

This PR expands the bulk editor’s filtering capabilities by introducing “needs improvement” filter options for search and social fields, backed by persisted per-field scores for SEO title and meta description to keep filtering and re-scoring consistent across the bulk editor and the post editor.

Changes:

  • Adds persisted per-field score support for SEO title and meta description (meta sanitization + indexable columns + migration).
  • Introduces a bulk-editor REST endpoint to persist computed per-field scores and returns rendered search fields from updates for client-side re-scoring.
  • Adds a “needs improvement” filter to the bulk editor (UI + store/query param wiring + server-side collectors), including score-based matching when SEO analysis is enabled.

Reviewed changes

Copilot reviewed 56 out of 56 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/WP/Inc/Meta_Test.php Adds WPSEO_Meta sanitization coverage for new score meta keys.
tests/Unit/Bulk_Editor/User_Interface/Scores_Route_Test.php Unit tests for the new score update REST route.
tests/Unit/Bulk_Editor/User_Interface/Posts_Route/Register_Routes_Test.php Updates posts route registration expectations (adds needs_improvement).
tests/Unit/Bulk_Editor/User_Interface/Posts_Route/Get_Posts_Test.php Covers needs-improvement filtering + SEO-analysis-disabled behavior in posts route.
tests/Unit/Bulk_Editor/User_Interface/Posts_Route/Abstract_Posts_Route_Test.php Adds Options_Helper dependency to posts route test setup.
tests/Unit/Bulk_Editor/User_Interface/Bulk_Editor_Integration/Enqueue_Assets_Test.php Verifies localized analysis config (locale + keywordAnalysisActive).
tests/Unit/Bulk_Editor/Infrastructure/Updates/Meta_Writer_Test.php Adds test for persisting score meta via Meta_Writer.
tests/Unit/Bulk_Editor/Infrastructure/Posts/Indexable_Posts_Collector/Get_Posts_Test.php Verifies SQL clause generation for needs-improvement in indexable collector.
tests/Unit/Bulk_Editor/Infrastructure/Endpoints/Update_Scores_Endpoint_Test.php Tests endpoint URL/namespace/route for score updates.
tests/Unit/Bulk_Editor/Domain/Updates/Update_Result_Test.php Ensures successful results can include rendered field payloads.
tests/Unit/Bulk_Editor/Domain/Updates/Post_Score_Update_Test.php Tests new Post_Score_Update value object.
tests/Unit/Bulk_Editor/Domain/Posts/Posts_Query_Test.php Covers new Posts_Query fields (needsImprovement + scoresEnabled).
tests/Unit/Bulk_Editor/Application/Updates/Score_Updater_Update_Test.php Tests server-side score persistence workflow and error handling.
tests/Unit/Bulk_Editor/Application/Updates/Bulk_Updater_Update_Test.php Asserts rendered search fields returned on successful search updates.
tests/Unit/Bulk_Editor/Application/Updates/Abstract_Bulk_Updater_Test.php Wires a Field_Renderer test double into Bulk_Updater tests.
tests/Unit/Builders/Indexable_Post_Builder_Test.php Extends indexable builder expectations to include new score fields.
src/models/indexable.php Adds new indexable model properties for per-field scores.
src/config/migrations/20260709144332_AddSeoTitleAndMetaDescriptionScores.php Migration adding score columns to the indexable table.
src/bulk-editor/user-interface/scores-route.php New REST route to persist per-post field scores.
src/bulk-editor/user-interface/posts-route.php Adds needs_improvement arg + passes scoring-enabled flag into Posts_Query.
src/bulk-editor/user-interface/bulk-editor-integration.php Localizes analysis config (locale + keywordAnalysisActive) for JS scoring.
src/bulk-editor/infrastructure/updates/meta-writer.php Adds write_score to persist score meta via canonical sanitization.
src/bulk-editor/infrastructure/updates/field-renderer.php Renders meta values with replacement vars for accurate re-scoring.
src/bulk-editor/infrastructure/posts/post-meta-posts-collector.php Implements needs-improvement filtering for meta-based collector.
src/bulk-editor/infrastructure/posts/indexable-posts-collector.php Implements needs-improvement filtering for indexable-based collector.
src/bulk-editor/infrastructure/endpoints/update-scores-endpoint.php Adds endpoint abstraction for update-scores route.
src/bulk-editor/domain/updates/update-result.php Adds optional rendered payload support to update results.
src/bulk-editor/domain/updates/post-score-update.php New domain object representing a post’s score update.
src/bulk-editor/domain/posts/posts-query.php Extends query object with needs-improvement fields + scoresEnabled.
src/bulk-editor/application/updates/score-updater.php Implements persistence logic for per-field score updates.
src/bulk-editor/application/updates/meta-writer-interface.php Adds write_score contract.
src/bulk-editor/application/updates/field-renderer-interface.php Defines rendering contract used by Bulk_Updater.
src/bulk-editor/application/updates/bulk-updater.php Returns rendered search fields for re-scoring after saves.
src/bulk-editor/application/posts/posts-collector-interface.php Introduces allowed needs-improvement field keys list.
src/builders/indexable-post-builder.php Populates indexable score columns from meta.
packages/js/tests/helpers/fields/AnalysisFields.test.js Adds tests for new hidden-field accessors in AnalysisFields.
packages/js/tests/bulk-editor/store/query.test.js Updates query slice tests for needsImprovement support.
packages/js/tests/bulk-editor/services/use-posts.test.js Verifies needs-improvement query param mapping + tab resolution.
packages/js/tests/bulk-editor/services/field-scores.test.js Tests bulk-editor main-thread field scoring + persistence calls.
packages/js/tests/bulk-editor/hooks/use-inline-edit.test.js Ensures inline edit wires up scorers and re-scores applied fields.
packages/js/tests/bulk-editor/bulk-editor-filters.test.js Tests new filter UI options + accessibility grouping.
packages/js/tests/analysis/deriveFieldScores.test.js Tests per-field score derivation and persistence to hidden inputs.
packages/js/src/initializers/analysis.js Persists derived per-field scores during editor analysis runs.
packages/js/src/helpers/fields/AnalysisFields.js Adds getters/setters for SEO title + meta description score inputs.
packages/js/src/elementor/helpers/is-relevant-change.js Ignores score fields in Elementor “relevant change” detection.
packages/js/src/bulk-editor/store/query.js Adds needsImprovement to query slice + actions/selectors.
packages/js/src/bulk-editor/services/use-posts.js Sends needs_improvement param, resolved per active tab.
packages/js/src/bulk-editor/services/field-scores.js New service to compute and POST per-field scores (bulk + single-field).
packages/js/src/bulk-editor/hooks/use-inline-edit.js Triggers re-scoring after saves and after Premium AI applies values.
packages/js/src/bulk-editor/constants.js Adds needs-improvement concepts and per-tab param mapping.
packages/js/src/bulk-editor/components/bulk-editor-filters.js Adds needs-improvement filter group and applied-count badge updates.
packages/js/src/analysis/refreshAnalysis.js Persists derived per-field scores on analysis refresh.
packages/js/src/analysis/deriveFieldScores.js Implements score derivation from assessment results + hidden field writes.
inc/class-wpseo-meta.php Registers new hidden meta fields and sanitizes them to 0–100.
css/src/bulk-editor-page.css Adds styling for the new filter group and red dot indicator.
admin/metabox/class-metabox.php Disables saving score meta when SEO analysis is disabled.

Comment thread src/bulk-editor/user-interface/posts-route.php Outdated
Comment thread tests/Unit/Bulk_Editor/User_Interface/Posts_Route/Register_Routes_Test.php Outdated
Comment thread src/bulk-editor/domain/posts/posts-query.php Outdated
Comment thread src/bulk-editor/domain/posts/posts-query.php Outdated
FAMarfuaty and others added 3 commits July 21, 2026 10:21
Both collectors duplicated the 1-70 "needs improvement" band (MIN/MAX ints
in the indexable collector, a [1,70] array in the post-meta fallback). Move
it to Posts_Collector_Interface::NEEDS_IMPROVEMENT_MIN_SCORE / MAX_SCORE and
reference it from both, so the band lives in one place. Also aligns the
$needs_improvement @param wording with the property doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s-improvement-filter-option-to-filter-dropdown
@vraja-pro

vraja-pro commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

AC 🚧

  • When trying to run the SEO data optimization, it gets stuck...
  • When filtering SEO titles in the filters, I get no posts even though I have posts without SEO titles.
  • I'm unable to save empty title or meta description when manually editing in the bulk editor.

Comment thread css/src/bulk-editor-page.css Outdated
@FAMarfuaty

FAMarfuaty commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the AC pass @vraja-pro — went through all three:

② SEO title filter returns no posts — Genuine bug, fixed. The post-meta collector (the path used when indexables are disabled) expressed "empty OR missing" through a WP_Query meta_query that OR-ed a NOT EXISTS clause with value comparisons. WordPress gives those value clauses their own INNER JOIN, which eliminated exactly the missing-meta-row posts the filter targets — so nothing matched. Rewrote it to use correlated NOT IN/IN subqueries (immune to that join elimination), with a WHERE-asserting unit test. efad8ef

① SEO data optimization gets stuck — This is the migration not having run in your environment, not the indexation code. AddSeoTitleAndMetaDescriptionScores adds the two score columns the indexable builder writes; until it runs, every indexable save fails on "Unknown column" and the batch never advances. It's being skipped because the migration runner gates on a strict version <, and this ph2 branch has likely already stamped yoast_migrations_free to 28.1 (from sibling ph2 migrations) before this migration existed. To clear it: run composer install (regenerates the DI container so the migration is registered), then force it via the PR's Running the migration manually section, and confirm the seo_title_score / meta_description_score columns exist. That resolves both the stuck indexation and the score half of the filter. ⚠️ Flagging for QA/release: this same-version gate could bite any environment already on 28.1 — worth ensuring the release bumps the version past what the ph2 branches recorded.

③ Can't save an empty title / meta description — I couldn't reproduce this from the current branch. There's no empty/truthy guard in the save path (Free's EditableFieldCell / use-inline-edit.js send the empty string unconditionally), the backend explicitly clears the field on empty (array_key_exists, unit-tested), and the Save button is only disabled while a save is in flight. This is also due to the fact that the migration was not run.

Comment thread css/src/bulk-editor-page.css Outdated
@vraja-pro

vraja-pro commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

AC 🚧 When maually editing a row, I get 6 requests in the network tab.

The per-row "Save" button (handleSave in table-row.js) calls onApplyField for each open field via forEach. Each onApplyField call fires 1 POST to the save endpoint, plus (before the current branch's fix) 1 POST to update_scores = 2 per field × 3 fields = 6 requests.

The current branch already adds the conditional rescore (remainingOpenFields.length === 0). But that fix has a subtle bug: all 3 onApplyField calls are dispatched synchronously from forEach, so they all read the same rowEdit snapshot. None will ever see remainingOpenFields.length === 0, so the rescore never fires from the per-row Save button.

The right fix: the per-row "Save" button should batch all open fields into one request per endpoint (like onApplyAll does) and rescore once after. For the current field sets, all 3 fields share the update_search endpoint → 2 requests total (1 save + 1 rescore).
I created a patch for a suggested fix, but it will affect other tests that need to be addressed.
onApplyRow.patch

…yRow

Replaces the per-field loop (N separate POSTs) on the row Save button with
a single onApplyRow call that merges all open fields into one request per
endpoint. Updates tests to reflect the new batching behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@vraja-pro vraja-pro left a comment

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.

CR & AC ✅
I ended up fixing it myself, we can create a followup issue if there is any objection.

@vraja-pro vraja-pro added changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog and removed changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog labels Jul 24, 2026
@vraja-pro
vraja-pro merged commit 9f9cbf8 into feature/bulk-editor-ph2 Jul 24, 2026
40 checks passed
@vraja-pro
vraja-pro deleted the 1346-bulk-editor-add-needs-improvement-filter-option-to-filter-dropdown branch July 24, 2026 11:56
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 innovation Innovative issue. Relating to performance, memory or data-flow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants