Website Revamp with WordPress Gutenberg via 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.

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, "OnCueCostumesOnline.com"
The legacy site, “OnCueCostumesOnline.com”

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){
	 .hide-on-mobile{
		 display: none
	 }
	.navigation-columns{
		flex-wrap: wrap !important;
	}

	.navigation-column{
		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) {
	.mobile-margin-top{ 
		margin-top: 32px !important;
	}
}

h6 a{
	text-decoration:none !important;
}

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

input[type="search"]{
       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('https://www.oncuecostumes.com/wp-content/themes/movie-cinema-blocks/assets/fonts/peacesans.ttf') 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 => {
    link.parentNode.removeChild(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 user@example.com. When I tried updating this to a verified address, WordPress would also try to send an email to user@example.com, 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.

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>
</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.

<style>
	.gridItemBorder{
		border-bottom: 2px solid #333;
		grid-column: span 2;
	}
</style>


<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>
</div>

The end result is just what we wanted:

css grid layout with row separators

Convert Images to WebP to Improve Website Performance

upgrading image files

In a recent post I discussed auditing existing websites for potential performance enhancements. In the process I discovered issues with the AntPace.com 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 AntPace.com, 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

Existing Website Maintenance, Audit, & Enhancement Plan

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 (ExampleBusiness@gmail.com) 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='//cdn.datatables.net/1.13.2/css/jquery.dataTables.min.css' rel='stylesheet' type='text/css'>
<script src="//cdn.datatables.net/1.13.2/js/jquery.dataTables.min.js"></script>

<script>
$(document).ready( function () {
	$('#dog-table').DataTable();
} );
</script>

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:

<script>
$(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) {
    $('#myTable').children('tbody').append(row);
  });
});
</script>

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:

<style>
  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;
}
</style>
<input type="text" id="search-input" placeholder="Search breeds...">
<button>Search</button>
<table id="dog-table">
  <thead>
    <tr>
      <th>Breed</th>
      <th>Origin</th>
      <th>Size</th>
      <th>Temperament</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Labrador Retriever</td>
      <td>Canada</td>
      <td>Large</td>
      <td>Friendly, outgoing, and active</td>
    </tr>
    <tr>
      <td>German Shepherd</td>
      <td>Germany</td>
      <td>Large</td>
      <td>Loyal, confident, and courageous</td>
    </tr>
    <tr>
      <td>Poodle</td>
      <td>France</td>
      <td>Small to Large</td>
      <td>Intelligent, elegant, and proud</td>
    </tr>
    <tr>
      <td>Bulldog</td>
      <td>England</td>
      <td>Medium</td>
      <td>Determined, friendly, and calm</td>
    </tr>
    <tr>
      <td>Beagle</td>
      <td>England</td>
      <td>Small to Medium</td>
      <td>Cheerful, determined, and friendly</td>
    </tr>
  </tbody>
</table>


<script>
// 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
    tableBody.append(...sortedRows);
  });
});

</script>

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

Results

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:

<style>
.underlined-text{
  text-decoration: underline;
}
</style>
<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:

<style>
a.hyperlink-text{
  text-decoration: underline;
}
a.hyperlink-text:hover{
  text-decoration: none;
}
</style>
<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:

<style>
a.hyperlink-text{
  text-decoration: none;
  box-shadow: inset 0 -1px 0 rgb(15 15 15);
}
a.hyperlink-text:hover{
  -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);
}
</style>
<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.

IntersectionObserver to lazy load images and assets on WordPress

wordpress homepage design

I write online a lot. Adding articles to this blog serves to build a catalog of technical solutions for future reference. Updating the homepage has improved user experience and SEO. The new design displays the most recent articles as clickable cards, rather than listing the entire text of each one. The changes for this were added to index.php file, in the child-theme folder. The theme’s original code already used a While() loop to iterate through the post records. My modification removed the article content, and only kept the title and image:

<div class="doc-item-wrap">
	<?php
	while ( have_posts() ) {
		the_post();
		echo "<div class='doc-item'><a href='". get_the_permalink() ."'><img class='lazy' data-src='".get_the_post_thumbnail_url()."'><h2>" . get_the_title() . "</h2></a></div>";
	} ?>
</div> <!-- doc-item-wrap -->

I used custom CSS, leveraging Flexbox, to style and position the cards:

