Transform Your Site: A Case Study on Redesigning with WordPress Gutenberg on AWS Lightsail

Website Revamp with WordPress Gutenberg via AWS Lightsail

AWS Lightsail

Dan owns a theatre costume shop called “On Cue Costumes” in Montclair, New Jersey. Like many of my clients, he had an existing website that needed to be modernized. It needed to be redesigned and responsive. This project reminded me a lot of an art website I did a few years ago. It had lots of pages, content, and images that needed to be transferred. And, like that project, I chose to use Amazon Web Services as our cloud provider and WordPress as the content management system. Last time, I installed WordPress and all of the server software manually using AWS EC2. This time I decided to use AWS Lightsail to setup a managed WordPress instance. The WordPress installation uses Bitnami as the package library.

selecting wordpress for aws lightsail

This greatly reduced the time it took to get things up and running. It also provides cost predictably (EC2 is pay as you go) and automatic backups. (When running WordPress on EC2 I would run nightly SQL dumps as a redundancy mechanism). A month before starting work on this website I purchased a new domain in my personal AWS account.

When time came to begin working, I created a new AWS account for Dan’s business. Route 53 made it easy to transfer the domain name. Then, I created a new hosted zone for that domain and pointed the A record to the Lightsail instance’s IP address. Set up was easy enough to not need a walk-through. But, just to be sure, I watched a YouTube first. I’m glad I did because the top comment mentioned “Why did you not set a static IP address before creating your A records?”

The documentation mentions that “The default dynamic public IP address attached to your Amazon Lightsail instance changes every time you stop and restart the instance”. To preempt that from being a problem, I was able to create a static IP address and attach it to the instance. I updated the A record to use that new address.

Here is a before shot of the business’s website:

The legacy site, ""
The legacy site, “”

WordPress Gutenberg

Now that the infrastructure was set up, I was able to login to wp-admin. The Lightsail dashboard gives you the default credentials along with SSH access details. I used a premium theme called “Movie Cinema Blocks”. It has an aesthetic that fit the theatre look and made sense for this business.

premium wordpress theme
The original layout provided by the theme

The Gutenberg editor made it easy to craft the homepage with essential information and photos. The theme came with a layout pattern that I adjusted to highlight the content in a meaningful way. In a few places where I wanted to combine existing photos , I used the built in collage utility found in the Google Photos web app.

I created a child-theme after connecting via sFTP and edited the templates to remove the comment sections. I added custom CSS to keep things responsive:

