Building a Related Posts block for WordPress

Please note: this tutorial is now out of date. I’ve updated the block and the whole tutorial here: Custom Related Posts block for WordPress 5.3

The St. Mary’s website contains a lot of content. Most of it is segmented into custom post types, with various taxonomies and relationships that connect things wherever possible. But sometimes, there are islands of content that should be connected. Rather than always adding manual links, we needed an easy way to connect and display different content types.

I figured this was a common need, so I searched first for a block that had already been built. Nothing out of the box was a good fit, but I kept searching for something to model my approach after. Finally, Hardeep on Zac Gordon’s Slack suggested WooCommerce Blocks. They have a lovely “Handpicked Products” block with a nice UI, so it was a nice approach to model my block after.

If you’d like to build your own Related Posts block, or try out mine, here’s what it looks like when you add it in the editor:

The block says "Choose the posts you want to display," followed by a "Currently selected" section including the default Post post type and "none selected" message; and an "Add to selections" section including a search input and default search results, with a "done" button to close editing mode

The block defaults to the built-in “Post” post type and shows the most recently published Posts as search results. From there, the user can change the post type, search for posts (which will search post titles and content), or select posts by pressing one of the search result buttons. Once they are done making selections, they can press Done to preview what it will look like on the front end.

Here’s what it looks like on the front end, and in the editor when you’re not in edit mode:

Screen shot of the list of related posts, including a thumbnail image and title for each

We’ll talk about conditional rendering at the end, because it’s fairly easy to build out different HTML and CSS for each post type. So, if you want related Posts to look one way, and related Pages to be a simpler list, you can accomplish that through this tutorial.

Server-side rendering and when to use it

Once the user chooses posts, we can get all their data, and we could just save it to the block and render it directly. So why wouldn’t we do just that? Well, what if you’re working on a site that changes frequently, and people are constantly changing featured images, titles, even permalinks? What would happen if we saved all that postdata, basically hard-coding the link into the page? The link might break, the image might be missing, and the title might have completely changed, giving end users a very confusing experience.

So how can we handle this? Save just the post IDs to the database, and get all their data dynamically. WordPress built a component just for this purpose – the ServerSideRender component.

The Core developers warn against using server-side rendering unless your block is heavily dependent on PHP logic intertwined with data, or you’re creating a legacy block for backwards compatibility and don’t want to rewrite the old code. I explored the various data available in the editor, and although it is possible to drill down and get the data, there’s still the problem of saving that end data to the database. If any of the data in the original post changes, it’s then out of date everywhere this block refers to the post. There may be a better way to handle all this, but I came to the conclusion that server-side rendering would meet the needs for this particular block quite well.

Step 1: Set up Webpack and files

I’ve tried create-guten-block as well as @wordpress/scripts, and I didn’t care for either of them. They both include a lot of overhead, I ran into security warnings with both of them, and @wordpress/scripts wouldn’t even compile my SASS into CSS files out of the box. So instead, I set up a custom webpack. You follow the full custom webpack tutorial (replacing “myblockname” with “related-posts”), or:

  • Install node.js on your computer, if it’s not already installed
  • Download the Step 1 code on GitHub (no longer available – please see version 2!)
  • In a command prompt, go to the folder you downloaded – “/related-posts-block/” – and type `npm install`. This will download all the Node packages.

You can activate the plugin at this point, just to see that we have a working block. In the editor, you’ll just see a <div> containing the word “Editor,” and on the front end, you’ll see a <div> containing the phrase “Front End.” If you’re like me, used to building PHP WordPress plugins, it seems like an awful lot of effort just to get to this point. In PHP, we would have simply created a folder, added a PHP file, and written a couple of lines of comments – and voila, we’d have a plugin.

Step 2: The simplest part of the JavaScript

Before you edit any JavaScript, make sure to go back to your command prompt, inside your “/related-posts-block/” folder, and type `npm run dev`. This tells Webpack to watch your files for changes and automatically compile everything whenever you save one of the files in that folder.

Let’s tweak the “/src/blocks/related-posts/index.js” file. We’ll add attributes to store data, update the save function to its final state (which is easier than you might expect!), and set up a call to a custom component we’ll build in a separate file.

const { registerBlockType } = wp.blocks;
import Block from './block';
 
registerBlockType('my/related-posts', {
    title: 'Related Posts',     
    category: 'widgets',
    icon: 'image-filter',
    attributes: {
        'editMode': {
            type: 'boolean',
            default: true
        },
        'postIds': {
            type: 'array',
            default: []
        },
        'postType': {
            type: 'string',
            default: 'posts'
        },
        'updated': {
            type: 'string',
            default: ''
        }
    },
    edit( props ) {
        return <Block { ...props } />;
    },
    save() {
        // Rendering in PHP
        return null;
    },
} );

Here’s what’s changed from the barebones block:

  • On line 1, we used a slightly shorter syntax to do the same thing – pull the `registerBlockType()` function from `wp.blocks`.
  • On line 2, we’re telling this file that it’s going to have to import a separate file called “block.js.” We’ll make that next.
  • In lines 8-24, we’re adding a set of attributes to keep track of all the data.
    • “editMode” is a boolean, meaning it is either true or false. We’ll use this to determine whether to show the search interface to select posts, or display the front-end preview.
    • “postIds” is the most important attribute. It will hold an array of the post IDs the user selects to display in the block.
    • “postType” is what will allow this block to work with more than just Posts. We’ll enable both Posts and Pages here, and once you’ve seen that pattern, you could easily add your own custom post types as options if you like.
    • “updated” is a bit of a cheater attribute. The Core `setAttributes()` function is asynchronous and doesn’t always fire when needed, so we’re using this attribute to hold a timestamp of the last time the “postType” was updated. This forces all the attributes to stay in sync and work together.
  • The `edit()` function now says to return a <Block /> component. This is what we told the file to import on line 2, and what we’ll build next.
  • The `save()` function now returns `null`. That’s because instead of saving post data to the database, it only has to save the attributes as a comment, and PHP will do the actual HTML rendering. This is the main difference between “regular” blocks (that save HTML to the database) and server-side-rendered blocks (which let PHP handle the output).

And now we’re at a crossroads. Our block is going to need to keep track of React state. State is a temporary storage area for data, so for example, when a user searches for a specific post title, we’ll use state to keep track of the post objects that were found by that search and keep those available until the user is done editing.

We don’t need to save this data as attributes, because again if anyone edits the posts themselves this data would become obsolete, but we do want to save it during the editing session so we’re not constantly searching for the same data we just searched for. And we need two new components – one that contains the entire search functionality, and another to wrap it.

