Over the past few weeks I’ve migrated every content site I have from WordPress to Hugo, a popular and easy to use static site generator. In this post, I’ll detail why I think WordPress is a bad solution for most people, and how I went about migrating my site in just a couple of hours on a weekend afternoon.

Oh, and before we get started, the site I’m talking about in this post is the one you’re currently on. The Pagespeed Insights test I just ran shows a 0.2 second first contentful paint and 100 points across the board for performance, accessibility, best practices, and SEO. I’ve never had a site achieve these performance metrics before – not NextJS, WordPress, Laravel … anything.

Take a look at the full report… it’s absurdly fast:

my cwv metrics, all very good!

Why I moved from Wordpress to Hugo

I’ve been using WordPress to build sites since at least 2008. It’s been my go-to anytime I need to build something that doesn’t have custom features or significant user generated content.

There are a few reasons:

  • It’s familiar. I’ve built a lot of muscle memory for working with it (both in the back end with theme development, and with wrangling the editor and a suite of plugins to create high quality content)
  • WP begets more WP. I have many other WP sites, so it makes it easy to streamline management, as opposed to having multiple projects with multiple frameworks or programming languages.
  • It’s common. It’s very easy to find affordable, optimized hosting. I typically just have a single, managed vps that I store multiple WP sites on, so my incremental expenses to adding another site are near nil, making it frictionless to spin up a new site for a random one-off idea.

Unfortunately for WordPress though, as my development skill and experience has grown, so too have my opinions on what kinds of technology are best for me. In the case of WordPress, it’s no longer my preferred tool for the job, and every month that goes by the itch to migrate my sites to something more in line with my preferences grows stronger.

Additionally, as I get older, I don’t have the desire to operate nearly as many sites. I’m not out slinging SEO tests, or affiliate projects anymore. I’ve really only got a couple sites that I care about and part of this migration will include shutting down, selling, and/or combining many sites I’ve had hanging around for years.

I suspect that many people who decide to drop WordPress do so for something else, switching one tool for another they already have in mind. In my case though, the decision was simply to get off of wordpress, and to do that I still had to do research to find a tool that satisfied my requirements.

Why not wordpress?

WordPress is great for a lot of people. As I mentioned before, cheap, optimized hosting is plentiful, as is documentation and available talent. There’s an entire booming industry around plugins and themes, and these days there is very little that WP can’t do.

However, in my case there are a growing list of reasons for why it’s not a great choice for me.

1. Most of my content doesn’t need to be dynamic

I’d say this is true for most people, actually. I can’t think of a good reason for a blog to use a database at all, other than to make it easy to publish content for non-technical people.

In reality, once you publish, you can often serve your posts/articles as a static html page, with no database requests whatsoever.

It’s highly inefficient to respond to every pageview request with a stampede of database requests. If you’re using off the shelf wordpress themes and several plugins (as many users are) there is no doubt in my mind that you’re firing dozens, perhaps hundreds of database requests on every pageload. This contributes heavily to site slowness – not to mention introducing several security concerns and attack vectors.

To be sure, modern WP professionals spend a lot of time minimizing requests, addressing latency, dialing in caching, and core web vitals performance.

They work very hard to end up very close to where static site generators already are.

2. I want a streamlined workflow

Operating WP sites at the level I do results in:

  • Frequent core and plugin updates to do
  • A variety of plugin subscriptions (WP Rocket, Advanced Custom Fields, Gravity Forms, etc)
  • Multiple login credentials to rotate and maintain
  • An ever increasing amount of comment and contact form spam
  • A non-zero percent chance one of your sites gets hacked due to bugs in rapidly changing integrations (plugins, wp core, themes)

I tend to write my content in markdown. I really prefer a basic markdown editor to the WordPress editor and once I write, I always dread copy/pasting my content in and formatting it and so on. I know there are integrations with many popular markdown editors that can do this, but they’ve always left a lot to be desired in my experience.

With a static site generator I can work directly in markdown and … that’s it. Every piece of content on this site is stored as a simple post-name.md file. What I write is converted into html at the build phase, nothing additional required of me. No formatting, no html, no fancy plugins, nothing.

Think → write markdown → publish.

It’s easy to see why operating a handful of static sites makes for a better workflow. I don’t have to worry much about formatting, beyond what I’m doing with markdown.

3. I want my site to be super fast

For Wordpress this typically means hiring one of the various cache plugins (paid, of course), cloudflare or another CDN, and twisting the dials on each until you hit your guardrail metrics.

With a static site it’s much more likely that you can simply build and deploy, since you’re hosting on the CDN to begin with. Cloudflare isn’t a CDN in front of your server, for all intents and purposes, it IS your server.

Why hugo?

I’ve used a variety of static site generators in the past, but as of late, I’m particularly smitten with Hugo. It allows me to write (and publish!) in markdown and is lightning fast (under 1ms per page) to build my sites. There are also very sane defaults for the SEO conscious in most of the themes I’ve looked at (though, ultimately this is theme dependent).

I’m also very happy with the ecosystem that enables such a streamlined workflow. The site you’re currently reading is built with Hugo and deployed to Netlify. I wrote this piece in markdown, pushed to my code respository at GitHub, and Netlify automatically grabbed the files, built the site, and deployed it globally to a lightning fast network of edge servers. For the WP faithfuls, it really does feel like magic.

There’s no database, there are no fancy caching configurations. Hugo generates this site as a series of HTML files and images, which are easy to store and retrieve at scale. Oh, and I’m well within the free tier. Even if my sites were more popular (here’s to hoping) it’s often very low cost to host and deliver static html files.

