Way back when WP was simple and adding a sidebar to the editor was a simple metabox, I had a very straightforward setup with a box that, on page load, would tell you if the data in the post matched the remote API, and if not would tell you what to update.
My plan was to have it update on refresh, and then auto-correct if you press a button (because sometimes the API would be wrong, or grab the wrong account — loose searching on people’s names is always rough). But my plan was ripped asunder by this new editor thingy, Gutenberg.
I quickly ported over my simple solution and added a note “This does not refresh on page save, sorry.” and moved on.
Years later brings us to 2024 and November being my ‘funenployment’ month, where I worked on little things to keep myself sharp before I started at AwesomeMotive. Most of the work was fixing security issues, moving the plugin into the theme so there was less to manage, modernizing processes, upgrading libraries, and so on.
But one of those things was also making a real Gutenbergized sidebar that autoupdates (mostly).
What Are We Doing?
On LezWatch.TV, we collect actor information that is public and use it to generate our pages. So if you wanted to add in an actor, you put in their name, a bio, an image, and then all this extra data like websites, social media, birthdates, and so on. WikiData actually uses us to help determine gender and sexuality, so we pride ourselves on being accurate and regularly updated.
In return, we use WikiData to help ensure we’re showing the data for the right person! We do that via a simple search based on either their WikiData ID (QID), IMDb ID, or their name. The last one is pretty loose since actors can have the same name now (oh for the days when SAG didn’t allow that…). We use the QID to override the search in cases where it grabs the wrong person.
I built a CLI command that, once a week, checks actors for data validity. It makes sure the IMDb IDs and socials are formatted properly, it makes sure the dates are valid, and it pings WikiData to make sure the birth/death etc data is also correct.
With that already in place, all I needed was to call it.
You Need an API
The first thing you need to know about this, is that Gutenberg uses the JSON API to pull in data. You can have it pull in everything by custom post meta, but as I already have a CLI tool run by cron to generate that information, making a custom API call was actually going to be faster.
I went ahead and made it work in a few different ways (you can call it by IMDb ID, post ID, QID, and the slug) because I planned for the future. But really all any of them are doing is a search like this:
/**
* Get Wikidata by Post ID
*
* @param int $post_id
* @return array
*/
private function get_wikidata_by_post_id( $post_id ): array {
if ( get_post_type( $post_id ) !== 'post_type_actors' ) {
return array(
'error' => 'Invalid post ID',
);
}
$wikidata = ( new Debug_Actors() )->check_actors_wikidata( $post_id );
return array( $wikidata );
}
The return array is a list of the data we check for, and it either is a string of ‘matches’ /true
, or it’s an array with WikiData’s value and our value.
Making a Sidebar
Since we have our API already, we can jump to making a sidebar. Traditionally in Gutenberg, we make a sidebar panel for the block we’re adding in. If you want a custom panel, you can add in one with an icon on the Publish Bar:

While that’s great and all, I wanted this to be on the side by default for the actor, like Categories and Tags. Since YoastSEO (among others) can do this, I knew it had to be possible:

But when I started to search around, all anyone told me was how I had to use a block to make that show.
I knew it was bullshit.
Making a Sidebar – The Basics
The secret sauce I was looking for is decidedly simple.
const MetadataPanel = () => (
<PluginDocumentSettingPanel
name="lwtv-wikidata-panel"
title="WikiData Checker"
className="lwtv-wikidata-panel"
>
<PanelRow>
<div>
[PANEL STUFF HERE]
</div>
</PanelRow>
</PluginDocumentSettingPanel>
);
I knew about PanelRow
but finding PluginDocumentSettingPanel
took me far longer than it should have! The documentation doesn’t actually tell you ‘You can use this to make a panel on the Document settings!’ but it is obvious once you’ve done it.
Making it Refresh
This is a pared down version of the code, which I will link to at the end.
The short and simple way is I’m using UseEffect
to refresh:
useEffect(() => {
if (
postId &&
postType === 'post_type_actors' &&
postStatus !== 'auto-draft'
) {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(
`${siteURL}/wp-json/lwtv/v1/wikidata/${postId}`
);
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`
);
}
const data = await response.json();
setApiData(data);
setError(null);
} catch (err) {
setError(err.message);
setApiData(null);
} finally {
setIsLoading(false);
}
};
fetchData();
}
}, [postId, postType, postStatus, siteURL, refreshCounter]);
The reason I’m checking post type and status, is that I don’t want to try and run this if it’s not an actor, and if it’s not at least a real draft.
The constants are as follows:
const [apiData, setApiData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
Right below this I have a second check:
if (postType !== 'post_type_actors') {
return null;
}
That simply prevents the rest of the code from trying to run. You have to have it after the UseEffect
because JS is weird and does things in an order. If you have a return before it, it fails to pass a lint (and I enforce linting on this project).
How it works is on page load of an auto-draft, it tells you to save the post before it will check. As soon as you do save the post (with a title), it refreshes and tells you what it found, speeding up initial data entry!
But then there’s the issue of refreshing on demand.
HeartBeat Flatline – Use a Button
I did, at one point, have a functioning heartbeat checker. That can get pretty expensive and it calls the API too many times if you leave a window open. Instead, I made a button that uses a constant:
const [refreshCounter, setRefreshCounter] = useState(0);
and a handler:
const handleRefresh = () => {
setRefreshCounter((prevCounter) => prevCounter + 1);
};
Then the button itself:
<Button
variant="secondary"
onClick={handleRefresh}
isBusy={isLoading}
>
{isLoading ? 'Refreshing...' : 'Refresh'}
</Button>
Works like a champ.
Output the Data
The data output is the interesting bit, because I’m still not fully satisfied with how it looks.
I set up a filter to process the raw data:
const filteredPersonData = (personData) => {
const filteredEntries = Object.entries(personData).filter(
([key, value]) => {
const lowerCaseValue = String(value).toLowerCase();
return (
lowerCaseValue !== 'match' &&
lowerCaseValue !== 'n/a' &&
!['wikidata', 'id', 'name'].includes(key.toLowerCase())
);
}
);
return Object.fromEntries(filteredEntries);
};
The API returns the WikiData ID, the post ID, and the name, none of which need to be checked here, so I remove them. Otherwise it capitalizes things so they look grown up.
Then there’s a massive amount of code in the panel itself:
<div>
{isLoading && <Spinner />}
{error && <p>Error: {error}</p>}
{!isLoading && !error && apiData && (
<>
{apiData.map((item) => {
const [key, personData] = Object.entries(item)[0];
const filteredData = filteredPersonData(personData);
return (
<div key={key}>
<h3>{personData.name}</h3>
{Object.keys(filteredData).length ===
0 ? (
<p>[All data matches]</p>
) : (
<div>
{Object.entries( filteredData ).map(([subKey, value]) => (
<div key={subKey}>
<h4>{subKey}</h4>
{value && (
<ul>
{Object.entries( value ).map( ([ innerKey, innerValue, ]) => (
<li key={ innerKey }>
<strong>{innerKey}</strong>:{' '} <code>{innerValue || 'empty'}</code>
</li>
)
)}
</ul>
)}
{!value && 'empty'}
</div>
))}
</div>
)}
</div> );
})}
</>
)}
{!isLoading && !error && !apiData && (
<p>No data found for this post.</p>
)}
<Button />
</div>
<Spinner />
is from '@wordpress/components'
and is a default component.
Now, innerKey
is actually not a simple output. I wanted to capitalize the first letter and unlike PHP, there’s no ucfirst()
function, so it looks like this:
{innerKey .charAt( 0 ).toUpperCase() + innerKey.slice( 1 )}
Sometimes JavaScript makes me want to drink.
The Whole Code
You can find the whole block, with some extra bits I didn’t mention but I do for quality of life, on our GitHub repo for LezWatch.TV. We use the @wordpress/scripts
tooling to generate the blocks.
The source code is located in folders within /src/
– that’s where most (if not all) of your work will happen. Each new block gets a folder and in each folder there must be a block.json
file that stores all the metadata. Read Metadata in block.json if this is your first rodeo.
The blocks will automagically build anytime anyone runs npm run build
from the main folder. You can also run npm run build
from the blocks
folder.
All JS and CSS from blocks defined in blocks/*/block.json
get pushed to the blocks/build/
folder via the build process. PHP scans this directory and registers blocks in php/class-blocks.php
. The overall code is called from the /blocks/src/blocks.php
file.
The build subfolders are NOT stored in Git, because they’re not needed to be. We run the build via actions on deploy.
What It Looks Like

One of the things I want to do is have a way to say “use WikiData” or “use ours” to fill in each individual data point. Sadly sometimes it gets confused and uses the wrong person (there’s a Katherine with an E Hepburn!) so we do have a QID override, but even so there can be incorrect data.
WikiData often lists socials and websites that are defunct. Mostly that’s X these days.
Takeaways
It’s a little frustrating that I either have to do a complex ‘normal’ custom meta box with a lot of extra JS, or make an API. Since I already had the API, it’s no big, but sometimes I wish Gutenberg was a little more obvious with refreshing.
Also finding the right component to use for the sidebar panel was absolutely maddening. Every single document was about doing it with a block, and we weren’t adding blocks.
Finally, errors in Javascript remain the worst. Because I’m compiling code for Gutenberg, I have to hunt down the likely culprit, which is hard when you’re still newish to the code! Thankfully, JJ from XWP was an angel and taught me tons in my 2 years there. I adore her.