This document maps exactly which pieces of Gutenberg core the Validation API plugin builds on, why each is used and how, and which capabilities the plugin had to invent because no core primitive exists for them.
Derived from the plugin's actual @wordpress/* imports, data-store usage, JS filters,
and PHP hooks.
| Package | What's pulled in | Role |
|---|---|---|
@wordpress/data |
createReduxStore, register, select, useSelect, useDispatch |
Heaviest dependency — the state spine |
@wordpress/element |
useEffect, useRef, useState |
React runtime |
@wordpress/hooks |
addFilter, applyFilters |
The extension backbone |
@wordpress/i18n |
__, sprintf |
Translation |
@wordpress/components |
PanelBody, PanelRow, ToolbarButton, Modal, TextControl |
UI primitives |
@wordpress/plugins |
registerPlugin |
Single mount point |
@wordpress/editor |
PluginSidebar |
Sidebar slot |
@wordpress/compose |
createHigherOrderComponent |
HOC wrapping |
@wordpress/blocks |
getBlockType |
Block metadata lookup |
@wordpress/block-editor |
BlockControls |
Toolbar slot |
core/editor—getEditorSettings,getEditedPostAttribute,editPost,lockPostSaving/unlockPostSaving,isSavingPostcore/block-editor—getBlocks(walk the block tree)core/validation— the plugin's own registered store (not core)
editor.BlockEdit— inject per-block validation + toolbar buttoneditor.BlockListBlock— apply per-block CSS classeseditor.preSavePost— async save-time gate (the one validation-adjacent filter core ships)BlockControls/PluginSidebar— SlotFill mount points
block_editor_settings_all— inject registry config into editor settingsenqueue_block_editor_assets,wp_enqueue_script,wp_set_script_translationsregister_rest_route+current_user_can— the/wp-validation/v1/checksendpointadd_filter/add_action/apply_filters— the plugin's own hook system
@wordpress/data (the spine).
The whole architecture is store-driven. The plugin calls createReduxStore / register
to stand up its own core/validation store, then uses useSelect / useDispatch /
select to (a) read editor state from core/editor and core/block-editor, and
(b) push computed validation results into its own store. The
useValidationSync → store → useValidationLifecycle split exists because of how
@wordpress/data subscriptions re-render — keeping the dispatcher and the subscriber as
siblings avoids a render loop.
@wordpress/hooks (the extension model).
The conceptual heart. Every validation check runs through applyFilters on
editor.validateBlock / validateMeta / validateEditor. External plugins register
checks purely by calling addFilter. The plugin contributes infrastructure; the actual
pass/fail logic lives in filter callbacks. This is the WordPress-idiomatic way to make
validation pluggable without a hard dependency.
core/editor store APIs.
getEditorSettings()— how the PHP registries reach JS: PHP injectsvalidationApiinto editor settings viablock_editor_settings_all, and JS reads it back here.getEditedPostAttribute— read meta/content values to validate.editPost— write meta back for the meta-field integration hook.lockPostSaving/unlockPostSaving,lockPostAutosaving/unlockPostAutosaving,disablePublishSidebar/enablePublishSidebar— the enforcement mechanism forerror-level checks. See §2a — this is the central piece of the plugin's enforcement story.
core/block-editor → getBlocks.
Walks the block tree so useInvalidBlocks can run every block check against every block
instance.
editor.preSavePost.
Belt-and-suspenders save gate. Even if the save lock is bypassed, this async filter throws
to abort the save when errors remain.
editor.BlockEdit + createHigherOrderComponent + BlockControls / ToolbarButton.
Wraps every block's edit component to surface a validation toolbar button without the block
author opting in.
editor.BlockListBlock.
Adds per-block CSS classes so invalid blocks get visual treatment in the canvas.
PluginSidebar + registerPlugin + PanelBody / PanelRow.
The grouped issue-list UI. registerPlugin('core-validation', …) is the single mount;
PluginSidebar provides the panel surface.
@wordpress/components (Modal, TextControl, etc.).
Standard UI primitives for the sidebar, inline modal, and the meta-field helper hook.
getBlockType (@wordpress/blocks).
Resolves block metadata/labels for display in issue messages.
block_editor_settings_all (PHP).
The PHP↔JS bridge. All three registries serialize their registered checks into
settings.validationApi, which JS reads via getEditorSettings().
register_rest_route + current_user_can('manage_options').
Exposes registered checks to the companion settings-admin plugin.
Post locking/unlocking is the core enforcement primitive of the whole plugin. An
error-level check is only meaningful if it can actually stop a save — and core's
lockPostSaving family is what makes that possible. The plugin uses it as a
two-layer defense.
src/hooks/use-validation-lifecycle.js subscribes to aggregate validation state from the
core/validation store and, on every change, dispatches against core/editor using a
single named lock, LOCK_NAME = 'core/validation'. The named lock is important: it scopes
the plugin's lock so it never collides with locks held by other plugins, and it lets the
plugin release exactly its own lock.
When any error-level failure exists across blocks, meta, or editor checks, it locks three things together:
| Dispatch | Effect |
|---|---|
lockPostSaving(LOCK_NAME) |
Disables Save/Update — the primary gate |
lockPostAutosaving(LOCK_NAME) |
Prevents autosave from persisting invalid content |
disablePublishSidebar() |
Forces the pre-publish checklist so the user sees why |
When all error-level failures clear, the mirror calls
(unlockPostSaving / unlockPostAutosaving / enablePublishSidebar) run and saving is
restored. The autosave and publish-sidebar dispatches are feature-detected
(if (lockPostAutosaving)) so the plugin degrades gracefully on older core.
Severity matters here: only mode === 'error' (blocks), hasErrors (meta), and
error-level editor checks drive the lock. warning-level failures deliberately do not
lock — they only surface feedback and body classes. This is the entire point of the graded
severity model: the lock is the hard stop reserved for error.
Scope guard: locking only runs in post-editor contexts (post-editor /
post-editor-template); the site editor is intentionally excluded.
src/hooks/pre-save-validation.js is the belt-and-suspenders backstop. The reactive lock
is state-driven and usually sufficient, but a lock can race: a direct dispatch, an
in-flight state update, or a programmatic save could slip through before the lock
propagates. So at save time the editor.preSavePost async filter re-checks
select('core/validation').hasErrors() and throws to abort the save if errors remain.
The two layers are complementary, not redundant:
- Lock = proactive UX — the Save button is visibly disabled, autosave is paused, the publish flow is interrupted. The user is told before they try.
- preSavePost throw = last line of defense — guarantees an invalid post can't be persisted even if the lock state is momentarily wrong.
Without lockPostSaving, "error" severity would be cosmetic. Core provides the lock
primitive but nothing that maps a set of declarative checks to it — the reactive
orchestration, the named-lock scoping, the autosave + publish-sidebar coordination, and the
preSavePost fallback are all plugin-authored glue. That glue is item 8 in the gap list
below.
These are the genuine gaps — everything the plugin had to invent because core provides no primitive for it. This list is essentially the scope of a Gutenberg core PR.
-
A validation store / data model. There is no
core/validation(or equivalent) in core. The entire store — invalid-blocks/meta/editor state,hasErrors/hasWarnings, per-block validation records — is bespoke. -
Declarative check registration (PHP + JS). Core has no
register_*_check()API, noAbstractRegistry, no concept of a "check" withnamespace/name/level/error_msg. The three registries are pure plugin invention. -
A severity model (
error/warning/none). Core's save flow is binary (locked or not). The graded severity plus the runtimevalidation_api_check_leveloverride filter don't exist in core. -
First-class validation filters.
editor.validateBlock/validateMeta/validateEditorare plugin-defined filter names. Core shipseditor.preSavePost(which the plugin reuses) but nothing for per-block / per-meta / per-document validation contracts. -
A meta-validation integration layer.
useMetaField/useMetaValidation(the@wordpress/validationimport path used in the doc examples) — value + onChange + help text + errorclassNamewired to a registered check — has no core equivalent. Core gives youuseEntityProp; it does not give you validated meta fields. -
Block-level validation surfacing. No core primitive ties a validation result to a block's canvas treatment or toolbar. The
BlockEdit/BlockListBlockwrapping + CSS side-effects reconstruct this manually. -
A standardized "where are my checks" REST surface.
/wp-validation/v1/checksexists only because core has no registry to introspect. -
Save-blocking driven by declarative results. Core has
lockPostSaving/lockPostAutosaving/disablePublishSidebar, but nothing that automatically maps a set of registered error-level checks to lock/unlock + a pre-save throw. TheuseValidationLifecycle+pre-save-validationcoordination (see §2a) is the missing glue — and it is the single most central piece of the plugin's enforcement model.
Core gives the plugin the mounting points, stores, slots, and the one save-gate filter
(editor.preSavePost). The validation concept itself — registry, checks, severity,
validation filters, the core/validation store, REST introspection, and meta/block
surfacing — is entirely the plugin's own contribution.
Note: the
@wordpress/validationimports appear only inside@exampledoc comments. That is the intended core package name (the merge target), not a real dependency today.