Skip to content

Decouple the block editor sidebar from the metabox hidden fields#23324

Open
vraja-pro wants to merge 105 commits into
trunkfrom
1275-decouple-the-metabox-from-the-block-editor-sidebar
Open

Decouple the block editor sidebar from the metabox hidden fields#23324
vraja-pro wants to merge 105 commits into
trunkfrom
1275-decouple-the-metabox-from-the-block-editor-sidebar

Conversation

@vraja-pro

@vraja-pro vraja-pro commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

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/editor uses.

This PR introduces a wpseo_disable_metabox_in_block_editor filter (default false) that, when enabled, switches the block editor to a REST-only save path: all sidebar writes go through core/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:

  • Adds wpseo_disable_metabox_in_block_editor filter to allow the block editor sidebar to save post meta via the REST API instead of metabox hidden fields.

Relevant technical choices:

  • `WPSEO_Meta' initi has register meta method that is untouched,
  • All setters guard against circular write-back and premature writes via shouldSkipMetaWrite: skips editPost when 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).
  • AnalysisFields setters queue meta writes before core/editor is ready (i.e. before getCurrentPostType() returns). The undoIgnore flag is now preserved per queued entry; flushPendingWrites() splits the batch so analysis scores flush via writeMetaWithoutUndo and user-editable fields flush via editPost. 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.
  • The cornerstone dispatch in initializeSnippetEditorSync is deferred in REST mode: if core/editor entity meta has not loaded yet at init time, AnalysisFields.isCornerstone would return false regardless of the saved value. A one-shot subscribe waits until meta is available and then dispatches the correct value.
  • Primary term meta is casted to a string since type is being checked via the rest route, a numerical id will be considered a number, not string.
  • Analysis scores (SEO, readability, inclusive language) and estimated reading time are written via writeMetaWithoutUndo() — a shared helper in rest-meta.js that calls editEntityRecord with { 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 both AnalysisFields and EstimatedReadingTimeFields to 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.
  • SearchMetadataFields was upgraded to handle REST meta mode: title and description getters/setters now use getMetaValue/setMetaValue instead of raw DOM element access, consistent with how AnalysisFields handles keyphrase and cornerstone. PostDataCollector now delegates all field reads and writes to AnalysisFields (focus keyphrase, scores, cornerstone) and SearchMetadataFields (SEO title, meta description), removing direct getElementById and rest-meta calls 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:

  • Enable the filter: add add_filter( 'wpseo_disable_metabox_in_block_editor', '__return_true' ); to a test plugin or functions.php.
  • Open a post in the Gutenberg block editor.
  • Confirm no Yoast hidden <input> elements exist in the DOM (inspect → search for yoast_wpseo_title) and also the metabox is not rendered.
  • Edit the following values, save the post and refresh page, confirm all values are persisted:
    • In search appearance: SEO title, slug, meta description.
    • Focus keyphrase.
    • All fields under Advanced tab (Specifically test fields whose PHP default_value is "0" (nofollow = "Yes", noindex = "Default").
    • Cornerstone Content.
    • All fields under Schema tab: page/article type.
    • All fields under Social appearance: Open Graph / Twitter fields ( title, description, image ).
    • Under post tab, the Primary category select.
  • Analysis scores (seo analysis, readability, inclusive language):
    • Improve or change analysis scores and check score updated.
    • Save and refresh page, confirm all scores values are persisted.
  • Estimate reading time: Go the Insights tab and check the reading time, add or remove content and check the reading time has changes.
  • Save and refresh and check reading time is persistant.

Regression:

  • Click on Get content suggestions button 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

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

QA can test this PR by following these steps:

  • Verify that without the filter enabled, all Yoast SEO metadata saves correctly in both the block editor and classic editor — no regression.
  • Enable the filter and verify the block editor sidebar saves all fields correctly via REST with no hidden form fields in the DOM.

Impact check

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

  • Block editor sidebar — all field types (snippet, advanced, schema, social, analysis scores, cornerstone, primary terms)
  • Classic editor metabox — should be fully unaffected
  • Elementor integration — should be fully unaffected
  • REST API exposure of post meta — all sidebar-editable fields are now REST-readable by users with edit_post capability (enforced by hide_meta_from_unauthorized_rest_response)

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 commited the results, if my PR introduces new images or SVGs.

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label.
  • I have added my hours to the WBSO document.

Fixes https://github.com/Yoast/reserved-tasks/issues/1280

vraja-pro and others added 5 commits June 1, 2026 12:47
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>
@vraja-pro vraja-pro added the changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog label Jun 1, 2026
vraja-pro and others added 9 commits June 1, 2026 14:55
…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>
@github-actions

github-actions Bot commented Jun 1, 2026

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.

…aboxInBlockEditor key

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

coveralls commented Jun 1, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 0

Coverage increased (+0.2%) to 53.86%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 140 uncovered changes across 9 files (191 of 331 lines covered, 57.7%).
  • 9 coverage regressions across 5 files.

Uncovered Changes

File Changed Covered %
src/initializers/post-meta-rest-fields.php 84 36 42.86%
packages/js/src/initializers/post-scraper.js 30 0 0.0%
packages/js/src/analysis/PostDataCollector.js 23 0 0.0%
admin/metabox/class-metabox.php 24 4 16.67%
packages/js/src/helpers/fields/AnalysisFields.js 55 45 81.82%
admin/formatter/class-metabox-formatter.php 5 0 0.0%
packages/js/src/components/PrimaryTaxonomyPicker.js 4 2 50.0%
packages/js/src/containers/SchemaTab.js 1 0 0.0%
packages/js/src/helpers/fields/SchemaFields.js 9 8 88.89%
Total (19 files) 331 191 57.7%

Coverage Regressions

9 previously-covered lines in 5 files lost coverage.

File Lines Losing Coverage Coverage
packages/js/src/initializers/post-scraper.js 5 0.0%
inc/class-wpseo-meta.php 1 20.45%
packages/js/src/analysis/PostDataCollector.js 1 0.0%
admin/metabox/class-metabox.php 1 19.95%
packages/js/src/containers/SchemaTab.js 1 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 70090
Covered Lines: 37446
Line Coverage: 53.43%
Relevant Branches: 18081
Covered Branches: 10043
Branch Coverage: 55.54%
Branches in Coverage %: Yes
Coverage Strength: 42934.02 hits per line

💛 - Coveralls

…editor is active

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

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 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_editor filter and passes a disableMetaboxInBlockEditor flag to JS via wpseoScriptData.
  • Exposes Yoast post meta fields in REST (show_in_rest, single) and updates JS field helpers / collectors to read/write via core/editor when 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.

Comment thread inc/class-wpseo-meta.php
Comment thread admin/metabox/class-metabox.php
Comment thread admin/metabox/class-metabox.php
Comment thread packages/js/src/initializers/post-scraper.js
vraja-pro and others added 5 commits June 1, 2026 17:43
…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>
Comment thread inc/class-wpseo-meta.php

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread inc/class-wpseo-primary-term.php
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>
Comment thread packages/js/src/ai-content-planner/hooks/use-apply-outline.js
Comment thread packages/js/src/analysis/PostDataCollector.js
vraja-pro and others added 4 commits June 18, 2026 10:33
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>

@vraja-pro vraja-pro Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in this file 2 unrelated linting warnings for naming: store and postDataCollector.

vraja-pro and others added 6 commits June 18, 2026 12:13
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>
@vraja-pro vraja-pro force-pushed the 1275-decouple-the-metabox-from-the-block-editor-sidebar branch from 3104b1c to 81483df Compare June 18, 2026 10:41
vraja-pro and others added 2 commits June 18, 2026 14:06
@vraja-pro vraja-pro force-pushed the 1275-decouple-the-metabox-from-the-block-editor-sidebar branch from 5754d30 to 5a6f161 Compare June 18, 2026 11:25
@FAMarfuaty

Copy link
Copy Markdown
Contributor

One thing on Post_Meta_Rest_Fields::register_post_meta(): the show_in_rest registration and add_post_type_support( $post_type, 'custom-fields' ) aren't behind the filter, only the rest_after_insert hook is.

I checked core — the custom-fields call is actually needed: for CPTs the REST meta field only exists in the schema if the post type supports custom-fields (class-wp-rest-posts-controller.php:2742), so without it the REST save would silently break on custom post types. So don't remove it.

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 post_type_supports, and it adds a (hidden-by-default) Custom Fields option in the editor. Our own _yoast_wpseo_* keys never show up there since they're protected, so it's minor visually — but it's still a global change, which doesn't quite match "classic editor fully unaffected".

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 product).

@FAMarfuaty

Copy link
Copy Markdown
Contributor

Two more, ranked by how much they worry me.

1. Advanced/Schema meta now bypasses wpseo_edit_advanced_metadata (I think this blocks)

The Advanced tab (noindex, nofollow, canonical, robots-adv, bctitle) and Schema tab are gated behind the wpseo_edit_advanced_metadata capability + disableadvanced_meta option — see inc/class-wpseo-meta.php:325,352, which return [] so the fields aren't even rendered for users without it. disableadvanced_meta defaults to true, and the capability only goes to editor/wpseo_editor/wpseo_manager, not authors.

The new register_meta opens all these keys with auth_callback => current_user_can( 'edit_post' ). Two issues:

  • They're protected (_-prefixed) keys, which WP refuses to write via REST by default — the auth_callback here explicitly overrides that deny.
  • It only checks edit_post, not wpseo_edit_advanced_metadata.

So an Author editing their own post can set noindex/canonical/schema via REST, which they can't do in the UI — SEO-impacting (deindex, canonical) and unconditional, so it applies with the filter off too. Can we have the auth_callback for the advanced/schema fields also check wpseo_edit_advanced_metadata? Gating the whole registration behind the filter (from my earlier comment) shrinks the blast radius but doesn't fully fix this.

2. Default-value "0" fields — untested round-trip

The Advanced items are unchecked in the test plan, and this is exactly where I'd expect trouble: register_post_meta sets default from default_value, while remove_meta_if_default / dont_save_meta_if_default delete meta equal to the default, and shouldSkipMetaWrite skips writes equal to current. Three layers all keying on "equals default", so noindex/nofollow may not persist in REST mode. Could we do a manual save + refresh on those before merge? Might be fine, but I want to confirm the setting doesn't silently get dropped.

vraja-pro and others added 2 commits June 18, 2026 21:51
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>
@vraja-pro

vraja-pro commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author
  1. I guarded the custom fields support with the filter and a check if block editor is supported.
  2. I added different auth_callback for schema and advanced settings.
  3. Removed default args to match the old register meta (even though in rest it only read the default ).

…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>
@vraja-pro vraja-pro force-pushed the 1275-decouple-the-metabox-from-the-block-editor-sidebar branch 2 times, most recently from 493ff24 to ed540a8 Compare June 19, 2026 07:42
default value of primary term is taken care in the frontend

@FAMarfuaty FAMarfuaty 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 & AT: ✅

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.

4 participants