Not everything needs to be secure

Just saving this for future reference. I got on the “all HTTPS all the time” bandwagon without questioning it, because enough of the sites I create do collect user data that needs to be secure. But some — like this blog, for instance — do not.

But here’s an angle on it that I hadn’t considered:

If Google succeeds, it will make a lot of the web’s history inaccessible. People put stuff on the web precisely so it would be preserved over time. That’s why it’s important that no one has the power to change what the web is.

Dave Winer

Google of course is always trying to change what the web is, just as Facebook does. I really got into a lather over AMP because it was immediately clear to me as a web developer how it is bad for the open web. Forcing everything to HTTPS is not quite as obviously “wrong,” but when you investigate it… yeah, it is.

This site uses HTTPS because… well, why not? I use Let’s Encrypt, so it’s free and easy. And I configured the server to automatically redirect HTTP traffic, so old links still work. But people shouldn’t be expected to understand what I understand about the web in order to use it… and not just as passive consumers, but as active contributors.

That’s the real power of the web, and what we lose when we let companies like Google or Facebook change the nature of what the web is.

I’d like to end with another quote from Winer:

The web is not safe. That is correct. We don’t want every place to be safe. So people can be wild and experiment and try out new ideas. It’s why the web has been the proving ground for so much incredible stuff over its history.

Lots of things aren’t safe. Crossing the street. Bike riding in Manhattan. Falling in love. We do them anyway. You can’t be safe all the time. Life itself isn’t safe.

Occam’s Razor as it applies to DevOps

A client emailed me today about an unusual problem. The “Create an Account” link on their website was not pointing to a page of their site, but instead to an IP address. An IP address that, when I ran a whois on it, turned out to be owned by China Telecom.

That was disconcerting. I’m not passing any judgment on anyone in particular in any country in particular, but the fact is, a hugely disproportionate number of online attacks against U.S. sites come from a small number of foreign countries, China being one of them. So, I was alarmed.

My first guess, since I’ve seen WordPress sites get hacked… a few times (much less often since I started using Wordfence as standard practice on every site I build), was that hackers had somehow hijacked this link and were trying to route my client’s users over to their systems in some kind of spoofing/phishing/whatever attack.

Only, the link didn’t actually do anything. There’s no web server listening on that IP address. Hmm.

Next up I looked at the HTML source, since I wasn’t quite sure if the code in question was being generated by their theme (which I built) or by a plugin like WooCommerce, so I wanted to see the CSS classes on the wrapper. Sure enough, it was in code I had built.

When in doubt, always blame your own code first.

But the fact that it was my code wasn’t what really caught my attention. It was the fact that the URL in the HTML wasn’t the IP address at all. It was just a string of 8 digits.

Suddenly it was clear to me that this was not a hack attempt, and China Telecom (or anyone using its services) had precisely zero to do with whatever was going on. This was a peculiar code problem, and it was at least indirectly my fault.

That 8-digit number… it looked very familiar. I knew that, due to some of the peculiarities of this client’s site, the ID values in their wp_posts table are on that order of magnitude. Bingo. This wasn’t a URL at all. It was a WordPress post ID, being incorrectly inserted here because Advanced Custom Fields was outputting the raw ID value instead of using it to look up the corresponding post URL.

Why was that happening? Well, that was ultimately my fault too, having to do with some logic that was designed to tell ACF where to load my custom field groups. When the site was built, I had that code running (technically) too early, then ACF released an update that started throwing up admin notices if you did that, so I rewrote it to run later… only I didn’t realize that my change was causing my code to get applied to a hook after it had already run, so… it wasn’t working. It was a very isolated set of circumstances, so it went unnoticed for months. As far as I know, the only user-facing issue it created was this weird integer URL on the “Create an Account” link on a page few people ever visit.

The simplest explanation is the most likely.

Even if the details are anything but simple. Was someone in China trying to hack my client’s site to phish for their customers’ data, or did I just write some flawed code? Hmm.

On Apple products and the subjectivity of “expensive”

