implenton - Mészáros Róbert

Scheduled GitHub Actions

For FormBus, I used two GitHub workflows. One is for code quality check by running ESList. The other is to publish a new version of the package on npm.

Both workflows are triggered when a commit is pushed to a branch. While everything happens without any extra effort, I'm the one who is initializing the events.

But GitHub Actions are not limited to these kinds of workflows. You can also schedule events. You can run workflows at specific intervals, and this opens up new possibilities for automation.

Because you can choose to run a Ubuntu image, you can do almost anything with a bit of scripting.

For example, you can rebuild your website every midnight on Netlify. Simply use curl to hit a webhook.

name: Scheduled workflow
    - cron: '0 0 * * *'
    name: Rebuild website
    runs-on: ubuntu-latest
      - name: Build hook request
        run: curl -X POST -d '{}'

Forgiving regex to extract key-value pairs from plain text files

Recording data manually (typing them with your fingers) in plain text files is still a viable option, even though not that common.

If what you are recording consists of multiple data values, you need some kind of key-value format. A simple format like this does the job:

First key:
Lorem ipsum dolor

Second key:
Nunc volutpat cursus

The rules can be summarized like this:

  • the key ends with a colon :
  • the value is on the next line
  • key-value pairs are delimited by empty lines

Whatever format you choose, you have to leave room for human error, e.g., extra spaces are very common.

Or you might want to allow some flexibility, or you expect long strings of text that should be broken into multiple lines to increase readability.

Here's an entry with some exaggerated formatting issues:

Lorem ipsum

Beta gamma:
dolor sit amet
consectetur adipiscing

delta: quam vehicula   

Curabitur interdum massa

Maecenas ac felis


Morbi at lobortis

In the end, if you are looking to extract a list of keys and values, you need a regex rule that goes beyond the basics:

        "Beta gamma",
        "Lorem ipsum",
        "dolor sit amet\nconsectetur adipiscing\nelit",
        "quam vehicula",
        "Curabitur interdum massa",
        "Maecenas ac felis",
        "Morbi at lobortis"

The regex that allows this amount of forgiveness looks like this:

[ ]*(.+):[ ]*\n?((?:.+\n?[^\s])*)

Use this pattern with the preg_match_all function, and you have a solid start to do something with the data.

To deconstruct the pattern, head over to RegEx101 where you will see the captured groups highlighted and the tokens annotated.

Commit without file changes in Git

If you have a workflow where commits trigger certain events (e.g., CI/CD), and you have to do a test commit just to see what happens, then use the --alow-empty flag instead of committing a dummy change in a file.

git commit -m 'Test CI' --allow-empty

The Git man page describes the -allow-empy flag like this:

Usually recording a commit that has the exact same tree as its sole parent commit is a mistake, and the command prevents you from making such a commit. This option bypasses the safety, and is primarily for use by foreign SCM interface scripts.

SCM stands for source-control management. Git is one of the many options; others are Subversion, CVS, etc.

Hands-on guide for curl

I used curl from pinging a website to downloading a file or scrapping entire websites with it, but it never crossed my mind to interact with my mail provider with it.

Everything curl is an extensive guide for all things curl, mostly written by Daniel Stenberg, the founder of cURL project.

This is not a dry, technical help manual, but very hands-on, with many examples.

In the Using curl section, you will find how to read or send an email with curl.

Global state management for Alpine.js

Spruce recently hit version 2. If you are not familiar with it, it's a global state management library for Alpine.js. Currently, the go-to solution if you want to share data between components.

The major update comes with breaking changes, but the update is well worth it; it's an improvement over the previous version.

Bootstrap, RTL, and logical properties

Bootstrap 5 Beta 1 was released with RTL (right-to-left) support.

From this version, the class naming convention uses start and end instead of left, right when it comes to spacing utilities, like margin. This is similar to how flex properties work.

Even if you are not using Bootstrap, following this naming practice makes your code more future-proof.

Command Line Interface Guidelines

Command Line Interface Guidelines is "an open-source guide to help you write better command-line programs, taking traditional UNIX principles and updating them for the modern day".

Even if you can't make use of the proposed guidelines at the moment, I recommend going over the Foreword and the Philosophy section as it contains some general ideas related to CLI and computer interaction.

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 can take 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 that 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 to 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" }