.doc-item-wrap{
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}
.doc-item{
    width: 30%;
    padding: 20px;
    border: 3px solid #f0503a;
    margin: 15px;
    background: black;
    flex-grow: 1;
    text-align: center;
}
.doc-item:hover{
    background-color: #34495e;
}
.doc-item p{
    margin: 0px;
    line-height: 40px;
    color: white;
}
.doc-item img{
    display: block;
    margin: 0 auto;
}
.doc-item h2{
    font-size: 22px;
    color: white;

}
@media(max-width: 1000px){
	.doc-item{
		width: 45%
	}
}
@media(max-width: 700px){
	.doc-item{
		width: 100%
	}
}

The media queries adjust the size of the cards (and how many are in a row), based on screen size.

Look and feel of the design

Card layout design is a common way to arrange blog content. It gives visitors a visual overview of what’s available. It also stops the homepage from duplicating content that’s already available on the individual post pages.

You can see this pattern throughout the digital world. Card layout translates well across screen sizes and devices. Since I put much effort into writing, making it organized was a priority. This implementation can be extended to add additional content (such as date, description, etc.) and features (share links, animations, expandability). And, it fits nicely with what WordPress already provides.

Lazy loaded images

Image content can often be the biggest drag to site speed. Lazy loading media defers rendering until it is needed. Since this blog’s homepage has an image for each post, this was essential.

While iterating through post records the image URL is assigned to a custom data-src attribute on the image tag, leaving the normal src blank. This assures the image is not immediately retrieved nor loaded. I wrote a JavaScript function to lazy load the images, relying on the IntersectionObserver API. The card’s image does not load until a user scrolls it into view. This improves the speed of the page, which has a positive effect on SEO and UX.

The code creates a IntersectionObserver object.  It observes each of the image elements, checking to see if they are within the browser viewport. Once the image elements come into view, it takes the image URL from the data-src attribute, and assigns it to the tag’s src – causing the image to load.

document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          // lazyImage.srcset = lazyImage.dataset.srcset;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } 
});

The original JS code was referenced from a web.dev article. Web.dev is a resource created by Google that provides guidance, best practices, and tools to help web developers build better web experiences.

You can also use this same method for lazy loading videos, backgrounds, and other assets.

IntersectionObserver to lazily load JavaScript assets

I discovered another application for the IntersectionObserver implementation that I used above to load lazy load images. The Google Lighthouse performance score on my homepage was being dinged due to the “impact of third-party code”.

impact of 3rd party js on performance scores

The largest third-party resource was is used for the reCaptcha on a contact form at the bottom of my homepage. It makes sense to not load it until the user scrolls down to that section – especially because the UX expects them to take time to fill out the form before hitting “submit” anyway.

Using the same pattern as above, I created a new `IntersectionObserver` and passed the contact form section as the target:

function captchaLazyLoad(){
	contactCaptchaTarget = document.getElementById('contactSection')
	if (!contactCaptchaTarget) {
        return;
    }
	let contactCaptchaObserver = new IntersectionObserver(function(entries, observer) {
		if (entries[0].isIntersecting) {
            var script = document.createElement('script');
		    script.src = "https://www.google.com/recaptcha/api.js";
		    document.body.appendChild(script);
            contactCaptchaObserver.unobserve(contactCaptchaTarget);
        }
	})
	contactCaptchaObserver.observe(contactCaptchaTarget);
}

I included this function to the already existing `DOMContentLoaded`  event listener just below the loop to observe lazy image elements:

<script>
function captchaLazyLoad(){
	contactCaptchaTarget = document.getElementById('contactSection')
	if (!contactCaptchaTarget) {
        return;
    }
	let contactCaptchaObserver = new IntersectionObserver(function(entries, observer) {
		if (entries[0].isIntersecting) {
            var script = document.createElement('script');
		    script.src = "https://www.google.com/recaptcha/api.js";
		    document.body.appendChild(script);
            contactCaptchaObserver.unobserve(contactCaptchaTarget);
        }
	})
	contactCaptchaObserver.observe(contactCaptchaTarget);
}

document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          // lazyImage.srcset = lazyImage.dataset.srcset;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });

    //
    captchaLazyLoad()

  } else {
    // Possibly fall back to a more compatible method here if IntersectionObserver does not exist on the browser
  }

});

</script>

This update raised my Lighthouse performance score by fifteen points!