Building a custom WordPress block without a React/JSX build process

I spent two solid days trying to make this work. And it’s not purely because I’m too stubborn to learn React/JSX. That’s part of it.

It’s more that I’m too stubborn to introduce an arbitrary build process into my development workflow. I have my own idiosyncratic ways of doing things, and I’m coding solo, so I generally don’t need to conform to someone else’s standard practices. As long as what I’m doing has its own internal consistency and safeguards, and produces a reliable product, who cares how I get there?

I actually have introduced a few “build” processes into my own workflow, using Git (out of necessity) for version control of my WordPress Plugin Directory plugins, my own custom versioning process for non-repository plugins, and “minifying” all of my JavaScript and CSS (using online minifiers).

But I am not doing any significant amount of block development. Certainly not enough to learn JSX and implement the arbitrary build process into my workflow, adding thousands of unnecessary dependency files to my development codebase, etc.

For the latest version of ICS Calendar Pro (version 6), currently in development, I just needed to add a simple custom block that lets users insert a calendar (a Custom Post Type) by selecting one from a dropdown list. It’s a more user-friendly option than what they’ve had to do up to this point: copying a small code snippet from the CPT’s admin page and pasting it into a Shortcode block.

Ideally my new block would render a preview of the calendar in the Block Editor, but that’s secondary to just having a way to insert the block itself and pick from the list of saved calendars.

I found the official list of block development examples, and made some progress with the “no build” example. I also found a great article from Cello Expressions (maker of the Sheet Music Library plugin), called Dynamic Blocks with Block.json, Vanilla JS, and No Build Process. I really thought that was going to get me exactly what I needed… until I realized that their particular needs did not involve having any kind of configuration options in the Block Editor itself.

Then I found DahmaniAdame/block.js on GitHub Gist. It was exactly what I needed. A straightforward, no-build example, using Vanilla JavaScript, of a simple block with a dropdown list of posts in the inspector sidebar.

The only problem: it worked perfectly if I stuck with built-in post types, but it would cause a vaguely defined block error when I switched it to using my CPT.

Eventually I realized the problem was here:

const title = post.meta.some_meta1 && post.meta.some_meta1 !== ''
		? post.meta.some_meta1
		: post.title.rendered;

For an as-yet undetermined reason, I’m not able to retrieve meta data about my CPT. (Yes, I changed some_meta1 to the actual key of my meta data field, view.) I decided I didn’t really need to retrieve that meta data, so I just removed the conditional, i.e.:

const title = post.title.rendered;

With that change (and a few others to change the post type I’m querying and remove a few other things I didn’t really need), my block was working perfectly! So much so, in fact, that I decided to try to push it to do more things. There, I ran into some trouble, because my block does a lot of JavaScript stuff when it renders, and I haven’t (yet) figured out how to get that JavaScript to run when the content renders in the Block Editor, so the HTML output of the block does get inserted into the Block Editor when the user selects from the dropdown, but the JavaScript that actually reveals it on the front-end page isn’t running. (Yes, I used enqueue_block_editor_assets to load my JavaScript and CSS files. And I also added logic to try to force my plugin’s JavaScript initialization event to trigger, but something more complicated is going on.)

Anyway… it’s still not 100% perfect, but I really wish I had found that Gist before I spent two full days banging my head against the wall over this!

How to get Safari not to display the disclosure triangle on an HTML5 <details> <summary> tag in 2025

I hate blog posts with “in (insert current year here)” in the title, but it seems important here, because none of the stuff I’m finding in the usual locations, even StackOverflow, CSS-Tricks, or the Mozilla Developer Network really seems to be getting this right.

So, you’re using the HTML5 <details> tag, and you want to customize the disclosure toggle element on the <summary> tag? Most browsers play nicely with summary::marker in CSS, but not Safari.

Forget this (the old rule everyone has been recommending but that doesn’t seem to do anything, at least, not anymore):

details summary::-webkit-details-marker { display: none; }

Fortunately, the solution (at least, what seems at this very moment to be working for me) is even easier, once you find it:

details summary { list-style: none; }

(OK I did actually find this solution on the MDN link above, but it also includes the bit that I said doesn’t seem to do anything.)

A practical example

Let’s say you want to get rid of the triangle and replace it with a plus sign (+) that changes to a minus sign (-) when toggled open. You can certainly improve the aesthetics of this with color, or images, or some margin/padding tweaks, but this is the essence. Confirmed to be all you need in both Safari and Firefox (the only browsers I currently have installed on my Mac), but it should work in Chrome too.

