3 min read

Governance: Enforcing Block Templates for Custom Post Types

In a Block Theme, the global site layout (Header, Footer, Sidebar) is controlled by HTML files in the templates/ folder.

However, you often want to control the Content Area itself. For example, ensuring every "Case Study" starts with a specific Cover Image and Heading structure, and preventing the client from deleting them, effectively turning the Block Editor into a structured data form.

This is done via PHP when registering the Custom Post Type (CPT).

1. The template Argument

When you register a CPT using register_post_type(), you can pass a template array. This defines the initial blocks that appear when a user clicks "Add New".

'template' => array(
    array( 'core/cover', array( 'overlayColor' => 'black' ) ),
    array( 'core/heading', array( 'placeholder' => 'Case Study Title' ) ),
    array( 'core/paragraph', array( 'placeholder' => 'Write the summary here...' ) ),
),

2. The template_lock Argument

By default, the template just provides a starting point. Users can delete the blocks. To enforce structure, use template_lock.

  • 'all': Strict. Users cannot add new blocks, remove blocks, or reorder blocks. They can only edit the content/attributes of the existing blocks.
  • 'insert': Users cannot add new blocks or remove existing ones, but they can move them (rarely used).
  • false: (Default) No locking.

3. Real World Example: "Team Member" CPT

Let's create a "Team Member" CPT where the layout is strictly locked:

  1. Image: (Locked)
  2. Name: (Heading, Locked)
  3. Bio: (Paragraph, Locked)
  4. Social Links: (Social Icons, Locked)
function my_register_team_cpt() {
    $args = array(
        'label'        => 'Team',
        'public'       => true,
        'show_in_rest' => true, // Required for Block Editor
        'supports'     => array( 'title', 'editor', 'thumbnail' ), // 'editor' is required

        // Define the fixed block structure
        'template' => array(
            array( 'core/image', array(
                'align' => 'center',
            ) ),
            array( 'core/heading', array( 
                'placeholder' => 'Full Name',
                'level'       => 2,
                'textAlign'   => 'center'
            ) ),
            array( 'core/paragraph', array( 
                'placeholder' => 'Job Title & Bio...',
                'align'       => 'center'
            ) ),
            array( 'core/social-links', array( 'layout' => array( 'type' => 'flex', 'justifyContent' => 'center' ) ), array(
                array( 'core/social-link', array( 'service' => 'linkedin' ) ),
                array( 'core/social-link', array( 'service' => 'twitter' ) ),
            ) ),
        ),

        // Lock the entire container
        'template_lock' => 'all', 
    );
    register_post_type( 'team_member', $args );
}
add_action( 'init', 'my_register_team_cpt' );

4. Nested Templates (The "Container" Strategy)

If you want a locked "Hero" section at the top, but want to allow free-form editing below it, you cannot use template_lock => 'all' on the CPT itself, as that locks the entire post.

The Solution: Use a Group block as a container in your template, and lock that specific block using nested locking logic.

'template' => array(
    // 1. Locked Hero Section (Group Block)
    array( 'core/group', array( 
        'tagName' => 'section',
        'templateLock' => 'all', // Locks children inside this group
        'lock' => array( 'remove' => true, 'move' => true ) // Prevents deleting this group
    ), array(
        array( 'core/heading', array( 'content' => 'Mandatory Header' ) ),
    ) ),

    // 2. Free editing area (A paragraph to start)
    array( 'core/paragraph', array( 'placeholder' => 'Add free-form content here...' ) ),
),
'template_lock' => false, // Allow adding blocks at the root level (below the hero)