WordPress Collaboration: Block Locking, Patterns Library, and Editorial Workflow

WordPress 6.4+ provides a complete set of tools for managing content contributor workflows: block locking (prevent specific blocks from being moved or deleted), a curated synced-pattern library (reusable components that only editors can modify), and post_status-based editorial stages. Combining these three features creates a structured content workflow without a third-party editorial plugin.

Problem: A WordPress editorial team working in the block editor accidentally overwrites each other's content — there is no block-level locking mechanism, and the patterns library and template library are not coordinated across the team.

Solution: Use Block Locking (introduced in WordPress 5.9) — set "lock": {"move": true, "remove": true} in block metadata or via the block editor's lock controls to prevent non-admin users from moving or deleting specific blocks. Manage shared content through the Patterns library (wp_block post type) and restrict pattern editing with the edit_posts capability check on the patterns REST endpoint.


The code below locks blocks programmatically on the server side, registers a custom post status for an editorial review stage, restricts pattern editing to editors, and adds a custom workflow status to the block editor toolbar via a JavaScript plugin.


 _x( 'In Review', 'post status', 'my-plugin' ),
        'public'                    => false,
        'exclude_from_search'       => true,
        'show_in_admin_all_list'    => true,
        'show_in_admin_status_list' => true,
        'label_count'               => _n_noop( 'In Review (%s)',
                                                'In Review (%s)', 'my-plugin' ),
    ] );
} );

// 2. Restrict who can edit synced patterns (reusable blocks)
add_filter( 'block_editor_settings_all', function ( array $settings, WP_Block_Editor_Context $ctx ): array {
    if ( ! current_user_can( 'edit_others_posts' ) ) {
        // Hide synced pattern editing for contributors/authors
        $settings['__experimentalBlockPatternCategories'] = array_filter(
            $settings['__experimentalBlockPatternCategories'] ?? [],
            fn( $cat ) => $cat['name'] !== 'reusable'
        );
    }
    return $settings;
}, 10, 2 );

// 3. Lock blocks in a template via filter (programmatic lock)
add_filter( 'render_block_data', function ( array $block ): array {
    // Lock all core/buttons blocks on the homepage from being removed
    if ( is_front_page() && $block['blockName'] === 'core/buttons' ) {
        $block['attrs']['lock'] = [
            'move'   => true,
            'remove' => true,
        ];
    }
    return $block;
} );

// 4. Add "In Review" status to post status dropdown in editor
add_action( 'enqueue_block_editor_assets', function () {
    wp_enqueue_script(
        'my-plugin-review-status',
        plugin_dir_url( __FILE__ ) . 'js/review-status.js',
        [ 'wp-plugins', 'wp-edit-post', 'wp-element', 'wp-components', 'wp-data', 'wp-i18n' ],
        '1.0.0',
        true
    );
} );


// js/review-status.js — add "In Review" to the block editor status selector
const { registerPlugin }        = wp.plugins;
const { PluginPostStatusInfo }  = wp.editPost;
const { SelectControl }         = wp.components;
const { withSelect, withDispatch, compose } = wp.data;
const { __ }                    = wp.i18n;

const ReviewStatusPanel = compose(
    withSelect( select => ( {
        status: select( 'core/editor' ).getEditedPostAttribute( 'status' ),
    } ) ),
    withDispatch( dispatch => ( {
        onStatusChange: status => dispatch( 'core/editor' ).editPost( { status } ),
    } ) )
)( ( { status, onStatusChange } ) =>
    wp.element.createElement( PluginPostStatusInfo, null,
        wp.element.createElement( SelectControl, {
            label:    __( 'Review Stage', 'my-plugin' ),
            value:    status,
            options:  [
                { label: __( 'Draft',     'my-plugin' ), value: 'draft'     },
                { label: __( 'In Review', 'my-plugin' ), value: 'in-review' },
                { label: __( 'Published', 'my-plugin' ), value: 'publish'   },
            ],
            onChange: onStatusChange,
        } )
    )
);

registerPlugin( 'my-plugin-review-status', { render: ReviewStatusPanel } );


NOTE: Block locking set via render_block_data applies only at render time and does not persist in the post content — users with block editor access can still remove the lock by editing the block attributes directly in the code editor view; for content that must truly be immutable, use a server-side rendered dynamic block with no editable attributes instead of locking a static block.