Migrate away from WordPress to Hugo

I’m no stranger to trying different CMS’s to see which one works best in terms of functionality, speed, and ease of SEO use. Over time – and many mistakes later – I’ve found that Hugo is the best for all three of those criteria. It’s very functional, fast to build, and easy to tweak for SEO. When you couple that together with the AWS backbone, you get a fast loading website where all the static builds happen automatically as soon as you update your Github repository. I’m writing this post on how I migrated away from WordPress to Hugo and used AWS Amplify to host my blog & website.

Introduction – WordPress Years

I started this blog on WordPress and then transferred it to Jekyll, then back to WordPress, and so on. You can read about my ‘passion’ for trying different CMS platforms by checking out my CMS tag, but the reality is that WordPress is really easy for newbies to get up and running quickly. Many popular blogs and sites run WordPress reliably and manage all their content with it. This is why it’s super sticky and it has some great themes to use out of the box. It’s when you want to ‘pimp’ or optimize your WordPress installation where you start running into problems.

For example, it relies on a database backend, and every time you visit the site it dynamically serves up the page. This is all good until you realize that you pay a loading time penalty for it. It can take a while. Plus you have to worry about malicious code injections from hackers that spam your site for the latest penis enhancement drug or Bitcoin scam. These code injections were the main impetus for me to start looking to migrate to a static blog generator.

If you want to do any SEO with WordPress you have to know PHP. I guess that’s ok but I never really cared much for PHP and found it boring. If you’re in that same boat, you’re going to have to use a plugin like Yoast. That’s great until you realize you have to start paying for it to get any benefit out of it. Do you need to modify your footer uniquely? You need to use a plugin. Want to back up your entire website? You have to use another plugin (and pay usually). Before you know it, you have like 10 or more plugins that are costing you money on top of what you pay for hosting.

All these plugins you need end up slowing your site down a lot too, which affects your SEO in a really big way. At the end of the day, you’re out of money and have a slow site. Sure it might look pretty but if you want to grow organic traffic you have to focus on great and valuable content that loads fast, is SEO optimized, costs a hell of a lot less, and makes YOUR life easy.

Hugo and AWS Amplify (and Github)

Let me be the first to warn you. Running Hugo on AWS Amplify (and Github) is not as easy as running WordPress with plugins. If you’re happy with your WordPress infrastructure and process, then just close your browser now and move on. However, if you want to have fine-grained control over your blog/website and feel comfortable doing a bit of ‘hacking,’ then please continue reading.

I just settled on using Hugo to make running my blog easier. I no longer have to deal with plugins and pay for them. Of course, this forces me to be more hands-on with how I want to maximize SEO, how I want to leverage the Indie Web, and even Web Monetization. I also have to do manual backups now instead of using Vaultpress, but with Github, that’s become so easy that it happens on autopilot.

Writing content for a Hugo-generated site is like a dream. The build times are fast, it has great image processing ability, and I can check out how my site will look before I deploy it if I run the development server. So I can do a lot of ‘offline things’ and know that once I push my update to my Github repo, AWS will rebuild and deploy my site automatically.

Of course, I have to pay for the usage of AWS Amplify but it will be A LOT cheaper than using the VPS at Dreamhost ($16/month). Sorry, Dreamhost but I think you’re not getting renewed this year.

Install Hugo

First things first. The move will take some time and if you follow these steps, should go off without a problem. However to prevent any mishaps we’re going to start with setting up a ‘staging’ environment. The goal is to build a clone of your existing WordPress site with Hugo and AWS Amplify so that when it’s all up and running you just ‘cut over’ to it.

Follow these steps:

  1. Download Hugo to your local machine and then read the quickstart introduction on how to build a site
  2. Grab a theme and install it for your Hugo installation
  3. Configure the config.toml file to your liking, especially on how your permalink structure is
  4. Startup your Hugo dev server using hugo server and then navigate to http://localhost:1313

You should see an empty local website running at that web address.

One of the biggest mistakes I made was NOT reading up on the permalink structure setting in the config.toml. I created hours of work for myself in creating canonical aliases for posts that I reorganized. So pay close attention to the configuration of permalinks in your config.toml file.

Sign up for an AWS account

The next step is easy, sign up for an AWS account here. We’ll get back to this later!

Sign up for a Github Account

This step is also easy, you’ll need to sign up for a Github account. You could use Gitlab, Bitbucket, and others, but I use Github for personal and work projects. It’s really easy to use for backing up your code and files BUT a little harder to use. You will need to get familiar with Git and I plan on writing a Git tutorial in the future.

How to Migrate Posts from WordPress to Hugo

Now we get into the fun part, the porting of your WordPress posts and pages to your local Hugo installation! First, you’ll need to extract your WordPress posts from your database. WordPress has a generic Export function but that’s not going to map the WordPress fields to the Hugo fields out of the box. What you will need is to use a Hugo Importer from WordPress.

If the WordPress exporter doesn’t work, try using WordPress to Jekyll exporter plugin AND then the Jekyll to Hugo importer.

Once you have your files, open one up to see if the YAML front end is correctly formatted. The YAML front end is where all the fine-grained control happens for your Hugo-powered website. It’s where you can control all aspects of your SEO metadata as well as options for Table of Contents, Keywords, Categories, Slugs (permalinks), titles, drafts, aliases, etc.

It should look something like this:

title: Post Title
date: 2020-07-21
slug: post-title  << this builds to /content-directory-post-is-in/post-title/index.html
- Word1
- Word2
- Word1
- Word2

The key is to have all your post metadata in between the ---‘s at the top of the post. Note, this will be a markdown file with a .md extension. If you’re not familiar with markdown, you can read up on it here. It takes a moment to get used to markdown but once you do you never have to worry about formatting what you write in the ‘frontend’, markdown handles all that for you when your post gets translated into HTML.

Take all your exported posts to move them to your local instance of Hugo and put them in the /content/posts/ directory. Note, Hugo loves content organization and you should think about if you want to use content folders or not. You can read up on content organization here.

