How to Restrict Blocks to Specific Page Templates in the WordPress Gutenberg Editor

Note: The link to the full example is here. It’s a small plugin so feel free to download it, install it, and use it as you follow along with this guide.

Why is this necessary?

The Gutenberg editor in WordPress allows infinite possibilities for developers to provide custom block solutions for content editors. At some point though all of that flexibility can turn into an issue when specific templates require specific types of blocks. You can end up with a situation where editors create a page, choose a template, spend an hour creating the layout for it with blocks, only to find out one or more of the blocks they used don’t actually belong to that template type. Not only can this lead to user frustration, it can also lead to a lot of support requests asking, “Why does this page not look right?”.

For our in-house Apollo 2 theme, our team solved this issue by actually limiting the available blocks in the editor based on what template was currently selected by the editor. We have five different templates to choose from with some blocks, like our “Landing Page Hero” block, that need to only show as an option when the appropriate template, in this case, “Landing Page”, is selected.

Gutenberg doesn’t provide a way to handle this situation out of the box yet, but luckily can implement it using Gutenberg’s data modules api.

Getting Started

For this tutorial we are going to pretend like we have a WordPress theme with a template called “Super Cool” and that our bosses asked us to restrict the core/code block to only be available on the “Super Cool” template.

Our plan for adding our restrictions fall into two segments:

  1. Hiding/Showing blocks based on the template selected:
    • Listen for when a template updates.
    • When it does, check to see which blocks don’t belong to the current template and which ones do.
    • Remove/add the appropriate blocks.
  2. Hiding/Showing templates based on the blocks in the editor:
    • Listen for when restricted blocks are added to the editor.
    • When they are, update the editor settings to only show the relevant templates under the “Page Attributes” setting.

Before we dive in, if you’re not familiar with Gutenberg’s data modules api, I recommend getting familiar with it to have a base understanding of what’s happening the rest of the tutorial.

Note: When you enqueue your block editor script, make sure you set wp-blocks and wp-editor as dependencies of your script. Otherwise you may not have access to the necessary libraries within the wp object. You’ll also need to make sure you enqueue this in the footer.

wp_enqueue_script('your-script', '/path/to/script', ['wp-blocks', 'wp-editor'], '1.0', true);

Note: I’ll be using es6 syntax here, so if you’re following along make sure to use babel. Specifically make sure you have the plugin-proposal-class-properties enabled. Of course you can also just write this using a different syntax.

Hiding/Showing Blocks

To get started we need is to listen to the redux store for when our template is selected. Let’s start by setting up our entry point for when the editor loads and create a class that will handle all of the logic for restricting blocks.

/** main.js **/
import BlockRestrictor from './BlockRestrictor'

/*
 * Add a mapping of block names and what templates they are
 * restricted to. Notice here we're referencing the template
 * file name. 
 */
const blockTemplateRestrictions = {
  'core/code': [
    'template-super-cool.php',
  ],
}

wp.domReady(() => {
  const restrictor = new BlockRestrictor(blockTemplateRestrictions)

  restrictor.run()
})
/** BlockRestrictor.js **/
const { data } = window.wp
const { select, dispatch, subscribe } = data
const { getEditedPostAttribute } = select('core/editor')
const { isTyping } = select('core/block-editor')
const { getBlockType } = select('core/blocks')
const { addBlockTypes, removeBlockTypes } = dispatch('core/blocks')

class BlockRestrictor {
 /**
  * Defines the map of block types and the templates they are restricted to.
  *
  * @type {object}
  */
  blockTemplateRestrictions = {}

  /**
   * Currently selected template.
   * @type {string}
   */
  currentTemplate = ''

 /**
   * Map block names to the actual block object.
   *
   * @type {object}
   */
  unregisteredBlocks = {}

  constructor(blockTemplateRestrictions) {
    this.blockTemplateRestrictions = blockTemplateRestrictions
  }

  /**
   * Initiates listening to the redux store for when a restricted block is either
   * added or removed.
   */
  run() {
    this.currentTemplate = getEditedPostAttribute('template') || 'default'
    
    // @TODO: Add way to actually restrict blocks here

  /**
   * subscribe fires whenever the redux store in gutenberg updates
   */
    subscribe(() => {
   
   /**
     * ensure we don't run our logic when the user is typing.
     */
      if (isTyping() === true) {
        return false
      }
			
      const newTemplate = getEditedPostAttribute('template') || 'default'

      if (this.currentTemplate !== newTemplate) {
        this.currentTemplate = newTemplate
        // @TODO: Add way to actually restrict blocks here
      }
    })
  }
  // @TODO: Add functions to restrict blocks.
}

