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.

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

Tracking Google Ad Conversions in WordPress

google ads

If you’re a business owner or marketer, you’re likely familiar with Google Ads as a strong way to drive traffic to your website. Setting it up effectively can require some technical know-how, especially when integrating with WordPress. Allow me to walk you through properly integrating Google Ads with Google Analytics and Google Tag Manager, the right way, to maximize your website’s performance and data tracking capabilities.

Why Use Google Ads for Your Website?

Google Ads is an excellent tool for driving targeted traffic to your website. By bidding on keywords relevant to your business, you can show ads to users actively searching for what you offer. But, to really understand the value of that traffic, it’s crucial to track it correctly. That’s where tools like Google Analytics and Google Tag Manager come in.

Option 1: Directly Connect Google Analytics and Google Ads with Custom JavaScript

For many businesses and marketers, a straightforward setup of Google Analytics and Google Ads is enough to track important website interactions without needing a more complex setup. If you’re looking to avoid additional tools or platforms, you can connect Google Analytics and Google Ads directly and use custom JavaScript to track key actions on your website. Here’s how to do it:

Site Kit for WordPress

Step 1: Install Google Analytics (GA4)

To begin, you’ll need to install Google Analytics on your WordPress site. There are a couple of ways to do this:

  • Using a Plugin: WordPress plugins like Site Kit by Google simplify the process. After installation, the plugin will automatically insert the Google Analytics tracking code into your website, and you’ll be able to see key data like page views and user behavior within your Google Analytics dashboard.
  • Manual Code Insertion: If you prefer not to use a plugin, you can manually add the GA4 tracking code to your WordPress site. To do this, copy the tracking script from your Google Analytics account and paste it into your site’s header or footer file (usually found in your theme settings or via your website’s code editor). This will start collecting data immediately.

Step 2: Link Google Analytics to Google Ads

Once Google Analytics is set up, the next step is to link it with your Google Ads account. This will allow you to see how your ad campaigns are performing in terms of real user behavior and conversions on your website.

  • In Google Analytics, navigate to the Admin section and find the Google Ads Linking option under the Property settings.
  • Follow the prompts to link your Google Ads account. Once linked, data such as user behavior, conversions, and audience insights will flow between Google Ads and Analytics, allowing you to optimize your campaigns based on how visitors interact with your site.

Step 3: Set Up Custom JavaScript for Conversion Tracking

For more advanced tracking, you can use custom JavaScript to monitor specific actions on your site, like button clicks, form submissions, or external link redirects. Here’s how you can do this:

custom javascript

  • Google Ads Conversion Tracking: Google Ads provides a conversion tracking code that you can place directly into your website’s HTML. For example, if you want to track when users complete a form or click a specific button, you can insert this conversion tracking code into the relevant part of your site’s HTML.
    • Navigate to your Google Ads account, go to Tools & Settings > Conversions, and create a new conversion action.
    • Google Ads will generate a conversion tracking code that includes a JavaScript snippet. You can add this code directly into your website’s HTML (either through your WordPress editor or directly into the page code) to track actions.
  • Tracking Button Clicks: If you want to track a button click (for example, a “Contact Us” button), you can insert the conversion tracking code into the button’s HTML attributes. This will trigger a conversion event each time a user clicks the button, allowing you to monitor how often it leads to valuable actions on your site.
  • Tracking Form Submissions: Similarly, if you want to track when users submit a contact form, you can embed the conversion tracking code into the form’s submission page or thank-you page. This is especially useful for tracking lead generation or contact inquiries.

Here is a sample of the code I used:

<script>
document.addEventListener('DOMContentLoaded', function() {
    var buttons = document.querySelectorAll('.cta-consultation'); // Select all elements with the class 'cta-consultation'

    // Check if there are any buttons to avoid errors
    if (buttons.length > 0) {
        buttons.forEach(function(button) {
            button.addEventListener('click', function() {
                // Fire the Google Ads event
                gtag('event', 'conversion', {
                    'send_to': 'AW-XXX/XXX',
                    'event_callback': function() {
                        console.log('Conversion tracked!'); // Optional: for testing purposes
                    }
                });
            });
        });
    }
});
</script>

Step 4: Monitor and Optimize Conversions

Once the custom JavaScript is in place, Google Analytics will start tracking the data, and you’ll be able to see how users are interacting with your site in the Conversions section of Google Ads. Over time, you can monitor the performance of your campaigns and make adjustments to improve your return on investment (ROI).

Key Advantages of This Approach

Simple and Direct: For small or less complex websites, directly connecting Google Analytics and Google Ads using custom JavaScript is a simple and effective solution.