Hugo’s Development Server

Once the posts are in the /content/posts/ directory, you should see Hugo rebuilding the site in your terminal.

It will then generate the website as a live preview. Any editing that’s saved will automatically rebuild and you can see your results in real-time. The great part about this is the debugging part. Since Hugo is written in Go, it doesn’t ‘suffer fools’ and will break if it’s not perfect. This is good and bad, it’s good because it will tell you exactly what went wrong. It’s bad because you need to fix that mistake first before you can build your website in production.

I use this development server ALL the time. It lets me confirm if what I’m about to push to Github is what I want the world to see. This is where I test new partial templates, new shortcodes, and try new Hugo features. The development server will become your best friend and you can read up on some more of its functionality here.

Use Github to backup your site

I use Github for my code and project-related stuff. The concept of Git is just brilliant. It’s version control, process control, and backup all rolled into one. Granted, Github is an extra step in my regular workflow but it makes sure that my blog remains consistent over time. No weird hiccups unless I screw up! If I screw up, Git has a ‘rollback feature’ so I can undo my mistake quickly. The older I get the more I realize that Git is the true power here, especially if you’re in a heavy development environment.

You’re not going to take your entire local installation of Hugo plus your content and sync them with a Github repository. The terminology you will hear is that you’re going to ‘push your code to a remote repo.’ Github is the remote repository where you’re going to push your content and files too.

You can use the git command line but I like to use Github Desktop. Makes things easy!

  1. First, you’re going to create local repository in Github Desktop
  2. You’re going to select the folder of your entire Hugo website, I name my folder neuralmarkettrends.com and when I create a local repo, it automatically names the remote repo the same name.
  3. Then you click ‘Publish’ and it will create the remote repo on Github. Note: your site is NOT backed up yet
  4. To back up the site in your remote repo you will need to create a commit message and then push it
  5. Once you push it to your Github repo, it should populate

Make sure that your repository is marked as private, this way no one can see your posts and stuff.


How to Put Hugo into Production with AWS Amplify

Once I had my blog backed up on Github I just followed the AWS Amplify instructions and generated the site successfully!

There was one big tweak that I made. AWS Amplify uses Hugo version 0.55 and that’s not compatible with many of the better themes. I had to update the version of Hugo the build was using to 0.74.2. It’s not hard, all I had to do was go to the Build Settings and follow these instructions.

End Notes

I migrated to AWS Amplify for selfish reasons. I was about to renew my VPS at DreamHost for over $200 and thought that it was a tad bit high. Using AWS Amplify I expect to pay around $5 a month based on my past traffic, if it gets more active then I’ll pay more but that’s ok.

The added benefit to migrating here is that I get a wicked fast load time on the AWS backbone. I get like 1 second mobile device load times and millisecond desktop device load times. Wowsers!


I realize that doing this migration is tricky for many non-hacker types of people, but you can learn if you want to. One of the things we didn’t go over in this post is using a custom domain with AWS Amplify. That was a bit tricking to set up and I’ll write another post on that shortly. Still, these steps should get you to a mirrored site of your WordPress blog on AWS Amplify.

Hugo SEO Tutorials

I made the switch over to Hugo a short while and love the fast builds. You can read all about me ‘crushing’ on it in my What I Learned using Hugo post. There’s one main thing I want to highlight in this post and it’s how to optimize Hugo for SEO. Hugo, just like any other CMS, is pretty good out of the box but you need to optimize it. Here are my Hugo SEO tips for optimization.

SEO Tip 1 – Run your site through Web.dev

My nephew alerted me to Web.dev which is an open-source SEO, Performance, Accessibility, and Best Practice analyzer. I ran my site through there, as well as my sections (blogs, tutorials, and reviews), and found some really valuable insight. I always knew that my images were clogging up my page load times but I never knew how bad they were.

I rank above a score of 90 for SEO, Performance, and Accessibility but Performance is what I need to work on. If you find the link, “view report” you should click on it and read it. It tells you everything you need to know and how to fix your site. I need to rethink my ‘featured image’ banner and how I insert images into my posts going forward and I suspect it will take me a few months to clean everything up. Hat tip to my nephew and his SEO consulting business!

SEO Tip 2 – Use a Content Delivery Network (CDN)

This was perhaps the easiest and fastest boost to all the web.dev categories were just hooking up a CDN to my blog. It was easy using my host provider Dreamhost.com. I forgot to mention in my review of Dreamhost that they also give you a basic membership to Cloudflare. I created an account with Cloudflare and go an instant boost in performance. On the flip side, one of the biggest boosts to performance would be generating different image sizes for different browser sizes. This was one of the recommendations from Web.dev and Cloudflare can do that automatically if you buy a business subscription for $400/month. That’s a non-starter with me and I’ll work on the method that Adam posts about in his blog.

SEO Tip 3 – Image Compression

This ties back to Tips 1 and Tips 2 above but image compression makes a huge difference. Even if you have a text-heavy site with the occasional image, it’s worth compressing them. I downloaded the open-source ImageOptim and ran it over my entire set of images in my /static folder. Mind you, the /static folder is what’s causing my entire performance problem. This folder is a leftover from my WordPress, Blot, Jekyll, and Expression Engine days.

In Hugo, anything file in the /static folder gets uploaded so it’s really easy to just dump all your images and files in there and then link to them in your blog post. This will kill your performance over the long run and it’s been doing it to me. Hugo has a wonderful want to auto-process images IF you use the right content organization. In Hugo, the content organization is fluid and it’s made porting this site over from Pelican easy. Once again, this easiness can trip you up in the long run if performance is what matters to you, so you need to spend some time thinking of the content organization, which is SEO Tip 4 below.

SEO Tip 4 – Content Organization

A bit of warning here but Hugo’s online documentation is very terse, which is what the Go Lang spec is like. If you read between the lines they’re assuming you already know what you are doing and I find that attitude to be a barrier for deeper adoption of this awesome platform. Hopefully, as I write more about Hugo and build out tutorials, this will make it easier for readers to grasp.