export default BlockRestrictor

Right now our restrictor doesn’t do much. All it does is listen to the store to see if the template changes or not. Let’s change that by adding the logic necessary to start restricting blocks.

First, we need a way to figure out which blocks to add and remove. When you go to create a new page, and the default template is activated, we don’t want the code block to show up as an option. However, when a user switches from the default template to “Super Cool” template, we need to add the code block back. Then if the editor changes their mind and decides to go back to default…well we need to remove the code block again. Let’s add a method to our class to figure that out for us.

/**
 * Helps decide which blocks we actually want to add or remove from
 * the store.
 */
templateBlockRegistry() {
  let blocksToAdd = []
  let blocksToRemove = []

  Object.keys(this.blockTemplateRestrictions).forEach((blockName) => {
    if (this.blockTemplateRestrictions[blockName].includes(this.currentTemplate)) {
      blocksToAdd.push(blockName)
    } else {
      blocksToRemove.push(blockName)
    }
  })

  return {
    blocksToAdd,
    blocksToRemove,
  }
}

Great, now that we have a way to see what blocks need to be added or removed from the editor, we need to communicate that information with Gutenberg’s redux store. Let’s add the following method to our class to handle this.

/**
 * Either removes or adds blocks to the store based on what the current
 * template is. 
 */
restrictBlocksToTemplate() {
  const { blocksToAdd, blocksToRemove } = this.templateBlockRegistry()

  if (blocksToRemove.length) {
    blocksToRemove.forEach((blockName) => {
      const blockExists = getBlockType(blockName)
      const isRegistered = typeof this.unregisteredBlocks[blockName] === 'undefined'
      if (blockExists && isRegistered) {
        this.unregisteredBlocks[blockName] = getBlockType(blockName)
      }
    })
    removeBlockTypes(Object.keys(this.unregisteredBlocks))
  }

  if (blocksToAdd.length) {
    let registeredBlocks = []
    blocksToAdd.forEach(blockName => {
      const blockExists = typeof getBlockType(blockName) === 'undefined'
      const isUnregistered = typeof this.unregisteredBlocks[blockName] !== 'undefined'
      
      if (blockExists && isUnregistered) {
        registeredBlocks.push(this.unregisteredBlocks[blockName])
        delete this.unregisteredBlocks[blockName]
      }
    })
    addBlockTypes(registeredBlocks)
  }
}

This method looks like a lot, but all it’s doing is looping over the blocks we need to add or remove, checking to see those blocks exist, and then communicating with the redux store to add or remove them.

Now all we need to to do is hook up our logic in our run function.

run() {
  this.currentTemplate = getEditedPostAttribute('template') || 'default'
    
  this.restrictBlocksToTemplate()

  subscribe(() => {
    if (isTyping() === true) {
      return false
    }

    const newTemplate = getEditedPostAttribute('template') || 'default'

    if (this.currentTemplate !== newTemplate) {
      this.currentTemplate = newTemplate
      this.restrictBlocksToTemplate()
    }
  })
}

If you’ve been following along, when going to create a new page, you should notice the code block isn’t available. However, if you switch templates to Super Cool Template, the code block is available again!

Whitelisting Templates

So we can restrict blocks now, but what happens when you add a restricted block to “Super Cool” template and then try to switch templates? Well as it is now, you’ll get an error because Gutenberg is trying to reload a non existing block. So what we need to do now is actually limit the available templates to only those that support the blocks currently in the editor.

First, let’s create a new class to handle whitelisting templates and hook it up to our entry file.

import BlockRestrictor from './BlockRestrictor'
import TemplateWhitelister from './TemplateWhitelister'

/**
 * Defines the map of block types and the templates they are restricted to.
 *
 * @type {object}
 */
const blockTemplateRestrictions = {
  'core/code': [
    'template-super-cool.blade.php',
  ],
}

