Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/nadimtuhin/wp-gutenberg-a11y-enforcer/archive/refs/heads/main.zip _ ______ ____ ____
| | /| / / __ \ / __ \/ __ \
| |/ |/ / /_/ // /_/ / /_/ /
|__/|__/\____/ \____/ \____/
Gutenberg Enforcer
A WordPress plugin that enforces accessibility (a11y) rules on Gutenberg block content — both server-side (PHP, via a content_save_pre filter) and client-side (JavaScript, via the blocks.getSaveElement Gutenberg hook).
Features
| Layer | Mechanism | Effect |
|---|---|---|
| Backend (PHP) | content_save_pre filter |
Strips non-compliant blocks before they are persisted to the database |
| Frontend (JS) | blocks.getSaveElement Gutenberg filter |
Injects a sentinel element that triggers block validation failure in the editor, preventing save |
Accessibility rules enforced
core/image— must have a non-emptyaltattribute (WCAG 2.1 SC 1.1.1).
Additional rules can be added to Enforcer::validateBlock() (PHP) and getA11yViolations() (JS).
Architecture
wp-gutenberg-a11y-enforcer/
├── wp-gutenberg-a11y-enforcer.php ← Plugin entry point; registers WP hooks
├── includes/
│ └── enforcer.php ← PHP Enforcer class
├── assets/
│ └── js/
│ └── editor.js ← Gutenberg editor script (JS filter)
└── tests/
└── EnforcerTest.php ← PHPUnit test suite
Backend hook: content_save_pre
Enforcer::filterContent() is registered on content_save_pre. When a post is saved:
- WordPress passes the raw post content (serialised block HTML) through the filter.
parse_blocks()splits the content into individual block objects.- Each block is evaluated by
validateBlock(). - Blocks that fail validation are dropped; compliant blocks are re-serialised.
- The cleaned content is returned to WordPress and written to the database.
add_filter( 'content_save_pre', [ $enforcer, 'filterContent' ] );
Frontend hook: blocks.getSaveElement
assets/js/editor.js is enqueued via enqueue_block_editor_assets. It adds a filter on blocks.getSaveElement:
- For every block being serialised in the editor,
getA11yViolations()is called. - If violations are found, a hidden sentinel
<div>with adata-a11y-enforcer-violationattribute is appended to the save element. - The modified element differs from the stored markup, causing Gutenberg block validation failure (red border + "Attempt Block Recovery" prompt).
- A
console.warnmessage details the violation for developers.
addFilter(
'blocks.getSaveElement',
'wp-gutenberg-a11y-enforcer/enforce-a11y',
enforceA11yOnSave
);
Note: The JS layer is a soft guard — it surfaces violations inside the editor. The PHP layer is the hard guard — it removes non-compliant blocks before they reach the database, regardless of client behaviour.
Requirements
- PHP ≥ 7.4
- WordPress ≥ 5.8 (Gutenberg bundled)
- Composer (for development / testing)
Installation
As a WordPress plugin
-
Clone or download this repository into your
wp-content/plugins/directory:git clone https://github.com/nadimtuhin/wp-gutenberg-a11y-enforcer.git \ wp-content/plugins/wp-gutenberg-a11y-enforcer -
Activate the plugin from the WordPress admin dashboard or via WP-CLI:
wp plugin activate wp-gutenberg-a11y-enforcer
Development setup
cd wp-content/plugins/wp-gutenberg-a11y-enforcer
composer install
Running Tests
The test suite runs outside a WordPress installation using lightweight function stubs.
./vendor/bin/phpunit
Expected output:
PHPUnit 9.x by Sebastian Bergmann and contributors.
............ 12 / 12 (100%)
Time: ~0s, Memory: 6.00 MB
OK (12 tests, 15 assertions)
Test coverage
| Test | Description |
|---|---|
testBlockValidationPassesWithAlt |
core/image with alt → passes |
testBlockValidationFailsWithoutAlt |
core/image no alt → fails |
testBlockValidationFailsWithEmptyAlt |
core/image empty alt → fails |
testNonImageBlockAlwaysPasses |
Non-image block always passes |
testFilterContentStripsImageBlockMissingAlt |
filterContent removes bad image block |
testFilterContentKeepsImageBlockWithAlt |
filterContent keeps valid image block |
testFilterContentKeepsParagraphBlocks |
filterContent keeps unrelated blocks |
testFilterContentHandlesMixedBlocks |
Mixed content: strips bad, keeps good |
testFilterContentReturnsOriginalWhenParseBlocksMissing |
Safe when WP parser absent |
testSerializeBlocksProducesBlockComments |
Serialiser produces correct WP block comments |
testRegisterDoesNotThrow |
register() hooks cleanly |
testEnqueueEditorScriptDoesNotThrow |
Script enqueueing doesn't throw |
Extending
Add a new PHP rule
In includes/enforcer.php, extend validateBlock():
public function validateBlock( array $block ): bool {
if ( $block['blockName'] === 'core/image' && empty( $block['attrs']['alt'] ) ) {
return false;
}
// Example: require heading blocks to have non-empty content
if ( $block['blockName'] === 'core/heading' && empty( $block['innerHTML'] ) ) {
return false;
}
return true;
}
Add a new JS rule
In assets/js/editor.js, extend getA11yViolations():
function getA11yViolations( blockName, attributes ) {
const violations = [];
if ( blockName === 'core/image' && ! attributes.alt ) {
violations.push( 'core/image missing alt text.' );
}
// Example: warn on decorative images without explicit role
if ( blockName === 'core/image' && attributes.alt === '' && ! attributes.decorative ) {
violations.push( 'core/image with empty alt must mark the image as decorative.' );
}
return violations;
}
Contributing
- Fork the repo and create a feature branch.
- Write a failing test first (TDD).
- Implement the change.
- Run
./vendor/bin/phpunit— all tests must pass. - Open a pull request.
License
MIT