Hugo believes that you organize your content with a purpose. The same structure that works to organize your source content is used to organize the rendered site. As displayed above, the organization of the source content will be mirrored in the destination. (via Hugo)

Start here on Hugo’s Content Organization page and zoom into this screenshot image:

This is how Hugo prefers to organize your content. On the surface, it’s very simple and easy, but you can easily misuse it. Especially when dealing with images. The trick is to add an /images folder below your /posts folder or whatever you name it. For example, my structure looks like this:

- content
    |- about/
        |- _index.md
    |- blog/
        |- 2020-05-28-SEO-for-Hugo.md
        |- other posts
    |- tutorials/
        |- rapidminer/
            |- rapidminer posts
        |- other tutorial posts
    |- reviews/
        |- review posts

What I need to correct is placing /images folders below the /blog, /tutorials, and /reviews sections because of Page Bundles. What are page bundles? They are awesome ways to process files, images, and templates in specific ways for a particular section. On my blog, the /blog, /tutorials, and /reviews are MY sections. Page bundles break out into Image Processing and Page Resources and this is where the magic happens.

The Image Processing ability of Hugo lets you automatically crop, resize, fill, save with lower quality, and much more by just putting the images in a folder that’s associated with your blog posts.

Using the Page Resources you can include and humanely process different file types to be associated with your particular post. I found this to be cool and it reminds me of what I wrote about content creation and management. Specifically this:

“ In essence, content types are the building blocks of dynamic, future-friendly content across systems. They provide the structure used by people and computers to explicitly express meaning.”

So what I need to do is add a /images and /files folder under each section. HOWEVER, I need to think about this because how I organize this going forward (and even apply to older posts) is going to have a huge impact on my performance, good or bad. Here’s my initial take on the future folder structure.

- content
    |- about/
        |- _index.md
        |- files/
        |- images/
    |- blog/
        |- 2020-05-28-SEO-for-Hugo.md
        |- other posts
        |- files/
        |- images/
    |- tutorials/
        |- rapidminer/
            |- rapidminer posts
            |- files/
            |- images/
        |- other tutorial posts
    |- reviews/
        |- review posts
        |- files/
        |- images/

You should spend time thinking about this too instead of just writing away without thinking about the end in mind (thanks Stephen Covey!).

SEO Tip 5 – Hugo Themes

The last SEO Tip I’ll leave you with is selecting an appropriate theme. On the surface, this sounds easy but if you navigate over to Hugo Themes you’ll see over 300 of them. Some look and respond better, and others are just simple and very basic. I’ve tried several of them and some have better RSS handling (something I’ll write about later) and others have better social media integration. Hugo has many shortcodes and internal templates to make social media easy but some templates take advantage of that better.

I opted for a mobile responsive with a beautiful featured image header that’s now eating into my performance. C’est La Vie. Still, you have to carefully review the templates or write your own.

SEO Tip 6 – Responsive Images!

A lot of CMS’s (like WordPress) do this automatically when you upload an image but Hugo doesn’t do it out of the box. You can argue that Hugo doesn’t make it easy like WordPress but a counterpoint would be you can make changes to image resizing on ‘the fly’ like in Hugo. I have no thoughts on what is right here, I’m just trying to show you how I did by learning it from other tutorials so you can use responsive images for Hugo and amp up your SEO.

Responsive Images

What are responsive images? They’re just two to three (even four or more) resized images of your original image that get automatically displayed on your webpage based on your browser window size. If you had an original image of 1024px x 1024px in size and you end up viewing your site from a mobile phone, you might see a smaller version of it (400px x 400px) served to you on the fly. This is achieved by using the srcset tag in your HTML code. Modern browsers can pick up on this tag and supply the correct image sizes on loading. You just have to reference multiple image sizes in our HTML code and then let the browser do it automatically.

Still, this presents two problems. One, you have to create two of the three different sizes of your source image. Second, you have to then provide the reference HTML code to let the browser choose the right version. Since Hugo is a static generator, we can generate the HTML code automatically upon building time but how do you resize the images? You could manually do it but you’d be nuts to do that. The answer comes in the form of Hugo Page Resources. By creating a shortcode you can have Hugo resize the images and generate the correct HTML during your build time.

Laura’s and Adam’s tutorials

When I started learning about Hugo’s Page Sources and its built-in Image processing, I had no clue on how to use it. It wasn’t until I found Laura Kalbag’s and Adam Will’s tutorials that things began to make sense. Adam uses a javascript library to autogenerate his image sizes whereas Laura uses Hugo to take care of that as Resize operation with Page Resources. I predominately ended up using Laura’s way of resizing the images on the fly.

Content Organization

To use the image processing functions in Hugo, you will need to organize your content in a specific way. I wrote about content organization above and that’s what I started to do. I need to write a script to process all my old markdown posts, but I’ll relegate that to Python for another day.

You’ll need first need to name your post index.md under a directory and show the image in the same folder, like below:

If you inspect the image above, you’ll notice I called an SVG type of image. Hugo can handle JPG, SVG, and PNG quite easily. I have not tested it out for WEBP files yet. Once you’ve done that, the next step is to write a shortcode to kick off the processing.


I’ve come to love shortcodes, they’re amazing in the Hugo world. A shortcode is just that, a short word to handles some sort of processing behind the scenes. Hugo comes preloaded with some built-in ones like {{}} or {{}} which handles loading in videos, Instagram images, and other stuff. Well you can create your own and that’s what Laura did, she created the img shortcode that handled the Hugo resizing and auto-generating of the different image sizes.

The way she did was put in her markdown file the following shortcode:

{{some alt caption}}

The img is the shortcode img.html but you can call it whatever you like. The processing is done in the img.html file and I used Laura’s as a template.

Her code, in its entirely is this:

{{/* get file that matches the filename as specified as src="" in shortcode */}}
{{ $src := .Page.Resources.GetMatch (printf "*%s*" (.Get "src")) }}