wp.domReady(() => {
  const restrictor = new BlockRestrictor(blockTemplateRestrictions)
  const templateWhitelister = new TemplateWhitelister(blockTemplateRestrictions)

  restrictor.run()
  templateWhitelister.run()
})

import pick from 'lodash/pick'
import intersection from 'lodash/intersection'

const { data } = window.wp
const { select, dispatch, subscribe } = data
const { isTyping, getBlocks } = select('core/block-editor')
const { getBlockType } = select('core/blocks')
const { updateEditorSettings } = dispatch('core/editor')

class TemplateWhitelister {
  /**
   * Defines the map of block types and the templates they are restricted to.
   *
   * @type {object}
   */
   blockTemplateRestrictions = {}

  /**
  * Restricted Blocks currently in the editor
  *
  * @type {array}
  */
  currentRestrictedBlocks = []

  /**
   * Page Templates loaded with the page.
   *
   * @type {object}
   */
  defaultPageTemplates = {}
  
  constructor(restrictedBlocks) {
    this.defaultPageTemplates = select('core/editor').getEditorSettings().availableTemplates
    this.blockTemplateRestrictions = restrictedBlocks
  }
  
  /**
   * Initiates watching the redux store for a template change.
   */
  run() {
    // @TODO loop over blocks currently in the editor, and if we find blocks that
    // are restricted, only show templates in the "Page Template" selector that
    // support the current blocks
    subscribe(() => {
      if (isTyping() === true) {
        return false
      }
      // @TODO same as above
    })
  }
}

export default TemplateWhitelister

In our TemplateWhitelister class we need a way to scan the blocks currently in the editor and then check if any of them are restricted and what templates they’re restricted to. Let’s add a method to our class to handle figuring this out for us.

/**
 * Recursively checks the editor to see if there are any blocks that are restricted to 
 * specific templates.
 * 
 * @param {array} blocks 
 * @return {object}
 */
checkForRestrictedBlocks(blocks) {
  let foundTemplates = []
  let foundBlocks = []

  blocks.forEach(block => {
    if (typeof this.blockTemplateRestrictions[block.name] !== 'undefined') {
      foundTemplates.push(this.blockTemplateRestrictions[block.name])
      if (!foundBlocks.includes(block.name)) {
        foundBlocks.push(block.name)
      }
    }

    if (block.innerBlocks.length > 0) {
      const { templates, restrictedBlocks } = this.checkForRestrictedBlocks(block.innerBlocks)
      
      if (templates.length > 0) {
        foundTemplates.push(templates)
      }

      restrictedBlocks.forEach(blockName => {
        if (!foundBlocks.includes(blockName)) {
          foundBlocks.push(blockName)
        }
      })
    }
  })

  return {
    templates: intersection(...foundTemplates),
    restrictedBlocks: foundBlocks,
  } 
}

Next, now that we have what templates we want to allow, we need to add a method that communicates with the redux store so that only those templates show in the “Page Templates” select field.

/**
 * Updates the available templates in the editor. Ensures editors can only select
 * templates that allow blocks currently being used.
 * 
 * @param {array} templates 
 */
updateWhitelistedTemplates(templates) {
  if (templates.length > 0) {
    updateEditorSettings({ availableTemplates: pick(this.defaultPageTemplates, templates) })
  } else {
    updateEditorSettings({ availableTemplates: this.defaultPageTemplates })
  }
}

Finally, let’s hook these methods up in our run method.

run() {
  const blocks = getBlocks()
    const { templates, restrictedBlocks } = this.checkForRestrictedBlocks(blocks)
    this.currentRestrictedBlocks = restrictedBlocks

    this.updateWhitelistedTemplates(templates)

    subscribe(() => {
      if (isTyping() === true) {
        return false
      }

      const blocks = getBlocks()
      const { templates, restrictedBlocks } = this.checkForRestrictedBlocks(blocks)

      if (restrictedBlocks.length !== this.currentRestrictedBlocks.length) {
        this.currentRestrictedBlocks = restrictedBlocks
        this.updateWhitelistedTemplates(templates)
      }
    })
 }

Conclusion

Congrats! You now have a way to restrict blocks to specific templates. There’s other features you can add here like a notice listing the restricted blocks or explaining why some templates aren’t available. Hopefully the Gutenberg team will add a way to do all of this with a filter but for now this is a great way to simply the editing experience for your users.