details summary {
    list-style: none;
}
details summary::before {
    content: "+";
}
details[open] summary::before {
    content: "−";
}

How to replace line break tags (<br>) with spaces in CSS

You can’t. This post on TimonWeb with the same title as mine is wrong. As is this one from Gallagher Website Design, which of course it is, because it’s exactly the same.

And now that all of the search engines (even my beloved DuckDuckGo) are using AI to gather answers, the LLMs are permanently ensconcing this incorrect answer to the question.

It doesn’t work.

I get why it might seem like it works. In fact it’s kind of not really doing anything… at least in terms of adding the space. The only reason there’s a space between the words where the <br> tag got removed is because there’s a space there in the HTML.

HTML honors spaces. Well, one space. If there’s a line break, that’s the space. If you get rid of the line break and there’s still space in the HTML, then HTML renders a space. But if removing the <br> tag pushes two words together, they’re really going to get pushed together.

The only way (at least, the only way I’ve been able to find, and I’ve spent more time on this than it’s worth) to get there to be a space when you remove the <br> tag, is to actually have a space in the HTML.

Since I’m dealing with WordPress, which is the reason it’s a problem, I have also come up with a WordPress-specific solution. The reason it’s a problem in WordPress, at least in the Block Editor (a.k.a. Gutenberg) is that if you insert a line break in your text, i.e. by typing Shift-Enter, it inserts a <br> tag without any space between the words.

I guess Gutenberg is trying to be efficient by not adding an unnecessary space. You know, just like how it makes efficient use of CSS class names and inline styles like this:

<div class="wp-block-columns are-vertically-aligned-center is-layout-flex wp-container-core-columns-is-layout-7d06e0e1 wp-block-columns-is-layout-flex" style="margin-top:var(--wp--preset--spacing--80);margin-bottom:var(--wp--preset--spacing--80)">

I’d insert the eyeroll emoji here, but I use my No Nonsense plugin to prevent WordPress from unnecessarily… uh, I mean efficiently, loading a bunch of emoji-related JavaScript on every page of every one of the 42% of the websites in the world it powers.

Let’s see, now where was I? You can use the the_content filter to force WordPress to add a space after <br> tags in page content:

function my_the_content($content) {
    $content = str_replace('<br>', '<br> ', $content);
    return $content;
}
add_filter('the_content', 'my_the_content', 11, 1);

(Yes, this might be adding an extra space in places where there’s already a space after the <br> but it doesn’t matter… as I said, HTML honors the space, but only one space.)

Once you’ve got that in place, then you can write some CSS to selectively remove <br> tags from display. In my case, I want to remove them in Cover blocks on mobile devices, and my mobile breakpoint is 1024 pixels. So here’s the CSS I wrote:

@media screen and (max-width: 1024px) {
    .wp-block-cover__inner-container br { display: none; }
}

Note here that I’m just hiding the <br> tags altogether, which is all you really can do. But as long as that <br> tag is followed by an actual space (or an actual line break) in the HTML code itself, then you’ll get a space between the words.

No one planned for this

I have a client whose website I’ve been managing for over a decade.

Long before I was working with them, the client registered their domain with a company called NameSecure. As you can see by clicking that link, their website still exists, but they were long ago purchased by Network Solutions (one of the worst companies in the world). NameSecure’s zombie, circa 2002 website lives on though. I can still log into the client’s account. I’ll come back to that in a minute.

When I started working with the client, my preferred hosting company was Media Temple. As you can see by clicking that link, their website doesn’t exist anymore, after they were acquired and dismantled several years ago by GoDaddy. The company whose name, I still believe, was supposed to be “God Addy,” as in “the god of addresses,” when some nascent techbro registered it in 1999, before marketing stepped in and retconned it to the slightly less awful name that the world has known ever since they kicked off their sexist, years-long Super Bowl ad campaign of the early 2000s. (And yes, I’ve read the contrary history on Wikipedia.)

GoDaddy mostly absorbed everything Media Temple-related, and the client set me up with “Delegate Access” so I can get into their GoDaddy account. That’s where we made our DNS changes a few years ago to point to the Digital Ocean server I was running their site on until Digital Ocean became a disaster. And it’s where I went again last year when I moved their site to WP Engine. (Don’t even get me started.)

Well they’ve had it with WP Engine’s ludicrous overage charges (and so have I), so now we’re moving on to the hosting-company-that-only-mostly-sucks-rather-than-completely-sucks du jour, Hostinger. So far the worst I can say about Hostinger is that sometimes their web control panel just stops responding. But I blame React (and therefore, indirectly, Facebook/Meta) for that.