{{/* set image sizes, these are hardcoded for now, x dictates that images are resized to this width */}}

{{ $tinyw := default "500x" }}
{{ $smallw := default "800x" }}
{{ $mediumw := default "1200x" }}
{{ $largew := default "1500x" }}

{{/* resize the src image to the given sizes */}}

{{ .Scratch.Set "tiny" ($src.Resize $tinyw) }}
{{ .Scratch.Set "small" ($src.Resize $smallw) }}
{{ .Scratch.Set "medium" ($src.Resize $mediumw) }}
{{ .Scratch.Set "large" ($src.Resize $largew) }}

{{/* add the processed images to the scratch */}}

{{ $tiny := .Scratch.Get "tiny" }}
{{ $small := .Scratch.Get "small" }}
{{ $medium := .Scratch.Get "medium" }}
{{ $large := .Scratch.Get "large" }}

{{/* only use images smaller than or equal to the src (original) image size, as Hugo will upscale small images */}}
{{/* set the sizes attribute to (min-width: 35em) 1200px, 100vw unless overridden in shortcode */}}

<img loading = "lazy"
  {{ with .Get "sizes" }}sizes='{{.}}'{{ else }}sizes="(min-width: 35em) 1200px, 100vw"{{ end }}
  {{ if ge $src.Width "500" }}
    {{ with $tiny.RelPermalink }}{{.}} 500w{{ end }}
  {{ end }}
  {{ if ge $src.Width "800" }}
    {{ with $small.RelPermalink }}, {{.}} 800w{{ end }}
  {{ end }}
  {{ if ge $src.Width "1200" }}
    {{ with $medium.RelPermalink }}, {{.}} 1200w{{ end }}
  {{ end }}
  {{ if ge $src.Width "1500" }}
    {{ with $large.RelPermalink }}, {{.}} 1500w {{ end }}
  {{ end }}'
  {{ if .Get (print $medium) }}
    src="{{ $medium.RelPermalink }}"
  {{ else }}
    src="{{ $src.RelPermalink }}"
  {{ end }}
  {{ with .Get "alt" }}alt="{{.}}"{{ else }}alt=""{{ end }}>

Let’s dig deeper into this code to see what’s going on.

Image Processing

Hugo has some great built-in image processing functions. Things like resize, fill, fit, filter, and extract EXIF data. These work only if you call them using a page resource, hence the content organization setup I posted about above.

Typically if you wanted to resize an image you’d have to call it as {{ $image := $resource.Resize "600x" }} in Hugo. So you would be able to call this resource from your markdown post using a shortcode. Since the $image part is a global resource, you can call it anywhere you like.

What this part of the code does is get the name of the img with its extension as the $src variable declaration, so when you have img src='name.jpg' you parse the .jpg part and then resize it into 4 different sizes (500px, 800px, 1200px, and 1500px).

{{/* get file that matches the filename as specified as src="" in shortcode */}}
{{ $src := .Page.Resources.GetMatch (printf "*%s*" (.Get "src")) }}

{{/* set image sizes, these are hardcoded for now, x dictates that images are resized to this width */}}

{{ $tinyw := default "500x" }}
{{ $smallw := default "800x" }}
{{ $mediumw := default "1200x" }}
{{ $largew := default "1500x" }}

{{/* resize the src image to the given sizes */}}

{{ .Scratch.Set "tiny" ($src.Resize $tinyw) }}
{{ .Scratch.Set "small" ($src.Resize $smallw) }}
{{ .Scratch.Set "medium" ($src.Resize $mediumw) }}
{{ .Scratch.Set "large" ($src.Resize $largew) }}

{{/* add the processed images to the scratch */}}

{{ $tiny := .Scratch.Get "tiny" }}
{{ $small := .Scratch.Get "small" }}
{{ $medium := .Scratch.Get "medium" }}
{{ $large := .Scratch.Get "large" }}

Since I’m just learning Go Lang and still bumbling my way through Hugo, what I gather from the above code is this. We create four variables of type ‘default’ called $tinyw, $smallw, $mediumw, and $largew. These then get passed into the $src.Resize function that’s built into Hugo to resize them into the 4 new sizes. These new sizes with filenames get assigned to the $tiny, $small, $medium, and $large variables.

After that, it’s a matter of substitution into HTML, which is called out into the remaining code.

<img loading = "lazy"
  {{ with .Get "sizes" }}sizes='{{.}}'{{ else }}sizes="(min-width: 35em) 1200px, 100vw"{{ end }}
  {{ if ge $src.Width "500" }}
    {{ with $tiny.RelPermalink }}{{.}} 500w{{ end }}
  {{ end }}
  {{ if ge $src.Width "800" }}
    {{ with $small.RelPermalink }}, {{.}} 800w{{ end }}
  {{ end }}
  {{ if ge $src.Width "1200" }}
    {{ with $medium.RelPermalink }}, {{.}} 1200w{{ end }}
  {{ end }}
  {{ if ge $src.Width "1500" }}
    {{ with $large.RelPermalink }}, {{.}} 1500w {{ end }}
  {{ end }}'
  {{ if .Get (print $medium) }}
    src="{{ $medium.RelPermalink }}"
  {{ else }}
    src="{{ $src.RelPermalink }}"
  {{ end }}
  {{ with .Get "alt" }}alt="{{.}}"{{ else }}alt=""{{ end }}>

There’s some cool stuff happening here especially with the .RelPermalink tag. Because of my new content folder structure, Hugo knows how to assign a relative permalink to these images in the folder. It just handles it all upon the final build. This sounds pretty mundane but I find it sweet.

Lazy Loading of Images

I still have more work here, especially around lazy loading images but that’ll use javascript and I hate working with JS. Such is life.

It turns out there is a loading attribute you can use called loading = "lazy" that works in modern browsers automatically. So I added this into the above code. Thanks James Loh!

SEO Tip 7 – How to Increase Your Ranking Keywords

