Stupid CSS tricks: Flexbox method for integrating a horizontal divider into a heading, with centered text

I’m mainly writing this here so I’ll find it again in a few years when I need to do this and can’t remember how I did it before.

Say you want a nice looking, centered text header integrated with a horizontal rule, with the line on both sides of the text and a bit of a gap, like this:

Here’s My Nice Header

The only HTML involved there is this:

<h4 class="my-nice-header">Here's My Nice Header</h4>

Besides Flexbox, obviously, I’m leaning heavily into CSS Pseudo-elements to make this work. It occurred to me immediately that I could use the ::before and ::after pseudo-elements for the lines, but my real flash of insight (at least it felt like a flash of insight to me) was using the ::first-line pseudo-element to apply Flexbox styling to the text itself, without needing anything like a nested <span> tag.

Here’s the exact CSS code I’ve included in the page to render this header:

.my-nice-header {
  align-items: center;
  display: flex;
  gap: 1rem;
  width: 100%;
}
.my-nice-header::before, .my-nice-header::after {
  background: gainsboro;
  content: '';
  display: block;
  flex: 1;
  height: 1px;
}
.my-nice-header::first-line {
  display: block;
  flex: 1;
  text-align: center;
}

Note: I removed some font styling and margins that aren’t pertinent to the actual “trick” itself. I left in the background color, because you need to specify some color for the lines not to be invisible.

YouTube’s recommendation algorithm is pushing AI-generated nostalgia slop on me

I watch a lot of YouTube. Most of what I watch on YouTube is related to music or video games, but I also have a penchant for videos about cooking, architecture, and the history of 20th century technology. A couple of my favorite channels are Tasting History with Max Miller and Phil Edwards.

By this point all of the algorithms know I am a 50-something GenXer, with a moderate affliction of nostalgia. So as much as I know listicles (or the video equivalent) are clickbait… well, I take the bait.

So, you know just as well as the algorithm did that I would not scroll past a video called “15 FORGOTTEN Sandwiches That FADED From Your Family Table.”

Go ahead, watch it.

I was struck immediately by a few things: first, the weird overuse of “aged film” effects on the apparently stock video clips that vaguely corresponded to the sandwiches being described.

Next, I noticed a weird monotony to the narrator’s delivery. I didn’t like it, but I figured it was just his style.

But that was when things got weird. Out of nowhere, the introduction of the “Mock Ham Salad Sandwich” was spoken in a different voice, with a strong Asian accent. Then it was back to Mr. Monotone.

Suddenly it all clicked. This entire video was AI generated.

I’m not sure if the video content itself is AI-generated, or if it’s just… um… AI-concatenated. I didn’t scrutinize it super closely, but I didn’t see any of the telltale signs, like mangled text, deformed human hands, objects spontaneously transforming into something else.

It might all just be stock footage. But a) I do know AI-generated video has improved a lot recently, and b) it also seems unlikely that they’d have found enough marginally-relevant (and some of this is very marginal) stock video footage for each of these sandwiches.

I was struck specifically by the pimento cheese sandwich, and how the shot of it shows a paper wrapper with the Masters logo. Yes, the pimento cheese sandwich is inextricably tied to the Masters golf event at Augusta National in Georgia. Why wasn’t that detail mentioned in the narration? (It’s probably worth noting that this is a fact I’m able to recall only because I saw a Facebook post about it a few days ago.)

The channel that posted this video only has two videos, both posted this week. Combined, they have fewer than 1000 views.

Both of their videos share a similar format. But what really freaked me out was that my YouTube home page also had another video from a different channel that was similarly nostalgia-stoking and, based on the little preview that played, had the same telltale “aged film” effect.

Who is behind this crap? How much worse is this all going to get?

Pro tip: AI isn’t ready to replace your experienced web developers yet

I’ve been meaning to write about this for a few months, and although I know LLMs are evolving rapidly, I think it’s probably still relevant. (Let’s see ChatGPT pull off convincingly human snark.)

Earlier this year, I received an email from a client.

