How to Convert a Shortcode to a Gutenberg Block

Editors Note: December 2nd, 2021 - The WordPress block editor has evolved considerably in the last four years and the recommendations here may no longer be relevant for your site. We leave such posts published for historical reference.

Shortcodes are a quintessential WordPress feature. They’ll continue to work when WordPress 5.0 is released, but should be converted to to Gutenberg blocks for the best possible user experience.

Why are Shortcodes Popular?

Shortcodes are popular because they let WordPress users store a macro in the editor that transforms into a beautiful butterfly on the frontend.

For example, a GitHub Gist shortcode might like this in the editor:

[gist url=""]

The same shortcode then renders to the following JavaScript on the frontend:

<script src=""></script>

Your browser will then turn that JavaScript into something like this:

“Macro” is a fundamental term in this example. What’s stored in the database is structured data that’s then dynamically transformed on display. This dynamic transformation makes it simpler to:

  1. Render different presentation in different contexts. For instance, AMP doesn’t support arbitrary JavaScript, so you might want to render a HTML link instead.

  2. Change the presentation in the future without updating all stored instances. For example, you may decide to wrap your <script> tag with a <div class="github-gist"></div> for easier styling.

Standard Gutenberg blocks store pure static HTML in the database. A dynamic block gives you the same flexibility as a shortcode by delaying rendering to the point of presentation.

Let’s recap with a comparison table:

     Dynamic Block Standard Block
Better user experience than shortcodes Yes Yes
Render context-specific output on the frontend vs. RSS vs. AMP Yes No
Change output without transforming all stored references Yes No

Converting a Shortcode to a Dynamic Gutenberg Block

Without any intervention, your existing shortcode will look like this in Gutenberg:

Not any worse than the Classic Editor, but we could make it so much better! Let’s do so—there are two key components:

  1. Registering the block and its controls with JavaScript.

  2. Registering the dynamic block callback with PHP.

Registering the Block and Its Controls in JavaScript

To get a head start on creating a new Gutenberg block, first run wp scaffold block with WP-CLI v1.5.0 or later. This will set up a blocks folder in your plugin with some of the basic files:

Make sure to load the scaffolded PHP file from your main plugin file, otherwise it won’t be loaded.

We’re ready to dive into the Gutenberg block itself. Because this isn’t a comprehensive introduction, only an explanation of dynamic blocks, refer to the Gutenberg handbook to learn more.

The block.js file (which you may see as index.js elsewhere) within your block folder is where you’ll be modifying the block’s JavaScript registration. The top of the JavaScript file might look something like this:

( function( wp ) {
        var el = wp.element.createElement;
        var __ = wp.i18n.__;

        // Visit to learn more
        wp.blocks.registerBlockType( 'github-gist-gutenberg-block/github-gist', {

To register the block preview and controls, you’ll need to modify the edit callback. Then, modify the save callback to change what Gutenberg stores in post content in the database.

Here’s an annotated version of the modifications we made to the GitHub Gist edit callback:

 * Called when Gutenberg initially loads the block.
edit: function( props ) {
        var url = props.attributes.url || '',
            focus = props.focus;
        // retval is our return value for the callback.
        var retval = [];
        // When the block is in focus or there's no URL value,
        // show the text input control so the user can enter a URL.
        if ( !! focus || ! url.length ) {
                // Instantiate a TextControl element.
                var controlOptions = {
                        // Existing 'url' value for the block.
                        value: url,
                        // When the text input value is changed, we need to
                        // update the 'url' attribute to propagate the change.
                        onChange: function( newVal ) {
                                        url: newVal
                        placeholder: __( 'Enter a GitHub Gist URL' ),
                        // el() is a function to instantiate a new element
                        el( InspectorControls.TextControl, controlOptions )
        // Only add preview UI when there's a URL entered.
        if ( url.length ) {
                var id = 'gist-' +;
                // setTimeout is used to delay the GitHub JSON API request
                // until after the block is initially rendered. From the response,
                // we update the rendered div.
                        jQuery.getJSON( url.trim(/\/$/) + '.json?callback=?',
                                var div = jQuery('#'+id);
                                var stylesheet = jQuery('<link />');
                                stylesheet.attr('ref', 'stylesheet');
                                stylesheet.attr('href', data.stylesheet);
                                stylesheet.attr('type', 'text/css');
                }, 10 );
                retval.push( el( 'div', { id: id } ) );
        return retval;

Here’s an annotated version of the GitHub Gist save callback:

 * Called when Gutenberg "saves" the block to post_content
save: function( props ) {
        var url = props.attributes.url || '';
        // If there's no URL, don't save any inline HTML.
        if ( ! url.length ) {
                return null;
        // Include a fallback link for non-JS contexts
        // and for when the plugin is not activated.
        return el( 'a', { href: url }, __( 'View Gist on GitHub' ) );

Et voila! Your Gutenberg block’s JavaScript is registered. But you still need to register a callback in PHP to dynamically render it on the frontend.

Registering the Dynamic Block Callback in PHP

Fortunately, dynamic blocks can work interchangeably with your existing shortcode callback. Simply call register_block_type alongside your add_shortcode:

 * Register the GitHub Gist shortcode
function gggb_init() {
        add_shortcode( 'github-gist', 'gggb_render_shortcode' );
        register_block_type( 'github-gist-gutenberg-block/github-gist', array(
                'render_callback' => 'gggb_render_shortcode',
        ) );
add_action( 'init', 'gggb_init' );

When your block is rendered on the frontend, it will be processed by your render callback:

function gggb_render_shortcode( $atts ) {
        if ( empty( $atts['url'] )
                || '' !== parse_url( $atts['url'], PHP_URL_HOST ) ) {
                return '';
        return sprintf(
                '<script src="%s"></script>',
                esc_url( rtrim( $atts['url'], '/' ) . '.js' )

Note: this render callback is intentionally different than the Gutenberg block’s edit callback. Our preference is to use GitHub’s provided JavaScript embed code because this lets GitHub change the embed’s behavior at a future date without requiring a developer to make changes.

Just the Beginning

Congrats, you did it! If you’ve made it this far, you’re either on top of your game or completely baffled. Here are some concepts we’ve touched on:

  • Dynamic blocks are advantageous over standard blocks because they let you render different output based on context (frontend vs. RSS vs. AMP) and change the output at a future date without updating all stored references.

  • Gutenberg blocks can be registered with both JavaScript and PHP. JavaScript registration gives you control over the editing interface, the block preview, and what ends up stored in post content. PHP registration enables you to dynamically handle rendering with a PHP callback.

  • Even when you’re building a dynamic block, you’ll want to save some fallback HTML in the post content for when the plugin isn’t active, etc. For the GitHub Gist block, this is a View Gist on GitHub link.

Check out the example code on GitHub and feel free to open a new issue if you have any questions.

Thanks for reading!