I wanted to share this tip with readers who are interested in increasing their ranking keywords with very little effort. I was able to nearly double my ranking keywords in as little as two weeks by doing a few optimization tweaks.

It all started with some FB messaging to my niece and nephew over in Berlin. Covid19 had them pretty much shut-in for a while so they started working and optimizing their travel blog. Over that time they’ve turned into quite the SEO mavens and we started to share tips between us. Along the way, I learned a lot on how to tweak my blog so that it would start working (aka rank) better in the Google Search Engine.

My initial amount of ranking keywords was only 31. 31?!?! That was LESS than my decent/ok domain authority of 32!

How is it even possible that after 13 years of blogging my ranking keywords were a measly 31? This had me bothered for days until I realized the problem. The real culprit was ME and my ADHD of NOT sticking with a blogging platform. I had killed YEARS worth of built-in SEO by constantly porting my blog between different blogging platforms. Inbound links, comments, and canonical links were all shredded by different permalink structures, non-optimized SEO templates, and high-performance overheads.

So lesson 1, find one CMS platform and stick with it. If you choose to migrate at a later date you’ll have to think beyond doing global 301 redirects, you have to think about how to pull all your SEO parts from around the web to the new platform.

80% Content and 20% Optimization

My number 1 tip is to write great content. That’s it. If you did that on a modern CMS platform with a decent template, then you’ve given yourself a good shot at ranking in Google. I consider great content to be 80% of the SEO battle. Your great content is what will give you long-term ‘sticking’ power and is also known as evergreen content.

The Internet is littered with so much crap that tries to rank quickly in Google or other search engines. All that noise is such a futile endeavor IMHO and I stay away from that. I work hard to write relevant, useful, and great evergreen content that my readers can use and learn from. It’s no secret that the most popular section on my blog is my tutorials section. I’ve helped 100’s maybe even 1000’s of readers over the years and that’s what continues to drive me today.

The last 20% is optimization. I’m not interested in catching every possible incremental SEO tweak but just the big ones. Like I wrote above, I’d rather write content BUT there are some important things I needed to do here from a performance standpoint that’s starting to pay off.

My analytics are now showing that organic searches are now the #1 driver of traffic to my site. Before I implemented these tweaks, it was referrals and directs. While that’s nice and all (thank you), new traffic needs to come from organic searches. That’s what you want.


One thing I implemented in my templates (or at least checked if it was there) was JSON for Linking Data. This is a great way to strip down the metadata from Google and other search engines to quickly parse what the page is about. Mine looks like this:

<script type="application/ld json">
  "@context": "http://schema.org",
  "@type": "NewsArticle",
   "sameAs" : [
  "mainEntityOfPage": {
    "@type": "Blog Post",
    "@id": "{{ .Permalink }}",
    "headline": "{{ substr (replace (.Content) "\n" "") 0 110 }}",
    "author": "{{ .Site.Params.author }}",
    "publisher": {
      "@type": "Organization",
      "name": "{{ .Site.Params.author }}",
      "logo": "{{ .Site.BaseURL }}{{ .Site.Params.logo }}",
    "image": "{{ .Site.BaseURL }}{{ .Params.image }}",
    "datePublished": "{{ .Date.Format "2006-01-02" }}"
  "headline": "{{ substr (replace (.Content) "\n" "") 0 110 }}",
  "alternativeHeadline": "{{ .Site.Title }}",
  "datePublished": "{{ .Date.Format "2006-01-02" }}",
  "dateModified": "{{ .Lastmod.Format "2006-01-02" }}",
  "url": "{{ .Permalink }}",
  "wordCount": "{{ .WordCount }}",
  "author": {
    "@type": "Person",
    "name": "{{ .Site.Params.author }}"
  "publisher": {
    "@type": "Organization",
    "name": "{{ .Site.Params.author }}"
  "image": "",
  {{if .Params.categories }}"genre": "{{ range .Params.categories }}{{ . }}{{ end }}",{{ end }}
  {{if .Params.tags }}"keywords": "{{ range .Params.tags }}{{ . }},{{ end }}",{{ end }}
  "description": "{{ .Description }}"

What this bit of code does is identify the author, images, headlines, word count, keywords, tags, and so much other metadata that identifies your post. That’s the first step in getting keyword rank, being discoverable, and easy for the search engine to identify and index you.

The above code is part of a partial template that I inserted into my post template. Since I use Hugo, a static website generator, what happens is that when I build my site, all this information from my post gets automatically generated into HTML code.

This was one of the little tweaks I made that appears to be paying off now.

Granted, I didn’t create the above code and I have to thank Laura’s great work here. I just used her shortcode template and now my images automatically get resized during build time. Just this small thing has turbocharged my website performance from an F to an A/A on GTMedia and from 40/100 to 90 /100 on Web.dev. Coupled with a CDN, this brought my website out of the dregs of a Google Search.


Switching CMSs over and over again killed all my inbound links. I started noticing 1000’s failed referrers and errors in my log files. I solved a lot of these issues by writing 301 redirects in my .htaccess file but I still lost a lot of great inbound links.

This problem stumped me for a while. I didn’t want to go ahead and edit a .htaccess file for every possible permalink permutation (aka sin of the past) when I went from CMS to CMS, so I had to think of a nicer way. I wanted to take a canonical approach by actually coding in any redirects right into my post HTML code.

But how? That sounded like a huge pain in the butt. Thankfully Hugo has a great solution to this, Aliases.

Aliases are a simple way for you to set different up redirects for your blog post. Instead of coding them into the .htaccess file, you just do it in the YAML front-matter section of the post. Then when the post gets built for production, it sets up the canonical links right in the header of the HTML file.

For example, my RapidMinter Tutorials have always been a crowd please, starting with my inaugural posts back in 2007. When I first started on WordPress the permalinks I used were something like this:


when I migrated to a more friendly blog URL, I switched it to:


Then when I moved to other CMSs, they ranged from all many variants like:




And now:


I handled the /blog/entry/post-title problem by doing a redirect to /post-title/ which got me 50% of the way there. I handled the remaining 50% by using Aliases like this:

title: Post Title
slug: post-title  << this builds to /content-directory-post-is-in/post-title/index.html
- /post-title  << this catches traffic for neuralmarkettrends.com/post-title
- /post-title.html   << this catches traffic for neuralmarkettrends.com/post-title.html


This works like a charm and lets me reclaim old linking traffic, but there’s another HUGE untapped benefit here.

By using Aliases, you modify your old permalink with new and improved keywords in the permalink! This is a great way to review your old post and try new keywords to rank for, all by updating your post with the old permalink as aliases and then a brand new ‘slug’ with better keywords that highlight your posts.

Here’s an example of what I’m testing out.

I wrote a great RapidMiner tutorial in 2007 that was titled “Building an AI financial market model – Lesson I.” There was Lesson II, III, and IV. My original permalinks were formed this way building-an-ai-financial-market-model-lesson-i

This is terrible because 1) they’re too long and 2) they don’t make sense to what I want to rank for. I want to rank for ‘RapidMiner’, ‘Model’, ‘AI’, and ‘Finance’. Why? Because I know that a lot of people search for these words.

So I put modified the Slug and create Aliases in the form of this:

slug: rapidminer-ai-finance-model-1
- /building-an-ai-financial-market-model-lesson-i/
- /building-an-ai-financial-market-model-lesson-i.html
- /tutorials/building-an-ai-financial-market-model-lesson-i/

Google now knows to associate ‘RapidMiner’, ‘Model’, ‘AI’, and ‘Finance’ keywords with my posts, and the old permalink slugs are associated with that.

I will be working on Aliases for the next foreseeable future here as I change my permalinks to be within the max 75 character length AND keyword dense.

Update Old Content

This is something I neglected for a LONG time. I have over 1,000 blog posts here. Some long and some really short and really short posts work against you for SEO as well. So what to do?

I like to write about passive and active investing and I have A LOT of old posts on stocks, mutual funds, ETF’s etc. I plan on doing a ‘where are they now’ update as I stumble across old posts and hopefully point out my investing follies. 13 years of posts is a lot of content alright but what good is it if you can’t revisit it and review what you did wrong or right? Isn’t that what these so-called ‘blogs’ are about? If you can’t learn from your past, how can you navigate your future?

Take the time to refresh your old content with new information, no matter how small or inconsequential it might be. These updates tell the search engines that your blog remains active and will help you with your ranking.

Double the Ranked Keywords

Using the JSON-LD partial and Aliases has made a huge impact on my ranking. The boost to performance from responsive images is a big help too and as I work through updating older content I think I’ll eventually triple or quadruple my ranking keywords. Just these small tweaks helped me double my ranking keywords (as measured by Moz) in 2 weeks. Nice. I can’t wait to see what happens at the end of the Summer here.

I messed this up bad with Hugo in the beginning. All my old content was always neuralmarkettrends.com/some-url and when I did the default install with this theme, I placed my content in folders. So all the new permalinks ended up being neuralmarkettrends.com/blog/some-url or neuralmarkettrends.com/tutorials/some-url.

While this is OK, it messed up all my backlinks and SEO juice. I spent weeks fixing aliases in my blog posts so that neuralmarkettrends.com/some-url would correctly resolve to neuralmarkettrends.com/tutorials/some-url.

All that hard work was for nothing when I realized I could’ve just added this in the config.toml file:

  blog = "/:slug/"
  tutorials = "/:slug/"

Save yourself the time and set this up right the first time.

Seo Tip 8 – Using Hugo Extended with AWS Amplify

I switched themes for my thomasott.io site and it started to break the AWS Amplify automation. What had happed was the new theme used PostCSS and AWS Amplify’s build settings didn’t reflect the right steps to take to make it work.

The first error I was getting was this: Error building site:POSTCSS: failed to transform "css/main.css" (text/css): resource "sass/sass/style.sass_b1195a7e151e6f4379f823b1b2b4f87a" not found in file cache

The ability to bundle or compile this particular sass/sass/style.sass_b1195a7e151e6f4379f823b1b2b4f87a the resource is not available in the generic Hugo software, instead, it’s in the Hugo Extended version. It turns out that when you test your website on a Mac using Hugo install via Brew, you get the Hugo Extended version. The only problem is that AWS Amplify pulls down the generic Hugo release.

So what did I do? Well, I spent a lot of time searching the Interwebz for a solution and there were some who just said, ‘install the extended version.’ My question is how to do it on AWS Amplify?

My first thought was to just download the extended version of Hugo via a wget and then install it as part of the build process. Little did I know that was only PART of the process.

I ran into difficulty because to use Sass with this theme, I needed to install a few javascript libraries using the NPM manager.

So for anyone that gets stuck on this using Hugo and AWS Amplify, this is what my final build settings looked like:

version: 1
        - 'npm i -g postcss postcss-cli autoprefixer@9.8.6'
        - 'wget https://github.com/gohugoio/hugo/releases/download/v0.78.2/hugo_extended_0.78.2_Linux-64bit.tar.gz'
        - 'tar -xf hugo_extended_0.78.2_Linux-64bit.tar.gz'
        - 'mv hugo /usr/bin/hugo'
        - 'rm -rf hugo_extended_0.78.2_Linux-64bit.tar.gz'
        - 'hugo version'
        - 'hugo'
    baseDirectory: public
      - '**/*'
    paths: []

I needed to install postcss, postcss-cli, and autoprefixer version 9.8.6 first and then get the hugo_extended_0.78.2_Linux-64bit.tar.gz release.

Now it works like a charm.

End Notes

I hope you found this post useful. Hugo is very powerful and I find it to be leaps and bounds better than the standard database-driven CMS like WordPress with its 1,000’s of confusing plugins. Still, given enough easy-to-understand tutorials, I believe Hugo can rise to the top 5 CMS’s quite quickly.

Hugo, the Static CMS

If you’ve been reading this blog for some time you’d know that I like to try different CMS’s. I made some mistakes over the years and I transferred my posts between different CMS. This ended up killing a lot of my backlinks, but that’s ok. I ended up settling on using Markdown to write my posts (love it) and a static CMS generator. Originally I used Pelican and it was pretty good. It was based on Python and had some really neat ways to slice and dice your posts, leverage SEO, and do cool things with it.

However, it was SLOW in building the entire website. Sometimes it would take 10 minutes to build my website, whether it was for production or running the development server. I just resigned myself to dealing with these speed issues because I was getting into a groove with Pelican. It works, who cares if it was slow.

Then I discovered Hugo.

Enter Hugo

I’ve been taking an Udemy course in learning Go Language (GoLang) and like how simple it is. The course has been one of the best ones I’ve ever taken and I’ve learned many of the basics of computer programming. It reminded me of my Fortran course in college! The beauty of GoLang is that it takes advantage of multiple CPUs with ease. It’s very easy to program in the language and it makes the complex very simple. In some ways, it reminds me of Python BUT it’s statically typed and compiled. That’s a BIG plus in my eyes because I’m getting annoyed at all the dependency issues with Python. If it compiles, it works!

Hugo generates my site in like 20 seconds compared to the 10 minutes it took Pelican to do the same thing. Yowzers, that’s FAST!

Of course, there are a few little best practices we need to follow AND here’s what I learned using Hugo so far.

YAML Front Matter is best IMHO

YAML, a front matter format, is the way you tell the static generator what the blog post is about. You can tell the generator what category the blog post is in, what the permalink is, title, date, and so on. It starts with three ‘—‘ and ends with ‘—‘. I was introduced to YAML when I used Jekyll, the ruby-based static generator. Ruby has its own annoying problems and I dislike it too. When I migrated to Blot.IM, YAML was not used. I had to use one of Dave’s YAML to Blot converters which essentially removed the —‘s. Admittedly, this was a cleaner way of writing a post. I could do something like:

title: my awesome title
date: 2020-05-15

Start writing my blog post here...

When I migrated to Pelican, it could just use the old Blot.IM format and all my posts would generate. Easy peasy, right? It wasn’t until I started getting annoyed by the generation speeds and devserver in Pelican that I considered making a move elsewhere.

Hugo has some specific ways you write posts and interestingly enough, they give you 3 options: TOML, YAML, or JSON. I found the JSON one interesting because GoLang makes working with JSON so easy and awesome. If you ever had to work with JSON data using Pandas you’d think you died and gone to heaven using JSON in GoLang.

I thought about what moving to TOML or YAML files would mean. In Blot.IM I could create new metadata information that would parse when I had at the top of my blog post, for example:

title: my awesome title
date: 2020-05-15
new_metadata: something

Start writing my blog post here...

I thought that was pretty neat so I made things like:
headerImage: \url\to\pic


twitter: name

which then allowed me to hack my templates.

I had to do this when I wanted to enable OpenGraph and Twitter Cards in Blot, which didn’t come native with the templates available. So yeah, Dev people like me kinda liked Blot.IM for giving us the ability to customize things. Still, this hacking was painful, and eventually, I got it to work well. I just wished there was an easier way.

Using YAML I was still able to do all those things and all I needed to do was add back --- at the top and bottom of the post. While the non-YAML way made things cleaner, the YAML way made reading data a bit easier for my eyes. I knew exactly what section in the blog post was dedicated to the ‘Front Matter.’ With HUGO I could still define my metadata BUT I found out it had a plethora of predefined metadata (known as Front Matter Variables) that just works out of the box. This was pure heaven for me.

I started going down the rabbit hole then and discovered that I didn’t need to do all that Open Graph and Twitter Card hacking in Hugo that I had to do in Blot.IM. This is what sold me on Hugo going forward.

Internal Templates and Shortcodes

Lo and behold, Hugo made things easy by automatically internalizing Open Graph, Twitter, YouTube, and other things through the use of internal templates and shortcodes. Putting in Disqus and Google Analytics was easy as adding a line in your config.toml file (global config file for your blog) by doing this:

#DisqusShortname = "my-Disqus-name"
googleAnalytics = "UA-XXXXXXX-X"

Using a YouTube shortcode, you can easily embed a video by using its shortened URL reference like this:

{{ < youtube xyz12345s > }}

All you need to do is call these variables in your various page templates like this:

    {{- template "_internal/opengraph.html" . -}}
    {{- template "_internal/schema.html" . -}}
    {{- template "_internal/twitter_cards.html" . -}}

and the would just work. Easy peasy, lemon squeezy.

There’s also a section on Hugo Pipes which is meant for preprocessing things like images that I haven’t explored yet.

Just the YAML, internal templates, and shortcodes have made my entire blogging life so much easier. The only issue is writing a script or something to make sure I have all the required YAML in my blog posts. I think the answer might be in me building one of those Hugo Pipes.

Enable Unsafe HTML in Markdown

Of course, there are some weird things I had to deal with in the basic setup of Hugo. For example, it restricts all HTML code in your markdown files by default. A lot of my old blog posts have HTML in them (from the Jekyll days) so I would end up with a lot of blank posts. I don’t remember where I figured this out but I had to add some parameters to the config.toml file so that it would accept ‘unsafe HTML.’

It was as simple as adding the following to the config.toml file:

  defaultMarkdownHandler = "goldmark"
      unsafe = true  # Enable user to embed HTML snippets in Markdown content.
    codeFences = false  # Disable Hugo's code highlighter as it conflicts with Academic's highligher.
    startLevel = 2
    endLevel = 3

Enabling search in this particular theme I’m using is a bit of a pain but there are ways around them. Hugo does support using javascript types of search. Of course, you can use Google to do it (and I just might go that route) but I’m working on getting that implemented here. It’s one of the missing pieces because a custom 404 page can only go so far.

End Notes

Hugo is powerful, fast, and makes blogging simple again. While there are some bumps in transitioning to this static generator, I think it’s the one that will be able to grow with this site. I look forward to writing more tutorials over the next few weeks.

Installing Ghost CMS on AWS Lightsail

This tutorial is not about porting Hugo posts to the Ghost format but instead, it’s about how I got a custom domain, CDN, and removed the :80 port from a new Ghost install.

First off, setting up the machine on AWS Lightsail is pretty easy. It was as simple as selecting the Ghost install and taking the cheapest instance possible. I think the cost is like $3.50 a month, which is pretty sweet IMHO.

The harder part was then the configuration of everything. When the instance spins up you get a dynamic IP assigned to it. It’s not a pretty custom domain name but rater something like, not easy to remember and it does change when you reboot the instance.

I had to set up a static IP (an added yearly cost) to avoid the IP from changing because I was rebooting this instance a lot.

Once I got the static IP, then I had to attach my custom domain thomasott.io to it. I thought that while I was doing that I should make sure that it’s SSL enabled and that my content would be served through a CDN (Content Delivery Network) for scalability and robustness.

Just those tasks forced me through down a rabbit hole that’s taken me about a day (on and off timewise) to configure my instance. It was fun because I got to tinker with domains, CDNs, and other backend-related stuff, something I normally don’t do.

This tutorial is about how to turn your site into a real Ghost site once you go your AWS Lightsail instance running.

The Instance

I’m not going to go into detail about setting up the instance, but the four screenshots below show you how easy it is.

First click on ‘Create Instance.’

Then select your Instance location.

Then select Ghost.

You’ll notice there are a lot of other things you can install too. My next goal is to set up a WordPress and Django site as a test and work through the machine configurations like this one.

Finally, you select the size of the instance you want. I went with the cheapest option.

That’s really it, you launch it and it goes live.

Logging in to Ghost

This was an annoying part, I didn’t know where to log into the Ghost admin panel at first. The answer is:


But what are the credentials? The Ghost instance is managed by Bitnami and they have specific instructions on how to log into your Ghost instance. You have to SSH into the instance and run a command to get the password. The default username is bitanmi.

Once you SSH into your instance, get the password, then go to:


Then log in and make a new user. That user will be you and don’t forget your login credentials.

Remove Bitnami header

Once I had a user and tested logging in successfully, I then looked to get rid of that annoying Bitnami header. This was easy to do, I just used the bnhelper tool.

All I did was SSH into the instance and run sudo /opt/bitnami/bnhelper-tool

This led me to this screen and I selected Remove the Bitnami Banner.


Set up Static IP on AWS Lightsail

My next step was to attach a static IP to the instance. That was pretty easy too, all I did was navigate to Networking and clicked on Create static IP.

Then I followed the instructions but selecting where my instance was and filling out the rest of the information.

Set up Custom Domain Zone

Now, this part got tricky, setting up the Custom Domain Zone. Here I wanted to attach my AWS registered domain thomasott.io to this static IP.

In order to do that I needed to make sure that the DNS servers that this instance requires are the same DNS servers set up in my Route 53 Hosted Zone.

There are some important things you need to set up in Route 53 to make sure everything resolves correctly. The problem is that you’ll be setting them up a bit out of order.

The key things for Route 53 are this:

  1. Make sure the Domain Name Servers are the same in Route 53 and your Lightsail instance of Ghost
  2. You point an A Name record to the Cloudfront Distribution URL (we haven’t set this up yet)
  3. That your SSL certificate is verified so you can get https (we haven’t set this up yet either)

The problem is that you won’t have all the information to finish this configuration until you set up your CDN and SSL certificates.

Set up the CDN

It’s advisable to serve your images and content through a CDN (Content Delivery Network). Why? Because it helps with load times and uptime. Ever since I switched to a CDN my SEO for my other blog has skyrocketed. You should consider this too.

To set that up, just click on the Create distribution button and

Then follow the instructions by selecting your instance (mine is called Ghost-1).

Once you do that, you’ll get a CloudFront URL where your blog will be served too.

Important! Now you’ll have to navigate back to Route 53. In your Hosted Zone for your custom domain, create an ‘A’ Record but select Alias to Cloudfront distribution.

If you select the correct Instance location, your newly created Cloudfront distribution should populate automatically.


Set SSL Certificates on AWS Lightsail

We’re still not done yet with setting up the custom domain. For that, you’ll need to attach SSL certificates and then reference them back in Route 53.

Click on the Distribution button and navigate down to the enable custom domain option.

Select enable.

Then navigate lower on the page and click Create certificate. Fill in your apex domain name (not http://www.your-custom-domain.com, but custom-domain.com).

Once you create your SSL certificate, it will ask you to verify it. You will need to take that information and create a CNAME record in Route 53. Once you do that, you should be done with all your Custom Domain Name/Zone configurations.

The remaining steps are related to the backend of Ghost and the configuration of the web and production server.

Update Machine Name

SSH into your instance and update your machine name. To do that you’ll need to do the following commands referenced here.

sudo /opt/bitnami/apps/ghost/bnconfig --machine_hostname example.com

where you replace example.com with your custom domain.

Then to make sure it ‘sticks’ when reboot you’ll have to rename the bnconfg file like so:

sudo mv /opt/bitnami/apps/ghost/bnconfig /opt/bitnami/apps/ghost/bnconfig.disabled

Update httpd-vhost.conf

This next step caused me grief because I couldn’t find the damn httpd-vhost.conf file at first.

I finally found it at /opt/bitnami/apps/ghost/conf/

You have to edit this file and update it with your custom domain information. I followed ‘Approach B’ in these instructions.

Then I restarted the webserver.

Update Production settings

Once I got to this point everything started to work well except I had the port :80 appended to my custom domain thomasott.io. it drove me nuts.

It wasn’t until I read this post on how to remove it that I realized there’s this production setting for Ghost.

Once I removed the :80 from the JSON file and restarted the instance, everything worked.

End notes

I’m probably forgetting something because these were small tweaks that were numerous but not hard. I will update this post with any missing information when I remember it.

%d bloggers like this: