Tutorial: Convert shortcodes into blocks

WordPress 5.0 was released in late 2018, and it brought a new editor. It is a Block Editor, but it is better known as Gutenberg. And, the new editor introduced the concept of blocks, and the block is set to replace both widgets and shortcodes with more user-friendly controls.

The adoption of the concept of the blocks since then was on the rise, but still, many developers have problems with how the editor and blocks are implemented, based mostly on JavaScript (React). My work with WordPress is not really editor-oriented, so I did not need to learn too much about the blocks. I recently decided to embrace the blocks by starting to convert existing shortcodes (and later widgets) into blocks, considering that Block Editor will soon introduce full site editing and a new theming framework to go with that. It will be essential to have blocks replacing shortcodes to allow users to have more control over settings without experimenting with attributes.

The goals for the tutorial

While I still find the majority of the development of the block very complicated, documentation and example very lacking, the process of using server-side rendering and creating settings controls for blocks for the purpose of reusing existing shortcodes code is pretty straightforward and easy to implement, even if you don’t plan to invest a lot of time in learning blocks development ‘deeply’.

Before we go on, make sure to check out my previous article about getting started with block development. It explains what I needed to do, what I used to learn the process. And, the tutorial you are reading now is the product of my learning about the block’s development. This tutorial is barely scratching the surface of blocks development, it is made with one specific development case – shortcodes to blocks, and it covers few important goals:

  • Create two or more blocks in a single plugin
  • Add blocks that will reuse existing shortcodes code for rendering
  • Implement settings for blocks that will mimic the shortcodes attributes
  • Use ServerSideRendering component of the block editor
  • Use of some basic block components to build settings sidebar

What this tutorial doesn’t cover:

  • Shortcodes that are wrapping the content, for that you will need to have the block content editable
  • Implementing formatting toolbar for the block content

These things might be covered in future tutorials, and I plan to learn more about that when I get to the shortcodes that need more complex blocks implemented.

Also, I assume that you are a WordPress developer, have at least basic knowledge of using Git, have the Node and NPM installed, up to date and functional. If you need help with setting up Node/NPM and setting local WordPress environment, check out this WordPress.org article related to the block’s development. The Server Side Rendering library used for this is included from WordPress 5.3, so previous versions of WordPress can’t be supported.

Benefits of this approach

If your plugin is old, and the shortcodes it adds are in use on many websites, it is not simple to just replace shortcodes with blocks. It is best to have shortcodes function without any changes, add blocks that will use shortcodes rendering without duplicating the code, and reusing what is already there and working, and give your users a chance to maybe in the future start using blocks, and even replace shortcodes with blocks.

There is no need to rewrite most of the codebase that already works for the sake of blocks. Reuse what you have and give users the choice of using blocks instead of shortcodes. And, if your users don’t want to use blocks, fine, the shortcodes are still there and working. It is all about the choice.

The tutorial plugin

For this tutorial, I have prepared a plugin. This plugin implements 2 shortcodes, both of which have few attributes, and one CSS file is loaded to implement styling for both the shortcodes. So, the blocks we are going to make will use the same PHP code to render block content and will implement settings in the block editor sidebar to match shortcodes’ attributes.

The plugin uses PHP autoloader to load all required PHP classes, and everything is namespaced.

The plugin is available as a GitHub project: Shortcodes To Blocks. You can fork the project, and use it as a starting point for adding blocks to your own plugins converting shotcodes to blocks.

Included shortcodes

This plugin registers two shortcodes: Press Notice ([press-notice]) and Press Meta ([press-meta]). Both of them are defined in the class ‘Shortcodes’, and they have various attributes for colors, elements, text… Press Notice shortcode also has settings for colors, and they are applied using CSS variables.

Examples for both shortcodes

So, the class Shortcodes includes methods to render shortcodes based on the attributes, and these same methods we are going to use when we get to making the blocks. Plugin registers, and loads a CSS file with basic styling for both blocks. We will need to use this file for blocks too when blocks are loaded into the editor.

Getting started with the plugin

Get the plugin from GitHub: Shortcodes To Blocks.