Until the Apple Watch came along, I had gone 20 years without wearing a watch. And before that, the most I had ever spent on one was $50. A watch was not an object I was willing to spend more money on than that. But then Apple released a very compelling product, and I went for it. (Even if, in retrospect, the original iteration of the Apple Watch was… underwhelming.)

I’ve owned three Apple Watches: the original (a.k.a. “Series 0”), a Series 3, and my current one, a Series 5. But after visiting a local Apple Store yesterday and looking at the latest models, I decided it was time to upgrade my Apple Watch experience once again, so today I ordered a Series 8… and for the first time, I sprung for the extra $100 to get one with cellular. Now I can finally go out running without bringing my phone. This will transform my running experience… once the snow melts and I’m back outside, that is. (But actually it will transform my experience on the indoor track too. I hate having my phone in my pocket when I run!)

Of course, running without my iPhone means that also means I need wireless earbuds. I may be willing to drop a lot of cash on a lot of Apple products, but AirPods are one area where I feel their options are way overpriced. Well… maybe not overpriced for what they offer, but more than I am willing to spend, for features I don’t really care about.

What’s worse, I can’t stand hard plastic earbuds, and Apple’s only option with silicone tips is the $250 “Pro” models. There’s no way I’m paying that much of a premium for something so easily lost. I’d rather stick with my tried-and-true $9 wired Panasonic earbuds. (But of course you can’t use wired earbuds with the watch!)

Anyway, I found these no-name Bluetooth earbuds on Amazon for $20. I’m going to give them a try before I go for anything higher-end. Hopefully they won’t explode while I’m wearing them!

Bootstrap 5 Carousel: position captions outside (i.e. below) the images

(If you want to get right to the point, a full code example is provided at the bottom of this post.)

I’m not a Bootstrap expert. During most of its evolution, I’ve mostly ignored it in favor of rolling my own… everything. I finally really embraced Bootstrap when I had a rush project in October 2022 that was way too precisely designed to work with WordPress, especially Gutenberg. (And the client didn’t need editing capabilities.) So I decided to hand-code it, but to use Bootstrap to… uh… bootstrap my HTML/CSS layouts.

I think Bootstrap 5 is excellent. I wish Gutenberg was built on top of it instead of the idiosyncratic house of cards it’s actually built on, but whatever.

As it happens, I’m actually now using Bootstrap 5 with Gutenberg for some custom blocks, specifically a Carousel block. One of the options I want to provide in my block is the ability to show the carousel’s captions and controls outside of the image, but apparently, at least with Bootstrap 5, that’s not an option.

I decided to Google for a quick solution before creating my own and I came across… this. I’m sure it gets the job done, but it seems severely over-engineered, so here I’m presenting my own comparatively simple, CSS-only solution.

Basically there are two things you need to do: 1) move the absolute-positioned caption text below the image, and 2) add padding to the bottom of the container, so the caption has somewhere to go instead of just overlapping the content below it.

Let’s start with the second item first:

.carousel { padding-bottom: 4rem; }

There may be some trial and error here, as you need to make sure you’re accommodating captions of varying length. I will admit this is not fully thought out here, and unlike the rest of what is about to follow, it may be a deal breaker under certain circumstances. But let’s assume your captions are a fairly consistent length, and you can determine how much padding you need.

Getting the caption pushed below the images is easy…

.carousel-caption { top: 100%; }

…except, oops, vertical overflow is hidden. Let’s fix that:

.carousel-inner { overflow: visible; }

Of course, if you have your transition effect set to slide (which is the default), that now spews stuff all over the page in an ugly way. But we can fix that by hiding overflow on the outer carousel element instead:

.carousel { overflow: hidden; }

You might, at this point, wonder why I didn’t just set overflow-y: visible on .carousel-inner which seems perfectly reasonable, and which, of course, I tried. But for reasons I couldn’t be bothered with investigating, that ended up causing .carousel-inner to just show a vertical scrollbar and not display the caption unless you scrolled it. Ugh. No matter, the above takes care of it.