I’ve got their site ready to go on Hostinger, so now it’s just down to the DNS update.

I started by logging into GoDaddy, and was surprised to see there are no domains listed for their account. Then I pieced together what must have happened. We had canceled their Media Temple hosting several years ago, but there was still a (no longer necessary) annual SSL certificate auto-renewal that kept happening. The client finally reached out to me in time to stop the auto-renewal this year, so we canceled it, effectively ending all of the client’s paid services with GoDaddy. But their domain continued to use the Media Temple name servers.

I’m just guessing here, but it seems that canceling the last paid GoDaddy service also shut off access to the domain management tools. So, their undead DNS zone file endures on Media Temple’s zombie name servers, but with no way to make any edits.

No worries, I thought. Let’s just jump back to NameSecure. It’s ancient, but it is where their domain is actually registered, and they do let you set your domain to use their own name servers. (I always prefer to keep name servers at the registrar, whenever possible, anyway.)

But… oh dear. NameSecure’s zone file editor only lets you create A, MX, and CNAME records. No TXT records. Seriously… no TXT records??? In 2025, those are essential for a variety of reasons. So, we can’t do that.

OK… then I guess our only alternative is to… ugh… move the name servers over to Hostinger. That is obviously what Hostinger wants us to do. That’s what hosting companies always want you to do, and the main reason I don’t want to do it. It’s a lot more of a pain to switch hosting companies when your current host is handling your DNS. It’s not truly lock-in, but it might as well be.

Anyway, that’s where we are. No one back in the late ’90s and early 2000s planned for the 2025 domain registrar zombie apocalypse.

And now, I’m just sitting here, waiting for the name server change to propagate, so I can actually edit the client’s zone file in Hostinger. This is the next-worst thing I can say about Hostinger: if you host domains, let them edit the DNS zone file even before they’re using your name servers! It would be so, so nice if I could plug in all of the client’s MX records while I’m waiting for propagation. As it is, there’s going to be some indeterminate amount of time when their email absolutely will stop working, between when the name server change propagates, making Hostinger allow me to actually edit the damn zone file, and when I actually get those records plugged in and they propagate.

Ideally, this should only take a few minutes. But name server changes can take anywhere from a few minutes to a day or more to propagate. Do I just need to sit here on the edge of my seat that entire time?

I guess so.

Well, that has me all caught up on this stupid situation. I don’t have much else to say about it… I just have to wait. But all of this just feels like another nail in the coffin of this stupid industry that never planned ahead and also never seems to learn from past mistakes.

“We’re sorry, your order for $3.7 trillion worth of bumper stickers has failed.”

One of my web clients emailed me about a rash of fake orders they received a few days ago.

Usually these fake orders are for very small amounts, because the people (or bots) placing them are testing stolen credit cards, so they try to fly under the radar.

I’m not sure what was going on here. Needless to say, my client was unable to fulfill this order. And I’m not masking their IP address (the only part of this that isn’t fake) because they don’t deserve anonymity.

This person (most likely not a bot in this case) placed 174 orders in a span of just under 2 hours. Many of them were for reasonable amounts. Some were… like this. A few included clumsy attempts at code injection. None of them went through, of course, and no real harm was done. I’m just a bit mystified at what this person was hoping to achieve.

It almost seems like the work of a bored teenager, trying to flex their ersatz hacking skills.

It’s just kind of unfortunate that this wasn’t real. The sales tax alone could have fully funded the Minnesota and Hennepin County governments for quite a while. (I looked it up. The state’s revenue is around $2 billion per month. At that rate, this single order could have kept the state fully funded for about 13 years. Oh, and this was only one of several orders for amounts like this.)

It gets even better! I checked the order notes, and it turns out PayPal rejected the order because the quantity exceeded the allowed value. No shit. Why doesn’t WooCommerce limit the quantity input field? Ridiculous.

OK, so how do we fix this?

It seems obvious that we should be able to set a maximum value for the quantity input. And there are WooCommerce add-ons that give you a lot of flexibility to set minimum and maximum quantities on an individual product basis. There are plenty of reasons a complex ecommerce store might want or need those kinds of rules, but I want something more basic. I just want to set a hard upper limit of 100 on the quantity field for all products in this store.

The WooCommerce quantity field is an <input type="number"> field. And that field has min and max attributes you can use to set the valid number range. A number input is a special text input field that has up and down arrow keys that let you increment/decrement the value by amounts controlled by the step attribute.

