Adding A Custom Icon To The Social Links Block Should Not Be This Hard
I recently came across the need to add a custom icon to the Social Links block in WordPress. The icon I needed to add was for a platform called PlayHQ, a sports league management platform for many major sports in Australia. While it’s not exactly a social media platform, it’s pretty important to have quick and easy links to the platform as it’s where all fixtures, ladders and stats are kept. This also felt like a good opportunity to sink my teeth into some native block editor code, having given up trying to keep up with development years ago because of how unstable it was initially, and having almost exclusively relied on Advanced Custom Fields for custom blocks ever since.
I assumed it would be as simple as filtering an array of icons. How very wrong I was.
My first step was to look at the Gutenberg repository on GitHub. I’m not sure how much of what’s been merged into core is still available as uncompiled files, nor where I would find them, so searching the plugin meant it would easier to find what I was looking for. It didn’t take me long to find where all the blocks were stored, under /packages/block-library/src. Searching for the social links block, I found there were two similarly named blocks, social-link
and social-links
. This wasn’t a huge surprise, as the social links block acts as a wrapper block to house the icons, each icon being a block itself. As I’m just trying to add a custom icon I figured I probably just needed to look at the social-link
block.
Now there are dozens of different services here and only one social link block. This is pretty smart, as for the most part the block will be the same, just the icon changing. In the variations.js
file I could see a big array of all the different services. Each service being an object with some important information like the name of the icon, and the icon itself in SVG format stored as a variable. “This is easy”, I started to think to myself, “I’ve worked with block variations before”.
Except that wasn’t quite true. Following a little more research, it turns out this was the first time I’d encountered block variations. What I was thinking of was block styles, while very similar in theory they are very different in implementation. Adding custom block styles are easy by comparison, you can create a style with just a couple of lines of PHP. On the other hand, you can’t register block variations with PHP. This excellent video from Nick Diego at WP Engine does a really good job of highlighting the difference.
So now it was off to the documentation to see how to register a block variation. And to my pleasant surprise, the official documentation was quite good in this regard. Following the example code, I was able to reproduce creating a block variation in no time with just a few lines of code. Much credit must go to the documentation team as documentation around developing for the block editor has come a long way in recent times.
There was just one small problem, the icon. In all the core services, the icon is stored as a variable. Storing the icon as an SVG in a string isn’t hard, except that’s not what’s needed here. Believe me, I tried it. Using some form of ES6 trickery, it appears as though it’s storing it as some form of literal SVG object. I don’t know for certain, but that’s just my educated guess. The world of modern Javascript is still largely a mystery to me at this stage. This is where the real fun would begin.
Other than having to spend a bit of time learning how block variations work, everything made sense up until this point. However it was now clear I needed to add a new build process specifically for this block, just so that it could have an icon. Now I felt it was starting to get into the realm of overkill. This was also what had turned me away from developing for the block editor when it was first introduced. Because of my unfamiliararity with ES6, I was relying on other peoples build scripts, usually Zac Gordon’s from his WordPress Block Development course. Unfortunately, because of how incredibly unstable the environment was, the build script would stop working every time there was an update, and because I was relying on someone else’s build script, I had to wait for it to be updated accordingly to start developing again, and after being stuck in that cycle for a few weeks I walked away and never looked back. Thankfully, very soon after Block Lab (now Genesis Custom Blocks) and Advanced Custom Fields announced PHP based block creation tools and I and many others breathed a large sigh of relief. So now at this point I’m quite apprehensive about having to rely on someone else’s build script that will likely break with each WordPress update. I was almost prepared to give up at this stage but thankfully I didn’t.
I soon discovered that WordPress now has a Node package called @wordpress/scripts
. A reusable collection of scripts provided by WordPress itself that includes the relevant build scripts that I need, yes please. At this stage I was still writing my custom code for this icon in the theme of the site I was working on, which was already using a custom Grunt script I had written to compile scripts and styles. My directory structure looks a bit like this:
- assets
- js
- src
- file1.js
- file2.js
- scripts.min.js
- src
- css
- src
- _file1.scss
- _file2.scss
- styles.min.css
- src
- js
If you’ve worked with the WordPress build scripts before, you will know that this structure is quite different to what the build script wants. So the first time I tried to run the build process for this specific script and saving the results alongside my other scripts, it wiped all my other scripts. If I wasn’t using Git for version control management, I would have lost all that previous work without warning. I tried changing the destination, and this time it wiped the entire theme. This is exactly the kind of terrible behaviour that deters people from using build scripts and the command line in general. Unfortunately most of the documentation around updating the destinations of these files is written for people who are creating custom blocks. Obviously, we aren’t creating a custom block here and as such there is no block.json
file to edit. I eventually found a helpful article that did help me improve things with a Webpack configuration file, but it still wasn’t quite flexible enough to get the files in the right places. It had turned out that setting up a build script was just as painful as I’d expected, although not quite for the reasons I had anticipated. I was almost ready to give up, but I decided to move future development of this project into a plugin and give it one last shot. And finally, after being careful to arrange my files in a way the build script expected demanded, it worked!
Now if you look carefully, you can see the icon working correctly in the block inserter. However, it wasn’t displaying in the block itself, we still had the default link icon. So it was back to the Gutenberg repo to figure out what was going on.
Looking at the social-list.js
file, we can see it has two handy functions, getIconBySite()
and getNameBySite()
. Both of these functions search the variations
variable we saw in the varations.js
file that had all the data about each icon, and return either the name or the icon based on which variation is being used. That’s all well and good, except that variable doesn’t include our new custom icon because it’s searching the original variable, not the current list of registered variations. That code is being compiled and run before we even run our code, so there’s seemingly no way for us to inject our new icon into the original list. Yet again, I had hit a roadblock and felt like giving up.
Then I remembered, the only person who responded to my initial tweet asking if anyone had any idea how to do this had suggested looking into the blocks.registerBlockType
filter. I knew of the filter but I’m not sure if I would have thought to look into it more if it wasn’t for Ross’ suggestion.
So after consulting the documentation for the blocks.registerBlockType
filter, it wasn’t immediately clear whether this was possible yet. The filter simply allows a variable called settings
to be changed, which has a ton of various different settings included in it. However, when I went back to the Gutenberg repository, I noticed in the index.js
file where the block is registered, there is a variable called settings
that is being passed to the block as it’s registered. And before it’s registered, there is an object added inside that settings
object called variations
, which is being imported from the variations.js
file. A console.log
confirmed my suspicions, this was it! I had found a way to filter the settings to include our custom icon in the variations
variable so the icon could load properly. So I added a wp.hooks.addFilter
call inside my wp.domReady
function and hit refresh.
And nothing happened.
It’s worth noting here that we’re starting to see the first signs of having to repeat ourselves to try and get this thing working. You’ll notice I’m now storing the PlayHQ icon object in a variable, and passing that variable to registerBlockVariation
as well as having to add it to the settings.variations
array. Variables are designed to be reused, but I don’t think it’s ideal having to pass the same variation data multiple times here. None of this would have even been neccesary if the getIconBySite()
and getNameBySite()
functions were checking against the registered variations for the block and not just the originally defined variations. I’m hoping that’s due to a technical limitation and not just laziness.
Adding scripts inside a document ready function is a very standard best practice. As someone who’s written most of their Javascript related to the jQuery library, pretty much every line of Javascript I’ve written has been inside a document ready function. You generally don’t want to be executing scripts before the document has finished loading, otherwise things will break. However in this case, for some reason, the filter never runs. When I moved the filter from inside the document ready function, it finally worked. At last, the icon now displayed in the block itself.
Unfortunately, you might notice that there are still issues here. The icon is showing up correctly in the block itself, but not in the contextual toolbar or the block settings panel on the right, and the name of the block is still just “Social Icon”. For crying out loud. To figure out why, we need to go back to the Gutenberg repository and have another look at the variations.js
file. At the bottom of that file we can see a little loop that’s setting an isActive
property for each variation. I guessed that the way it picks up which icon and name to display is based on this isActive
property, so I copied the code over from that variations.js
file and tried yet again.
At long last, it was finally finished. The variation was added, the icon and name showed up everywhere they were supposed to, and the build process worked seamlessly. Now to check the front end…
And now is where I started getting angry. I’d put in so much work to get to this point and on the front end, the thing that literally everyone else but me will see, I have nothing to show for it.
Now I’m effectively back to square one. Having looked at the content the block creates, it turns out it’s a dynamic block, so the icon doesn’t actually save any markup, including the icon. That means that the actual HTML is being defined somewhere in PHP. I feel much more comfortable working with PHP and having sorted the editor, I felt like I was on the home stretch at least.
Now back to the GitHub repository yet again, opening up the index.php
file gave me what I needed. I followed the rabbit trail until I found the block_core_social_link_services()
function. This looks awfully familiar. In fact, it’s effectively just the variations
Javascript variable, but written for PHP. Now in true WordPress fashion, there should be a filter at the end of the function that allows me to just inject my new icon.
Except there isn’t. There is no apply_filters
function call in the entire file. I checked the WordPress core code, just in case things were different between the Gutenberg plugin and core and found the same thing. No filter. Now there’s no technical limitation here, this is laziness to not bother to include a filter. Or, dare I say it, perhaps arrogance that the list was exhaustive and would never needed to be expanded upon by a third party. I sure hope not.
Again we’ll notice that the code is repeating itself again, and this time we can’t rely on variables. The SVG markup is being defined in both PHP and Javascript to be used for this block. I’m hoping there’s a good reason as to why the icons weren’t just saved as SVG files and imported as required instead of having to write the same code multiple times. I can forsee some new contributor who thinks that updating an icon following a redesign is an easy patch to make, only to be completely demoralised when they realise that the code is not DRY. I’m hoping there’s a good reason for this, because I’m not entirely sure why the developers have ignored such fundamental development practices.
Now that there’s no filter for the list of services and their icons, we’re going to have to rely on filtering the full markup of the block. Thankfully, as it’s a block variation, we can be fairly confident of the structure of the block. For this, we can use the render_block
filter. In fact, Gravity Forms has some good examples of how to manipulate HTML markup with PHP. So I tried to use the DOMDocument
class to replace the markup of the SVG with our new PlayHQ icon.
Warning: DOMDocument::loadHTML(): Tag svg invalid in Entity
Oh come on. It’s not even WordPress letting me down this time. Apparently DOMDocument
still can’t handle HTML5 tags, which is frankly embarassing.
So now that DOMDocument
is off the table, I had to figure out a new approach to replace the SVG markup. I’ve used the render_block
filter a few times in the past, while not ideal for this situation, I know at the very least I can edit the block markup. Thankfully, the block will only display one icon at a time, so will only have one SVG element. Considering that, I decided to try exploding the markup. Get the markup before the SVG, and the markup after the SVG, and inject the new SVG icon in the middle. As disgusting as it felt to write, the code worked. After dozens of hours across several days, plenty of swearing, screaming, hitting the desk and overthinking, we finally have a new social link icon working.
Well, except it’s not even done yet. There’s still styles to write for the different colour schemes. But at least I won’t have to jump through any hoops to write some CSS.
There is no good reason why it has to be this hard. While what I’m trying to achieve is absolutely niche, it’s quite sad to see such basic development principles like DRY code and allowing data to be filtered be completely abandoned. In an ideal world, I should just be able to add a filter that points to an SVG icon file.
That said, I’m far more optimistic about developing for the block editor than before I started this little experiment. I came into this prepared to be disappointed and almost expecting to be unable to complete it, but I did. The provision of the @wordpress/scripts
package and much improved documentation makes me feel far more confident in starting to approach development for the block editor again.
But it seems we still have a long way to go.