implenton - Mészáros Róbert

NPM Publish GitHub Action

Compared to Packagist, where a new version of a package is automatically fetched from VCS repository tags, on npm, you are required to publish (manually) a new version.

It's just running a CLI command, but it's an extra step in the process that can be easily overlooked.

To cut this extra step, you can use NPM Publish GitHub Action that triggers the publishing process when the version is changed in the package.json file.

It's well documented and can be done in a matter of minutes. Here's the commit for adding it to FormBus.

Alpine.js directives and WordPress sanitization

WordPress has functions with sensible defaults for when you want to filter untrusted HTML. To extend the default allowed list, you can use the wp_kses_allowed_html filter.

Mostly I see people reaching for this when dealing with complex SVG structures where lesser-known tags and attributes pop up.

Another scenario is when working with JavaScript libraries using directives, such as Alpine.js or Vue.js.

The problem with directives and WordPress

By default, if you run trough wp_kses_post an HTML containing Alpine.js directives, all are stripped out.

$alpineJsHtml = <<<'EOD'
    <div x-data="{ isVisible: false }">
        <button type="button"
            x-on:click.debounce.250="isVisible = !isVisible">
            Toggle with debounce
        <p x-show="isVisible">I'm visible</p>

echo wp_kses_post($alpineJsHtml);

The output:

    <button type="button">
        Toggle with debounce
    <p>I'm visible</p>

Behind the scenes, WordPress determines if an attribute is allowed using the wp_kses_attr_check function. Besides the data-* wildcard attribute, all other attributes have to be exactly predefined, and unfortunately, there is no filter attached to the function to pass custom conditions.