The plugin contains everything you need, but, once you download it, you have to initialize it, and download all the dependencies needed for the build process. You need a local, working WordPress installation, and you need to get the project from GitHub inside the plugins directory. When you have that downloaded, you will have the plugins in the WordPress directory: \wp-content\plugins\shortcodes-to-blocks\. Everything is done after that is executed inside this directory using a command-line interface. Now, you need to run the first command, to install the package and download all the dependencies. Before you do that, here is the screenshot showing the downloaded Git project plugin and opened command prompt.

Plugin directory inside WordPress local installation and open command promp

So, the first thing you need to run is:

npm install

This will download all the dependencies (it will be over 450MB of packages) inside the new node_modules directory. Now, you are ready to see what is inside the plugin project, what things you can customize, and how it is all put together.

There are various NPM commands you should know when it comes to this package, and they are all listed on the package information page here. For the reference purpose, here is what you are going to use often:

# start the build for development and webpack watch process
npm start

# build production code
npm run build

# update packages to the latest versions
npm run packages-update

Plugin Structure

The plugin root directory contains few files you should not change if you don’t know exactly what you are doing (.editorconfig, .gitignore, .package-lock.json). But, there is one file that is the core of the @wordpress/create-block package. The file where you can make the first configuration for the build process is package.json. This file is a simple JSON file, and the first few lines are setting the project name, version, description, author license. You can change all that. I prefer not to touch anything else in there. The most important value in there is the ‘name’. I prefer setting this to be the namespace of all the blocks build. So, for this plugin, it is set to shortcodes-to-blocks and that will be the namespace for both blocks added later.

Next, we have a directory called src where the JavaScript source files for blocks are, and we have build directory where the NPM package will build our blocks JavaScript file from the source files (and what we are actually loading inside WordPress).

Finally, we need a PHP file where the blocks will be registered and loaded, and that is all inside the class named Blocks in the code\basic directory.

Blocks in JavaScript

Directory src contains source code for our blocks. Each block has its own directory, and you also have an index.js file that is loading both blocks. So, if you need to add more blocks, create a new directory for a new block, add an empty index.js file (it doesn’t have to be named that way, it can be anything else, but it is important to use that for linking) inside the new directory, and import that file in the main src\index.js directory.

Block Source

So, the main index file right now contains:

import './press-notice/index.js';
import './press-meta/index.js';

Add new import directive for any additional block you want to add. Main reason to use directory for each block is to better organize it’s files. This example here has every block in one file. But, for more complex blocks, you will need to have additional files to split parts of the code to make better organization.

Both of the blocks we use here look like this, with the attributes and edit not displayed here, so you can see full plugin structure for what we need.

import {registerBlockType} from '@wordpress/blocks';
import ServerSideRender from '@wordpress/server-side-render';
import {useBlockProps} from '@wordpress/block-editor';
import {PanelBody, TextControl, ToggleControl} from '@wordpress/components';
import {InspectorControls} from '@wordpress/editor';
import {__} from '@wordpress/i18n';