Installing Hugo locally

Hugo runs on your local machine, which means that we need to install it.

Head over to the official site and follow the instructions there. In my particular case I used the Linux instructions as I’m on Windows 11 (running ubuntu on WSL), and it worked without a hitch.

Once you’ve got Hugo installed, you can generate your first site with:

hugo new site site-name-here

Next, you’ll need a theme. If you want to use an off-the-shelf one (there are many), my recommendation is to install it as a Git submodule. The official quickstart guide recommends the Ananke theme for new users and I think that’s a good choice.

Here are the instructions (with my comments to explain what each line does):

git init // initialize the git repo
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke themes/ananke // add the theme as a submodule
echo "theme = 'ananke'" >> config.toml // you can also do this manually by editing config.toml, but this line adds the theme to your config automatically

Then simply open your site directory in your code editor and start writing markdown in the /content directory 💪.

Migrating content

I have about a hundred posts on this site, of various types. There’s the blog, conversion gold, marketing patterns, and a video course on spaced repetition. All of these are distinct content types with distinct templates and fields on the back end. I made heavy use of Advanced Custom Fields to construct the previous site.

Moving this by hand would have been painful, copy and pasting each piece and adding the markdown markup, but luckily there’s a great script available to do this for you.

Simply install node into your environment, download a wordpress export (from wp-admin > tools > export) and run the script:

npx wordpress-export-to-markdown

You’ll be prompted to answer a handful of questions about how you’d like the output stored (date folders, post folders, etc), and for Hugo I recommend using post based folders. Also, make sure to export posts, pages, AND custom post types.

Once you’ve got your export, drag and drop it into your /content folder in your Hugo directory.

Setting up sitemaps

By default, Hugo will generate a couple of taxonomy types: categories, and tags. Feel free to use them to organize your site, but I prefer to simply use sections instead.

I do still tag my content in the front matter of each markdown file so that I can organize and filter further for recirculation and such (in progress on this site), but what this means is that Hugo will still create these tag pages and by default include them in my sitemap.xml file, which is not exactly what I want. Google Search Console will yell if you’ve got things in your sitemap that are also blocked in your robots.txt file.

So the fix for me, was to strip these content types out by creating a new sitemap.xml in my /layouts/_default folder and changing the range query to:

  {{ range .Data.Pages }}
  {{ if not (or (hasPrefix .RelPermalink "/tags") (hasPrefix .RelPermalink "/categories")) }}

Here’s the default “embedded” sitemap.xml template if you need it for reference.

Setting up robots.txt

Next, we want to block the tag and category pages from search engine spiders in robots.txt.

Just add a couple of lines:

User-agent: *

Disallow: /tags/
Disallow: /categories/

Sitemap: https://terrygodier.com/sitemap.xml

Note that I’ve also added our sitemap to it, which is a best practice.

You can see my robots.txt here.

Optimizing Core Web Vitals

Core Web Vitals can be annoying, but the fact remains that Google has committed to this as a ranking signal, and I do believe it is a valuable heuristic for determining actual user experience.

Having a very fast site is important to me, and it’s one of the primary reasons I opted for a static site served on a CDN in the first place.

my cwv score, all 100s!

my cwv metrics, all very good!

So, what did I have to do to make this a reality? Nothing.

This was my score out of the box 🚀

Deploying to Netlify

If you want, you can generate your site locally (just by using the terminal command hugo) and upload your files in the Netlify interface, but I prefer to be able to make a push to my github repo and have it automatically build and deploy.

github to netlify deploy settings

You can configure this trigger in the Netlify interface. You can also specify a branch (like main) in the case that you want to work on a feature branch (these also can be built, but on a separate staging url for testing).

Creating the redirects

Part of the migration for me included re-organizing my content on the site to be more browseable and marketable. I wanted to be able to excavate some of the larger writing projects I’ve undertaken and offer them up separately as features.

So, that meant I needed redirects. Netlify makes it simple – I just created a _redirects file in my /static folder with this format:

# comment
/previous/url   /new/url

// example:
# reviews
/blog/fmail/    /reviews/fmail/
/blog/endel/    /reviews/endel/

… and voila, 301 redirects to keep an SEO happy.

Adding a custom domain

Adding a custom domain to your Hugo site hosted on Netlify is no different than any other server for the most part, it’s just a simple DNS configuration. I opted to use their nameservers, so I just added those to my config at Gandi.

The only issue I had was that at some point I must have tried Netlify in the past on this domain because I wasn’t able to add it without getting an error message that a dns record existed on another account.

error: terrygodier.com or one of its subdomains is already managed by Netlify DNS on another team.

The process for freeing up your domain is simple: just make a post on their support forum here with the following details:

  • netlify app: your-app-name.netlify.app
  • custom domain: your-custom-domain.com

Here’s my post for reference. I was good to go within a couple of hours.

Inserting javascript snippets

Next, one of my favorite features of Netlify is the ability to inject javascript snippets in the admin panel, instead of adding them to your code.

You can add them to either the end of the </body> tag or the html </head>. I serve my analytics snippet through this feature.

All in all, I’m unbelievably happy with my new setup. I feel very confident, having my content exist locally, on github, and on netlify, no databases to deal with, no plugin security concerns, no additional subscription payments… the list goes on.

Migrating from WordPress to Hugo (or another static site generator) gets my highest overall recommendation.