Because we need access to state in those components, we’re going to need to build our components as classes. So here’s the choice: we could either set up each class in its own file, so they’re separated by purpose, or we could include everything in our single JS file.

Personally, I’m in favor of fewer files. It seems more natural to me to keep everything together, so when I need to go from one part to the next, I just search or browse through the one file where I can find everything. However, a more typical React approach is to segregate each component into its own little file, so things seem more modular.

It also helps us break everything into smaller chunks to walk through. So, for this tutorial, we’re going to give each component its own separate file.

So, next up is the slightly more complex file “/src/blocks/related-posts/block.js”:

const { Button, Disabled, Placeholder, Toolbar } = wp.components;
const { BlockControls, ServerSideRender } = wp.editor;
const { Component, Fragment } = wp.element;
import { SearchPostsControl } from './searchposts.js';
 
export default class Block extends Component {
    renderEditMode() {
        const { props } = this;
        const { attributes, setAttributes } = this.props;
        const onDone = () => {
            setAttributes({ editMode: false });
        }
        return(
            <Placeholder label='Choose the posts you want to display' >
                <SearchPostsControl { ...props } />
                <Button isDefault onClick={ onDone }>
                    Done
                </Button>
            </Placeholder>
        );
    }
    render() {
        const { attributes, setAttributes } = this.props;
        const { editMode } = attributes;
        return (
            <Fragment>
                <BlockControls>
                    <Toolbar controls={ [ { icon: 'edit', title: 'Edit', onClick: () => setAttributes({ editMode: !editMode }),
                                isActive: editMode
                            }
                        ] }
                    />
                </BlockControls>
                { editMode ? (
                    this.renderEditMode()
                ) : (
                    <Disabled>
                        <ServerSideRender block='my/related-posts' attributes={ attributes } />
                    </Disabled>
                ) }
            </Fragment>
        );
    }
}

Lines 1-3 pull in all the Core components we’ll need. For example, we’ll use a Core <Button> the user can click to signal they’re done editing and want to preview how the currently selected posts will look on the front end.

Line 4 refers to the last JavaScript file, which we’ll create next. It will create the search interface for the block.

On line 6, we define the <Block> component that we’re using back in the “index.js” file. It contains two functions.

`renderEditMode()` is called if the “editMode” attribute is true, which it is by default. So, as soon as the user adds the block, `renderEditMode()` is called. It adds a Placeholder component, which WordPress currently styles as a gray box to signify it’s in an editable state rather than showing you what the block will look like on the front end, and inside the placeholder, there is a <SearchPostsControl> (from our next file) and a <Button> the user can press when they’re done selecting posts.

The `render()` function returns a <Fragment>, which is a handy WordPress-specific component. React requires every `render()` function to return a single HTML element, but sometimes you don’t really need that extra HTML element because you’re putting it inside something else. The WP Fragment satisfies the React requirement for a single containing element, but it actually creates no HTML. Inside the Fragment, the BlockControls include a toolbar. This way, when the block is selected in the editor, the user can toggle between edit and preview mode. This works just like the “Done” button, toggling the “editMode” attribute.

Finally, if “editMode” is true, the `renderEditMode()` function gets called. Otherwise, a <Disabled> component is returned – disabled meaning it’s not possible for the user to edit anything in this mode – and inside that, a ServerSideRender component handles everything else. This is where PHP takes over, and this is why our block doesn’t need to actually save any HTML to the database. PHP is going to take just the attributes, which will be stored as comments, out of the database, and then use those attributes to build out the HTML to display – both on the front end, and here in the editor, when we’re not in edit mode.

Before we get to the PHP, though, we need to create that last JavaScript file: “/src/blocks/related-posts/searchposts.js”. Since it’s got the most functionality, we’re just going to build a simple placeholder first, and build it out a bit at a time.

const { Button, MenuGroup, MenuItem, SelectControl, TextControl } = wp.components;
const { Component, Fragment } = wp.element;
 
export class SearchPostsControl extends Component {
    constructor(props) {
        super(props);
    }
    render() {
        return(
            <div className='search-posts-control'>
                <div className='posts-selected'>
                    <h2>Currently selected:</h2>
                </div>
                <div className='posts-search'>
                    <h2>Add to selections:</h2>
                </div>
            </div>
        );
    }
}

Remember, we’ve built this Component as a class, because that will allow it to keep track of state eventually. Right now, all it does is create a couple of divs with headings. Let’s give the divs a touch of style so the button doesn’t float up to the right:

“/src/blocks/related-posts/editor.scss”:

.search-posts-control { width:100%; text-align:left; }
.posts-selected, .posts-search { min-height:7em; margin-bottom:1em; padding:0 1em 1em; border:1px solid #777; }

Phew. That’s a lot of files. You could activate the plugin now, but if you use the “Done” button you’ll get an error. That’s because we haven’t actually set up the PHP rendering, so let’s build in a placeholder for that, too. (We’ll cover the PHP rendering part at the very end, once we finish all the JavaScript.)

“/plugin.php”:

<?php
/*
Plugin Name: Related Posts tutorial block
*/
add_action('init', 'my_register_related_block');
function my_register_related_block() {
    // register our JavaScript
    wp_register_script(
        'related-block',
        plugins_url('/build/index.js', __FILE__),
        array('wp-blocks', 'wp-element', 'wp-editor')
    );
    // register our front-end styles
    wp_register_style(
        'related-block-style',
        plugins_url('/build/style.css', __FILE__),
        array('wp-block-library')
    );
    // register our editor styles
    wp_register_style(
        'related-block-edit-style',
        plugins_url('/build/editor.css', __FILE__),
        array('wp-edit-blocks')
    );
    // register our block
    register_block_type('my/related-posts', array(
        'editor_script' => 'related-block',
        'editor_style' => 'related-block-edit-style',
        'style' => 'related-block-style',
        'render_callback' => 'get_related_posts',
        'attributes' => array(
            'editMode' => array(
                'type' => 'boolean',
                'default' => true
            ),
            'postIds' => array(
                'type' => 'array',
                'default' => []
            ),
            'postType' => array(
                'type' => 'string',
                'default' => 'posts'
            ),
            'updated' => array(
                'type' => 'string',
                'default' => ''
            )
        )
    ));
}
 
function get_related_posts($attributes) {
    return '<div>Front end view will be displayed here.</div>';
}
?>

We’ve done two very important things here:

  1. We added the same attributes in PHP that we set up in JavaScript. If you don’t tell PHP what to do with all the attributes the JavaScript will be passing it, you’ll get error messages.
  2. We added a very simple function that, for right now, just displays a div. We’ll build out the real PHP rendering once we finish “searchposts.js”.

Now if you activate the plugin and add the block, you can use the “Done” button and see our little hard-coded div. Once you hit Publish, you can see how that same div in the editor is displayed on the front end. That’s our real goal: when someone in the editor leaves the actual editing mode, we want them to see what the block will look like on the front end, without having to hit “preview.”

Step 3: Change post types

Our “postType” attribute defaults to Posts. But what if our user wants to show related Pages or a custom post type?

“/src/blocks/related-posts/searchposts.js”:

const { Button, MenuGroup, MenuItem, SelectControl, TextControl } = wp.components;
const { Component, Fragment } = wp.element;
  
export class SearchPostsControl extends Component {
    constructor(props) {
        super(props);
    }
  
    changePostType(newType) {
        // Clear postIds, update postType Attribute
        let { setAttributes } = this.props;
        setAttributes({ postType: newType });
    }
  
    render() {
        let { attributes: { postType } } = this.props;
        return(
            <div className='search-posts-control'>
                <div className='posts-selected'>
                    <h2>Currently selected:</h2>
                    <SelectControl
                        label='Post Type'
                        value={ postType }
                        options={ [
                            { label: 'Post', value: 'posts' },
                            { label: 'Page', value: 'pages' }
                        ] }
                        onChange={ (val) => { this.changePostType(val) } }
                    />
                </div>
                <div className='posts-search'>
                    <h2>Add to selections:</h2>
                </div>
            </div>
        );
    }
}

Let’s skip down to the `render()` function first. In the “Currently selected” section, we have a new <SelectControl>. This creates a dropdown menu where the user can change the post type. This is one of the biggest pieces you may want to customize – if you’re using custom post types, you may want to add those here so they can be selected. The trick is in determining whether your post type should be plural or singular. If you register a post type like “books” but make the URL singular – i.e. “http://example.com/book/” – then use the singular version as your value. But if you make the URL plural – i.e. “http://example.com/books” – then use the plural version. This will matter later when we actually pull posts from the REST API.

Our dropdown has an `onChange` function that sends the value to a new `changePostType()` function. Let’s skip back up to line 9, where we define the `changePostType()` function. It expects the new post type to be passed in. All we need to do right now is set the “postType” attribute to the new type.

If you try out the block now, it’s slightly more exciting – you can change the dropdown value! Now let’s get to the good part.

Step 4: Get default posts

It’s time to give the user a way to select posts. Rather than making them search before we show any options, we’ll offer up some defaults. We’ve already set our “postType” attribute to default to Posts, so let’s grab the latest bunch and display their titles in case the user just wants to select some of the latest posts in this block.

First, let’s finalize our editor CSS so it’s ready for upcoming new elements – “/src/blocks/related-posts/editor.scss”:

.search-posts-control { width:100%; text-align:left; }
.posts-selected, .posts-search { min-height:7em; margin-bottom:1em; padding:0 1em 1em; border:1px solid #777; }
.posts-selected button.is-destructive { position:relative; max-width:calc(100% - .1em); overflow:hidden; }
.posts-selected button.is-destructive:after { display:block; position:absolute; right:0; width:28px; content:'x'; border-radius:100%; color:#fff; background:#d00; }
.posts-list div[role="menu"] { max-height:15em; overflow-y:auto; }
.posts-list button { position:relative; padding:1em 1em 1em 3em; background:#fff; border-bottom:1px solid #aaa; }
.posts-list button:before {display:block; position:absolute; left:.5em; font-family:FontAwesome; font-size:1.5em; content:''; color:#191e23; }
.posts-list button[data-ischecked="true"]:before { content:''; color:#0085ba; }

Note: you’ll need to have Font Awesome installed and running in the editor for this to come out looking right. First check to see whether it’s already there – Gravity Forms, Wordfence, and some other plugins and themes enqueue it by default. But if it’s not enqueued on the admin side, add these lines in “plugin.php” to make this work:

add_action('admin_enqueue_scripts', 'related_block_enqueue');
function related_block_enqueue() {
    wp_enqueue_style('font-awesome', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css');
}

Next up, let’s work on our “/src/blocks/related-posts/searchposts.js” file. This step is a little bigger because there are a few moving parts.

const { Button, MenuGroup, MenuItem, SelectControl, TextControl } = wp.components;
const { Component, Fragment } = wp.element;
 
export class SearchPostsControl extends Component {
    constructor(props) {
        super(props);
        this.state = {
            resultObjects: [],
            resultButtons: []
        };
        this.buildResultButtons = this.buildResultButtons.bind(this);
        this.changePostType = this.changePostType.bind(this);
        this.searchFor = this.searchFor.bind(this);
    }
 
    changePostType(newType) {
        // Clear postIds, update postType Attribute
        let { setAttributes } = this.props;
        setAttributes({ postType: newType });
        // Clear state and run a new search
        this.setState({ resultObjects: [], resultButtons: []}, this.searchFor(newType));
    }
 
    searchFor(searchPostType = '') {
        let { attributes: { postType } } = this.props;
        let finalPostType = postType;
        // If a post type was explicitly passed to the function, use that instead
        if(searchPostType != '') {
            finalPostType = searchPostType;
        }
        // Make REST API call to get post objects - excluding current ID, but including the postType and keyword if present
        let currentId = wp.data.select('core/editor').getCurrentPostId();
        let path = '/wp/v2/' + finalPostType + '?exclude=' + currentId;
        wp.apiFetch( { path: path } ).then( ( posts ) => {
            this.setState({ resultObjects: posts });
        }).then( () => this.buildResultButtons() );
    }
 
    buildResultButtons() {
        let { setAttributes } = this.props;
        let resultButtons = this.state.resultObjects.map(function(item, ind) {
            let isChecked = false;
            // Save the opposite value for onClick
            // Must have default true, because if nothing is selected, it's false, and true is what it should change to
            let toCheck = true;
            if(isChecked == true) {
                toCheck = false;
            }
            return(
                <MenuItem id={ item.id } data-ischecked={ isChecked } onClick={ () => this.updateSelectedIds(parseInt(event.target.id), toCheck) }
                >
                    { item.title.rendered }
                </MenuItem>
            );
        }, this);
        // Save timestamp in milliseconds - this forces the setAttributes call for postIds to work
        let timeNow = Date.now();
        this.setState({ resultButtons: resultButtons }, setAttributes({ updated: timeNow }));
    }
 
    updateSelectedIds(id, val) {
        console.log(id + ' checked is ' + val);
    }
 
    componentDidMount() {
        this.getStartingData();
    }
 
    getStartingData() {
        this.searchFor('');
    }
 
    render() {
        let { attributes: { postType } } = this.props;
        return(
            <div className='search-posts-control'>
                <div className='posts-selected'>
                    <h2>Currently selected:</h2>
                    <SelectControl
                        label='Post Type'
                        value={ postType }
                        options={ [
                            { label: 'Post', value: 'posts' },
                            { label: 'Page', value: 'pages' },
                        ] }
                        onChange={ (val) => { this.changePostType(val) } }
                    />
                </div>
                <div className='posts-search'>
                    <h2>Add to selections:</h2>
                    <MenuGroup label='Search Results' className='posts-list' >
                        { this.state.resultButtons }
                    </MenuGroup>
                </div>
            </div>
        );
    }
}

First off, on lines 7-10, we’ve set a default state for both `resultObjects` and `resultButtons`. Both are empty arrays. We’ll use them shortly to hold information about posts.

Next, on lines 11-12, we’ve bound two new functions to `this`. Binding them ensures they will have access to the current component’s state. Unlike functional PHP, where variables are global by default, in JavaScript everything is strictly scoped. So, we’re basically saying, “make this function realize it is part of this component, and give it access to the rest of this component.” Now we need to actually make those functions.

On lines 19-20, we’ve added functionality to our `changePostType()` function. Before, we were setting the “postType” attribute and calling it a day. Now that we’re populating posts, though, we also need to clear the state of `resultObjects` and `resultButtons` – because those are from the old post type. Once we clear that state, we’re also calling our new `searchFor()` function – and explicitly passing it the new post type, so it can immediately grab an updated set of posts. That’s because our call to `setAttributes()` is asynchronous – it will finish when resources are available to process – but we want the list of posts (or pages, or CPTs) to update immediately.

Lines 23-36 define a new function called `searchFor()` that currently accepts one argument: the post type. We’ll add search by keyword functionality later.

Inside our function, we first check the current value of the “postType” attribute. However, because that attribute may be outdated, if a post type was explicitly passed in as an argument, we let that override the post type. We also identify the ID of the current post, so we can exclude it from search results. (We don’t want a post to be “related” to itself and confuse end users.) Then, we call the endpoint that grabs the most recent posts from the calculated post type (excluding the current post). This typically gets 10 posts, although some sites may be set to show a different number in the REST API by default.

Promises in React

We call `wp.apiFetch()` to get the post objects, followed by a series of `.then()`s. These `.then()`s work as conditions: “After the apiFetch is finished, then set the state, and after setting the state is finished, then call our other new function `buildResultButtons()`.” This ensures that each piece finishes before the next begins – so we’re working with complete data throughout.

If you’re used to functional PHP, you’re used to things happening in the order you call them. However, JavaScript is different. Many calls are asynchronous, and if you’re relying on the output of operation A to affect the outcome of operation B, you have to explicitly say so. This is what the `.then()` part of the promise does for us: we can wait until value A is finalized before we run operation B.

However, you can’t just add `.then()` to any old function. It has to be set up as a Promise first – like `wp.apiFetch()`. It was created with a way to determine whether or not it is finished getting the data. Once it does finish getting the data (in this case, a set of post objects), it saves the array to `resultObjects` in state. And `.then()` it calls another function called `buildResultButtons()` to convert the post objects into buttons the user will be able to use to select specific posts.

Take a deep breath. You’re with me so far. Next we’ll look at the new `buildResultButtons()` function on lines 25-48. Converting the post objects we just got from the REST API into buttons isn’t too hard. First, we define `setAttributes()` so we can refer to it by shorthand.

Next, we get the `resultObjects` we just stashed out of state and map them. An array map just means, “for each of the items in this array, follow this set of instructions.” Those instructions are pretty simple for now: `return()` a <MenuItem>. A MenuItem creates a HTML <button> element – I used <MenuItem> because a group of them can go together nicely inside a <MenuGroup>. Each <MenuItem> has a special data attribute set to “false,” because it’s a default and nothing is selected yet, but on click, we’re sending the post ID and “true” to our third new function, because once the user clicks on a button, it will be selected.

Once all those post objects have been mapped, we save the resulting buttons in our `resultButtons` state. If you read that line carefully, you’ll notice that we call `setAttributes()` as a callback.

Why setAttributes() needs a callback

If you’re used to working in React, you may know that their function `setState()` supports callbacks. That means when you set a state, you can tell it “Wait for the state to finish updating, and then run this other function.” That’s exactly what we need here – whenever the user changes something, we need to wait for that updated data to exist in attributes, and then move on to the next function.

Unfortunately, the WordPress Core developers determined that the `setAttributes()` function was different enough from `setState()` that it would not support callbacks. This is why I created the “updated” attribute – it forces the other attributes to update properly. If `setAttributes()` supported a callback, we wouldn’t have to have this extra attribute.

Step 4 conclusion

There are 3 more helper function stubs. `updateSelectedIds()` is called whenever the user clicks on one of the result buttons. Eventually we’ll use this to save the post IDs they select. For right now, we just need a placeholder function, so we console out the post ID and whether it is checked or unchecked after being clicked. (Right now, since they’re all unchecked when they’re loaded, they’ll always show checked in the console.)

`componentDidMount()` is a built-in React lifecycle method. It’s similar to a plain JavaScript `onLoad` event – when the component is fully loaded, it runs. This is actually the complete function – all it needs to do is call our helper function, `getStartingData()`.

`getStartingData()` is pretty simple. Right now, since we’re only dealing with search, it calls our `searchFor()` function so that when the component is mounted, it pulls up results right away, without waiting for the user to interact with the block.

Step 5: Search for posts

We could just output a massive dropdown of all of the posts on the site, but if you’re working on a site with thousands of posts, it would be very hard to find the one you’re looking for. So, let’s make a simple search function to make it easier for the end user to find the posts they want to select.

“/src/blocks/related-posts/searchposts.js”

const { Button, MenuGroup, MenuItem, SelectControl, TextControl } = wp.components;
const { Component, Fragment } = wp.element;
 
export class SearchPostsControl extends Component {
    constructor(props) {
        super(props);
        this.state = {
            resultObjects: [],
            resultButtons: []
        };
        this.buildResultButtons = this.buildResultButtons.bind(this);
        this.changePostType = this.changePostType.bind(this);
        this.searchFor = this.searchFor.bind(this);
    }
 
    changePostType(newType) {
        // Clear postIds, update postType Attribute
        let { setAttributes } = this.props;
        setAttributes({ postType: newType });
        // Clear state and run a new search
        this.setState({ resultObjects: [], resultButtons: []}, this.searchFor(newType, ''));
    }
 
    searchFor(searchPostType = '', keyword = '') {
        let { attributes: { postType } } = this.props;
        let finalPostType = postType;
        // If a post type was explicitly passed to the function, use that instead
        if(searchPostType != '') {
            finalPostType = searchPostType;
        }
        // Make REST API call to get post objects - excluding current ID, but including the postType and keyword if present
        let currentId = wp.data.select('core/editor').getCurrentPostId();
        let path;
        if(keyword != '') {
            path = '/wp/v2/' + finalPostType + '?search=' + keyword + '&exclude=' + currentId;
 
        } else {
            path = '/wp/v2/' + finalPostType + '?exclude=' + currentId;
        }
        wp.apiFetch( { path: path } ).then( ( posts ) => {
            this.setState({ resultObjects: posts });
        }).then( () => this.buildResultButtons() );
    }
 
    buildResultButtons() {
        let { setAttributes } = this.props;
        let resultButtons = this.state.resultObjects.map(function(item, ind) {
            let isChecked = false;
            // Save the opposite value for onClick
            // Must have default true, because if nothing is selected, it's false, and true is what it should change to
            let toCheck = true;
            if(isChecked == true) {
                toCheck = false;
            }
            return(
                <MenuItem id={ item.id } data-ischecked={ isChecked } onClick={ () => this.updateSelectedIds(parseInt(event.target.id), toCheck) }
                >
                    { item.title.rendered }
                </MenuItem>
            );
        }, this);
        // Save timestamp in milliseconds - this forces the setAttributes call for postIds to work
        let timeNow = Date.now();
        this.setState({ resultButtons: resultButtons }, setAttributes({ updated: timeNow }));
    }
 
    updateSelectedIds(id, val) {
        console.log(id + ' checked is ' + val);
    }
 
    componentDidMount() {
        this.getStartingData();
    }
 
    getStartingData() {
        this.searchFor('');
    }
 
    render() {
        let { attributes: { postType } } = this.props;
        let label = 'Search for ' + postType + ' to display';
        return(
            <div className='search-posts-control'>
                <div className='posts-selected'>
                    <h2>Currently selected:</h2>
                    <SelectControl
                        label='Post Type'
                        value={ postType }
                        options={ [
                            { label: 'Post', value: 'posts' },
                            { label: 'Page', value: 'pages' },
                        ] }
                        onChange={ (val) => { this.changePostType(val) } }
                    />
                </div>
                <div className='posts-search'>
                    <h2>Add to selections:</h2>
                    <TextControl
                        label={ label }
                        type='search'
                        onChange={ (val) => this.searchFor('', val) }
                    />
                    <MenuGroup label='Search Results' className='posts-list' >
                        { this.state.resultButtons }
                    </MenuGroup>
                </div>
            </div>
        );
    }
}

Down in our `render()` function, under the “Add to selections” heading, we’ve added a search input (the <TextControl>) that calls our `searchFor()` function every time its value changes (and passes the value to the function, so we know what we’re searching for). It also passes an empty string, because we want the search function to use the “postType” attribute which has already been set by this point.

On line 21, there’s a related change – we pass two parameters into the `searchFor()` function. In this case, we have a post type but not a keyword, so it’s basically the opposite of having a keyword but no overriding post type.

In the `searchFor()` function itself, lines 33-39, we have to do a little more work to set up our REST API endpoint now that we may or may not have a keyword. If a non-empty keyword has been passed into our `searchFor()` function, then the endpoint is “/wp/v2/posttype?search=keyword&exclude=id”.

Otherwise, the endpoint is “/wp/v2/posttype/?exclude=id”. We then carry on with actually calling the REST API, setting the `resultObjects`, and building the `resultButtons` as before.

Step 6: Save selected posts

The user can currently select a post type and search for posts. However, when they click on one of the buttons to select it, we’re just consoling out the ID and “true.” Let’s actually save the selected post IDs to our “postIds” attribute, so we can display them both in the editor and on the front end.

“/src/blocks/related-posts/searchposts.js”:

const { Button, MenuGroup, MenuItem, SelectControl, TextControl } = wp.components;
const { Component, Fragment } = wp.element;
  
export class SearchPostsControl extends Component {
    constructor(props) {
        super(props);
        this.state = {
            selectedButtons: [],
            resultObjects: [],
            resultButtons: []
        };
        this.buildResultButtons = this.buildResultButtons.bind(this);
        this.changePostType = this.changePostType.bind(this);
        this.updateSelectedIds = this.updateSelectedIds.bind(this);
        this.searchFor = this.searchFor.bind(this);
    }
  
    changePostType(newType) {
        // Clear postIds, update postType Attribute
        let { setAttributes } = this.props;
        setAttributes({ postType: newType });
        // Clear state and run a new search
        this.setState({ resultObjects: [], resultButtons: []}, this.searchFor(newType, ''));
    }
  
    searchFor(searchPostType = '', keyword = '') {
        let { attributes: { postType } } = this.props;
        let finalPostType = postType;
        // If a post type was explicitly passed to the function, use that instead
        if(searchPostType != '') {
            finalPostType = searchPostType;
        }
        // Make REST API call to get post objects - excluding current ID, but including the postType and keyword if present
        let currentId = wp.data.select('core/editor').getCurrentPostId();
        let path;
        if(keyword != '') {
            path = '/wp/v2/' + finalPostType + '?search=' + keyword + '&exclude=' + currentId;
  
        } else {
            path = '/wp/v2/' + finalPostType + '?exclude=' + currentId;
        }
        wp.apiFetch( { path: path } ).then( ( posts ) => {
            this.setState({ resultObjects: posts });
        }).then( () => this.buildResultButtons() );
    }
  
    buildResultButtons() {
        let { setAttributes } = this.props;
        let resultButtons = this.state.resultObjects.map(function(item, ind) {
            let isChecked = item.checked;
            // Save the opposite value for onClick
            // Must have default true, because if nothing is selected, it's false, and true is what it should change to
            let toCheck = true;
            if(isChecked == true) {
                toCheck = false;
            }
            return(
                <MenuItem id={ item.id } data-ischecked={ isChecked } onClick={ () => this.updateSelectedIds(parseInt(event.target.id), toCheck) }
                >
                    { item.title.rendered }
                </MenuItem>
            );
        }, this);
        // Save timestamp in milliseconds - this forces the setAttributes call for postIds to work
        let timeNow = Date.now();
        this.setState({ resultButtons: resultButtons }, setAttributes({ updated: timeNow }));
    }
  
    updateSelectedIds(id, val) {
        let { attributes: { postIds } } = this.props;
        let stateSelected = postIds;
        // Update copy of selectedIds
        if(val == true) {
            stateSelected.push(id);
        } else {
            let idIndex = stateSelected.indexOf(id);
            stateSelected.splice(idIndex, 1);
        }
        // Update copy of resultObjects
        let posts = this.state.resultObjects;
        for(var i = 0; i < posts.length; i++) {
            // if this post ID is in attributes, set checked to true
            posts[i].checked = false;
            for(var j = 0; j < stateSelected.length; j++) {
                if(posts[i].id === stateSelected[j]) {
                    posts[i].checked = true;
                    break;
                }
            }
        }
        // Save resultObjects to state, and then rebuild result buttons
        this.setState({ resultObjects: posts }, function() {
            this.buildResultButtons();
        });
    }
  
    componentDidMount() {
        this.getStartingData();
    }
  
    getStartingData() {
        this.searchFor('');
    }
  
    render() {
        let { attributes: { postType } } = this.props;
        let label = 'Search for ' + postType + ' to display';
        return(
            <div className='search-posts-control'>
                <div className='posts-selected'>
                    <h2>Currently selected:</h2>
                    <SelectControl
                        label='Post Type'
                        value={ postType }
                        options={ [
                            { label: 'Post', value: 'posts' },
                            { label: 'Page', value: 'pages' },
                        ] }
                        onChange={ (val) => { this.changePostType(val) } }
                    />
                </div>
                <div className='posts-search'>
                    <h2>Add to selections:</h2>
                    <TextControl
                        label={ label }
                        type='search'
                        onChange={ (val) => this.searchFor('', val) }
                    />
                    <MenuGroup label='Search Results' className='posts-list' >
                        { this.state.resultButtons }
                    </MenuGroup>
                </div>
            </div>
        );
    }
}

On line 8, we’ve added the final state – `selectedButtons`. Like the others, it starts out as an empty array.

Note that on line 15, we’re now binding `updateSelectedIds()` to `this`. Now that we’re building out its full functionality, we’re going to need access to this component’s state.

Let’s skip down to lines 69-95, where most of the changes are happening. First, we get the current value of the “postIds” attribute (in case anything is already selected). We save that to a local variable, `stateSelected`. If the value is true – meaning the post was previously unselected, and now the user has clicked to select it – we add the post ID to `stateSelected` array. If the value is false, we remove it from the array.

Next, we access the `resultObjects` state – the post objects. We loop through them, and if any of those post IDs are now in the `stateSelected` array – meaning one of the posts in the search results has been selected – we set their “checked” property to true. We’ll use that shortly to visually show the user that the post they just clicked is now selected.

Finally, we save the updated `resultObjects` to state, and we use a callback (so we’re sure that update has finished first) to call `buildResultButtons()` again. This is when, if one of the posts in the search results has been selected, it will actually be updated.

On lines 50-56, we’re now looking at the “checked” property of each post object. Instead of assuming the user has just added the block, our function can now handle that default but also a situation where the user has interacted with some of the buttons and selected them.

On line 61, the `return()` portion of `buildResultButtons()` has a `data-ischecked` property. Once a post ID is selected, this value becomes true, and the editor CSS we’ve already added will make it look like a checkbox inside the button is now checked.

Step 7: Display selected posts

At this point, if you add the block and click on some of the posts, you can visually see in those search results which posts you’ve chosen. But what if you have to search multiple times? Every time the search results change, you could lose track of which posts you’ve chosen.

Also, what if you publish this block and come back to it a year later? Chances are the default search results will show much newer posts, and you won’t be able to tell which posts are selected unless you exit editing mode.

Let’s make it easier to tell what’s selected – and easier to remove selections – by filling out our “Current selections” section.

Take a deep breath. This is the last of the JavaScript!

const { Button, MenuGroup, MenuItem, SelectControl, TextControl } = wp.components;
const { Component, Fragment } = wp.element;
 
export class SearchPostsControl extends Component {
    constructor(props) {
        super(props);
        this.state = {
            selectedButtons: [],
            resultObjects: [],
            resultButtons: []
        };
        this.buildResultButtons = this.buildResultButtons.bind(this);
        this.buildSelectedButtons = this.buildSelectedButtons.bind(this);
        this.changePostType = this.changePostType.bind(this);
        this.getStartingData = this.getStartingData.bind(this);
        this.searchFor = this.searchFor.bind(this);
        this.updateSelectedIds = this.updateSelectedIds.bind(this);
    }
 
    changePostType(newType) {
        // Clear postIds, update postType Attribute
        let { setAttributes } = this.props;
        setAttributes({ postIds: [], postType: newType });
        // Clear state and run a new search
        this.setState({ selectedButtons: [], resultObjects: [], resultButtons: []}, this.searchFor(newType, ''));
    }
 
    searchFor(searchPostType = '', keyword = '') {
        let { attributes: { postIds, postType } } = this.props;
        let finalPostType = postType;
        // If a post type was explicitly passed to the function, use that instead
        if(searchPostType != '') {
            finalPostType = searchPostType;
        }
        // Make REST API call to get post objects - excluding current ID, but including the postType and keyword if present
        let currentId = wp.data.select('core/editor').getCurrentPostId();
        let path;
        if(keyword != '') {
            path = '/wp/v2/' + finalPostType + '?search=' + keyword + '&exclude=' + currentId;
 
        } else {
            path = '/wp/v2/' + finalPostType + '?exclude=' + currentId;
        }
        wp.apiFetch( { path: path } ).then( ( posts ) => {
            for(var i = 0; i < posts.length; i++) {
                // if this post ID is in selectedIds state, set checked to true
                posts[i].checked = false;
                for(var j = 0; j < postIds.length; j++) {
                    if(posts[i].id === postIds[j]) {
                        posts[i].checked = true;
                        break;
                    }
                }
            }
            this.setState({ resultObjects: posts });
        }).then( () => this.buildResultButtons() );
    }
 
    buildSelectedButtons() {
        let { attributes: { postIds, postType } } = this.props;
        // If post IDs are selected, get their titles and show buttons
        if(postIds.length > 0) {
            let selectionButtons = postIds.map(async(item) => {
                let path = '/wp/v2/' + postType + '/' + item;
                return wp.apiFetch( { path: path } ).then( (post) => {
                    return(
                        <Button
                            isDefault
                            isDestructive
                            onClick={ () => this.updateSelectedIds(item, false) }
                        >
                            { post.title.rendered }
                        </Button>
                    );
                });
            });
            Promise.all(selectionButtons).then((finalButtons) =>
                this.setState({ selectedButtons: finalButtons })
            );
        }
        // If no post IDs, show paragraph
        else {
            this.setState({ selectedButtons: <p>None selected</p> });
        }
    }
 
    buildResultButtons() {
        let { setAttributes } = this.props;
        let resultButtons = this.state.resultObjects.map(function(item, ind) {
            // Determine whether this item is checked
            let isChecked = item.checked;
            // Save the opposite value for onClick
            // Must have default true, because if nothing is selected, it's false, and true is what it should change to
            let toCheck = true;
            if(isChecked == true) {
                toCheck = false;
            }
            return(
                <MenuItem
                    id={ item.id }
                    data-ischecked={ isChecked }
                    onClick={ () => this.updateSelectedIds(parseInt(event.target.id), toCheck) }
                >
                    { item.title.rendered }
                </MenuItem>
            );
        }, this);
        // Save timestamp in milliseconds - this forces the setAttributes call for postIds to work
        let timeNow = Date.now();
        this.setState({ resultButtons: resultButtons }, setAttributes({ updated: timeNow }));
    }
 
    updateSelectedIds(id, val) {
        let { attributes: { postIds } } = this.props;
        let stateSelected = postIds;
        // Update copy of selectedIds
        if(val == true) {
            stateSelected.push(id);
        } else {
            let idIndex = stateSelected.indexOf(id);
            stateSelected.splice(idIndex, 1);
        }
        // Update copy of resultObjects
        let posts = this.state.resultObjects;
        for(var i = 0; i < posts.length; i++) {
            // if this post ID is in attributes, set checked to true
            posts[i].checked = false;
            for(var j = 0; j < stateSelected.length; j++) {
                if(posts[i].id === stateSelected[j]) {
                    posts[i].checked = true;
                    break;
                }
            }
        }
        // Save resultObjects to state, and then rebuild result buttons
        this.setState({ resultObjects: posts }, function() {
            this.buildSelectedButtons();
            this.buildResultButtons();
        });
    }
 
    componentDidMount() {
        this.getStartingData();
    }
 
    getStartingData() {
        this.buildSelectedButtons();
        this.searchFor('');
    }
 
    render() {
        let { attributes: { postType } } = this.props;
        let label = 'Search for ' + postType + ' to display';
        return(
            <div className='search-posts-control'>
                <div className='posts-selected'>
                    <h2>Currently selected:</h2>
                    <SelectControl
                        label='Post Type'
                        value={ postType }
                        options={ [
                            { label: 'Post', value: 'posts' },
                            { label: 'Page', value: 'pages' },
                        ] }
                        onChange={ (val) => { this.changePostType(val) } }
                    />
                    { this.state.selectedButtons }
                </div>
                <div className='posts-search'>
                    <h2>Add to selections:</h2>
                    <TextControl
                        label={ label }
                        type='search'
                        onChange={ (val) => this.searchFor('', val) }
                    />
                    <MenuGroup label='Search Results' className='posts-list' >
                        { this.state.resultButtons }
                    </MenuGroup>
                </div>
            </div>
        );
    }
}

Line 8 adds our final state: “selectedButtons”. Like the other parts of state, this starts out as an empty array.

Line 13 binds our new `buildSelectedButtons()` function.

Lines 59-85 create our main new feature – the `buildSelectedButtons()` function. First, we get the current “postIds” and “postType” attributes. Then, if there are any IDs, we map the postIds array. Inside the map, first we get the full post object from the REST API. Since `wp.apiFetch()` is a Promise, we’re able to use `.then()` to return a <Button> for the post only after we get the post object back. That way, we can use the post title as the button’s text, since the post ID wouldn’t be very user-friendly.

Finally, we use `Promise.all().then()`, which is a way to make sure all the promises have resolved and then complete another action, to set the `selectedButtons` state to hold the buttons.

If there aren’t any IDs in “postIds”, we instead set state to a short paragraph informing the user that they haven’t selected any posts yet.

Now that we have our main function, there are a couple of other bits to update.

In our `changePostType()` function, when the user changes the post type, we do two additional things. Instead of just setting the “postType” attribute, we also reset the “postIds” attribute to an empty array on line 23. Similarly, on line 25, we now have one additional state to clear – the `selectedButtons`.

Our `searchFor()` function also needs some updates now that we’re keeping track of which posts are selected. We now pull the “postIds” attribute as well as “postType”. We continue our REST API call to whichever endpoint we need, and then on lines 45-54 our `wp.apiFetch().then()` function does a little more.

We now loop through all the search-result posts, and inside that loop through our postIds, to see if any of them are a match. If so, we set a “checked” property to true for that search-result post. If not, we set the “checked” property to false. This property is what our `buildSelectedButtons()` function looks at to determine whether to set the button’s `data-checked` attribute to true or false, and that is what drives whether the button displays a checkmark (post is selected) or an empty checkbox (post is not selected).

Our `updateSelectedIds()` function also needs a tweak. On line 137, once the `resultObjects` state is set, we need to call `buildSelectedButtons()` to rebuild the selections.

On line 146, we’ve added a call to the new function in `getStartingData()`. This is for re-editing. Let’s say our trusty user adds our block to a new post called Post A, publishes the post, and leaves it alone for awhile. The following week, they publish a new post called Post B.

Post B is highly related to Post A, so our user goes back into Post A to add Post B as a related post. If we didn’t call `buildSelectedButtons()` when the block first loaded in the editor, the buttons wouldn’t exist on load, so it wouldn’t be clear to our user what posts had already been chosen.

Line 167 contains the last bit of magic. Our `render()` function now always shows whatever is in the `selectedButtons` state. This will either be an array of buttons – one for each post that’s been selected – or that paragraph that tells the user nothing has been selected yet.

That’s it – the JavaScript is done!

…But wait, there’s more.

Step 8: Server-side render with PHP

If you’ve added the current block to an editor, it’s pretty exciting being able to search for posts and pages and add and delete them. But what about actually using that data? If you exit editing mode in the editor, or try to view the page on the front end, you’ll see our little hard-coded div as a reminder… we still need to take the attributes and translate them into the final content.

Since this block uses ServerSideRender, there’s a lot more PHP than you would find in an average block. Just how much PHP depends on how you want to display your posts. You might want to display all of your content in the same way, or you might choose to add conditionals so you can show Posts very differently than you do Pages. This part is up to you, though I’m including an example of different Posts and Pages to get you started.

Let’s add a few front-end CSS touches to make this block look polished.

“/src/blocks/related-posts/style.scss”

div.related-block { margin:1em; padding:1em; background:#ddd; box-shadow:0 0 1px #777; }
div.related-posts { display:flex; flex-wrap:wrap; justify-content:space-around; }
div.related-post { flex:1 0 200px; max-width:200px; max-height:200px; margin:.5em; position:relative; }
div.related-post span { position:absolute; bottom:0; left:0; padding:.5em; font-size:.8em; line-height:1em; width:100%; background:rgba(0, 0, 0, .8); color:#fff; }
div.related-post a:hover span, div.related-post a:focus span { background:rgba(0, 50, 10, .8); }

The last of our edits will be to “/plugin.php”.

<?php
/*
Plugin Name: Related Posts tutorial block
*/
add_action('init', 'my_register_related_block');
function my_register_related_block() {
    // register our JavaScript
    wp_register_script(
        'related-block',
        plugins_url('/build/index.js', __FILE__),
        array('wp-blocks', 'wp-element', 'wp-editor')
    );
    // register our front-end styles
    wp_register_style(
        'related-block-style',
        plugins_url('/build/style.css', __FILE__),
        array('wp-block-library')
    );
    // register our editor styles
    wp_register_style(
        'related-block-edit-style',
        plugins_url('/build/editor.css', __FILE__),
        array('wp-edit-blocks')
    );
    // register our block
    register_block_type('my/related-posts', array(
        'editor_script' => 'related-block',
        'editor_style' => 'related-block-edit-style',
        'style' => 'related-block-style',
        'render_callback' => 'get_related_posts',
        'attributes' => array(
            'editMode' => array(
                'type' => 'boolean',
                'default' => true
            ),
            'postIds' => array(
                'type' => 'array',
                'default' => []
            ),
            'postType' => array(
                'type' => 'string',
                'default' => 'posts'
            ),
            'updated' => array(
                'type' => 'string',
                'default' => ''
            )
        )
    ));
}
 
function get_related_posts($attributes) {
    $output = '<div class="related-block"><h2>You might also like...</h2>';
    // Posts
    if($attributes[postType] == 'posts') {
        $output .= '<div class="related-posts">';
        foreach($attributes[postIds] as $postId) {
            $postObject = get_post($postId);
            $output .= '<div class="related-post"><a href="' . get_the_permalink($postObject) . '">';
            if(has_post_thumbnail($postObject)) {
                $thumbId = get_post_thumbnail_id($postObject);
                $altText = get_post_meta($thumbId, '_wp_attachment_image_alt', true);
                $output .= '<img src="' . get_the_post_thumbnail_url($postObject, array(200,200)) . '" alt="' . $altText . '" />';
            } else {
                $output .= '<img src="https://via.placeholder.com/200" alt="No image found" />';
            }
            $output .= '<span>' . $postObject->post_title . '</span></a></div>';
        }
        $output .= '</div>';
    // Pages
    } elseif($attributes[postType] == 'pages') {
        $output .= '<ul>';
        foreach($attributes[postIds] as $postId) {
            $postObject = get_post($postId);
            $output .= '<li><a href="' . get_the_permalink($postObject) . '">' . $postObject->post_title . '</a></li>';
        }
        $output .= '</ul>';
    }
    // Always return output
    return $output;
}
?>

Nothing has changed about our initial block setup. All of the changes are from line 52 on, in the `get_related_posts()` callback function. This is the function that server-side renders the block.

This example always outputs one wrapper div, div.related-block. Inside that div, there’s a heading that says “You might also like…” followed by the related posts or pages. If the user has chosen Posts, this example outputs a grid including the featured image and title of each post. If the user has instead chosen Pages, it outputs a simple unorganized list with no images.

Changing a regular block’s HTML

If you’ve built “regular” blocks before where you actually save HTML to the database, you’re probably familiar with the headache of changing markup. You have two choices:

Option A – you can update just your JavaScript `save()` function, and then go manually fix every place you’ve added the block. When you only change your `save()` function’s HTML output, the Block Editor throws an error saying it found different output saved in the database than what it expected. If you’re working on a new block, this is slightly annoying but not a big deal. But if you’ve been using a block for awhile and face this every time you run into another place you used the block, it can be a nightmare.

Option B – you can add a `deprecated` property to your block. This tells the Block Editor, “Hey, this block used to have a different HTML output. Here’s what it looked like, so you can recognize it and update things for me.” The downside here is, this code has to stay in your block forever. So, if you keep changing markup, your code gets more and more bloated.

Changing a server-side-render block’s HTML

One really nice thing about server-side rendering is, if you ever want to change the HTML you’re outputting, you’re free to do so without touching any JavaScript or running into JS errors. PHP always handles rendering, both in the editor and on the front end, so the JavaScript isn’t trying to go back and parse anything. It can just read the attribute comments it’s saved to the database and move on.

Conclusion

I hope by breaking everything up into steps, I’ve helped you (and my future self) better understand how this block is set up and why. However, I’m still fairly new to block development, and I’m sure I don’t always code things optimally. So, if you cringed or cried out “That’s not the right way to do that!” while you read through, or even just “That code isn’t working like she said it would!”, I would be very grateful if you’d reach out and let me know what I could do better.

5 Comments

  1. Melissa

    Elaine, this is a great tutorial! Thank you for it. I followed it step-by-step and everything worked except the posts array did not actually save (so nothing shows up on the page or when I reload the editor). I went in and replaced the full code with what you had on git and still no luck. The big difference is that I am still using cgb blocks and the block is part of a plugin with multiple blocks. The only differences are in the php file and the location of my files. Do you have any initial thoughts on this? Thanks!

    • Elaine

      Hi Melissa – I’m glad it was helpful! I did find that with this version, when you add the block to a post, you have to change the post type at least once. So if your default post type is “post,” change it to something else, and then back to “post”, before you select any posts. For some reason, that caused WP to save the attributes to the database and display everything.

      I did find a code fix, and I’m working on a couple of other fixes now – one is calling the REST API once for all the selected buttons, instead of calling it separately for each post ID; the other is handling post IDs that get deleted externally. Once I have those in place I’ll come back and update both Git and this tutorial.

Leave a Reply

Your email address will not be published. Required fields are marked *