That’s pretty much it, as far as the captions go. But if you’re using the controls (previous/next arrows) or indicators (dots/lines for the number of slides and current selection), you’ll notice there’s some weirdness to their placement, so let’s fix that too. The indicators just get shoved to the bottom of the container, so your bottom padding can accommodate that. But if you want to move them back up onto the image, you just need to offset that extra padding, like this:

.carousel-indicators { bottom: 4rem; }

Make that value the same as the bottom padding you added to .carousel itself.

As for the controls, since you’ve made the overall container taller, they’re now a bit too low rather than being vertically centered on the image. Guess what… setting their bottom value to match the extra bottom padding fixes their placement too!

.carousel-control-next, .carousel-control-prev { bottom: 4rem; }

So, putting it all together as concisely as possible, here’s what we have:

.carousel { overflow: hidden; padding-bottom: 4rem; }
.carousel-caption { top: 100%; }
.carousel-inner { overflow: visible; }
.carousel-control-next, .carousel-control-prev, .carousel-indicators { bottom: 4rem; }

WooCommerce code snippet: convert the Order Notes field into an EU VAT ID field

The scenario: My WooCommerce store has no need for the Order Notes field. In fact, up until now I had it hidden on the checkout page. But what my site does need is an EU VAT ID field. The portion of my business that takes place in Europe is, so far, well below the VAT reporting threshold, but I am increasingly being asked by customers to provide an invoice containing their VAT ID.

Well, my site does already produce PDF invoices. But there was no way for customers to include their VAT ID on the invoice. Until now.

A simple code snippet converts the existing WooCommerce Order Notes field into an EU VAT ID field, including changing it from a <textarea> to an <input type="text"> field. Put this in your theme’s functions.php file, or wherever else is appropriate in your setup:

add_filter('woocommerce_checkout_fields', function($fields) {
    $fields['order']['order_comments'] = array_merge(
            'class' => array('eu-only'),
            'label' => 'EU VAT ID',
            'type' => 'text',
    return $fields;
}, 10, 1);

That’s it. You can stop right here. But you may notice a line in there that seems unnecessary: 'class' => array('eu-only')

What’s that all about? Well, I’m using that with a bit of jQuery to enhance the functionality: only showing my new EU VAT ID field when the user’s selected Billing Country is an EU country.

Here’s a JavaScript function you can use to dynamically show/hide elements with an .eu-only CSS class, depending on a given passed-in value:

function showHideEUOnly(val) {
    var eu = ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'ES', 'SE', 'GB'];
    if (eu.indexOf(val) != -1) {
    else {

I obtained the list of EU VAT-applicable countries here, and I decided to include 'GB' (the United Kingdom) in the list, despite… y’know, uh… Brexit, because I have the vague impression that UK customers may still be impacted by VAT policies. (Being a dumb American, I don’t know much about it. I think maybe the UK has its own VAT now? Anyway, suffice to say, you may want to modify your list of 2-digit country codes in the eu array, as applicable to your situation.)

This function isn’t going to do anything unless it’s called though, so let’s do that. Here’s a bit of jQuery that will call it both on the initial page load and any time the Billing Country field changes:

jQuery(function() {
    if (jQuery('body').hasClass('woocommerce-checkout')) {
        jQuery('select[name="billing_country"]').on('change', function() {

Both of these JavaScript snippets can go in a script.js file in your theme, or wherever else is appropriate in your setup.

That’s the end of the story, but there’s more…

Incidentally, there’s more to my custom setup. I’ve significantly modified the layout of my checkout page. I’ve got WooCommerce configured for billing addresses only, with this setting in Shipping Options:

I then used CSS to hide everything else in the second column (including, up until now, the Order Notes field) and moved the product summary and payment information up into that space. Explaining all of that is outside the scope of this post, but one thing you may find useful is my CSS for hiding the “Additional Information” <h3> heading. This selector is a bit of overkill, but it works:

body.woocommerce-checkout .woocommerce > form.checkout .col2-set > .col-2 .woocommerce-additional-fields > h3:first-child { display: none; }

There’s context in my CSS file to justify all of that, but you should be able to accomplish the same with just this:

.woocommerce-additional-fields > h3 { display: none; }