@media(max-width: 1630px){
	.navigation-column .wp-block-navigation{
		justify-content: center !important;
@media(max-width: 1550px){
	.navigation-columns .menu-column{
		flex-basis: 45% !important;

@media(max-width: 1000px){
		 display: none
		flex-wrap: wrap !important;

		flex-basis: 100% !important;
    	flex-grow: 1 !important;
	.navigation-column h1{
		text-align: center;
	.navigation-column .header-download-button{
		justify-content: center !important;
	.navigation-column .wp-block-navigation a{
		font-size: 14px;

@media (min-width: 1000px) {
    .hide-on-desktop {
        display: none !important;

.homepage-posts img{
	border-radius: 10px;
.homepage-posts a{
	text-decoration: none;

@media (max-width: 780px) {
		margin-top: 32px !important;

h6 a{
	text-decoration:none !important;

.entry-content a{
	text-decoration: none !important;

       background-color: white !important;

I kept the existing color palette and I used a free web font, called Peace sans, for the site logo:

@font-face {
  font-family: 'peacesans';
  src: url('') format('truetype');
  font-weight: normal;
  font-style: normal;

.logo-font {
  font-family: 'peacesans', Lexend Deca, sans-serif;

The biggest challenge was importing all of the content from the existing website. We added each costume show as a post and used categories for taxonomy. The “Latest Posts” block in Gutenberg allowed us to showcase the content organized by that classification.

The original website displayed a simple list of all records. It has separate pages only for shows that have images. Others are just listed as plain text. After manually adding all of the hyperlinked content, over one-hundred remained that were only titles.

list of shows

To remove the hyperlinked entries, so that I could copy/paste the rest, I used some plain JavaScript in the browser console:

// Get all anchor elements
const allLinks = document.getElementsByTagName('a');

// Convert the HTMLCollection to an array for easier manipulation
const linksArray = Array.from(allLinks);

// Remove each anchor element from the DOM
linksArray.forEach(link => {

It was easier to copy the remaining entries from the DOM inspector than it was from the UI. But that left me with <li> markup that needed to be deleted on every line. I pasted the result into Sublime Text, used the Selection -> Split into Lines control to clean up all at once, and found an online tool to quickly remove all empty lines.  I saved it as a plain text file. Then, I used a plugin called “WordPress Importer” to upload each title as an empty post.

Contact Form Email

The contact page needed to have a form that allows users to send a message to the business owner. To create the form, I used the “Contact Form 7” plugin. “WP Mail SMTP” let us integrate AWS SES to power the transactional messages. Roadblocks arose during this integration.

Authoritative Hosted Zone

The domain verification failed even though I added the appropriate DKIM CNAME records to the hosted zone I created in this new account. At this point, I still needed to verify a sending address. This business used a @yahoo account for their business email. I decided to use AWS WorkMail to create a simple info@ inbox. (In the past, Google Workspace has been my go-to). This gave me a pivotal clue to resolving the domain verification problem.

aws workmail warning

At the top of the WorkMail domain page, it warned that the domain’s hosted zone was not authoritative. It turns out, that after I transferred this domain from my personal account to the new one the nameserver records continued to point to the old hosted zone. I deleted the hosted zone in the origin account, and updated the NS records on the domain in the new account to point to the new hosted zone. Minutes later, the domain passed verification.

Sandbox limits

The initial SES sandbox environment has sending limits – only 200 per day and only to verified accounts. Since messages were only being sent to the business owner, we could have just verified his receiving email address. The main issue was that the default WordPress admin email address was literally When I tried updating this to a verified address, WordPress would also try to send an email to, causing SES to fail. I requested production access, and in the mean time tried to update that generic admin address in the database directly.

SSH Tunnel and Database Access

When looking at the server files in FileZilla, I noticed that phpMyAdmin was installed. I tried to access it from a web browser, but it warned that it was only accessible from localhost. I set up an SSH tunnel to access phpMyAdmin through my computer. From my Mac Terminal, I commanded:

ssh -L 8888:localhost:80 <your-username>@<your-server-ip> -i <your-ssh-key>

It told me that there was a bad permissions issue with the key file (even though this was the same file I had been using for sFTP). I fixed it from the command line:

chmod 600 onCueCostume-LightsailDefaultKey-us-east-1.cer

Once connected, I could access the server’s installation of phpMyAdmin from this browser URL: `http://localhost:8888/phpmyadmin`

Presented with a login screen, I didn’t know what credentials to use. From the Lightsail dashboard I connected to the server via web terminal. I looked up the MySql password with this command: `cat /home/bitnami/bitnami_credentials`

I assumed the username would be root – but no, it turned out to be user.

Production Access

After requesting production access, AWS responded, “It looks like we previously increased the sending limits for at least one other AWS account that you do not appear to be using. Before we make a decision about your current request, we would like to know why you cannot send from your existing unused account.”

What did this mean? I think it happened because I used my credit card on this new account, before switching it over to the business owner. This probably set off an automatic red flag on their end. Interestingly, the correspondence said “To protect our methods, we cannot provide any additional information about how we identified the related accounts.”

I responded and explained the situation honestly, “I am not aware of another account that I am not using. I do have my own AWS account for my web design business, but it is unrelated to this account – and it is not unused.” Within a few hours, production access was granted.

New Website

The original scope of this project was to build a new website showcasing content that already existed.  We were able to finalize the layout and design quickly. Here is a screenshot of the homepage:

homepage design of a wordpress website

To write this article, I referred to my ChatGPT chat log as a source of journal notes.

Adding SSL Certificate for HTTPS

The Bitnami installation comes with a tool `bncert-tool` that handles everything. It even sets up redirects to ensure your site always uses the secure HTTPS protocol. Refer to this AWS documentation: “Enable HTTPS on your WordPress instance in Lightsail“. You can run it from the command line interface. If it is not already installed you can download it: `wget -O`

At first, I tried installing the certs manually, using Let’s Encrypt – which was a bad idea. I encountered a problem resulting in a mismatch between the SSL certificate (server.crt) and the private key (server.key). This prevented Apache from restarting (panic). I was able to resolve the problem by generating a new self-signed certificate (relief). Make sure you take back-ups and snapshots before messing with anything via command line.


Lightsail allows for manual and automatic snapshots. My WordPress installation was about 40 gigabytes. Storage costs five cents per gigabyte – which is about two dollars per month to store a single snapshot.


CSS Grid: Border Between Each Row

CSS Grid borders

User Interface Layout with CSS

CSS Grid is a layout system that allows you to design web interfaces with rows and columns. In earlier times, developers used less elegant techniques to arrange UI elements. In the 1990s, using HTML tables was the standard way. Many of my earliest clients used tables for their entire website and email newsletter code bases. During that time Dreamweaver was a popular product that let designers build websites and generated source code that was mostly tables. The semantic problem what this approach was that we were using tables for non-tabular data.

By the 2000s, the CSS float property became popular for multi-column layouts. We didn’t see Flexbox until the mid 2010s. You can read about examples of its use in the layout of my blog  and an image carousel I built.

CSS Grid

Grid is the latest addition to the toolbox of options. For a recent project I used CSS Grid to create a two column web application. Here is some example HTML:

<h1>Below is a grid of data</h1>
<div style="display: grid; grid-template-columns:repeat(2, 1fr); grid-gap: 24px">
	<div>Row 1, Column 1</div>
	<div>Row 1, Column 2</div>

	<div>Row 2, Column 1</div>
	<div>Row 2, Column 2</div>

	<div>Row 3, Column 1</div>
	<div>Row 3, Column 2</div>

The grid container has a property: `grid-template-columns:repeat(2, 1fr);`. The CSS property grid-template-columns is used to define the number and size of columns in a CSS Grid layout. This repeat() function is used to repeat a pattern a specified number of times. In this case, the pattern is defined by the second argument, 1fr. fr stands for “fractional unit.” This property is describing two columns, with each item taking up an equal amount of space. This is what it looks like:

grid css layout

I wanted a horizontal line to separate each row. Usually, just adding a border-bottom to each of the grid items will work. In this scenario, the grid design called for a grid-gap property that forced the border to break.

css grid-gap

We fixed this by adding a new grid item between each “row” (that is, every third element). Those elements would represent each border line. I set the grid-column property of those elements to span two columns.

		border-bottom: 2px solid #333;
		grid-column: span 2;

<h1>Below is a grid of data</h1>
<div style="display: grid; grid-template-columns:repeat(2, 1fr); grid-gap: 24px">
	<div class="gridItem">Row 1, Column 1</div>
	<div class="gridItem">Row 1, Column 2</div>

	<div class="gridItemBorder"></div>

	<div class="gridItem">Row 2, Column 1</div>
	<div class="gridItem">Row 2, Column 2</div>

	<div class="gridItemBorder"></div>

	<div class="gridItem">Row 3, Column 1</div>
	<div class="gridItem">Row 3, Column 2</div>

The end result is just what we wanted:

css grid layout with row separators

Background Parallax Webpage with CSS

parallax website CSS

Parallax scrolling is a technique where the background moves at a different speed than the foreground. Ideally, the effect has many layers: a background, a mid-ground, and a foreground. These layers moving at different speeds create the illusion of depth and immersion

  • The background layer forms the foundation of the parallax effect. It typically consists of large, visually captivating images or patterns that set the scene and establish the mood of the website.
  • The mid-ground layer serves as an intermediary between the background and foreground, providing additional visual interest and depth.
  • The foreground layer contains the primary content and interactive elements that users engage with directly.

My favorite example comes from the classic Sonic the Hedgehog game on the Sega Genesis. In that game, the background layer encompasses lush landscapes, while the intermediate layer consists of trees and obstacles that add depth to the scene. Sonic and collectibles represent the foreground layer, with Sonic’s speedy movements contrasting the slower-paced background and intermediate layers.

HTML & CSS Parallax Effect

We can use HTML and CSS to achieve a parallax scrolling effect by manipulating the position and properties of background images or layers.


<div class="parallax-container">
  <div class="parallax-background"></div>
  <div class="content">
    <!-- Your content here -->


.parallax-container {
  position: relative;
  overflow-x: hidden;
  overflow-y: auto; /* Enable vertical scrolling */
  height: 100vh; /* Set the container height to full viewport height */

.parallax-background {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 200%; /* Adjust height to create a taller background for parallax effect */
  background-image: url('background.jpg');
  background-size: cover;
  background-position: center;
  z-index: -1; /* Ensure background is behind content */

.content {
  padding: 20px;
  /* Other styles for your content */

  1. The .parallax-container serves as the main container for the parallax effect. It has relative positioning to contain the absolutely positioned background layer.
  2. The .parallax-background div contains the background image. It’s absolutely positioned to cover the entire container and set behind the content with a negative z-index.
  3. Adjusting the height of .parallax-background to be taller than the container creates the parallax effect when the user scrolls.
  4. The .content div holds the content and is positioned over the parallax background.

My Background Webpage

A few years ago, I made a single webpage to describe my professional background and experience. I decided to use a parallax background effect for this page. You can visit it here.

I really love the way the main logo text initially seems to blend into the foreground but remains static as you scroll. Upon scrolling, you’ll observe that the top text remains fixed while the second line moves independently, creating a distinct visual effect. It feels unexpected. The contrasting image backgrounds creates a fun juxtaposition.

And, it’s all done with CSS (no JavaScript necessary). You can create a simple parallax effect by adjusting the positioning of background images or layers using CSS properties like background-position and background-attachment. This allows the background to move at a different rate than the foreground content as the user scrolls, creating the illusion of depth.

Here is the code that I used to build this example:

<!doctype html>
<html class="no-js" lang="en">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<link rel="shortcut icon" href="" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="">
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="">
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="">
<link rel="apple-touch-icon-precomposed" href="">
<title>Anthony Pace Background and Experience</title>
<meta name="description" content="Anthony Pace is a web developer, designer, and database architect. His daily work includes writing software and creating user-centered experiences. The technologies that he use most frequently are HTML5, CSS3, Javascript, PHP, and MySql.">
<meta name="keywords" content="Anthony Pace, web, development, design, marketing, websites, creative services, Bronx, New York">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="" integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous">
<link href='' rel='stylesheet' type='text/css'>
 	<script type="application/ld+json">
	      "@context": "",
	      "@type": "BreadcrumbList",
	       "itemListElement": [
	                "@type": "ListItem",
	                "position": 1,
	                "item": {
	                  "@id": "",
	                  "name": "Home",
	                  "image": ""
	                "@type": "ListItem",
	                "position": 2,
	                "item": {
	                  "@id": "",
	                  "name": "Background",
	                  "image": ""
	  <script type="application/ld+json">
	        "@context": "",
	        "@type": "WebPage",
	        "name": "Anthony Pace Background & Experience",
	        "description": "The page describes Anthony's professional background and experience.",
	        "lastReviewed": "<?php echo date("Y-m-d", time() - 60 * 60 * 24); ?>",
	        "reviewedBy": {
	            "@type": "Person",
	            "name": "Anthony Pace"
  	body { 
		font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
		font-size: 16px;
		margin: 0px;
	article h1 { font-family: 'Lobster Two'; font-size: 60px; margin: 25px 0; line-height: 1em; }

	.story { height: 1000px; padding: 0; margin: 0; width: 100%; max-width: 1920px; position: relative; margin: 0 auto; border-top: 1px solid rgba(255,255,255,0.3); border-bottom: 1px solid rgba(0,0,0,0.4); box-shadow: 0 0 50px rgba(0,0,0,0.8);}

	#first { background: url(images/textured.webp) 50% 0 repeat fixed; }
	#second { background: url(images/ufo.webp) 50% 0 no-repeat fixed; }
	#fourth { background: url(images/abstract2.webp) 50% 0 no-repeat fixed; }
	#third { background: url(images/desertEarthSet.webp) 50% 0 no-repeat fixed; }
	#last { background: url(images/textured.webp) 50% bottom repeat fixed; }

	/* Introduction */
	#first #antpaceLogoBgDiv { background: url(/images/anthony-pace-logo.webp) 50% 100px no-repeat fixed; min-height: 1000px; padding: 0; margin: 0; width: 100%; max-width: 1920px; position: relative; margin: 0 auto; }
	#first article { width: 100%; top: 300px; position: absolute; text-align: center; }
	#first article p,
	#first article a { color: #ccc; }
	#first article a { text-decoration: underline; }
	#first article a:hover { color: #fff; }

	#second { padding: 50px 0;}
	#second article { 
		 color: #fff; 
		 width: 445px;
		 margin-left: 100px; 
		 padding: 10px 20px; 
		 text-shadow: 0 -1px 0 rgba(0,0,0,0.5); 
		 line-height: 1.5em; 
		 box-shadow: 0 0 25px rgba(0,0,0,0.3); 
		 border: 1px solid rgba(150,150,150,0.1); 
	#second article p { margin-bottom: 25px; }
	#second article a { color: #ff0;}

	#third article {
		background:  #333; 
		color: #fff; 
		padding: 10px 20px; 
		margin: 100px 0 0 60%; 
		text-shadow: 0 -1px 0 rgba(0,0,0,0.5); 
		line-height: 1.5em; 
		color: #fff; 
		position: absolute; 
		top: 0; box-shadow: 0 0 25px rgba(0,0,0,0.3); 
		border: 1px solid rgba(150,150,150,0.1); 

	#third article p { width: 300px; margin-bottom: 25px; }

	#fourth article {  background:  #333;margin-left: 10%; text-shadow: 0 -1px 0 rgba(0,0,0,0.5); line-height: 1.5em; color: #fff; position: absolute; top: 0; }
	#fourth article p { width: 300px; margin: 50px 0; }
	#fourth img { position: fixed; left: 50%; box-shadow: 0 0 25px rgba(0,0,0,0.7); z-index: 1; border:5px solid white;}

	/* The End */
	#last .last { background: url(images/thanks.webp) 50% 100px no-repeat fixed; height: 1000px; padding: 0; margin: 0; width: 100%; max-width: 1920px; position: relative; margin: 0 auto; }

	@media (max-width: 625px) {
		#last .last {
	@media (max-width: 711px) {
	@media (max-width: 639px) {
			background-size: contain !important;

	@media (max-width: 555px) {
		#second article {
		#second p, #second h1 {
			padding:6px 12px;

	@media (max-width: 885px) {
		#third article {
			margin: 100px 0 0 10%; 
	@media (max-width: 422px) {
		#third article {
			margin: 100px 0 0 0; 
		#third article p{
			padding:6px 12px;
		#third article h1{
	@media (max-width: 767px){
		footer {
			text-align: center;
	#third article p, #second article p{



  <div id="main" role="main">

	<!-- Section #1 / Intro -->
	<section id="first" class="story" data-speed="8" data-type="background">    	
		<div id="antpaceLogoBgDiv" data-type="sprite" data-offsetY="100" data-Xposition="50%" data-speed="-2"></div>		

			<img id="experienceAndBackgroundImg" src="images/experienceAndBackground.webp" alt="Experience and Background"  />
			<p>Anthony Pace - <a style="text-decoration: none;" href=""></a> - <a style="text-decoration: none;" href="tel:6465330334">646-533-0334</a></p>

	<!-- Section #2 / Background Only -->
	<section id="second" class="story" data-speed="4" data-type="background">
		    	<p>I love coding and visual design. I've been doing both for over two decades. I am a web developer, designer, and database architect. My daily work includes writing software and creating user-centered experiences. <strong>The technologies that I use most frequently are HTML5, CSS3, Javascript, PHP, and MySQL.</strong> </p>
		    	<p>Currently, I am employed by a company in New York City where I directly manage other developers. I am in charge of developing apps (native and HTML5) and building &amp; maintaining data driven websites. I also play a large role in the strategy and implementation of marketing campaigns.  </p>
				<p> My position as marketing manager emphasizes measuring the effectiveness of our marketing actions, and then making decisions based on the data. I use advanced A/B testing, google analytics, email campaign reports, and user surveys to figure out what works best for the company. I also manage the logistics of our presence at live exhibitions.
	<!-- Section #3 / Photos -->
	<section id="third" class="story" data-speed="6" data-type="background" data-offsetY="250">    	
	    	<div class="textbox">    	
		    	<p>In primary school I began experimenting with HTML as well as creating applications with Visual Basic. I quickly learned about various software design concepts and built a strong interest in Computer Science. Despite this, in college I studied philosophy. The abstract and logical thinking techniques I fostered have applied very much to my career in technology. </p>
		    	<p>Since then, I've continued to educate myself about computer science. Books and online lectures facilitate me moving towards a level of expertise relevant to my career. Social media and online communities help me to keep up on the latest technologies and industry trends.</p>
				<p>When finished with college I created a web design and marketing business. I built clientele using both digital and traditional marketing strategies. I've been a professional developer since 2008.</p>

	<!-- Section #4 / HTML5 Video -->
	<!--<section id="fourth" class="story" data-speed="8" data-type="background" data-offsetY="250">
	    	<div class="textbox">    	
		    	<p>I have acquired and honed a variety of abilities as a technologist. I am a full stack web developer with a passion for programming. Some of the most useful skills that I have built include:
						<li>Project Management
						<li>Business Management
						<li>Software Engineering
				</p>For a more technical description of my abilites please visit my <a style="color:white;" href="" target="_blank">resum&eacute;</a>.</p>
	</section>	-->

	<!-- Section #5 / The End-->
	<section id="last" data-speed="8" data-type="background" data-offsetY="250">    
		<a href="/blog/"><div class="last" data-type="sprite" data-offsetY="-1600" data-Xposition="50%" data-speed="-2"></div>	</a>
		<p style=" text-align: center; padding: 50px;font-size: 24px;"><a href="/blog/about/" style="color:white; text-decoration: none;">Continue to my blog &nbsp;&nbsp;<i class="fas fa-arrow-right"></i></a></p>
  </div> <!-- // End of #main -->
<?php include $_SERVER["DOCUMENT_ROOT"] . '/footer-shared.php'; ?>
<?php include $_SERVER["DOCUMENT_ROOT"] . '/components/analytics.php'; ?>


Take a look:



Optimize Your Site’s Speed: Convert Images to WebP Format

upgrading image files

In a recent post I discussed auditing existing websites for potential performance enhancements. In the process I discovered issues with the Lighthouse estimates (a topic I covered briefly in a post about progressive web apps).

Lighthouse performance estimate

My performance only scored 50 out of 100. One “opportunity” (estimated to save 2.16s) was to “serve images in next-gen formats”. AVIF (based on the AV1 video codec) and WebP are considered next generation because of “superior compression and quality characteristics compared to their older JPEG and PNG counterparts“. I chose to upgrade my images WebP (instead of AVIF) mostly due to greater browser support.

Before I upload images to this site, I usually run them through TinyPNG. Sometimes, I’ll also do some manual pre-processing in the GIMP, like cropping or scaling images down. I know that there  must be a better way to do all of that through a proper CI pipeline – but for now that is my low tech solution. Now that I need to come up with a new process for next-gen image formats, it might be a good time to explore a more automated solution.

Convert multiple images to next-gen WebP file format

For converting single images, manually, one at a time I could use the GIMP or an online service. I used a command line tool on my mac called webp tools to bulk upgrade all of the files in an image directory.

brew install webp

For a single file I can use this command:

cwebp -q 80 software-education.png -o software-education.webp

To hit all of my image files in that folder I ran loops over the existing file formats. You should change this to include any other file formats used in your project.

for i in *.jpg; do cwebp -q 80 "$i" -o "${i%.jpg}.webp";done
for i in *.png; do cwebp -q 80 "$i" -o "${i%.png}.webp";done

I saved them to a new folder, so that I could easily delete all of the old files at once and then replace them. Alternatively, I can save them in the same directory and then run a command to delete all of the .png files: rm *.png

New folder amongst legacy image files

My website has been around for a while, so there are unused legacy media files. I manually searched my code base (ctrl + shift + f) for the file names listed in the various /image directories throughout my project, and deleted them if I found no references. (Maybe having so many disjointed image folders is part of a bigger problem with this project). If I did find it, I updated the file extension to webp. Later, I wrote a script to automatically find and delete unused image files from a project.

This change increased my Lighthouse performance score by ten points (I gained an additional two but upgrading MariaDB from version 10.2 to 10.4). I did this process is a few other directories of my project to complete the upgrade. I was going to convert my Apple touch icon files to a next-gen format too, but the documentation specify the use of the PNG format (good thing I checked).

WordPress and WebP

Having adopted WebP as the new standard file format for, I decided to upload .webp images to my posts – starting with this one. When I tried, WordPress complained: “This image cannot be processed by the web server. Convert it to JPEG or PNG before uploading.”

error from wordpress when trying to upload a next generation image file

This surprised me. I upgraded WordPress to the latest version (6.3.1 at that time), but it didn’t help. After further investigation, it looked like the problem had to do with the PHP image module gd.

I SSH’d into my EC2 instance to see what I could do. When I tried to install gd for my PHP version 7.4, I got a version mismatch error. It had to due with Amazon Linux 2 (the OS I run on AWS EC2) not supporting PHP 7.4 as it approaching or have passed their end-of-support dates. Every time I tried to installthe gd module, Amazon Linux Extras (the default Amazon Linux 2 package mechanism) would try to pull the version compatible with PHP 7.2. In an attempt to make it work, I manually disabled ‘amazon-linux-extras’, installed the Remi repository and made sure it was prioritized as my package manager. Still, “Packages skipped because of dependency problems”.

Amazon Linux 2 is at EOL.

The same thing happened when I tried using ImageMagick instead. This made me consider my Linux distribution. Not having gd installed what causing other problems when uploading media through WordPress (responsive image sizes are not being generated).

I had been wanting to upgrade the size of my EC2 instance anyway, so this might be the right time. I am considering Amazon Linux 2023 or a Bitnami image.  As you know, I’ll write a blog post about which I choose and the implementation details

Ensure Your Website’s Success: Maintenance, Audit, and Enhancement Plans

Existing website work

This post is all about how I help clients with their existing websites. If you are a fellow web developer reading this, use it for ideas on how to serve more people. My mission is to help businesses achieve more through their web presence.

If your curious about how I can help you with your website, send me a message. Below are some ways I will serve you.

Content updates & maintenance

I try to be a company’s go-to guy that can be called whenever anything comes up with their website.

If someone has a website, even if it is a static brochure site, it will eventually need help. It could be small content updates to reflect inevitable changes to the business. Or, one day something goes wrong. The website suddenly doesn’t work at all. Maybe someone notices something broken, be it major or minor. I want them to think of me and know that I can help.

There’s some businesses that regularly make content updates themselves using a CMS (like WordPress, Wix, Shopify, etc.). They could need occasional help with customizations, CMS or plugin update issues, and more. I let clients know that when issues arise, I’m just a text message away.

Audit & enhancement

A client usually requests this service in the form of a complaint: “My website is too slow”, “I’m not getting enough sales/conversions/leads from my website”. My response is to do a formal audit and analysis. The result includes recommendations for improvement and an estimate (time/price) for implementation.

Other times, clients do not even know that there is a potential risk (security issues) or unrealized upside (UX issues) that needs attention. Providing those insights with empathy and transparency helps businesses see that value. Below is a list of audit types I offer:

  • Security: Is your website secure? Does it use https? Is WordPress up-to-date? Is your site vulnerable to being taken over by hackers?
  • Accessibility: Is your website usable for people with disabilities? Many businesses don’t realize that this is a legal requirement under the ADA.
  • SEO: Technical SEO, optimization, and more
  • Design UI/UX: Does your website look like it’s stuck in the past? How does it perform on different mobile devices? Is there brand consistency? Is the user experience the best that it can be? Maybe it is slow and bloated from plugins/integrations and needs a refresh.

Once we figure out what needs attention, I can create a plan and strategy to enhance your website. We can take it one step and a time, and focus on what will have the greatest impact for you and your business. You can read more about my process for web development with freelance clients in another post.

steps to handling a web development client

Preemptive audits to win new clients

I approach clients (existing or perspective) with a value proposition from a place of wanting to give. This is especially true for small businesses that appear to be leaving honey 🍯  on the table.

My process for engaging a new client, especially cold ones, is to review their website and make note of any obvious improvements. One example is using a generic email address ( instead of their own domain. Others include broken 404 links and bad mobile UX.

Google Chrome’s built in Lighthouse feature helps me to highlight low performance in existing websites of potential clients.

low performing lighthouse scores for a small business

Screaming Frog SEO Spider is another utility that I use to identify (and pitch) improvements to new client web projects.


Training & education

Sometimes, managers and stakeholders want to know how things work or how to make some changes themselves. That’s why I offer technical training, education, and tutoring.

Your business might have a designer or marketer that is ready to add some tech skills to the mix. I can help with that too. Contact me about the personalized tech training that I offer.

Disaster recovery & best practices

When a tech disaster happens you need to have a plan for recovery. Do you have backups? What is the RPO and RTO for your organization when “the business is on fire”? Work with me to be prepared in these situations. I apply proven strategies and make sure your digital presence is resilient.

A moment of preparation is worth a week of remediation. Sailing ahead of the storm is possible by following best practices  How does your organization manage passwords and credentials? What apps do your employees use? Knowing the right questions is the only way to build valuable answers.

Debugging Mobile CSS on Chrome for Android

debug mobile css on Android

On this website (this very one you are reading) I have a resume page. On it, I display a timeline styled with CSS. I use the border-radius property to create circles for the years on that timeline. The years connect between <div> elements that represent my work experience.

timeline displayed on a resume webpage
An image can be worth a forty-two words

It looks great (at least,  I think so). Except on mobile. Specifically, on Android running Chrome. The circle appears oblongly squished.

Broken CSS on Android
Broken CSS on Android

I could not recreate this bug on my laptop, even when I used Chrome Developer Tools to simulate mobile. Not being able to reproduce a bug consistently is a very frustrating experience.

Chrome developer tools
I couldn’t recreate the mobile bug on my laptop. *Sigh*

USB debugging

I would have to use remote debugging via Chrome DevTools. The first step was to enable “Developer Options” on my Android device. I was using a S21 Galaxy Ultra. On this device I navigated to “Settings” -> “About Phone” -> “Software information”. Then I triple tapped the “Build number” and received a toast message “Developer mode activated”. This feels familiar, as it has been a similar process on previous Android devices that I have owned. I’d post a picture of my software information screen, but am worried that some of the data could be used maliciously by strangers on the internet.

“Developer options” was revealed in my “Settings” app. From there, I could turn on “USB debugging”

usb debugging in the developer options menu

Now, I could connect my Android to my MacBook. From my MacBook, I opened Chrome and navigated to chrome://inspect/#devices

view usb connected devices running chrome

As long as my Android also had Chrome running, I could see my device listed with any open browser tabs. Clicking “inspect” opened a new window on my laptop that mirrored my cellphone’s screen. When I scrolled on one, it reflected on the other.

Inspect mobile device from desktop

CSS Solution

I could inspect elements from the developer tools panel. This was exactly what I needed to debug the problem. And, the fix ended up being pretty easy. Here’s the original, relevant CSS:

.timeline .year {
  font-size: 1em; 
  line-height: 4;
  border-radius: 50%;
  width: 6em; 

The issue was ultimately with the width and height properties. The circles look correct on my desktop browser because the width and height rendered to nearly identical – 64px by 66px. To get a circle with CSS you need an element with the same height and width (the greater the difference, the less even and more stretched the circle appears), and then set the border-radius to 50%. On Android, that same element was being rendered as 64px by 85px. This was most probably happening because the height was not being explicitly defined.

As a remedy, I hard-coded the width and height properties. I changed the unit of measurement to pixels for both.

.timeline .year {
  font-size: 1em; 
  line-height: 4;
  border-radius: 50%;
  width: 65px;
  height: 65px;

This fixed the circle from being stretched. But, now the text was not centered that way that it should be.

Fixed CSS displays a circle

That was happening because of the line-height property. Since it was set without a unit of measure, most browsers would interpret it to mean a multiplier of the current font-size. And, the font-size was also using a relative unit of measure, em. Right away, that was a code smell. It was a browser issue, where along the way values were not being calculated as expected.

On mobile, the font-size was being calculated to 20.8px. On desktop, the font-was was being calculated to 16px. I fixed this by changing the line-height value to an absolute unit of measure:

.timeline .year {
  font-size: 1em; 
  line-height: 65px;
  border-radius: 50%;
  width: 65px;
  height: 65px;

Sort an HTML Table Using JavaScript

Sort an HTML Table Using JavaScript

For a recent side project I was tasked with enhancing an existing HTML table. That table displayed search results. The records were dynamic, populated by an AJAX call after the “search” button was pressed. One of the requests was to let users click on the column headers to sort the table. Each click would organize the data, toggling ascending and descending, based on the column values.

A table with data about dogs

My first idea was to use a front-end library. I love abstractions, and hate reinventing the wheel. I’ve used the DataTables jQuery plug-in before, and thought it might be a good fit. All I had to do was include two CDN file references – one for CSS styles and another for JavaScript functionality. After that, I could select the table by ID and call a single function:

<link href='//' rel='stylesheet' type='text/css'>
<script src="//"></script>

$(document).ready( function () {
} );

This added quick and easily out-of-the-box functionality, with bells and whistles.

It seemed great, but the extras that it added, such as pagination and front-end search were unnecessary, and actually got in the way of the design specification. Those might be easy enough to clean up by passing options into the method call, or even with a bit of custom CSS, but still seemed like overkill.

Further, the DataTables library would occasionally throw errors about an “incorrect column count”, depending on what results my back-end provided. That was because the data model, and possible front-end actions for this app were more complex that you might think. The HTML wasn’t strictly semantic and the information wasn’t just tabular.

The more I thought about it, the more I felt that plain vanilla JavaScript should be enough to get the job done. And, since we’re living in the future, I decided to ask an AI chat-bot called ChatGPT.

I asked “How can I use jQuery to sort an HTML table by a specific column?”

Asking ChatGPT about coding

It told me that “You can use the sort method of JavaScript arrays combined with the map method to sort an HTML table by a specific column,” and gave me a specific code example!

I was very impressed. Here is the JavaScript code it provided:

$(document).ready(function() {
  var table = $('#myTable tbody tr').get();

  table.sort(function(a, b) {
    var A = $(a).children('td').eq(1).text();
    var B = $(b).children('td').eq(1).text();

    if(A < B) {
      return -1;

    if(A > B) {
      return 1;

    return 0;

  $.each(table, function(index, row) {

I added this code to a click-handler in my app, after adjusting the element selectors. Although it worked (kind of), it did not operate quite as I expected. It only performed the sort on a single column, and did not alternate the order on each click.

I continued to ask the chat-bot more questions, making refinements to the functionality. I wanted the code to toggle between ascending and descending on each click. Also, I figured it could be nice to avoid jQuery completely and just use basic JS.

Chat bot solving code problems

Eventually, it told me “To toggle between ascending and descending order when sorting the table, you can keep track of the current sorting order for each column in a separate array”. Below, you can see the full code implementation:

  table {
  border-collapse: collapse;
  width: 100%;

th, td {
  text-align: left;
  padding: 8px;
  border-bottom: 1px solid #ddd;

tr:nth-child(even) {
  background-color: #f2f2f2;

th {
  background-color: #4CAF50;
  color: white;
  cursor: pointer;

td:first-child {
  font-weight: bold;

td:nth-child(3), td:nth-child(4) {
  text-transform: capitalize;
#search-input {
  padding: 8px;
  margin-bottom: 12px;
  width: 100%;
  box-sizing: border-box;
  border: 2px solid #ccc;
  border-radius: 4px;
  font-size: 16px;

#search-input:focus {
  outline: none;
  border-color: #4CAF50;
<input type="text" id="search-input" placeholder="Search breeds...">
<table id="dog-table">
      <td>Labrador Retriever</td>
      <td>Friendly, outgoing, and active</td>
      <td>German Shepherd</td>
      <td>Loyal, confident, and courageous</td>
      <td>Small to Large</td>
      <td>Intelligent, elegant, and proud</td>
      <td>Determined, friendly, and calm</td>
      <td>Small to Medium</td>
      <td>Cheerful, determined, and friendly</td>

// Get the table element
const table = document.querySelector('table');

// Get the header row and its cells
const headerRow = table.querySelector('thead tr');
const headerCells = headerRow.querySelectorAll('th');

// Get the table body and its rows
const tableBody = table.querySelector('tbody');
const tableRows = tableBody.querySelectorAll('tr');

// Initialize sort order for each column
let sortOrders = Array.from(headerCells).map(() => null);

// Attach a click event listener to each header cell
headerCells.forEach((headerCell, index) => {
  headerCell.addEventListener('click', () => {
    // Extract the column index of the clicked header cell
    const clickedColumnIndex = index;
    // Toggle the sort order for the clicked column
    if (sortOrders[clickedColumnIndex] === 'asc') {
      sortOrders[clickedColumnIndex] = 'desc';
    } else {
      sortOrders[clickedColumnIndex] = 'asc';
    // Sort the rows based on the values in the clicked column and the sort order
    const sortedRows = Array.from(tableRows).sort((rowA, rowB) => {
      const valueA = rowA.cells[clickedColumnIndex].textContent;
      const valueB = rowB.cells[clickedColumnIndex].textContent;
      const sortOrder = sortOrders[clickedColumnIndex];
      const compareResult = valueA.localeCompare(valueB, undefined, { numeric: true });
      return sortOrder === 'asc' ? compareResult : -compareResult;
    // Rebuild the table in the sorted order


Using predictive language models as a coding assistant is very helpful. I can’t wait to see what other uses we find for this technology, especially as it gets better.

Online Ordering for a Restaurant Website

online ordering system

A Digital Transformation Case Study: Boosting Restaurant Sales with Custom Web Development and Online Ordering Integration

Client background & challenge

When I was younger I worked as a pizza delivery driver. Years later, the pizzeria where I once worked commissioned me to build their website. They were busier than ever thanks to online ordering (GrubHub, Seamless, UberEats), but were getting hit with high service fees.

They wanted their own website to be able to take orders for food online and send a notification to their iPad. That way they could avoid using apps like GrubHub that charged additional fees.

Project overview & execution

I used a service called GloriaFood that provides ready-made website templates, a secure payment process, and a messaging system. It integrates with Stripe for processing payments. There is an iPad app that receives push notifications when new orders are placed. The website builder required no code, and had a ton of options. I was able to register the pizzeria’s domain name directly though the admin portal, and generate a sales optimized website with hosting all setup.  It was “seamless” – pun intended!

GloriaFood admin panel

There are also options for integrating their ordering UI with an existing website, a Facebook page, or a dine-in QR code. The an option to publish a custom app required an additional cost per month.

pizza website

I even traveled to this restaurant’s physical location, selected and purchased a tablet computer for them, installed the GloriaFood app to receive orders, and connected it to their mobile printer.

gloriafood order received

It’s amazing how much I was able to accomplish without writing a single line of code. The most technical part of this project was setting up a Stripe account and putting the API keys into the GloriaFood admin panel. GloriaFood is a product by Oracle, a company that specializes in providing a wide range of software and hardware products and services.

Print Design

As an extra part of this project, I designed a business card with a QR code linking to the new website. The business owner planned to give this to customers who ordered through other food ordering apps such as GrubHub, Seamless, UberEats, and Slice.

Business card for pizza business website


Sales Increase

Since the launch of the new website, the restaurant has witnessed a notable surge in online orders, marking a 25% increase. This substantial rise not only signifies a successful digital transformation but also illustrates the growing customer preference for a seamless, direct ordering experience. The intuitive interface and easy navigation on the restaurant’s website have played a strong role in attracting and retaining customers, driving a higher volume of online orders and significantly contributing to the restaurant’s revenue growth.

Cost Savings

Transitioning from third-party ordering platforms like GrubHub, Seamless, and UberEats to a self-hosted online ordering system through has led to big cost savings. Third-party platforms usually charge hefty commissions, which eat into the restaurant’s profits and inflate prices for customers. With the new website, the restaurant has eliminated these intermediary costs, ensuring better profitability while also offering customers more competitive pricing.

Customer Feedback

The feedback received from both the restaurant management and its customers has been overwhelmingly positive.  The restaurant staff has praised the streamlined process, which has simplified order management and allowed for a smoother operation during busy hours.

CSS for Weighted Hyperlink Decoration

CSS for weighted hyperlink decoration

How to add an underline to website text should be covered in any intro to web development course. The old-fashioned HTML way uses a now-deprecated tag:

<u>This will appear underlined!</u>

Modern approaches use CSS to define such a style:

<p style="text-decoration: underline;">This will appear underlined!</p>

Even better, properly written code will separate the inline styles, like so:

  text-decoration: underline;
<p class="underlined-text">This will appear underlined!</p>

For hyperlink text, I might want to hide the underline when a user mouses over it. That’s easy with the “hover” pseudo-class:

  text-decoration: underline;
  text-decoration: none;
<a class="hyperlink-text">This will appear underlined!</a>

But, suppose I want to have that underline to become thicker instead of disappearing. That will require an advanced, super-secret CSS technique. To make it work, we will utilize box-shadow.

In the world of cascading style sheets, the box-shadow property adds a shadow effect around any HTML element. The shadow is described by its offsets, relative to the element. Leveraging this, we can create a shadow that looks like an underline. On hover, we can adjust the settings to change its appearance:

  text-decoration: none;
  box-shadow: inset 0 -1px 0 rgb(15 15 15);
  -webkit-box-shadow: inset 0 0 0 rgb(0 0 0 / 0%), 0 3px 0 rgb(0 0 0);
  box-shadow: inset 0 0 0 rgb(0 0 0 / 0%), 0 3px 0 rgb(0 0 0);
<a class="hyperlink-text">This will appear underlined!</a>

Point your cursor over any of the hyperlinks in this post to see what it looks like. Experiment with the above code to make it your own.