registerBlockType('shortcodes-to-blocks/press-notice', {
    apiVersion: 2,
    name: 'shortcodes-to-blocks/press-notice',
    title: __('Press Notice', 'shortcodes-to-blocks'),
    description: __('Display simple notice.', 'shortcodes-to-blocks'),
    icon: 'warning',
    supports: {
        customClassName: false,
    attributes: {  },
    edit: ({attributes, setAttributes}) => { },
    save: () => {
        return null

First part of this file is list of import directives. Lines 4 and 5 will contains list of different components used to generate sidebar, they are split into different packages (@wordpress/components and @wordpress/editor). You will add more components here, depending on what you use later in the edit section of the block.

Now, line 8 is where we register our block. Each block name has a ‘namespace’ part and the block name, so for this block we have ‘shortcodes-to-blocks’ as a namespace and ‘press-meta’ as a block name, so the block is registered as ‘shortcodes-to-blocks/press-meta’. Inside the registerBlockType function, the list of arguments contains various things, starting with apiVersion (optional), and again, the name of the block. This ‘name’ attribute is optional, I included it for the sake of completeness and later to compare with the PHP version of the code. After that, we have the title and description. Both of these use translation functions __(), the same functions that are used in PHP for WordPress translations. Translation textdomain for this plugin is also set to ‘shortcodes-to-blocks’. The icon can be one of the Dashicons, or it can be SVG.

Now, we have the ‘supports’ attribute, and for that one, we have disabled customClassName. Because the rendering for our blocks is done with the shortcode existing code, we are not using normal block rendering, and there is no point in having an additional class name to be set for the block. So, with this option disabled, the Advanced section for our block in the Sidebar will not have the ‘Additional CSS class(es)’ value.

Before we go through ‘attributes’ and ‘edit’, just a quick mention of the ‘save’. Save is set to return null, because we are using Server Side Rendering, and there is nothing for this function to do in this type of block.


Now, we have one of the most important elements of the new block code. Attributes are a list of all block settings with the type of attribute, default value, and additional elements depending on the type. To learn more about attributes, check out this article. Our block uses few types of attributes, here is the list:

press-notice/index.js – attributes
attributes: {
    'text': {
        'type': 'string',
        'default': 'This is just a notice.'
    'icon': {
        'type': 'string',
        'enum': [
        'default': 'warning'
    'class': {
        'type': 'string',
        'default': ''
    'varFontSize': {
        'type': 'integer',
        'default': 16,
        'minimum': 1
    'varLineHeight': {
        'type': 'string',
        'default': ''
    'varColorBackground': {
        'type': 'string',
        'format': 'hex-color',
        'default': ''
    'varColorBorder': {
        'type': 'string',
        'format': 'hex-color',
        'default': ''
    'varColorText': {
        'type': 'string',
        'format': 'hex-color',
        'default': ''

This list has to reflect all the shortcodes attributes you want to have available for editing in the block.


And, the most important element of the new block code, is the edit function that is used to render block in the editor. Before we analyze it, here is the full content of the Edit function.

press-notice/index.js – edit
edit: ({attributes, setAttributes}) => {
    const fontSizes = [
            name: __('Tiny', 'shortcodes-to-blocks'),
            slug: 'small',
            size: 10,
            name: __('Small', 'shortcodes-to-blocks'),
            slug: 'small',
            size: 14,
            name: __('Normal', 'shortcodes-to-blocks'),
            slug: 'normal',
            size: 16,
            name: __('Big', 'shortcodes-to-blocks'),
            slug: 'big',
            size: 20,

    return (
        <div {...useBlockProps()}>
            <InspectorControls key='settings'>
                <PanelBody title={__('Notice', 'shortcodes-to-blocks')}>
                        label={__('Notice Text', 'shortcodes-to-blocks')}
                        onChange={(value) => setAttributes({text: value})}
                        label={__('Notice Icon', 'shortcodes-to-blocks')}
                            {label: __('Warning'), value: 'warning'},
                            {label: __('Info'), value: 'info'},
                            {label: __('Yes'), value: 'yes-alt'},
                            {label: __('Star'), value: 'star-filled'},
                            {label: __('Flag'), value: 'flag'}
                        onChange={(value) => setAttributes({icon: value})}
                <PanelBody title={__('Typography', 'shortcodes-to-blocks')}>
                        label={__('Font Size', 'shortcodes-to-blocks')}
                        onChange={(value) => setAttributes({varFontSize: value})}
                        label={__('Line Height', 'shortcodes-to-blocks')}
                        onChange={(value) => setAttributes({varLineHeight: value})}
                <PanelBody title={__('Colors', 'shortcodes-to-blocks')}>
                        label={__('Background', 'shortcodes-to-blocks')}
                        onChange={(value) => setAttributes({varColorBackground: value})}
                        label={__('Text', 'shortcodes-to-blocks')}
                        onChange={(value) => setAttributes({varColorText: value})}
                        label={__('Border', 'shortcodes-to-blocks')}
                        onChange={(value) => setAttributes({varColorBorder: value})}
                <PanelBody title={__('Advanced', 'shortcodes-to-blocks')}>
                        label={__('Additional CSS Class', 'shortcodes-to-blocks')}
                        onChange={(value) => setAttributes({class: value})}

This is essential part of the block, and it defines the visual elements for the block inside the editor. In many cases, this function is usually defined in own file (edit.js), and imported into main file. I have decided against that for these types of blocks, where the edit part is practically the only thing we need, and it can be inside the main file.

The top of the function defines the list of values to use for font size. You can define lists where they are used, or you can do it this way, and have the lists on top of the edit. This code uses one more list, but that one is defined in the place of use. The ‘return’ for the edit is what actually displays the block. This has two elements, one is a component for Server Side Rendering and the other is Inspector Controls.

The ServerSideRendering component is the core of this block, and it is what the block uses to render its contents, by calling existing PHP code on the server-side. This component requires the name of the block (this has to match the actual full block name) and all the attributes for the block. As for the InspectorControls, we are using PanelBody components, each one creating a collapsable section inside the sidebar that can have a title, and it can hold multiple controls. The most commonly used controls are Select, Text, and Toggle, but you will often need ColorPalette, FontSizePicker, and LineHeight. Two example blocks in this plugin, show most of these basic controls.

Component overview

Each individual component (or control) used here follows a similar structure. The component has a label, value and it handles onChange event. Some components have additional elements, and you should start with the Components Reference to get more information about every available one. I can also recommend this website for more examples and components guides.

For every component, value is set based on the attribute value, and the onChange event takes modified value and stores that value in the attributes lists for the active block.

JavaScript compiling

The code for each block is actually quite useless when it comes to using the code as is, and it has to be built into usable JavaScript code for browsers to execute. To do that, there are two main methods. One is to start the main package @wordpress/create-block and have it track any changes to the source files, and compile debug version of the code of testing during development. To do that, inside the plugin directory, via command line run this command:

npm start

This will start the Webpack watch mode, where the Webpack (package used to actually compile the React code into usable JavaScript), and it will look like this:

Webpack listening for changes

Anytime you make changes to files in src directory, Webpack will detect the changes, and compile new version of the code and place it in the build directory.

When you finish with the work, click CTRL+C to abort the watcher, and you can then run the main build process to produce the final version of the JavaScript files (and other files, if needed). To do that, run this command:

npm run build

For our plugin, since we don’t include any additional CSS files for the blocks, we will get one JavaScript and one PHP file in the build directory. All the blocks will be compiled into build\index.js file and library dependencies will be in the build\index.assets.php file.

Blocks in PHP

Now that we have our blocks in JavaScript, it is time to add the PHP part. This involves the registration of blocks in PHP, and for this type of block, define render callbacks for the server-side rendering. Also, you need to set up the loading of the compiled JavaScript file and few other things. All the code for this plugin is in core\basic\Blocks.php file inside the Blocks class.

This class includes the run() method that starts the initialization process by adding a filter and an action:

private function run() {
	add_action( 'init', array( $this, 'blocks' ), 30 );
	add_filter( 'block_categories', array( $this, 'categories' ) );

Block Categories

On init action, we are going to add the code for blocks and other stuff. Block categories filter is used to add new categories for blocks. By default, all new blocks are added to the ‘widgets’ category, but you can add new categories and place your blocks in these. For this example, we are adding a new category:

public function categories( array $categories ) : array {
	return array_merge(
				'slug'  => 'shortcodesblocks',
				'title' => __( 'ShortcodesBlocks', 'shortcodes-to-blocks' )

Register JavaScript

Now, inside the method ‘blocks’ hooked to the ‘init’ action, we are first calling method to register blocks script.

private function _register_script() {
	/* load generated asset file to get JavaScript dependencies and various */
	$asset_file = include( SHORTCODES_TO_BLOCKS_PATH . 'build/index.asset.php' );

	/* register generated blocks JavaScript file using previously loaded assets */
	wp_register_script( 'shortcodes-to-blocks-editor', SHORTCODES_TO_BLOCKS_URL . 'build/index.js', $asset_file['dependencies'], $asset_file['version'] );

	/* set the translations domain and link it to the blocks script */
	wp_set_script_translations( 'shortcodes-to-blocks-editor', 'shortcodes-to-blocks' );

This method loads the index.asset.php file into the $asset_file array. This will contain two elements with list of JavaScript dependencies (making sure that Block Editor loads everything needed), and the random value for the version, to make sure after each build that new version is loaded correctly, and not cached. The index.js is registered with wp_register_script function call. And, finally, we are loading translations (if available) for this blocks JavaScript file.

Register Block(s)

Now, we call individual methods to register each of our blocks. If you add new blocks, they have to match JavaScript source with the PHP code. Here is one of the registration methods, matching the JavaScript code displayed previously for one of the blocks.

private function _register_notice_block() {
	register_block_type( 'shortcodes-to-blocks/press-notice', array(
		'apiVersion'      => 2,
		'name'            => 'shortcodes-to-blocks/press-notice',
		'title'           => __( 'Press Notice', 'shortcodes-to-blocks' ),
		'description'     => __( 'Display simple notice.', 'shortcodes-to-blocks' ),
		'category'        => 'shortcodesblocks',
		'icon'            => 'warning',
		'render_callback' => array( $this, 'callback_notice' ),
		'attributes'      => array(
			'text'               => array(
				'type'    => 'string',
				'default' => 'This is just a notice.'
			'icon'               => array(
				'type'    => 'string',
				'enum'    => array(
				'default' => 'warning'
			'class'              => array(
				'type'    => 'string',
				'default' => ''
			'varFontSize'        => array(
				'type'    => 'integer',
				'default' => 16,
				'minimal' => 1
			'varLineHeight'      => array(
				'type'    => 'string',
				'default' => ''
			'varColorBackground' => array(
				'type'    => 'string',
				'default' => ''
			'varColorText'       => array(
				'type'    => 'string',
				'default' => ''
			'varColorBorder'     => array(
				'type'    => 'string',
				'default' => ''
		'textdomain'      => 'shortcodes-to-blocks',
		'editor_script'   => 'shortcodes-to-blocks-editor',
		'editor_style'    => 'shortcodes-to-blocks'
	) );

This looks very similar to the registration in JavaScript, and indeed, many elements are present in both. The first thing is the block name, which has to match (obviously) both the namespace and name part. And, some of the arguments are the same (title, description, name, icon), and you don’t need to have all of these in both places – these parameters can be listed in JavaScript or PHP only, for the sake of clarity, we have them in both here. Now, you have the new argument called ‘category’, and this is set to the new category we registered previously. If you don’t want your own category, you can set this to ‘widgets.

Now, the first major difference is the argument ‘render_callback’. This has to be present in PHP only for blocks that use server-side rendering. And, this callback is a function or class method (callable value) that will actually render the block content based on the arguments provided from the block settings. We will see this in a bit. Now, the critical part is the list of attributes. This HAS TO MATCH all the attributes from JavaScript – it is important to include the type and the default value for every attribute. If this doesn’t match, the rendering of the block will return an error message. If you make changes to attributes, you have to make changes to both JS and PHP, so they are always in sync. One thing that I did differently, is the ‘format’ for some of the fields, that work in JavaScript, but it should not be specified in PHP. I am not sure why, but that was the only way I could make it work.

After attributes, there are few more things to set. Textdomain is the text domain used for translations. And, finally, we have editor_script and editor_style. These two are used to load registered JS and CSS files when the block is displayed inside the editor. So, here, we need to set the name of our JavaScript file with blocks (using the name from the wp_register_script function) and the name of the CSS file. For CSS, we are going to load CSS used for frontend shortcodes rendering – this way, when rendered in the editor, the block will look as it should be, using front end styling. In our plugin, this main CSS file is already registered inside the Plugin class. So, the editor_script value ‘shortcodes-to-blocks-editor’ matches JavaScript file registered in Blocks class in _register_script() method, and editor_style value ‘shortcodes-to-blocks’ matches CSS file registered in Plugin class in styles() method. These two will ensure that required files will be enqueued when the block editor is loaded.

PHP function ‘register_block_type’ has more arguments, and you can check them out here. Arguments for scripts and styles to load can be defined differently, so check out the documentation.

One big notice regarding this function. You can use the alternative function register_block_type_from_metadata() that requires a JSON file with all the arguments to load the and register block, and that is used when you start with a blank example plugin from this tutorial. But, that file doesn’t support the ‘render_callback’ argument, so for our purpose, we have to use the ‘register_block_type’ function.

Callback Render

Finally, we have reached the callback function that will render the block content based on the arguments.

public function callback_notice( $attributes ) : string {
	$atts = $this->normalize_attributes( $attributes );

	return Shortcodes::instance()->shortcode_notice( $atts );

This callback method/function receives the array with the attributes from the block. All attributes are passed through our normalization method, and after that, with a new set of attributes, we call the method for rendering the shortcode equivalent of our block and return the result. Now, the normalization method has two purposes – change the names for some of the attributes and updates some attribute values. In this case, the same method is used for both of our blocks, but you don’t have to do that.

private function normalize_attributes( $attributes ) : array {
	$output = array(
		'_source' => 'block'

	$map = array(
		'varLineHeight'      => 'var-line-height',
		'varFontSize'        => 'var-font-size',
		'varColorBackground' => 'var-color-background',
		'varColorText'       => 'var-color-text',
		'varColorBorder'     => 'var-color-border',
		'showSiteAdmin'      => 'show-site-admin',
		'showProfileEdit'    => 'show-profile-edit',
		'showLogIn'          => 'show-log-in',
		'showLogOut'         => 'show-log-out',
		'showRegister'       => 'show-register',
		'showHomePage'       => 'show-home-page',
		'showRSSPosts'       => 'show-rss-posts',
		'showRSSComments'    => 'show-rss-comments'

	foreach ( $attributes as $key => $value ) {
		$new_key            = $map[ $key ] ?? $key;
		$output[ $new_key ] = $value;

	if ( ! empty( $output['var-font-size'] ) ) {
		$output['var-font-size'] = $output['var-font-size'] . 'px';

	return $output;

Why normalization? Well, in JavaScript you can’t have attributes with a name using dashes. Sure, you can rename your shortcodes attributes to match the JavaScript, but if you are converting existing shortcodes, it is not a good idea to make such changes, or your old shortcodes might stop working. So, depending on your shortcode attributes, you might need such a method, and you may not need it. But, for some attributes, you may need to make adjustments. For instance, the value coming from the FontSizePicker is an integer, so we need to add ‘px’ to that value to make it valid.

Again, depending on your shortcodes, you may not need this normalization of attribute values, but make sure to properly test if all shortcode attributes can match JavaScript (using dashes for instance), and prepare them before passing them to shortcodes rendering function/method.

One more thing I added is the attribute ‘_source’. This is an attribute that is part of the shortcode also, and by default, it is set to ‘shortcode’, and its value is used for rendering the shortcode. When called for a block, this attribute will be set to ‘block’. This is an optional change, and you can add it to your existing shortcodes, or just skip it completely.

The Result

And, finally, here is the screenshot from the block editor showing both blocks in use, with sidebar open with settings for one of our blocks, and some settings changes made.

Both blocks and sidebar settings for block

The Resources

So, here are the important links related to this tutorial, including the GitHub project, WordPress Blocks development Handbook, and few other things I have found useful for this process.

Also, I have converted my plugin ArchivesPress to use blocks for the shortcodes it already had. This plugin is using the same approach as this tutorial, so you can see another full example for the conversion process, and see few more components in use to create block settings.

What’s Next?

My plan is to spend some time whenever I have free time, to work on updating all my existing plugins and start adding blocks by converting existing shortcodes and widgets. I also intend to learn more about some other block types using the hybrid approach – server-side rendering and the in editor content editing with the use of format controls, and later writing fully independent blocks (when needed).

Do you plan to convert shortcodes in your existing plugins into blocks, have you started on that already, and did you find this tutorial useful? What else you might need to do related to this type of conversion?

Please wait...

Leave a Comment