Decouple the block editor sidebar from the metabox hidden fields#23324
Decouple the block editor sidebar from the metabox hidden fields#23324vraja-pro wants to merge 105 commits into
Conversation
Add show_in_rest and single flags to all WPSEO post meta fields that the block editor sidebar can edit: linkdex, content_score, inclusive_language_score, is_cornerstone, meta-robots-noindex, meta-robots-nofollow, meta-robots-adv, bctitle, canonical, redirect, schema_page_type, schema_article_type, and all social (opengraph-*, twitter-*) fields. This is a prerequisite for saving these values via the WordPress REST API instead of the hidden metabox form, so that the block editor sidebar can work without rendering any hidden <input> elements. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… fields save When the filter returns true and the block editor is active: - meta_box() skips render_hidden_fields() so no hidden <input> elements are rendered in the DOM, preventing the legacy $_POST save path - save_postdata() returns early for REST requests so the hook does not overwrite meta that core/editor is saving via the REST API - wpseoScriptData.disableMetaboxInBlockEditor is set to true so the JS side knows to route writes through core/editor instead Classic editor and Elementor are unaffected: the guard checks WP_Screen::is_block_editor() and REST_REQUEST respectively. Filter defaults to false for full backward compatibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ox is disabled
All Fields helper classes (AnalysisFields, AdvancedFields, FacebookFields,
TwitterFields, SchemaFields) now check wpseoScriptData.disableMetaboxInBlockEditor.
When active:
- Setters dispatch editPost({ meta: { _yoast_wpseo_*: value } }) to
core/editor so WordPress saves the value via the REST API on post save.
- Getters read from core/editor.getEditedPostAttribute("meta") so initial
values are loaded from the REST response rather than absent DOM inputs.
When inactive the original DOM read/write path is preserved unchanged,
keeping classic editor and Elementor behaviour identical to before.
Because all Redux action creators (setNoIndex, setCanonical, setPageType,
setFacebookTitle, etc.) call these helpers, the entire sidebar wires up
to core/editor automatically with no changes to the action files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tabox is disabled
PostDataCollector: getKeyword, getMeta, getSnippetTitle, getSnippetMeta now
read from core/editor meta when disableMetaboxInBlockEditor is true, so the
analysis is initialised from the REST response rather than absent hidden inputs.
setDataFromSnippet dispatches editPost({ meta }) for title and metadesc instead
of writing to DOM elements.
post-scraper: replace all remaining direct jQuery/getElementById accesses for
Yoast fields with AnalysisFields helper calls, which now transparently route
through core/editor in REST meta mode:
- focuskw initial read and subscriber write use AnalysisFields.keyphrase
- is_cornerstone initial read and subscriber write use AnalysisFields.isCornerstone
- linkdex, content_score, inclusive_language_score reads use AnalysisFields
score getters
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend useYoastMetaSync to also read _yoast_wpseo_is_cornerstone from the core/editor meta object and dispatch setCornerstoneContent into yoast-seo/editor. This ensures that after a page reload the cornerstone state is correctly restored from the REST meta response, which is now the source of truth when wpseo_disable_metabox_in_block_editor is active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nerstoneContent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…melcase lint errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pdate imports Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… across the codebase Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nt rule Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
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. |
…aboxInBlockEditor key Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Coverage Report for CI Build 0Coverage increased (+0.2%) to 53.86%Details
Uncovered Changes
Coverage Regressions9 previously-covered lines in 5 files lost coverage.
Coverage Stats💛 - Coveralls |
…editor is active Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a feature-flagged path to save Yoast sidebar metadata in the block editor via the standard WordPress REST meta flow (using core/editor), instead of relying on PHP-rendered metabox hidden inputs and $_POST handling.
Changes:
- Adds a
wpseo_disable_metabox_in_block_editorfilter and passes adisableMetaboxInBlockEditorflag to JS viawpseoScriptData. - Exposes Yoast post meta fields in REST (
show_in_rest,single) and updates JS field helpers / collectors to read/write viacore/editorwhen REST-meta mode is active. - Adds shared meta-key constants and extends AI content planner meta syncing (including cornerstone) with updated tests.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/js/tests/ai-content-planner/initialize.test.js | Updates mocked dispatch to include setCornerstoneContent. |
| packages/js/tests/ai-content-planner/hooks/use-yoast-meta-sync.test.js | Adds cornerstone sync coverage and updates dispatch mocks. |
| packages/js/src/shared-admin/constants/meta-keys.js | Introduces centralized constants for Yoast REST meta keys. |
| packages/js/src/shared-admin/constants/index.js | Re-exports meta key constants via the constants barrel. |
| packages/js/src/initializers/post-scraper.js | Switches several DOM reads/writes to AnalysisFields helpers. |
| packages/js/src/helpers/SchemaFields.js | Adds REST-meta mode support using core/editor meta getters/setters. |
| packages/js/src/helpers/fields/TwitterFields.js | Adds REST-meta mode support using core/editor meta getters/setters. |
| packages/js/src/helpers/fields/FacebookFields.js | Adds REST-meta mode support using core/editor meta getters/setters. |
| packages/js/src/helpers/fields/AnalysisFields.js | Adds REST-meta mode support (keyphrase, cornerstone, scores) via core/editor. |
| packages/js/src/helpers/fields/AdvancedFields.js | Adds REST-meta mode support for advanced settings via core/editor. |
| packages/js/src/analysis/PostDataCollector.js | Reads/writes snippet fields via REST meta when metabox hidden fields are disabled. |
| packages/js/src/ai-content-planner/hooks/use-yoast-meta-sync.js | Syncs core/editor meta → yoast-seo/editor, including cornerstone. |
| packages/js/src/ai-content-planner/hooks/use-apply-outline.js | Uses meta-key constants when writing Yoast meta via editPost. |
| packages/js/package.json | Adjusts ESLint --max-warnings threshold. |
| inc/class-wpseo-meta.php | Adds show_in_rest/single to many meta fields to enable REST meta persistence. |
| admin/metabox/class-metabox.php | Adds filter plumbing, early REST/save bails, and exposes the JS flag in localized script data. |
…ements lint Extract setupYoastSEOGlobals, initializeAnalysisPlugins, and initializeSnippetEditorSync from initializePostAnalysis to bring its statement count under the 30-statement ESLint limit. Also allow the post-scraper to initialize when the metabox is intentionally disabled via wpseo_disable_metabox_in_block_editor, so window.YoastSEO.app and its Pluggable hooks are available for third-party integrations (e.g. featured-image.js). Reduce --max-warnings threshold from 37 to 34 to reflect removed warnings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When wpseo_disable_metabox_in_block_editor is active, AnalysisFields setters dispatch core/editor.editPost() to persist meta via REST. These setters are called during initPostScraper (DOM-ready), before core/editor has loaded the post type entity config. WordPress throws "The entity being edited (postType, undefined) does not have a loaded config" in that case. Add isEditorReady() which checks getCurrentPostType(); all setters now skip the dispatch silently when the editor is not ready yet. The skipped write is a no-op anyway (the value being written back is the same one just read from the store during initialization). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The object_subtype => 'post' restriction meant Yoast SEO meta fields were only exposed via the REST API for the built-in 'post' post type. Removing it registers the meta for all post types, so pages and custom post types also have their Yoast meta available through the REST API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…editor() meta_box() was calling render_hidden_fields() unconditionally, which would output hidden inputs in the block editor if the metabox callback were ever reached via a secondary code path (e.g. the IE fallback registration). This matches the docblock on is_metabox_disabled_in_block_editor() which states hidden fields should be disabled when the filter is enabled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
I moved the REST registration of the meta data to an initializer class.
- Separation of concerns
- Support registration for CPT (registering the fields after the CPT is registered)
- Easily add tests.
Keeping the regular register meta doesn't harm.
The DOM element reference was assigned but never read after the field value was migrated to PrimaryTermFields.get(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Analysis scores queued before the editor entity config is ready were flushed via editPost, landing on the undo stack despite the undoIgnore flag. Introduce flushPendingWrites() which stores the flag per entry and splits the batch into a writeMetaWithoutUndo call for scores and an editPost call for user-editable fields. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… and SearchMetadataFields Replace direct getMetaValue/setMetaValue calls and raw getElementById lookups in PostDataCollector with the canonical field helpers: SearchMetadataFields for title and description, AnalysisFields for the focus keyphrase. This removes all direct rest-meta and constants imports from PostDataCollector and fixes a subtle bug where getKeyword() used the post-only element ID, missing the hidden_wpseo_focuskw fallback for non-post contexts that AnalysisFields.keyphrase already handles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SearchMetadataFields: add REST meta mode tests for title and description getters (reads from core/editor store) and setters (dispatches editPost, skips no-op writes). AnalysisFields: add three tests for the flushPendingWrites fix — score writes queued before the editor is ready must flush via editEntityRecord with undoIgnore, user-editable writes must flush via editPost, and a mixed queue must split into both dispatches. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> tests: use meta key constants instead of hardcoded strings in field helper tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Addressed in this file 2 unrelated linting warnings for naming: store and postDataCollector.
Moves the snippet editor data init and store subscriber out of the post-scraper closure into a standalone exported function that takes store, postDataCollector, and app as explicit parameters. Removes the snippetEditor helpers, requestWordsToHighlight, and setCornerstoneContent imports from post-scraper.js as they are now only needed in the new module. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
initializeSnippetEditorSync is a reusable setup utility that takes all its dependencies as explicit parameters. Moving it to helpers/ aligns with the convention that initializers/ contains only top-level entry-point scripts (post-scraper, term-scraper, block-editor-integration, etc.). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move setupYoastSEOGlobals, initializeAnalysisPlugins, and snippet-editor-sync into a dedicated initializers/post-scraper/ subfolder. All three are sub-steps of the post analysis initialization pipeline rather than generic helpers, so grouping them under initializers/ keeps related code together and makes the top-level initializers/ directory easier to navigate. setupYoastSEOGlobals and initializeAnalysisPlugins are extracted as named exports that take their dependencies as explicit parameters; post-scraper.js becomes the thin orchestrator that calls them in sequence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The empty-string fallback and skip-on-unchanged tests are already covered by the title (REST meta mode) describe block, which exercises the same shared getMetaValue/setMetaValue code paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3104b1c to
81483df
Compare
…MetaOnReady Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5754d30 to
5a6f161
Compare
|
One thing on I checked core — the But because it runs unconditionally, every REST-enabled CPT now permanently gains custom-fields support, even with the filter off. Other plugins can see that via Can we gate the whole registration behind the filter? With it off the sidebar still uses the DOM hidden fields, so the REST exposure isn't doing anything there anyway. If we'd rather keep it always-on, let's call it out in the PR body and add a CPT without custom-fields support to the test matrix (right now we only test |
|
Two more, ranked by how much they worry me. 1. Advanced/Schema meta now bypasses
|
add_post_type_support( $post_type, 'custom-fields' ) was running unconditionally for every REST-enabled post type, permanently altering post_type_supports() even with wpseo_disable_metabox_in_block_editor off. Now gated on the filter being true and use_block_editor_for_post_type(), so classic-editor post types and sites with the metabox enabled are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eld auth callbacks The auth_callback for all meta fields previously only checked edit_post, which let an Author write noindex/canonical/schema values via REST — bypassing the wpseo_edit_advanced_metadata gate enforced in the metabox UI. Advanced and schema subset fields now mirror the WPSEO_Meta::get_tab_field_defs() gate: when disableadvanced_meta is on, writes are blocked unless the user also has wpseo_edit_advanced_metadata (or wpseo_manage_options). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> tests: fix auth callback tests — use andReturnUsing and reflection for WPSEO_Options whenHappen is only available for hook expectations; function expectations require andReturnUsing. WPSEO_Options::get returns null in unit tests because get_option returns [] (no defaults merged), so two of the schema auth_callback tests used a helper that forces the value into the static cache via reflection. WPSEO_Options::clear_cache() added to tear_down. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> refactor: inject Options_Helper and Capability_Helper instead of static class calls Replaces WPSEO_Options::get() and WPSEO_Capability_Utils::current_user_can() with injected Options_Helper and Capability_Helper instances. The helpers are captured by value into the auth_callback closure via `use`. Tests now mock the helpers directly — no reflection or static-cache manipulation needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> refactor: extract advanced meta auth callback to named public method Replaces the closure-with-use pattern for the advanced/schema auth_callback with a named public method auth_callback_for_advanced_meta(), which uses $this->options_helper and $this->capability_helper directly. The method reference [$this, 'auth_callback_for_advanced_meta'] is passed to register_post_meta, which is the same pattern WordPress expects. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
…t_Fields The unit tests relied on Brain Monkey to stub every WP function and a static WPSEO_Meta fixture, which made them fragile against internal field-count changes and required reflection hacks to control WPSEO_Options. The WP integration tests use a real WordPress environment (wp-env) to verify the behaviours that matter: meta keys registered with show_in_rest, the auth_callback_for_advanced_meta gate (edit_post + disableadvanced_meta + wpseo_edit_advanced_metadata in all combinations), and hide_meta_from_unauthorized_rest_response stripping fields for unauthorised users. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
493ff24 to
ed540a8
Compare
default value of primary term is taken care in the frontend
Context
The Yoast block editor sidebar currently relies on hidden
<input>elements rendered by the PHP metabox to persist state and save post meta. When the user edits values in the sidebar (SEO title, focus keyword, cornerstone, canonical, schema type, social fields, etc.) the JS writes those values back into those hidden DOM inputs, which WordPress then POSTs via its metabox-compatibility layer on every save.This tight coupling means the metabox must always be rendered — even when only the sidebar UI is needed — and makes it impossible to save Yoast data via the standard WordPress REST API path that
core/editoruses.This PR introduces a
wpseo_disable_metabox_in_block_editorfilter (defaultfalse) that, when enabled, switches the block editor to a REST-only save path: all sidebar writes go throughcore/editor.editPost({ meta })and WordPress saves them automatically when the post is saved. The metabox, its hidden fields, and the$_POST-based save hook are fully bypassed in that mode.Summary
This PR can be summarized in the following changelog entry:
wpseo_disable_metabox_in_block_editorfilter to allow the block editor sidebar to save post meta via the REST API instead of metabox hidden fields.Relevant technical choices:
shouldSkipMetaWrite: skipseditPostwhen the entity meta is not yet loaded (preventing early dispatches from overwriting saved values) or when the value is already equal to the current meta (preventing no-op writes that would dirty the post).AnalysisFieldssetters queue meta writes beforecore/editoris ready (i.e. beforegetCurrentPostType()returns). TheundoIgnoreflag is now preserved per queued entry;flushPendingWrites()splits the batch so analysis scores flush viawriteMetaWithoutUndoand user-editable fields flush viaeditPost. This keeps scores off the undo stack even when they were queued before the editor was ready — previously all queued writes landed on the undo stack regardless of the flag.initializeSnippetEditorSyncis deferred in REST mode: ifcore/editorentity meta has not loaded yet at init time,AnalysisFields.isCornerstonewould returnfalseregardless of the saved value. A one-shotsubscribewaits until meta is available and then dispatches the correct value.writeMetaWithoutUndo()— a shared helper inrest-meta.jsthat callseditEntityRecordwith{ undoIgnore: true }. This keeps them out of the block-editor undo stack, since they are computed values rather than user input. The helper is used by bothAnalysisFieldsandEstimatedReadingTimeFieldsto avoid duplication. This is important for the content planner undo functionality. In order to undo we only include the applied content in the undo history.SearchMetadataFieldswas upgraded to handle REST meta mode:titleanddescriptiongetters/setters now usegetMetaValue/setMetaValueinstead of raw DOM element access, consistent with howAnalysisFieldshandles keyphrase and cornerstone.PostDataCollectornow delegates all field reads and writes toAnalysisFields(focus keyphrase, scores, cornerstone) andSearchMetadataFields(SEO title, meta description), removing directgetElementByIdandrest-metacalls from the collector.Test instructions
Test instructions for the acceptance test before the PR gets merged
This PR can be acceptance tested by following these steps:
add_filter( 'wpseo_disable_metabox_in_block_editor', '__return_true' );to a test plugin orfunctions.php.<input>elements exist in the DOM (inspect → search foryoast_wpseo_title) and also the metabox is not rendered.default_valueis"0"(nofollow = "Yes", noindex = "Default").Regression:
Click on
Get content suggestionsbutton and select a suggestion, apply the content ( check the content is also applied to the focus keyphrase, SEO title, Meta description and primary category.Click the Undo button in the post editor.
Check the undo was also applied to the focus keyphrase, SEO title, meta description and primary category.
Edit a product and check metabox is there and repeat the tests.
Edit a page in gutenberg and repeat the tests.
Disable the filter and confirm classic behavior is unchanged, open the classic editor and elementor editor on a post and confirm Yoast saves normally without the filter.
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:
edit_postcapability (enforced byhide_meta_from_unauthorized_rest_response)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 commited the results, if my PR introduces new images or SVGs.Innovation
innovationlabel.Fixes https://github.com/Yoast/reserved-tasks/issues/1280