This proposal introduces a Validation API for the WordPress block editor -- a declarative framework for registering, executing, and displaying content validation checks in real time. The API enables theme and plugin developers to define validation rules for block attributes, post meta fields, and editor-wide document structure, providing immediate feedback to content creators and optionally preventing publication when critical issues are unresolved.
The Validation API plugin is a working reference implementation. It demonstrates the full lifecycle -- PHP registration, JavaScript execution via filters, centralized state management through a @wordpress/data store, and standardized editor UI -- built entirely on existing WordPress and Gutenberg APIs.
This proposal advocates for the framework only -- not specific validation checks, settings UI, or opinionated rules. Those remain the domain of plugins. What belongs in core is the infrastructure that makes declarative content validation possible for the entire ecosystem.
WordPress provides robust tools for creating and editing content in the block editor, but it lacks a unified system for validating that content against quality, accessibility, or editorial standards before publication. Today, developers who want to validate block content must independently solve the same set of problems:
-
No declarative check registration -- There is no standard way to declare "this block attribute must meet these criteria" or "this post must contain these elements." Every plugin builds its own registration system.
-
No real-time validation pattern -- Providing instant feedback as users edit requires understanding store subscriptions, React rendering, and performance optimization. Each plugin re-invents this.
-
No standardized validation UI -- There are no dedicated slots, components, or patterns for displaying validation results in the editor. Plugins create ad-hoc UI using
editor.BlockEditHOCs, custom sidebars, orPluginPrePublishPanel. -
No severity model -- The existing
lockPostSavingmechanism is binary (locked or not). There is no built-in concept of warnings vs. errors, configurable severity levels, or admin-controlled thresholds. -
Fragmented primitives -- The building blocks exist (
@wordpress/hooks,lockPostSaving,editor.BlockEdit,PluginPrePublishPanel,editor.preSavePost), but they are disconnected. Every plugin must assemble them into a validation system from scratch.
The result is that most plugins simply don't validate content at all, and those that do create inconsistent, incompatible experiences.
A Validation API would not introduce new low-level mechanisms. Instead, it provides a cohesive layer on top of existing, stable WordPress and Gutenberg APIs:
| Primitive | Current Use | Role in a Validation API |
|---|---|---|
@wordpress/hooks |
General-purpose JS event system | Execute validation logic via filters |
@wordpress/data |
Redux-like state management | Centralized validation state via dedicated store |
lockPostSaving |
Binary save prevention | Enforce error-level validation failures |
editor.BlockEdit |
Wrap block edit components via HOC | Display per-block validation toolbar and indicators |
editor.BlockListBlock |
Wrap block list items | Apply validation CSS classes to block wrappers |
PluginPrePublishPanel |
Inject content into pre-publish panel | Show validation summary before publishing |
PluginSidebar |
Custom editor sidebars | Consolidated validation results panel |
editor.preSavePost |
Async save interception (WP 6.7+) | Final validation gate before saving |
register_post_meta |
Meta field registration with REST validation | Server-side meta validation via validate_callback |
These are all stable, public APIs. A Validation API standardizes how they are used together for content validation.
Several Gutenberg issues and PRs have explored pieces of this problem space over the years, but none have proposed a unified validation framework:
- #4063 - Provide an API to validate block input -- One of the earliest requests (2017) for server-side block attribute validation. Endorsed by core contributors but never implemented as a framework.
- #14954 - Server-side block attribute validation -- Gravity Forms requested capability-based attribute restriction (2019). Resolution was to use
lockPostSavingas a workaround, highlighting the gap. - #13413 - Third-party save validation -- ACF team requested validation hooks during save (2019). Led to the
editor.preSavePostfilter (stabilized in WP 6.7), which provides the save-time hook but not the declarative framework. - #7020 - Pre-publish checkup extensibility -- Led to
lockPostSaving/unlockPostSaving(PR #10649), but without error messaging or severity levels. - #21703 - Classification of block validation types -- Proposes a classification system for block validation outcomes, though focused on markup validation rather than content quality.
- #71500 - Field API: Validation -- The DataViews/DataForm Field API introduced validation rules (required, pattern, custom validators). While scoped to the admin Data API, it demonstrates the value of a declarative validation model.
The common thread is that developers have repeatedly asked for a standardized way to validate block editor content. The primitives exist, but the framework does not.
- PHP for registration, JavaScript for validation -- Follow the existing block API pattern where PHP declares configuration and JavaScript handles runtime behavior.
- Declarative check registration -- Developers register checks with metadata (messages, severity, descriptions). The framework handles execution, UI, and save-locking.
- Three validation scopes -- Block attributes, post meta fields, and editor-wide document state each have distinct registration and execution patterns, but share a unified results model.
- Centralized state via
@wordpress/data-- All validation results flow through a dedicated Redux store, enabling any component to read validation state without duplicate computation. - Filter-first severity -- Every check passes through a filter that can override its level at runtime. No storage opinions in the framework.
- Extensible via hooks -- All validation logic runs through WordPress filters, allowing any plugin to add, modify, or override checks.
Checks are registered with a flat function call, using a namespace field to attribute them to the registering plugin. A function_exists guard ensures integrating plugins work correctly whether or not the Validation API is active:
add_action( 'init', function() {
if ( ! function_exists( 'validation_api_register_block_check' ) ) {
return;
}
validation_api_register_block_check( 'core/image', [
'namespace' => 'my-content-rules',
'name' => 'alt_text',
'error_msg' => __( 'Images must have alt text.', 'my-plugin' ),
] );
} );Validate individual block attributes such as image alt text, heading content, button labels, or custom block fields. Checks are registered per block type.
PHP Registration:
validation_api_register_block_check( 'core/image', [
'namespace' => 'my-plugin',
'name' => 'alt_text',
'level' => 'error',
'description' => __( 'Ensures images have alt text for screen reader users.', 'my-plugin' ),
'error_msg' => __( 'Images must have alternative text for accessibility.', 'my-plugin' ),
'warning_msg' => __( 'Alternative text is recommended for images.', 'my-plugin' ),
] );JavaScript Validation:
import { addFilter } from '@wordpress/hooks';
addFilter(
'editor.validateBlock',
'my-plugin/image-alt-text',
( isValid, blockType, attributes, checkName ) => {
if ( blockType !== 'core/image' || checkName !== 'alt_text' ) {
return isValid;
}
if ( ! attributes.url ) {
return true; // No image selected yet
}
return !! ( attributes.alt && attributes.alt.trim() );
}
);Validate WordPress post meta fields with real-time client-side feedback. The meta field is registered via register_post_meta() as usual, and the validation check is registered through the Validation API:
PHP Registration:
validation_api_register_meta_check( 'post', [
'namespace' => 'my-plugin',
'name' => 'required',
'meta_key' => 'seo_description',
'level' => 'error',
'description' => __( 'SEO description is required for all posts.', 'my-plugin' ),
'error_msg' => __( 'SEO description is required.', 'my-plugin' ),
'warning_msg' => __( 'Consider adding an SEO description.', 'my-plugin' ),
] );JavaScript Validation:
addFilter(
'editor.validateMeta',
'my-plugin/seo-description',
( isValid, value, postType, metaKey, checkName ) => {
if ( metaKey !== 'seo_description' || checkName !== 'required' ) {
return isValid;
}
return !! ( value && value.trim() );
}
);For server-side enforcement on save, use WordPress's native validate_callback parameter on register_post_meta(). The Validation API handles the client-side UX; server-side enforcement is a separate, complementary concern:
register_post_meta( 'post', 'seo_description', [
'single' => true,
'type' => 'string',
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => static function ( $value ) {
if ( empty( trim( (string) $value ) ) ) {
return new WP_Error(
'seo_description_required',
__( 'SEO description is required.', 'my-plugin' ),
[ 'status' => 400 ]
);
}
return true;
},
] );Validate the overall editor state: block order, document structure, required elements, and cross-block relationships. Checks are registered per post type.
PHP Registration:
validation_api_register_editor_check( 'post', [
'namespace' => 'my-plugin',
'name' => 'first_block_heading',
'level' => 'warning',
'description' => __( 'Ensures content begins with a heading for structure.', 'my-plugin' ),
'error_msg' => __( 'Posts must start with a heading block.', 'my-plugin' ),
'warning_msg' => __( 'Posts should start with a heading block.', 'my-plugin' ),
] );JavaScript Validation:
addFilter(
'editor.validateEditor',
'my-plugin/first-block-heading',
( isValid, blocks, postType, checkName ) => {
if ( checkName !== 'first_block_heading' ) {
return isValid;
}
if ( blocks.length === 0 ) {
return true;
}
return blocks[ 0 ].name === 'core/heading';
}
);| Level | Behavior | Use Case |
|---|---|---|
error |
Prevents saving/publishing | Critical accessibility or data integrity issues |
warning |
Shows feedback, allows saving | Recommendations and best practices |
none |
Check is disabled, skipped entirely | Temporarily or permanently inactive checks |
When level is omitted, it defaults to error.
Every active check passes through a filter that allows any plugin to override its severity at runtime:
apply_filters(
'validation_api_check_level',
$registered_level,
$context // [ 'scope' => 'block', 'block_type' => 'core/image', 'check_name' => 'alt_text' ]
);This means the framework has no storage opinions. The filter is the settings mechanism. A companion plugin can hook in and read from wp_options; an enterprise plugin can read from a remote API; a multisite network can enforce overrides globally. The core framework just fires the filter.
When any check fails at the error level, the API uses lockPostSaving to prevent publication and provides clear messaging about what needs to be resolved.
Validation results are managed through a dedicated @wordpress/data store. A single useValidationSync hook computes all validation and dispatches results to the store. All other consumers read from the store -- no duplicate computation.
Store structure:
{
blocks: [], // Invalid block results
meta: [], // Invalid meta results
editor: [], // Invalid editor check issues
blockValidation: {} // Per-block state: { [clientId]: { mode, issues } }
}Key selectors:
getInvalidBlocks()-- All blocks with validation failuresgetInvalidMeta()-- All meta fields with validation failuresgetInvalidEditorChecks()-- All editor check failuresgetBlockValidation( clientId )-- Validation state for a specific blockhasErrors()-- Whether any error-level failures existhasWarnings()-- Whether warning-level failures exist (and no errors)
This architecture separates concerns cleanly: useValidationSync handles computation, useValidationLifecycle handles side effects (save-locking, body CSS classes), and UI components like ValidationSidebar handle display. A reactive save-lock (lockPostSaving) is layered with an async safety net (editor.preSavePost throws if errors exist at save time) for defense in depth.
The framework provides standardized UI patterns:
- Block-level indicators -- CSS classes (
validation-api-block-error,validation-api-block-warning) applied to block wrappers viaeditor.BlockListBlock, plus a toolbar button with issue details viaeditor.BlockEdit - Consolidated sidebar -- A unified panel listing all validation issues across blocks, meta, and editor checks, with click-to-select for block issues
- Debounced per-block validation -- Validation runs are debounced (300ms default) to prevent performance issues during rapid editing
The API detects the current editor context and loads validation only where appropriate:
| Context | Validation Active | Details |
|---|---|---|
| Post editor | Yes | Standard post/page editing |
| Post editor with template | Yes | Validates content blocks within core/post-content only |
| Site editor | No | Template/global styles editing -- excluded |
The Validation API plugin demonstrates this framework. Key implementation details:
- PHP Registries — Singleton registries (
Block\Registry,Meta\Registry,Editor\Registry) extending a sharedAbstractRegistrybase class. The abstract holds defaults, required-field validation, namespace stamping, priority sort, andvalidation_api_check_levelfilter application; the concrete registries differ only in storage shape and scope-specific hook names. @wordpress/dataStore — A dedicated Redux store (core/validation) centralizes all validation state with actions, selectors, and a reducer.useValidationSynchook — Single computation point. CallsuseInvalidBlocks/useInvalidMeta/useInvalidEditorChecks, dispatches the results to the store.useValidationLifecyclehook — Manages editor-wide side effects:lockPostSaving/unlockPostSaving,lockPostAutosaving/unlockPostAutosaving,disablePublishSidebar/enablePublishSidebar, and body CSS classes.- Save-time gate — The
editor.preSavePostasync filter is subscribed as a belt-and-suspenders safety net on top oflockPostSaving: if errors exist at save time the filter throws, aborting the save. - JavaScript Validation — Validation logic runs entirely in JavaScript via WordPress filters (
editor.validateBlock,editor.validateMeta,editor.validateEditor). - Configuration Export — PHP configuration is passed to JavaScript via the
block_editor_settings_allfilter, delivering validation rules and editor context through editor settings. - Plugin Attribution — The
namespacefield in check registration args attributes checks to the registering plugin, enabling organized settings and REST API attribution.
Note on terminology: the two hooks above are called from small renderless sibling wrappers (
ValidationSync,ValidationLifecycle) under the rootValidationPlugincomponent. The sibling arrangement is deliberate — putting both hooks in a single parent component causes an infinite render loop becauseuseValidationLifecyclesubscribes to the store thatuseValidationSyncdispatches to. See docs/technical/README.md for the full explanation.
A read-only endpoint exposes all registered checks grouped by scope (block, meta, editor). This enables admin tooling and companion packages to read the validation configuration without parsing PHP internals. The reference plugin currently exposes this at GET /wp-validation/v1/checks; the final namespace in core is TBD during PR review (candidates: wp/v2/validation-checks, wp-block-editor/v1/validation-checks).
Plugins register checks using validation_api_register_block_check() (and related functions) with a function_exists guard, and validation logic is added via JavaScript filters:
- Integration Example Plugin -- A complete example demonstrating block, meta, and editor checks.
The validation-api-settings companion plugin provides an admin settings page built on WordPress DataForm. It reads registered checks from the REST API and lets admins override severity levels globally via the validation_api_check_level filter.
This separation is intentional: the core framework has no settings UI and no storage, making it suitable for core merge. The companion stays in plugin-land.
The proposal is specifically for the Validation API framework -- the infrastructure that enables declarative content validation.
Included:
- Check registration system (PHP registries for block, meta, and editor checks)
- Global registration functions (
validation_api_register_block_check(),validation_api_register_meta_check(),validation_api_register_editor_check()) - JavaScript validation filter hooks (
editor.validateBlock,editor.validateMeta,editor.validateEditor) - Dedicated
@wordpress/datastore (core/validation) for validation state - Severity model with runtime override via
validation_api_check_levelfilter - Validation result model (severity levels, issue reporting, standardized result objects)
- Post-locking integration (automatic
lockPostSavingbased on error-level failures) - Standardized UI components (block indicators, validation sidebar, toolbar button)
- Editor context scoping (post editor only, content blocks within templates)
- PHP action hooks for lifecycle events (
validation_api_initialized,validation_api_ready,validation_api_editor_checks_ready) - PHP filter hooks for check modification (
validation_api_check_args,validation_api_should_register_check,validation_api_check_level) - REST API endpoint for admin tooling (plugin exposes at
GET /wp-validation/v1/checks; final namespace in core TBD)
Not included (remains in plugin territory):
- Specific validation checks for core blocks (alt text, heading hierarchy, link text, etc.)
- Admin settings page for configuring check severity
- Any particular accessibility or content quality rules
- Opinionated defaults about what content should or should not be validated
The distinction is important: the API provides the capability to validate; plugins and themes provide the rules.
- Async validation -- Should the filter hooks support async validation for server-side checks (e.g., link checking, content analysis)?
- Block.json integration -- Could validation rules be declared in
block.jsonfor simple checks (e.g.,"required": trueon attributes), with JavaScript filters for complex logic?
- Gather feedback from the Gutenberg team and broader WordPress community
- Evaluate whether the API should live in Gutenberg (as a package) or in WordPress core
- Define a formal API specification based on the patterns proven in the reference implementation
- Develop a prototype within Gutenberg for testing and iteration
- Plugin: Validation API on GitHub
- Example Integration: validation-api-integration-example
- Companion Settings: validation-api-settings
- Developer Documentation: docs/guide/README.md
- Technical Reference: docs/technical/README.md