First, I should probably just mention that I have different types of client relationships. The kind I prefer to have is one where I’m involved with their web project from the beginning. Those clients see the true value of what I offer. (And I don’t mean “value” as in “cheap.” I mean “value” as in “worth the premium price.”)

Then there are the clients who, for whatever reason, fell into a working relationship with me after their website was already live. I’m generally reluctant to take on ongoing support for websites I didn’t build, but for various reasons, it does sometimes happen. I still avoid it when I can.

Let’s just put it this way: I have never been hired to take over support for an existing site, logged in, and thought, wow, the person who built this is really good at making websites. There’s a reason the client stopped working with them. But. If that’s the past experience the client is bringing to their relationship with me, they probably think everyone who does what I do sucks. Usually I have the opportunity to convince them otherwise, but not always. Some clients come in with an unshakeable predisposition against anyone who does what I do… especially ones who charge my rates.

As you may have guessed from those last three paragraphs, the email came from one such client. They have apparently been working with someone else (cheaper) to redesign their site, but in the meantime (going on multiple years now) they’re still stuck with me making updates to the piece of garbage I inherited. But they definitely try to keep my hours to a minimum. Which, all things considered, I get.

So, it was funny when I received this email from the client. It included a couple of attachments, both plain text (.txt) files. The client said they had created a web form they needed me to post on the site.

Well, first of all, we have Gravity Forms installed on the site, as with every WordPress site I build that needs forms. So, why didn’t they use that? I have no idea.

I opened up the text files. One was actually an HTML file. It contained their form, and some JavaScript for conditional interactivity — showing/hiding certain fields based on the selections in other fields. It was clean code and it looked like it worked. I was… surprised. (No CSS though, and obviously no page layout elements.)

Then I took a look at the other text file. It contained PHP code. It was well-structured, valid code. Technically.

But… well… I kind of just have to show it to you (with identifying details redacted, of course):

<?php
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $to = "redacted@example.com";
    $subject = "Redacted Form Submission";
    $message = "";
    
    foreach ($_POST as $key => $value) {
        $message .= ucfirst($key) . ": " . htmlspecialchars($value) . "\n";
    }
    
    $headers = "From: no-reply@example.com";
    if (mail($to, $subject, $message, $headers)) {
        echo "Email sent successfully.";
    } else {
        echo "Email failed to send.";
    }
}
?>

Although this may have been a reasonable “my first web form” tutorial for learning PHP in 2005, you can’t use this code today. Do not use this code today.

Aside from the fact that it’s not a complete page in itself — I mean, do you really want the confirmation after the user submits the form to just be “Email sent successfully” in Times New Roman, black text in the top left corner of a blank white page? Because that’s what would happen if this code ran as the response to the form submission — aside from that, this is missing so much that it would need to make it usable on a modern website.

Also set aside the fact that this is only coded to send off an email. No saving the information to a database, which you’d almost surely want on any database-driven website, especially given the current tenuousness email delivery, which I’ll get to shortly.

One safeguard is the fact that it probably wouldn’t even run successfully at all on most modern web servers, because few servers support the straight-up PHP mail() function anymore, because it’s so easy for spammers to abuse if they manage to hack into your site.

Even if the server does support the mail() function, you’ll never receive the email because, these days, any random web server that actually lets you use mail() is almost certainly already on every spam blocklist, or doesn’t have the necessary SPF, DKIM and DMARC DNS entries that receiving mail servers will check before accepting the incoming message.

Then there’s the fact that there is absolutely zero data validation or sanitization on the form input. It is trivially easy for hackers to abuse a script like this to inject arbitrary code, potentially granting them access to manipulate the contents of your database or even the server’s operating system.

Should I go on? I could go on. But I’ll leave it at that.

Here is where, Aristocrats-style, I deliver the punchline, which you’ve probably already deduced from the title of this post.

“Where,” I asked the client, “did you get this code?”

“ChatGPT.”

ChatGPT. Now, I know a lot of experienced developers these days are using LLMs to generate code, as an assistive tool to bootstrap their applications faster.