But here is what I find absolutely fucking maddening about number inputs: you can also type in them. You can type whatever you want in them. You can type letters. Special characters. And worst, numbers outside the range dictated by the min and max attributes. Browsers are “supposed” to validate the input and prevent submitting the form, but in practice I’ve seen that both be extremely hit-or-miss, and also vary wildly from browser to browser.

It makes no sense to me at all why browsers allow anything other than numerical characters in a number input, i.e. the digits 0-9, plus periods, commas, and hyphens. Why? WHY???

Sometimes I really hate this job.

Anyway… let’s pretend number inputs don’t suck. Hooks make it easy to set the max value for the WooCommerce quantity input. Here’s what I used:

function my_woocommerce_quantity_input_args($args, $product) {
    $args['max_value'] = 100;
    return $args;
}
add_filter(
    'woocommerce_quantity_input_args',
    'my_woocommerce_quantity_input_args',
    PHP_INT_MAX - 1, 2
);

You could add a conditional to only set the max_value to 100 when it’s not already set (the default value is -1 by the way), although I was having some trouble getting that to work and I didn’t want to bother troubleshooting it any further… especially since, as indicated by my previous rant, max doesn’t actually do anything to prevent users from typing in a ludicrously large number, or the entire text of the Declaration of Independence, if they so desire. (OK, to be fair, in my testing, Safari’s validation does prevent you from submitting the form with text in the field. But it totally ignores the max value. And I don’t understand why it doesn’t just wipe out invalid characters as you type.)

Side note on my code snippet above: I like to ensure that my code isn’t going to get changed by something else that runs later, so I have a habit of setting the priority on WordPress hooks to PHP_INT_MAX - 1 to get it to run as late as possible. (I use the - 1 to give myself an “out” if I need to add something even later.)

Anyway… we still need a solution, and that’s where we have to resort to JavaScript. (Specifically I’m using jQuery because it’s WordPress.) It’s 2025 and we still have to use JavaScript to work around mind-numbingly basic limitations of HTML. ¯\_(ツ)_/¯

Here’s the jQuery code I wrote to both a) force the quantity input to only accept non-negative integers, and b) actually force the min and max limits, as the user types. (Of course someone could still get around this by disabling JavaScript, but I’m pretty sure there are other steps in the ordering process that would fail without it anyway.)

jQuery('input[type="number"].qty').on('change keyup', function() {
    var min = jQuery(this).attr('min');
    var max = jQuery(this).attr('max');
    var q = jQuery(this).val();
    q = q.replace(/\D/g, '');
    if (typeof min != 'undefined' && min != '') {
        if (Number(q) < Number(min)) { q = ''; }
    }
    if (typeof max != 'undefined' && max != '') {
        if (Number(q) > Number(max)) { q = ''; }
    }
    jQuery(this).val(q);
});

A few notes on this:

  1. I’m using input[type="number"].qty as the jQuery selector because it efficiently identifies the quantity input on both the individual product page and the cart page in WooCommerce. I also probably don’t need to watch both the change and keyup events but I wanted to cover all the bases.
  2. After some trial and error with confusing behavior, I determined that the best UX for this comes from casting the values as Number() right at the point of comparison, and changing the value not to the min or max value, but to just empty it out.
  3. The .replace() method here uses a regular expression that rejects all non-numeric characters. If you need to accept negative numbers or fractions, you’ll need to change /\D/g to /[^\d.-]/g or /[^\d,-]/g if your locale uses commas as the decimal delimiter… or just /[^\d.,-]/g if you want to cover all the bases (and/or allow thousands separators).
  4. Still I am finding some odd behavior here with .replace() that I have never experienced before, and I’m having trouble narrowing down why. It seems to me that this code should be simply stripping non-numeric characters out of the string. So for example if I typed “56g” the value for q would become 56. But what I’m observing is that if the initial value for q is composed entirely of digits, it passes it along, but if there are any non-numeric characters in the string, the entire value gets wiped out. (I tested this with various regular expressions, e.g. \D, [^\d], [^0-9] etc. And I used alert() to output the value of q immediately after the .replace() line, to make sure it wasn’t an issue with the subsequent logic. I am absolutely mystified by this, but I’ve already spent more time on it than I can bill to the client, so I need to stop here.)
  5. I’ve been working with JavaScript for over 20 years, but it’s never been my primary area of emphasis… it still kind of feels like a foreign language to me, whereas PHP is my native tongue. So I don’t always do my JavaScript in the most “JavaScript-y” way.