Adding a custom sidebar in the WordPress Block Editor

Once again, I find myself noting down a code technique because the online documentation elsewhere is lacking. This time, I’m adding a custom “sidebar” panel to the WordPress Block Editor.

In a regular WordPress installation—without plugins and complex options which the Theme can provide—there are two sidebars: one for the site (or post) you’re editing, and one for the specific block you’re editing. Adding a new PanelBody1 to one of them can be the most obvious approach, but there are already a number of panels available and the view can quickly become swamped if you’re not careful.

A better approach can be to add a new sidebar, in which you can gather all of your options and fields, and lay them out in a considered way so that the user finds what they’re looking for more easily. This can also be a much better approach if you’re only providing the options to a user with sufficient user capabilities.

The code in this post is in React JSX syntax and so it’ll need to be compiled before loading it in the editor.

Adding the “plugin sidebar”

This sidebar is usually added by a plugin, hence the component name PluginSidebar. This component is the current version of the deprecated PluginDocumentSettingPanel, which is referenced in quite a lot of online code examples: hence this post.

The PluginSidebar is provided by the WordPress “Editor” Package, so make sure you’re using @wordpress/editor and not the outdated @wordpress/edit-post. (For the sake of easy writing, the code here deals specifically with posts of type Page, and not (for example) Posts or the Site Editor.).

import { TextareaControl, PanelBody } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { PluginSidebar } from '@wordpress/editor';
import { _x } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';

const sidebarKey = 'sht-technical-admin';

const TechnicalAdminSidebar = () => {
    const postType = useSelect(select => select('core/editor').getCurrentPostType(), []);
    const meta = useSelect(select => select('core/editor').getEditedPostAttribute('meta'), []);
    const user = useSelect(select => select('core').getCurrentUser(), []);
    const { editPost } = useDispatch('core/editor');

    const isAdmin = user?.capabilities?.includes('manage_options');

    if (postType !== 'page' || !isAdmin) {
        return null;
    }

    return (
        <PluginSidebar
            name={sidebarKey}
            title={_x('Technical administration', 'Plugin sidebar title', 'sht')}
            icon='admin-generic'
        >
            <PanelBody title={_x('SEA Code', 'Panel body title', 'sht')} initialOpen={true}>
                <TextareaControl
                    label={_x('SEA Code', 'Textarea label', 'sht')}
                    value={meta?.sea_code || ''}
                    onChange={value => editPost({ meta: { ...meta, sea_code: value } })}
                    rows={10}
                    help={_x('Help text to explain the use of the field.', 'Textarea help', 'sht')}
                />
            </PanelBody>
        </PluginSidebar>
    );
};

registerPlugin(sidebarKey, { render: TechnicalAdminSidebar });

This code will add a new sidebar panel if the current user has the sufficient “capability” assigned. The editorial interface will not be changed in any other way and the pre-existing sidebars for page and block will remain unaffected.

Example of the custom sidebar

Controlling access to the sidebar

In this instance, the PluginSidebar will only be displayed if the current user has the “manage_options” capability assigned. (Usually only those with the administrator role have this capability. An explanation of when to use roles and when to use capabilities is a separate topic, perhaps for another time.)

I couldn’t find a suitable reference on how to use canUser to get this specific capability, so I added some custom server-side REST API code to extend the user data response. As you can see, this data is only output as part of the REST API response if the user who is requesting the data has the “manage_option” capability. This is the case if the user who is using the editor is an administrator.

register_rest_field('user', 'capabilities', [
	'get_callback' => function ($user) {

		if (!current_user_can('manage_options')) {
			return [];
		}

		$user_object = get_userdata($user['id'] ?? null);
		return is_object($user_object) ? array_keys(array_filter($user_object->allcaps)) : [];
	},
	'schema' => [
		'type'  => 'array',
		'items' => ['type' => 'string'],
	],
]);

Saving and using the data

If you take a look at the React code, you should be able to see that it uses useSelect and useDispatch to get and set the value of a post meta field. This is a clean approach and allows me to use the REST API as it’s intended in the Block Editor, and also allows uncomplicated access to the database value through all of the usual means. In this specific case, I use the wp_head hook to output the value in the frontend of the website.

(This does, of course, require that the administrator of the site is responsible enough not to output random malicious code through this feature. If you have a good way of sanitising this kind of output, then I’m all ears.)

add_action('wp_head',function (){
	$sea_code = trim(strip_tags(get_post_meta(get_the_ID(), 'sea_code', true)));
	if (!empty($sea_code)) {
		echo '<!-- SEA CODE START -->' . chr(10) . '<script>' . $sea_code . '</script>' . chr(10) . '<!-- SEA CODE END -->' . chr(10);
	}
});

Footnotes

  1. My code uses the PanelBody component directly, but the official documentation shows that it should be used inside a Panel component. This causes layout problems whenever I’ve tried to use the wrapping component, so I just omit it. This seems to work fine. ↩︎

Leave a Reply

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