But those tools are only effective if you know how to write the correct prompts, and, critically, if you understand the code well enough that you can review it for accuracy and security before deploying it. These are not tools that are suitable for non-technical people to use to directly generate production code in 2025. Will they be someday? Probably. But we are nowhere near that point yet.

Fortunately, the client didn’t know how to install this code directly on their site, so they had to ask me for assistance with that final, crucial step. And I used the opportunity to inform them (more kindly and succinctly than here) why it was not usable as-is. It took me less than 15 minutes to replicate and test in Gravity Forms, and they were up and running with a functional, well-designed, and secure form.

So, again, please, if you don’t possess the ability to look at code and understand whether or not it will work, or what the security implications might be, don’t use ChatGPT (or any other AI) to write code.

Stupid PHP tricks: How to turn an array into a human readable list with “and” between the last two items

It seems like it should be simple. Humans list things all the time. If you have more than two items, you separate all but the last two with commas, and then the last two with the word “and”. If you’re pedantic, you might throw in an Oxford comma.

I needed to do the same in PHP. Specifically, I have an array of people’s names, and I want to turn that into a list that gets dropped dynamically into a sentence.

Just converting an array to a comma-separated list is easy:

$list = implode(', ', $array);

But that won’t put the “and” in there. Sure, you could write some code that finds the last comma and replaces it with “and,” but what if a person’s name has a generation suffix with a comma? You don’t want your list to say “Betty Johnson, James Smith and Jr.” when it should say “Betty Johnson and James Smith, Jr.”

I wasn’t able to find a good solution out there, so here’s what I came up with. First I combine the value of the last element in the array with the second-to-last, and remove the last. Then I do the implode().

if (count($array) >= 2) {
    $array[count($array)-2] .= ' and ' . $array[count($array)-1];
    unset($array[count($array)-1]);
}
$list = implode(', ', $array);

Note that you want to be sure that the array has at least two nodes before running the combination.

In case you’re thinking the implode() is going to be an issue when the array includes exactly two nodes, remember that the conditional is running, inserting the “and” and turning it into a one-node array, so implode() will just return the value of the single node, without the concatenation string.

Of course, if you want the Oxford comma, it’s going to be a bit more complicated. You’ll need a conditional that checks for three or more nodes and inserts “, and” in the combination of the last two nodes. Then of course you’d also keep the existing conditional for exactly two nodes, because you don’t want the comma if there are only two items in the list:

if (count($array) >= 3) {
    $array[count($array)-2] .= ', and ' . $array[count($array)-1];
    unset($array[count($array)-1]);
}
elseif (count($array) == 2) {
    $array[count($array)-2] .= ' and ' . $array[count($array)-1];
    unset($array[count($array)-1]);
}
$list = implode(', ', $array);

Gone phishing

Yesterday I got a curious, one-sentence email to the customer support address for my WordPress plugin with the subject “checkout” [sic].

Just wanted to confirm if everything went through.

The person had a weird* — but not too weird — name, and a Gmail address to match. I was immediately suspicious, but since there were no links in the email, I decided to give them the benefit of the doubt as just being a bad communicator, not a phishing attempt. I figured the worst I risked by replying was proving my email address was real, which… well, duh. So I tersely responded that there were no orders matching their name or email address.

This afternoon I got a reply:

I regret my mistake in not attaching the required files to my previous email, as I was unaware that our email system does not support large file attachments. With some assistance, I have now uploaded them to my OneDrive at [redacted] Sorry for the late response.

Don’t worry, I wasn’t stupid enough to click the link. I just blocked them and reported the phishing attempt instead. I’m glad my instinct was right, and I kind of wish I hadn’t felt the need to test it. I suspect I’m going to need to be extra vigilant about incoming emails for a while.

I definitely regret that — out of necessity — I replied with my real email address, but the support email for the plugin is just a forwarding address and can’t send email directly. Maybe it’s worth the $20/year it would take to change that.


*I feel compelled to explain what I mean by “weird,” because people can say someone has a weird name and basically mean something racist by it. That’s not the situation here. The surname was a very common English surname. The first name was the name of an animal that I have never heard used as a person’s name before.