Secure Your Website: Implementing reCAPTCHA on Contact Forms

reCAPTCHA for Contact Forms to Enhance Website Security

I first explored reCAPTCHA for this website’s contact form. After getting too many spam messages, I decided to add some protection. It is Google’s implementation of CAPTCHA (reimagined) and has a long history.

The Turing Test is a concept introduced by the British mathematician and computer scientist Alan Turing in 1950 as a way to evaluate a machine’s ability to exhibit intelligent behavior indistinguishable from that of a human. The test is named after Alan Turing, who proposed it in his paper titled “Computing Machinery and Intelligence.” The history of CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) traces back to the late 1990s when researchers began exploring ways to distinguish between humans and automated scripts or bots online.

Google’s reCAPTCHA service requires you to register the website you’re going to use it on:

register a site recaptcha

 

It provides “Up to 1,000,000 assessments/month at no cost”. Once registered you’ll get a site key (for the client side) and a secret key (for the server side).

PHP & JavaScript Implementation

You can use the keys to implement the Google reCAPTCHA service on a vanilla website. In your HTML contact form, you need to add the site key as an empty `<div> element’s `sitekey` data attribute:

<form id="contactForm">
    <div class="">
        <div><input type="text" placeholder="Name" name="name" value="" class=""></div>
        <div><input type="email" placeholder="Email" value="" name="email" class=""></div>            
        <div><textarea placeholder="Message" rows="3" name="message" id="contactMessage" class=""></textarea></div>
        <div class="g-recaptcha" data-sitekey="XX-SITE-KEY-HERE-XX"></div>
        <div><button type="button" id="contactMe" class="btn">Send</button></div>
    </div>
</form>

The `.g-recaptcha`  is transformed to a hidden input and visible user interface by Google’s JavaScript API. The UI is a checkbox that humans can click, but bots will miss. CAPTCHA mechanisms typically present users with tasks that are easy for humans to solve but difficult for automated scripts or bots.

contact form with reCAPTCHA

CAPTCHA challenges can take various forms, such as:

  1. Text-based CAPTCHA: Users are asked to transcribe distorted or obscured text displayed in an image.
  2. Image-based CAPTCHA: Users are prompted to identify objects, animals, or characters within a series of images.
  3. Checkbox CAPTCHA: This is what our example here is using. Users are asked to check a box to confirm that they are not a robot. This can be combined with additional checks, such as analyzing mouse movements or browsing patterns, to verify user authenticity.
  4. Interactive CAPTCHA: Users are presented with interactive puzzles or challenges, such as rotating objects or dragging and dropping items into specific locations.

I defer the loading of that script using lazy loading:

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);
}
captchaLazyLoad();

The assessment value is serialized with the form data and passed along via a RESTful AJAX POST request to the back-end service:

var notifications = new UINotifications();
$("#contactMe").click(function(){
	var contactMessage = $("#contactMessage").val();
	if(contactMessage.length < 1){
		notifications.showStatusMessage("Don't leave the message area empty.");
		return;
	}
	var data = $("#contactForm").serialize();
	$.ajax({
		type:"POST",
		data:data,
		url:"/contact.php",
		success:function(response){
			console.log(response)
			if(response === "bot"){
				notifications.showStatusMessage("Please confirm your humanity.");
				return;
			}
			notifications.showStatusMessage("Thank you for your message.");
			$("form input, form textarea").val("");					
		}
		
	});
});

The receiving PHP file takes the reCAPTCHA assessment value, combined with our secret key, and passes them along to Google’s “site verify” service:

$secret = "XX-SERCET-KEY-XX";
$captcha = $_POST["g-recaptcha-response"];
$verify=file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret={$secret}&response={$captcha}");
$captcha_success=json_decode($verify);
if ($captcha_success->success==false) {
	echo "bot";
	die();
}

If it is a bot, we `die()`.

reCAPTCHA v3 (2018) and WordPress Contact Form 7

In 2018, Google introduced reCAPTCHA v3, a version of CAPTCHA that works invisibly in the background to assess the risk of user interactions on websites. reCAPTCHA v3 assigns a risk score to each user action, allowing website owners to take appropriate action based on the perceived risk level.

A recent WordPress client messaged me “is there anyway NOT to receive anywhere from 5-10 of these a day. ” He was referencing spam messages coming from his website’s contact form. His contact page uses Contact Form 7, a popular WordPress plugin that allows website owners to easily create and manage contact forms.

I was able to easily integrate reCAPTCHA v3 by installing a 3rd party plugin. After installation, I was able to enter the site and secret keys into the WordPress dashboard. Version 3 does not require users to check any boxes or solve any challenges – it is seamless and invisible.

How to Fix a GoDaddy Parked Page and Revive Your Website

godaddy parked page fixed

It all started with a client text message that was hard to miss — their website was suddenly showing a GoDaddy “parked” page. This unexpected hiccup was more than just a minor inconvenience; it was a full-blown business disruption for my client.

The Challenge

When the client reached out, he was understandably frustrated. His website, which was supposed to be active and processing sign-ups, was inactive, displaying a message typically associated with unconfigured or newly registered domains. This was particularly perplexing as the site had been running smoothly just days before.

My first step was to verify the DNS settings. A parked domain page usually indicates issues with domain pointing, possibly due to changes in DNS or hosting services. The client told me he had tried to create a subdomain through his domain registrar, GoDaddy. It turned out that his domain’s name servers pointed to his web hosting company, DreamHost. Making DNS changes directly in GoDaddy led to a change in the domain’s nameserver records so that it became managed by GoDaddy directly, removing any reference to the web hosting package where the site actually lived.

The Solution

After identifying the mismatch, the next step was clear but not simple—access the DreamHost account to find the correct nameserver value to point the domain to. However, the client didn’t have the DreamHost login credentials readily available, which added another layer to our challenge.

Using SecurityTrails, I conducted a DNS history check. This not only confirmed that the name servers were indeed pointing to DreamHost but also provided a clear historical view of the DNS changes over time. I was able to copy and paste the name server values from the history log. Subsequently, we were able to recover access to the DreamHost account by going through account recovery processes, which involved verifying the client’s identity and ownership of the domain.

dns log

Once we regained access, I immediately updated the DNS records to ensure they correctly pointed to the client’s active web server. This change effectively removed the parked page error, restoring the site’s functionality.

Conclusion

This scenario underscored the critical importance of keeping domain and hosting details organized and accessible. It also highlighted how easy it is for essential information to become overlooked, especially when multiple service providers are involved over the years. Effective disaster recovery strategies are crucial for quickly restoring operations in the event of a service disruption, whether caused by human error, technical failures, or external threats.

With the website back up and running, and the domain correctly configured, the client was relieved and could focus on his business again. For me, it reinforced a key aspect of my work: solving complex tech puzzles not only requires technical know-how but also an investigative approach to untangle the often convoluted web of digital assets. My role transcends building websites—it’s about ensuring they continue to serve their purpose, even when unexpected disruptions occur.

While my tagline says, “I can build your website,” days like these remind me it should also include, “and I can rescue it too.” This experience serves as a testament to the value of professional web management, especially in a world where digital presence is synonymous with business viability.

Engaging Website Elements: Add Typing and Backspace Effects with JavaScript

javascript typewritter

On the homepage of my website, I wanted to add an animated effect that appears to backspace some text, and type out new text. Here is a video of the final product:

There are basic JavaScript tutorial snippets that can type things out:

var currentIndex = 0;
var text = 'This is a typing effect!'; /* The text */
var typingSpeed = 50; /* The speed/duration of the effect in milliseconds */

function typeWriter() {
  if (currentIndex < text.length) {
    document.getElementById("output").innerHTML += text.charAt(currentIndex);
    currentIndex++;
    setTimeout(typeWriter, typingSpeed);
  }
}

There are also more robust libraries that could do much more, like TypeIt JS. I shy away from using libraries for small implementations, so I want to write my own vanilla solution. First, I wrapped the dynamic portion in a <span> tag with its own ID:

<h2>Need help with your<br />&nbsp;<span id="dynamicText">website?</span></h2>

I added a line break AND a non-breaking whitespace character to ensure that the changing text used a consistent amount of space. Otherwise, on smaller screen sizes, you could see elements jump as the content changes.

Here is the JavaScript that controls the animation:

  const dynamicText = document.getElementById('dynamicText');
  let originalText = dynamicText.innerText;
  const newTexts = ["app?", "database?", "UI/UX?", "AI?", "API?", "blockchain?", "website?"];
  let textIndex = 0;
  let index = originalText.length;

  function deleteText() {
    dynamicText.innerText = originalText.slice(0, index--);

    if (index >= 0) {
      setTimeout(deleteText, 100); // Adjust the timeout to control the speed of deletion
    } else if (textIndex < newTexts.length) {
      originalText = newTexts[textIndex];
      setTimeout(typeNewText, 500); // Adjust the delay before typing new text
    }
  }

  function typeNewText() {
    if (textIndex < newTexts.length) {
      const newText = newTexts[textIndex];
      index = 0;

      function type() {
        dynamicText.innerText = newText.slice(0, index++);

        if (index <= newText.length) {
          setTimeout(type, 100); // Adjust the timeout to control the speed of typing
        } else {
          textIndex++;
          if (textIndex < newTexts.length) {
            setTimeout(deleteText, 500); // Adjust the delay before deleting the text
          }
        }
      }

      type();
    }
  }

The code snippet begins by declaring a constant variable dynamicText, which stores a reference to an HTML element with the id ‘dynamicText’. This element is where the typing effect will be displayed. Following this, a variable originalText is initialized with the initial text content of the ‘dynamicText’ element. This serves as the starting point for the typing effect.

Next, an array newTexts is defined, containing a list of texts that will be typed out in sequence after the original text is deleted. These texts represent the subsequent messages to be displayed in the typing animation.

Two numerical variables, textIndex and index, are declared to keep track of the current text being typed from the newTexts array and the index within the text being typed, respectively.

The deleteText() function is responsible for deleting the original text character by character until it’s fully removed. It utilizes the setTimeout function to control the speed of deletion. Once the original text is completely deleted, the function triggers the typeNewText() function to start typing the next text from the newTexts array.

Similarly, the typeNewText() function is defined to type out the new text character by character. It also utilizes setTimeout to control the speed of typing. Once the entire new text is typed, the function updates the textIndex to move to the next text in the array and triggers the deleteText() function again to delete the typed text and repeat the process with the next text.

Blinking Cursor

To add a dynamic touch, I incorporated a blinking cursor to the typing effect. The “cursor” itself if a vertical pipe bar character: “|”. I wrapped in a <span> tag and gave it a CSS class cursor.

<h2>Need help with your<br />&nbsp;<span id="dynamicText">website?</span><span class="cursor">|</span></h2>

Making it blink was as simple as adding some CSS rules:

.cursor {
  margin-left: 5px;
  animation: blink 1s steps(1) infinite;
  font-size: .9em;
}

@keyframes blink {
  50% { opacity: 0; }
}

Finally, I remove the cursor when the final word is being typed out:

if(textIndex+1 === newTexts.length){
	var cursorElement = document.querySelector('.cursor');
	if (cursorElement) {
	cursorElement.remove();
}

I add that code to the conditional block responsible for checking if all of the words have been iterated through. The final source looks like this:

const dynamicText = document.getElementById('dynamicText');
let originalText = dynamicText.innerText;
const newTexts = ["app?", "database?", "UI/UX?", "AI?", "API?", "blockchain?", "website?"];
let textIndex = 0;
let index = originalText.length;

function deleteText() {
  dynamicText.innerText = originalText.slice(0, index--);

  if (index >= 0) {
    setTimeout(deleteText, 100); // Adjust the timeout to control the speed of deletion
  } else if (textIndex < newTexts.length) {
    originalText = newTexts[textIndex];
    setTimeout(typeNewText, 500); // Adjust the delay before typing new text
  }
}

function typeNewText() {
  if (textIndex < newTexts.length) {
    const newText = newTexts[textIndex];
    index = 0;

    function type() {
      dynamicText.innerText = newText.slice(0, index++);

      if (index <= newText.length) {
        setTimeout(type, 100); // Adjust the timeout to control the speed of typing

		if(textIndex+1 === newTexts.length){
			var cursorElement = document.querySelector('.cursor');
			if (cursorElement) {
			cursorElement.remove();
		}
        }

      } else {
        textIndex++;
        if (textIndex < newTexts.length) {
          setTimeout(deleteText, 500); // Adjust the delay before deleting the text
        }
      }
    }

    type();
  }
}

window.onload = function() {
  setTimeout(deleteText, 1500);
};

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, "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.

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 bncert-linux-x64.run https://downloads.bitnami.com/files/bncert/latest/bncert-linux-x64.run`

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.

Snapshots

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.

 

Case Study: Fixing a WooCommerce Website for a New Client

I met Steven at his store on Bloomfield Avenue in Northern New Jersey. After I gave him my business card he told me his website needs help. The checkout wasn’t working, and users couldn’t even add products to their cart. This was how the previous web development vendor left things before their arrangement ended.

The website was powered by WordPress (managed by Bluehost), and used WooCommerce as its ecommerce solution. I helped him create a Stripe account, and connect it to his online store.  I finished configuring a premium WordPress theme called BeTheme, and gave him a multi-week marketing plan to help sales grow.

website screenshot

I used an image manipulation program (the GIMP) to create graphic assets used throughout the shop:

website graphic design

Many times I have to pick up where someone else left off. I could tell you another story about inheriting a Frankenstein tech stack from a previous vendor. They left off on non-talking terms after demanding back work payments to release the credentials to my team. My skill in figuring things out, regardless of the technology involved, shines in times like these.

My company tag line is “I can build your website” – it should really be “I can fix your website”. Business owners try to do it themselves, and often make it most of the way. When you need help, I am there to carry it over the finish line. I’ve been asked if services like Wix cuts into my business – it’s actually the opposite. Broken, incomplete, or unoptimized websites created on easy-to-use platforms have provided a solid market for my expertise.

Organic market

Local small businesses are what make neighborhoods unique and give families a chance to make a living themselves. It feels great to help people knowing we can both benefit. You can read more about the plan I use to help businesses with their existing website in another blog post.

Membership Discounts Without a Plugin

As part of the marketing plan, we decided to add membership accounts to the WordPress ecommerce website for Organic Sun Market. Enabling that capability was a few settings in the dashboard: WooCommerce > Settings > Accounts & Privacy

woocommerce accounts and privacy settings

I also added a “My Account” link to the site’s global navigation.

menu in wordpress

By default, WooCommerce provides a “My Account” page where users can log in, view their orders, update their information, and more. You can specify a custom page in the advanced settings: WooCommerce > Settings > Advanced

woocommerce advanced setting

The account page specified uses a WooCommerce short code to handle the content: [woocommerce_my_account]

account page shortcode

Change menu text if user is logged into WordPress

I wanted the “My Account” menu text to change if the user is not logged in. I was able to do this with the WordPress hook `wp_nav_menu` and a simple string replacement PHP function:

add_filter('wp_nav_menu', 'change_my_account_menu_item', 10, 2);

function change_my_account_menu_item($nav_menu, $args) {
// Check if the user is not logged in
    if (!is_user_logged_in()) {
        // Change "My Account" link to "Login/Register"
        $nav_menu = str_replace('My account', 'Login/Register', $nav_menu);
    }
    return $nav_menu;
}

To incentivize users to create an account, we offer a 5% discount to any one logged in. The checkout page contains conditional messaging (depending on wether they are logged in or not) to communicate this incentive.

conditional css messaging on checkout

Hide or show UI elements if user is logged into WordPress

I am able to apply that  style condition with two custom CSS classes, specific to the presence of the WordPress body class ‘logged-in’:

.only-show-while-logged-in{display: none;}
body.logged-in .dont-show-while-logged-in{display:none;}
body.logged-in .only-show-while-logged-in{display:block;}

Apply WooCommerce discount to logged in users

I applied the discount by using custom PHP code in the child theme’s functions.php file with the `woocommerce_before_calculate_totals` hook:

add_action( 'woocommerce_before_calculate_totals', 'no_discount_if_not_logged_in', 10, 1);
function no_discount_if_not_logged_in( $cart ) {
	if (is_user_logged_in()) {              
		foreach ( $cart->get_cart() as $cart_item ) {        
			$discount_eliminate = $cart_item['data']->get_regular_price();
			$discount_percentage = 5; // Set your desired discount percentage
			$discount_amount = $discount_eliminate * ($discount_percentage / 100);
			$new_price = $discount_eliminate - $discount_amount;

			$cart_item['data']->set_price($new_price);
		}
	}
}

Apply WooCommerce discount to logged in users on a specific category of products

Later, we changed the logic to be a 10% discount for logged-in members, but only on products that were part of a specific category called “bundles”.

add_action( 'woocommerce_before_calculate_totals', 'discount_for_specific_category', 10, 1);

function discount_for_specific_category( $cart ) {
    if ( is_user_logged_in() ) {
        // Define the category slug you want to apply the discount to
        $target_category = 'bundles';

        foreach ( $cart->get_cart() as $cart_item ) {
            $product_id = $cart_item['product_id'];

            // Check if the product belongs to the target category
            if ( has_term( $target_category, 'product_cat', $product_id ) ) {
                $discount_eliminate = $cart_item['data']->get_regular_price();
                $discount_percentage = 10; // Set your desired discount percentage
                $discount_amount = $discount_eliminate * ( $discount_percentage / 100 );
                $new_price = $discount_eliminate - $discount_amount;

                $cart_item['data']->set_price( $new_price );
            }
        }
    }
}

Print Design

Many local small businesses take their marketing offline and into the real world. Print marketing is a business I have been a part of for almost two decades. I have designed, delivered, and distributed flyers, menus, business cards and more. As the holiday season approached, Steven asked me to create a poster for one of his healthy products.

graphic design request via text message

He sent me a draft he has been working on, along with some inspiration examples that expressed the direction he wanted things to go. This was the final product:

Dog treats poster

And here it is hanging in the store front:

Printed poster design

Why Every Business Needs a Website in 2024

small business websites

Don’t Be Invisible: A Website Puts Your Small Business on the Map

Local small businesses without a brick-and-mortar presence fall into a unique category. They might be service-based businesses like freelance consultants (like myself), home repair services, personal trainers, or cleaning services that operate on a mobile basis or from a home office. These businesses rely heavily on word-of-mouth, local advertising, and community networking to attract and maintain their clientele.

Because this business model does not require a physical storefront, these owners may underestimate the value of a digital presence. They might perceive it as unnecessary, believing that their local reputation and personal customer relationships are sufficient for business growth and sustainability. Consequently, they may neglect their online visibility, not realizing the potential reach and efficiency gains from digital tools.

The digital gap for these businesses can be characterized by a lack of a professional website, minimal social media engagement, and reliance on outdated forms of communication like AOL or Gmail email addresses. While this might maintain a certain level of operation, it limits their ability to scale, reach new markets, and ultimately leaves honey on the table.

By not leveraging the digital space, these businesses miss out on the opportunity to build brand awareness beyond their immediate locality, engage with customers online, and streamline their operations through digital tools (customer management). As a result, they might struggle to compete with others who adopt a more integrated approach to physical and digital business practices.

Cleaning website design

Professional Image: Elevate Your Brand

Your website can be the first point of contact between your business and potential customers. Just as you wouldn’t attend a party in pajamas, you shouldn’t let your online presence be represented by a dated AOL email address or non-existent web page. A sleek, user-friendly website tells customers that you are a serious professional who invests in all aspects of your business. I can help you do it right with a custom domain and business email address.

Some businesses rely on on a third-party subdomain or a link-in-bio service. While these options may seem convenient and cost-effective in the short term, it looks unprofessional. It is harder for customers to remember and doesn’t carry the same weight of brand authority as a standalone domain. It’s like to setting up shop in someone else’s store.

Using a personal @aol.com or @gmail.com email address for business communications can inadvertently signal a lack of professionalism and an outdated approach to business. I can assist you in transitioning to a custom domain email that reinforces your credibility. Too often, I walk around and see businesses make the mistake of thinking this detail doesn’t matter.

Testimonials & Social Validation

Word-of-mouth is powerful, but in the digital age, testimonials and portfolios on your website can reach further and speak louder. They serve as a perpetual source of validation for your work, allowing potential customers to see the breadth and quality of your services at any time. It’s the online equivalent of a recommendation from a trusted friend, accessible to everyone, everywhere.

Logo

Having a logo that fits your business image goes beyond digital space. I can show you tips and details for getting it right and making work across online  and print platforms.

A sample web design logo

Organic & Paid Search (Unclaimed Digital Territory)

If you’re running a business without a website and have been relying solely on traditional methods, it’s time to unlock new opportunity. Customers are searching for your service right now!

Organic search refers to the natural listings on search engine results pages (like Google or Bing) that appear because of their relevance to the search terms, as opposed to being advertisements. By not having a website, you’re missing out on the chance to appear in these listings—a place where a significant portion of your potential customers start their journey.

And, this is another reason to get setup with your own domain name if you’re currently using something like Bitly. Search engines like Google give more credibility to websites with a clear, branded domain name, which can significantly impact your search rankings and, by extension, your visibility to potential customers.

On the flip side, paid search advertising allows your website to jump to the top of search results by paying for prime placement. Paid search campaigns through platforms like Google Ads can be tailored to target the exact demographic you want to reach, with the ability to adjust for location, language, and more.

If you’re ready to explore the untapped potential of online search, let’s chat. I’m here to guide you through every step of the journey.

Who needs a website?

A website is your digital calling card and can be a sales generator if we do it right. Here are some of the top industries that I’ve helped grow online:

  • Restauraunt websites: Enjoy commision-free online ordering. With enticing designs and easy navigation, your patrons can effortlessly browse menus, book tables, and more.
  • E-commerce websites: Expand your business horizons by selling online.
  • Wedding websites: Share your love story, manage RSVPs, provide event details, and create lasting memories for you and your guests.
  • Martial arts school websites: Enroll students online, share class schedules, highlight events, and build a digital community.
  • Real Estate websites: Highlight property listings with interactive galleries, virtual tours, and advanced search filters.

 

Boost Your Marketing: Integrating Newsletter Signups with HubSpot API

hubspot api

I do a lot of in-person prospecting to win new business. I used to focus on giving out my business card, and then hoping the potential client would reach out to me. I learned that its better to capture their information, and then follow up. I used to collect so many business cards. Now I use a CRM, Customer Relationship Manager, called HubSpot. It does a lot of things, but primarily it helps to manage contacts. I want to let users sign up to my newsletter, and add themselves as a HubSpot contact, directly from my website.

hubspot forms

Create a Custom Newsletter Sign Up Form on Your Website Powered by HubSpot

Out-of-the-box, HubSpot can generate a number of different web forms that can be easily embedded into any website. These forms are used to capture leads and contacts.

HubSpot Private App

My website (this website) is mostly hand-coded. I want to build my own custom form, and submit the data to the HubSpot service. This is possible with the “Private app” feature. You can find this under ‘Settings -> Integrations -> Private Apps’. This strategy acts as a work-around to remove the HubSpot logo from forms without having to upgrade to premium.

HubSpot Private Apps

After entering a name and description for your “app”, you’ll need to select permission scopes. I called mine “Antpace-Website” and described it “signup forms on antpace.com”. The only scope is gave it was called `crm.objects.contacts.write`

hubspot permission scopes

CRM API – Create Contact

To create a contact programmatically through the HubSpot API, we use our private app’s access token. In my website’s HTML, I create a simple form:

<div class="col-md-12">
    <h1 style="text-align: center;">Newsletter</h1>
    	<p>Joining the mailing list!</p>
        <form id="hubspotForm" class="styled-form">
            <input type="hidden" value="subscriber" name="lifecyclestage">
            <div>
            	<div><input type="email" placeholder="Email *" value="" id="hubspot-email" name="email" class=""></div>    
                <div><input type="text" placeholder="First Name" name="firstname" value="" class=""></div>
                <div><input type="text" placeholder="Last Name" name="lastname" value="" class=""></div>        
                <div><input type="tel" placeholder="Phone" value="" name="phone" class=""></div>            
                <div><input type="text" placeholder="Company" value="" name="company" class=""></div>            
                <div><input type="text" placeholder="Website" value="" name="website" class=""></div>            
                
                <div><button type="button" id="signupButton" class="btn">Sign Up</button></div>
            </div>
	</form>
</div>

I add basic jQuery JavaScript to pass the form data along to my backend service when the button is clicked:

<script>
$(function(){
	const notifications = new UINotifications();
	$("#signupButton").click(function(){
		const email = $("#hubspot-email").val();
		if(email.length < 1){
			notifications.showStatusMessage("An email address is required.");
			return;
		}
		const formData = {
			properties: {}
		};

		// Iterate over form fields and add them to formData
		$("#hubspotForm input").each(function() {
			const fieldName = $(this).attr("name");
			const fieldValue = $(this).val();
			formData.properties[fieldName] = fieldValue;
		});
		$.ajax({
			type: "POST",
			data: JSON.stringify(formData),
			url: "/hubspot-service.php",
			success:function(response){
				console.log(response);
				notifications.showStatusMessage("Thank you for signing up.");
                                $("#hubspotForm")[0].reset();
			
			}
			
		});
	});

});
</script>

The HubSpot API documentation says, “To create new contacts, make a POST request to /crm/v3/objects/contacts“. Some simple PHP cURL commands accomplishes this. Here is the content of hubspot-service.php:

<?php

$url = 'https://api.hubapi.com/crm/v3/objects/contacts';
$accessToken = 'xxx';

$headers = array(
    'Authorization: Bearer ' . $accessToken,
    'Content-Type: application/json',
);

$postData = file_get_contents("php://input");

$ch = curl_init($url);

curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

$response = curl_exec($ch);

if (curl_errno($ch)) {
    echo 'Error: ' . curl_error($ch);
}

curl_close($ch);
echo $response;

?>

You can see the end result by visiting my newsletter sign up page. Make sure you add yourself so you can stay up-to-date with technology tips for business.

Newsletter sign up page

I can use this same PHP service file for other HubSpot email sign up forms throughout my website.

Magic Squares in JavaScript

magic squares in javascript

magic square

In mathematics, a “magic square” is a matrix of numbers where each row, column, and diagonal add-up to the same number. That number is called the “magic constant“. The integers used are only positive, and do not repeat. The constant sum is determined by the size of the square and is described by a formula:

M = n * ((n^2 + 1) / 2)

Facts about the properties and classifications of these numeric formations have been discussed by scholars for millennia. Knowledge of this topic goes back thousands of years, and can be found referenced throughout the world.

History & Culture

Magic squares have a fascinating historical and cultural significance, often with mystical undertones. They are mentioned in the I Ching, Brhat Samhita, and other works concerned with the transcendental and occult. They can be seen used in art, divination, perfumery, recreational gaming, computer science, and more.

a computer programmer using the Brhat Samhita to generate a talismans
AI images generated using Midjourney

In the Brhat Samhita, the magic square is used as a symbolic representation of the planets. It makes use of magic squares in the creation of talismans for astrological purposes.

For the purposes of this blog post, we’ll view them through the lens of software engineering.

3×3 Magic Squares

There’s so much to cover on this topic. I’ll narrow it down to 3×3 lattices (magic squares can actually be any size), specifically in the context of the JavaScript programming language. A quad of numbers, like the one pictured above, can be described as a two-dimensional array:

const magicSquare = [[4, 9, 2], [3, 5, 7], [8, 1, 6]];

Squares of this size always have a magic constant of 15. And, the number 5 will be in the middle. There’s additional logic that explains which numerals can appear where and why. Those ideas are explored in the comments section of a HackerRank coding challenge titled “Forming a Magic Square”.

HackerRank Coding Challenge

This coding problem found on HackerRank asks programmers to figure out what it would take to convert a 3×3 matrix of integers into a magic square. The input array is almost valid, but requires a few replacements. For each change, we must track the difference between the numbers and return the total variance – referred to as the “cost”. The correct answer will be the lowest cost required to convert the input data into a magic square.

hackerrank coding challenge

The difficulty of this assignment is considered “medium”. The first step is realizing that there are a finite number of valid magic square configurations. As it turns out, there are exactly eight 3×3 permutations:

const magicSquares = [[[4, 9, 2], [3, 5, 7], [8, 1, 6]], 
                [[6, 1, 8], [7, 5, 3], [2, 9, 4]], 
                [[8, 1, 6], [3, 5, 7], [4, 9, 2]], 
                [[2, 9, 4], [7, 5, 3], [6, 1, 8]], 
                [[8, 3, 4], [1, 5, 9], [6, 7, 2]], 
                [[4, 3, 8], [9, 5, 1], [2, 7, 6]], 
                [[6, 7, 2], [1, 5, 9], [8, 3, 4]], 
                [[2, 7, 6], [9, 5, 1], [4, 3, 8]]];

Starting with any one of these, we can generate the other seven programmatically. The subsequent arrangements can be derived through rotation and reflection. Using JavaScript, I rotate an initial seed square three times to have the first four series. Then, I flip each one of those to get the final records.

function generateMagicSquares(magicSquare1){
	const magicSquares = [];
	magicSquares.push(magicSquare1);

	// we need to rotate it 3 times to get all rotations
	for(let i = 0; i < 3; i++){
		var rotation = magicSquares[i].map((val, index) => magicSquares[i].map(row => row[index]).reverse());
		// console.log(rotation)
		magicSquares.push(rotation);
	}

	// and then flip each one
	for(let i = 0; i < 4; i++){
		var flipped = magicSquares[i].map((_, colIndex) => magicSquares[i].map(row => row[colIndex]));
		magicSquares.push(flipped);
	}
	
	return magicSquares;
}

const magicSquare1 = [[4, 9, 2], [3, 5, 7], [8, 1, 6]];
const magicSquares = generateMagicSquares(magicSquare1);
console.log(magicSquares);

To solve this exercise, we’ll take the input array and compare it to each of the valid magic squares. We keep track of the cost on each iteration, and finally return the minimum.

function formingMagicSquare(s){
	const magicSquares = [[[4, 9, 2], [3, 5, 7], [8, 1, 6]], [[6, 1, 8], [7, 5, 3], [2, 9, 4]], [[8, 1, 6], [3, 5, 7], [4, 9, 2]], [[2, 9, 4], [7, 5, 3], [6, 1, 8]], [[8, 3, 4], [1, 5, 9], [6, 7, 2]], [[4, 3, 8], [9, 5, 1], [2, 7, 6]], [[6, 7, 2], [1, 5, 9], [8, 3, 4]], [[2, 7, 6], [9, 5, 1], [4, 3, 8]]];
	
	// let minCost = 100000;
	let minCost = Number.MAX_SAFE_INTEGER;
	let cost = 0;
	for(let i = 0; i < magicSquares.length; i++){
		cost = determineCost(s, magicSquares[i]);
		if(cost < minCost){
			minCost = cost;
		}
	}
	return minCost;
}

You’ll notice that the initial minimum cost is set to a very high number. As I loop through each of the valid magic squares, I check if the cost is lower than the current minimum and then replace the value.

With each comparison, subtraction is used to determine the cost to complete the transformation. That code loops through each digit of each row on both 2D arrays. The absolute value of the difference between each coordinate is tallied and returned.

function determineCost(inputArray, validMagicSquare){
	let cost = 0;
	for(let i = 0; i < 3; i++){ // each row
		
		for(let j = 0; j < 3; j++){ // each digit

			cost += Math.abs(inputArray[i][j] - validMagicSquare[i][j]);
		}
	}
	return cost;
}

The working code all stitched together looks like this:

<script>

function generateMagicSquares(magicSquare1){
	const magicSquares = [];
	magicSquares.push(magicSquare1);

	// we need to rotate it 3 times to get all rotations
	for(let i = 0; i < 3; i++){
		var rotation = magicSquares[i].map((val, index) => magicSquares[i].map(row => row[index]).reverse());
		// console.log(rotation)
		magicSquares.push(rotation);
	}

	// and then flip each one
	for(let i = 0; i < 4; i++){
		var flipped = magicSquares[i].map((_, colIndex) => magicSquares[i].map(row => row[colIndex]));
		magicSquares.push(flipped);
	}
	
	return magicSquares;
}

function determineCost(inputArray, validMagicSquare){
	let cost = 0;
	
	for(let i = 0; i < 3; i++){ // each row
		
		for(let j = 0; j < 3; j++){ // each digit

			cost += Math.abs(inputArray[i][j] - validMagicSquare[i][j]);
		}
	}

	return cost;

}

function formingMagicSquare(s){
	// const magicSquares = [[[4, 9, 2], [3, 5, 7], [8, 1, 6]], [[6, 1, 8], [7, 5, 3], [2, 9, 4]], [[8, 1, 6], [3, 5, 7], [4, 9, 2]], [[2, 9, 4], [7, 5, 3], [6, 1, 8]], [[8, 3, 4], [1, 5, 9], [6, 7, 2]], [[4, 3, 8], [9, 5, 1], [2, 7, 6]], [[6, 7, 2], [1, 5, 9], [8, 3, 4]], [[2, 7, 6], [9, 5, 1], [4, 3, 8]]];
	const magicSquare1 = [[4, 9, 2], [3, 5, 7], [8, 1, 6]];
	const magicSquares = generateMagicSquares(magicSquare1);
	
	// let minCost = 100000;
	let minCost = Number.MAX_SAFE_INTEGER;
	let cost = 0;
	for(let i = 0; i < magicSquares.length; i++){
		cost = determineCost(s, magicSquares[i]);
		if(cost < minCost){
			minCost = cost;
		}
	}
	return minCost;
}

const finalCost = formingMagicSquare([[4, 9, 2], [3, 5, 7], [8, 1, 5]]);
console.log(finalCost);
</script>

This solution was not intuitive to me and took some research and experimentation. It was interesting to learn about the concept of magic squares (and other shapes) along the way.

HackerRank challenge completed

Additional References

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