Alpine.js (2.6.0) has 14 directives, and most of them don't have a dynamic part or modifiers. Allowing them could look something like this:

    static function ($tags) {
        $alpinizedTags = ['div', 'section', 'template'];
        $alpineDirectives = [
            'x-cloak' => true,
            'x-data' => true,
            'x-if' => true,
            // ...

        foreach ($alpinizedTags as $alpinizedTag) {
            if (!isset($tags[$alpinizedTag])) {

            $tags[$alpinizedTag] = array_merge($alpineDirectives, $tags[$alpinizedTag]);

        return $tags;

Dynamic directives and modifiers

But with directives like x-on where you can listen for custom events, and add modifiers that represent milliseconds, there is no sane way to predefine all possible variations. Remember, the allowed attribute has to be an exact match; no wildcards are supported.

One option is to simply update the list whenever you use a new combination.

$alpineDirectives = [
    'x-on' => true, // not enough
    'x-on:click.debounce.250' => true,
    'x-on:change' => true,
    'x-on:open-menu' => 'true',
    // ...

Another solution is to deal with directives case by case basis instead of relying on a global list. This is how it would look with the wp_kses function:

echo wp_kses($alpineJsHtml, [
    'div' => [
        'x-data' => true,
    'button' => [
        'type' => true,
        'x-on:click.debounce.250' => true,

But we take can this idea further and construct the list of directives dynamically and allowing them automatically.

function allowedPostTagsWithAlpineAttrs($content)
    global $allowedposttags;

    // Anything that looks like an Alpine.js directive
    preg_match_all('/(x-[\w:.-]*)/', $content, $matches);

    if ($matches === false || empty($matches[0])) {
        return $allowedposttags;

    $allowedTags = $allowedposttags;
    $alpineAttrs = [];

    foreach ($matches[0] as $match) {
        $alpineAttrs[$match] = true;

    foreach ($allowedTags as $tag => $attributes) {
        $allowedTags[$tag] = array_merge($alpineAttrs, $attributes);

    return $allowedTags;

echo wp_kses($alpineJsHtml, allowedPostTagsWithAlpineAttrs($alpineJsHtml));

With this approach, we don't have to keep track of the modifiers or update the directives list if Alpine.js introduces a new one.

WordPress query parameters and developer experience

No matter how we implement our WordPress plugins, if we expect other (theme) developers to interact with our code, we should not expect more knowledge than it's assumed by WordPress core.

I like the fact that WordPress codebase is approachable and expressive, that you can do complex database queries without knowing SQL or the internals of the query class.

One way to keep the barrier low, maintain expressivity, and hide complexity is to introduce custom query parameters.

Let's say you provide a way to manage events. Instead of forcing theme developers to interact with your classes and methods, you can let them retrieve events using something that is already familiar to them:

$events = get_posts(
        'post_type' => 'event',
        'event' => [
            'location' => [
            'after' => '2020-08-15',

Internally then you can map this to a complex meta_query and date_query or even pass to your repository class.

Or you can extend existing paramaters with custom values:

$popularPosts = get_posts(
        'post_type' => 'post',
        'orderby' => 'popular',

Using the pre_get_post action, you can check the value of orderby and set the necessary query parameters. In general, it's a good idea to save others from knowing things that are not immediately important.

    static function (WP_Query $query) {
        if ($query->get('orderby') !== 'popular') {

        if ($query->get('post_type') !== 'post') {

        $query->set('orderby', 'meta_value_num post_date');
        $query->set('meta_key', '_your_meta_key_for_ranking');

Small details like these make a difference in the developer experience (DX).

Limit WordPress REST API route to an IP range

If you are exposing an API route for a specific service, check if they make requests or send responses from the same IP or IP range.

Especially if you are expecting payloads from webhooks or sensitive user data, it's a good security measure and easy to implement.

In WordPress, generally, the permission_callback is used for checking user's capabilities, but it's the appropriate place for doing other conditionals:

        'permission_callback' => static function (WP_REST_Request $request): bool {
            $ipRangeStart = ip2long('XXX.XXX.XXX.XX');
            $ipRangeEnd = ip2long('XXX.XXX.XX.XX');
            $requestIp = ip2long($_SERVER['REMOTE_ADDR']);

            return ($requestIp >= $ipRangeStart) && ($requestIp <= $ipRangeEnd);

HTML manipulation with PHP

While it would be nice from WordPress plugins to always offer a way to overwrite their template files, that’s not the reality.

This is especially frustrating when working with libraries like Alpine.js, which makes use of directives.

Parenthesis: A directive is just a HTML attribute that has a special meaning for the JS library, for example:

<div x-bind:class="{ '--open': isOpen }"></div>

Without Alpine.js the x-bind:class is a meaningless attribute that doesn't do anything.

DomDocument and DOMXPath

When there is no direct way of modifying the HTML, I tend to reach for the DomDocument class.

With DomDocument you can parse HTML code and remove parts of it, add new elements or attributes.

If you never used DomDocument before, here's how it typically looks and works:

$normalizedHtml = mb_convert_encoding($htmlContentThatComesFromSomewhere, 'HTML-ENTITIES', 'UTF-8');

// New up the objects and pass the HTML that you have
$dom = new DOMDocument();

$xPath = new DOMXPath($dom);
// Find all <sup> tags
$supTags = $xPath->query('//sup');

foreach ($supTags as $node) {
    // Get whatever that is in the <sup> tag
    $nodeContent = $node->nodeValue;

    // Create a new <a> tag
    $anchor = $dom->createElement('a');
    $anchor->setAttribute('href', '#');
    // Add the directives that are just attributes with certain value
    $anchor->setAttribute('x-data', '{}');
    $anchor->setAttribute('x-on:click', '$dispatch("open-footnotes")');
    // Set contents of the <a> tag to be the same as the <sup> tag
    $anchor->nodeValue = $nodeContent;

    // Replace the <sup> tag content with the <a> tag
    $node->nodeValue = '';

// Save the modification
$modifiedHtml = $dom->saveHTML();

This turns a HTML like this:

<p>Lorem ipsizzle<sup>1</sup> dolizzle sit break it down...</p>

into this:

<p>Lorem ipsizzle<sup><a href="#" x-data="{}" x-on:click="$dispatch('open-footnotes')">1</a></sup> dolizzle sit break it down...</p>

By the way, there are many wrapper packages that offer the same functionality and more with nicer API.

It's also handy to know that tools for converting CSS selector to XPath queries exist. This is because XPath queries quickly get more verbose than you expect. Selecting a plain HTML tag is straightforward, but selecting an element that has the has-blue-color class looks like this:

.//*[contains(concat(" ",normalize-space(@class)," ")," has-blue-color ")]

Nunjucks whitespace control and text file sitemaps

Most of the SEO tools (and WordPress plugins) generate an XML file for the sitemap. However, it’s perfectly fine to keep the sitemap in a simple text file that contains one URL per line.

If you are using Nunjucks as a templating engine, this is one of those rare cases when you have to care about the whitespace control.

As comparison, with the "regular" for loop tag:

{% for post in collections.sitemap %}
{{ post.url }}
{% endfor %}

the output looks like this:

The empty line before the first URL, between the URLs and after the last one, is part of the output. It's not a formatting mistake in this article.

This does not conform with the requirements, as we need exactly one URL per line without empty lines.

You can control the whitespace by adding a minus sign (-) to the start ({%) or end block (%}). If you add it, then it will strip all leading or trailing whitespace.

Here's what worked for me to generate the sitemap.txt:

{%- for post in collections.sitemap -%}
{{ post.url }}
{% endfor -%}

Notice how the beginning of the endfor does not have the - sign, but the rest do.

This resulted in the correct output, without empty lines:

CUBE CSS brings some fresh ideas

CUBE CSS is a CSS methodology put forward by Andy Bell. After using BEM for years, some ideas seem wild at first. Here's one of those.

Group ordering suggests that if we have multiple classes applied to a DOM element, we should group them to achieve clarity and communicate relatedness.

<div class="[ card ] [ section box ] [ bg-base color-primary ]"></div>

The usefulness of this becomes apparent if you consider utility-first CSS frameworks, like Tailwind CSS, because having ten, twenty, or more classes for one element is the norm.

Configure Netlify to run a specific PHP version

With all the hype around JS and Go based static site generators, it's easy to miss that you can use PHP with Netlify.

PHP comes preinstalled (with composer), but the default version is 5.6.

To switch to 7.4, you have to configure the build image. Create a netlify.toml config file in the root of your site repo. Define the environment variable like so:

    environment = { PHP_VERSION = "7.4" }