Option 2: Integrating Google Analytics and Google Ads Using Google Tag Manager

For businesses that want more flexibility and control over their tracking setup, using Google Tag Manager (GTM) offers a more advanced approach compared to directly connecting Google Analytics (GA4) and Google Ads. By connecting GA4 through GTM and setting up Google Ads to pull conversion data from GA4, you can streamline the process and enable more precise tracking, including custom click tracking. Here’s how to do it:

Step 1: Set Up Google Analytics (GA4) Through Google Tag Manager

Instead of directly embedding Google Analytics code into your WordPress site, you can set up GA4 using Google Tag Manager. This allows for more flexibility in managing and customizing tracking tags without manually editing the site code. Be careful to not have it set up twice (once directly on its own, and also in GTM), or else you may see double measurements

Install Google Tag Manager on Your WordPress Site

First, install GTM by either using a plugin like Site Kit by Google or manually adding the GTM container code to your site’s header. This will serve as the foundation for all tracking scripts on your site.

  • Create a GA4 Tag in GTM:
    • In GTM, create a new tag by selecting Google Analytics: GA4 Configuration.
    • Add your GA4 Measurement ID (found in your Google Analytics account) to the tag configuration.
    • Set the trigger to All Pages, so it tracks page views across your entire site.
    • Save and publish the changes in GTM, and your website will now be tracking page views and user behavior through GA4 without needing any code embedded directly in your site.

Step 2: Set Up Custom Click Tracking Through GTM Triggers

One of the major advantages of using Google Tag Manager is the ability to create custom triggers to track specific user interactions, such as button clicks or link clicks, without having to modify the site’s code.

Create a Click Trigger in GTM

To track specific interactions like button clicks, you’ll need to set up a trigger in GTM. In GTM, create a new Trigger and select the Click – All Elements option. Define the conditions for your trigger, such as when a user clicks a specific button or link. For example, if you want to track a button leading to a third-party calendar page, you can set the trigger to fire when a user clicks on the button with that specific URL. My conversion was based on clicking any button or link that led to a Google Calendar scheduling page. Since the editor we were using (Kubio) generated complex HTML around the button, I was having trouble targeting exact clicks. Ultimately, I used a selector that targeted click URLs that contained part of the destination:

gtm trigger

On another setup, I had to target “Just Links” for the event to fire. Make sure you test in preview mode to make sure it works!

Just links trigger

  • Create a Google Analytics Event Tag:
    • Create a new Google Analytics: GA4 Event tag in GTM.
    • Set the event parameters, such as what action or label you want to track (e.g., “Button Click” or “Contact Form Submission”).
    • Assign the click trigger you just created to this tag, so it fires when the user interaction happens.

gtm tags

Once your trigger is firing, the event will be recorded in GA4. This allows you to see detailed analytics on how users interact with specific elements on your site. You may have to wait until the next day to see it showing up and to continue the rest of this process.

Step 3: Link Google Ads to GA4 for Conversion Tracking

Once GA4 is collecting conversion data, you’ll want to make sure that Google Ads is also using this data for your ad campaigns. Instead of directly adding Google Ads tracking code to your website, you can import conversions from GA4 into Google Ads. Make sure you mark the event as a ‘Key Event’ in GA4:

ga4 key event

  • Link Google Ads to GA4: In your Google Analytics admin settings, find the Google Ads Linking option under the Property settings and link your Google Ads account.
  • Import Conversions into Google Ads: In your Google Ads account, navigate to Tools & Settings > Conversions. Click to import conversions from Google Analytics. You will now be able to track and optimize your Google Ads campaigns based on the conversion events recorded in GA4.

Step 5: Set Up Google Ads to Track Click Conversions

With the custom click tracking events flowing into GA4, you can now import these events into Google Ads as conversions.

  • In GA4, mark the click event you created as a Conversion (similar to how you would for other key actions).

key events

  • Then, in Google Ads, import the conversion from GA4, so your ad campaigns can track the performance of these custom click actions.

Conversions

This setup allows you to track highly specific user actions (like button clicks or form submissions) and optimize your Google Ads campaigns based on those actions.

GTM allows you to track custom events like button clicks, form submissions, and other interactions without modifying your site’s code. With Google Tag Manager, you can manage all of your tracking codes (for Google Analytics, Google Ads, and other tools) in one place. By pulling conversion data from GA4 into Google Ads, you can optimize your campaigns based on real user actions and get more detailed insights into how users interact with your site.

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 simplified cloud hosting solution. 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.