Moving From WordPress to an Azure Static Site with Hugo

on May 3, 2021

I’ve moved from WordPress to an Azure Static website built with Hugo. The conversion, including grooming all my old posts and a special setup for my free courses took up most of my personal time for a week (including a 4 day weekend).

Here’s why I made the change. I’ll also share the basic components I chose for my site, the tools and steps I used in the conversion, and some lessons learned from the implementation.

This is going to be a long one, so buckle up! :seat:

Why I ditched WordPress after ~12 years

I used WordPress for blogging from 2009 to 2021. Even after some cleanup, I have more than 500 blog posts. So I had a good amount of investment with WordPress. I knew that changing how I manage and publish my blog content would be a lot of work.

But I had significant pains with WordPress:

  • High Cost: I used an expensive WordPress host when I was running my own business but no longer wanted to pay the big bucks for hosting when I made my video content free and my site returned to “hobby” status. There are additional costs with WordPress for plugins for things like backups, security scanning, and flexible themes.
  • Bad Performance: When I downgraded to a cheaper host, my performance tanked. My site was often so slow that it was hard to tell if it was online or not, even with Cloudflare supposedly set up.
  • Low Portability: It took many hours to even get my WordPress site migrated to the cheaper host– just figuring out how to get the backups I had taken to work with a migration took hours with support from the backup tool company. (Turns out most of the tooling is optimized to restore to the same host, but that doesn’t mean you can’t keep trying to do it and having it fail :facepalm:.)
  • Significant Complexity: Once it was migrated, troubleshooting the performance issues with the new host was the cause of a lot of frustration. At times there was clearly an issue with underpowered hardware, but nobody could help me figure out why everything just generally sucked. Maybe there was some misconfiguration in my setup, but with a MySQL database, webserver, plugins, themes, Cloudflare, and who knows what else was going on at the host involved – I just had no interest in troubleshooting it.
  • Security Hassles: The more complex a system is, the harder it is to prevent vulnerabilities. It’s fairly common for WordPress sites to be hacked and have malicious code inserted to advertise for spammy links – this happened to me once a few years back.

I considered throwing my whole site away

I wasn’t having fun with the blog anymore after I’d migrated to the Terrible WordPress Host. Troubleshooting WordPress wasn’t appealing. I didn’t want to spend a bunch of money to have someone else do it, either. I considered abandoning the blog and simply moving on. :wastebasket: :raccoon:

The main reason I didn’t do that was that those friends in the community who DM’d me about my site being down mentioned that they found my content useful. This feedback helped me make the right decision. Thanks, friends. :revolving_hearts:

So I looked around to see if there was a newer way to blog which would be more fun for me, and which wouldn’t have all these problems.

Why a static website?

As someone who spends most of her time thinking about databases, I didn’t even know what a static site was until I started looking into new options for my blog.

In the early days of the internet, all web pages were pretty simple: just some HTML. Eventually with some CSS styling. Static websites are a return to this concept: when your browser loads a page on a static site, it’s mostly just loading pre-baked HTML and CSS.

This contrasts to dynamic websites, where pages may load different things for different people. There are also generally more types of interactions available on dynamic sites. But with the flexibility of dynamic sites comes the complexity and potential for related issues I mentioned above.

There are now a few open-source static website generators which are very popular both for blogs and other types of websites. The most popular are Jekyll, Hugo, and Gatsby. The main benefits of using a static site built with one of these generators are:

  • Low or No Cost: There are quite a few low-cost or free hosting options for static sites, including GitHub Pages (free), Azure Static Web Apps (Update May 18 2021: now Generally Available with a free tier), and free plans on providers like Netlify. Static site generators like Jekyll and Hugo are typically open source and completely free to use. An active community of users of these static site generators have created a large amount of free themes, many of which are eminently customizable.
  • High Performance: By pre-building the content and minimizing what the client browser does when loading pages, static sites can offer very high performance and fast page load time, even while having lower costs.
  • “Shifted Left” Complexity: Using a static site generator can seem quite complex if you aren’t used to working with a bit of code, version control, and cloud tooling. But if these things are appealing to you, the complexity is all in a learning curve. Once you’re on board, the normal process of blogging becomes quite simple, and the static site itself is generally quite simple in how it operates.
  • High Portability: Having the contents of your site all in source code gives you so much more portability than you have by using a content management system which keeps much of the content in a database. I am a huge fan of databases – I’ve spent my entire professional career thinking about how to best work with databases! But I don’t have a “golden hammer” view of databases. It makes much more sense to me that my personal blog just involve a bunch of text files and images which are checked into version control. This is easier to manage and much more portable long term. Not only can I move my static site easily between different hosts by simply deploying elsewhere, I also have a lot more options if I want to switch to something different later on.
  • Better Security: Static sites are generally much more simple than dynamic sites and have a lower surface area for attack. Also, as long as the repo holding my code is secure, I always have the option of clearing out my live environment and redeploying everything from scratch if I need to.

The downsides of static sites are that they are less accessible to people who prefer to work in a lower-tech fashion.

It also can take a little extra work to include interaction in your website and often involves integrating third party services into your site. You’ll see that I have workarounds for blog comments, newsletter subscription, and a comment page below, all of which leverage an external provider.

Why I chose Hugo and Azure Static Web Apps

I didn’t have a tiny project – remember, I had ~500 blog posts to migrate and a desire to do a custom setup to still offer ~30 free video courses online. Also, the performance of my current WordPress site was just abysmal. I frequently got DMs on Twitter from friends nicely letting me know that my site wasn’t working :cold_sweat:.

It felt advisable to ask my network for advice.

My first thought was that Chrissy LeMaire had written about migrating her WordPress site to GitHub Pages. But when I mentioned this on Twitter, Chrissy advised that she might approach this a different way if she had it to do again:

Good intermediate solution but not a good final solution! I haven’t blogged much since then because it’s a pain ๐Ÿ˜ญ In retrospect, I’d look for a WordPress to Markdown plugin, and put the work in upfront.

My next thought was… hey, didn’t my partner do something like this for his blog a while back? Uh, yeah, I thought of Chrissy first and my partner second. :woman-shrugging: Is that weird?

Jeremiah explained that he had fully converted his site up front (like Chrissy recommends). He is using Hugo with the Mainroad theme and deploying to an Azure Static Web App. He recommended this approach and said that his experience now is good. But he also mentioned that the tooling he used to convert his posts wasn’t terrific and that he’d look for another alternative for getting WordPress content into Markdown.

Coincidentally, Justin Bird mentioned on Twitter that he also moved to Hugo for his site. He said that he had used lonekorean’s WordPress-export-to-markdown project to convert his content.

Together, this was all enough to give me a good starting point, so I dove right in.

In other words, I didn’t do an exhaustive comparison of static web generators and hosts. From what I’ve read, there are multiple great choices here. I’m already interested to try Jekyll and Netlify in future projects, perhaps.

Overview of my static site setup

I’ve alluded to a lot of this so far, but here’s a summary of all the tooling and services I am using for this site, along with cost considerations:

And here are the third party integrations I’m using:

  • I’m using TinyLetter to support people who want emails for blog posts (see below for details). This is free.
  • I’ve embedded a Google Form in a simple contact page for people who want to send me a message (see below for details). This is free.
  • I’m using the GitHub issues API and a public repo for blog post comments (see below for details).This is free.
  • I have a large amount of YouTube videos which are embedded throughout the site via Hugo shortcodes. Also free!
  • I also have a few GitHub Gists which are embedded throughout the site using Hugo shortcodes. (For shorter code samples, these are in the posts themselves and formatted via Markdown, no extra functionality needed.) Totally free.
  • I’ve set up some ifttt applets to see new entries in the RSS feed. One automatically tweets while the other sends the info to my free Buffer account, which will automatically post the info to LinkedIn. Free.
  • Giphy hosts some animated gifs which I created as short demonstrations for some posts. These are embedded simply as images. (More info on this below.) Free as well.

The conversion process

Set up my Azure Static Web App, my Hugo project, and hook up publishing

I began by following the steps outlined in this excellent Tutorial: Publish a Hugo site to an Azure Static Web Apps Preview. This walked me through:

  • Creating my hugo project
  • Adding a theme (this is installed as a git submodule)
  • Setting up my Azure Static Web App in the Azure Portal
  • Deploying my Hugo app to my Azure Static Web

I carried all these steps in VSCode, the Azure Portal, and the GitHub web interface. This was all really accessible – the steps even automatically created my GitHub Action which does the publishing! :star:

Your Azure Static Web App is created with a default name. This enabled me to keep working on my project for a while and convert my existing content, then finally redirect my domain name when I felt ready to do so.

If you don’t have existing blog content to migrate, at this point you can just register a domain name, point it at the static site, and you’re off to the races!

Convert my WordPress content into Markdown and copy the files into my Hugo project

I used lonekorean’s WordPress-export-to-markdown project to do this conversion, as Justin Bird recommended. I had a good experience with this script.

The instructions for the script guided me through this basic process:

  • Installed Node.js
  • Exported “All content” from WordPress – this generates an XML file you save locally
  • Cloned down the WordPress-export-to-markdown script from GitHub
  • Initiated the wizard from the command line and stepped through it

During the conversion process, the script will go to your live WordPress site and save off images referenced in your posts. You can throttle the image downloads if you hit errors. I found that it did a great job in general of grabbing my images. It consistently failed on images which I’d imported from a different WordPress site at some point, for whatever reason, though. It was a small enough amount that I cleaned these up manually when I “groomed” my posts.

I ran through the export process a few times and iteratively copied the files into my Hugo project and tested to make sure the generated files matched my old WordPress URL structure. It’s great that the script is flexible about this!

In my case, all of my old blog URLs are in the format:

  • I didn’t use any year or month folders – I put all my posts in a single folder and all my images in a single folder for simplicity
  • The conversion script automatically put the post’s date, categories, and tags into the header of each markdown file
  • I used the Hugo setting below to structure the URL format for my blog posts – this picks up the date from the markdown file’s header and uses it in the URL structure for the published post
  posts = "/:year/:month/:day/:filename"

My basic folder structure in Hugo (excluding my courses, more on that later) looks like this:

โ”œโ”€โ”€ content
|   โ”œโ”€โ”€ posts
|   |   โ”œโ”€โ”€      // <-
|   |   โ””โ”€โ”€  // <-
|   |   โ””โ”€โ”€ {etc}                // <- ~500 files total (one per post)
|   โ””โ”€โ”€   // <-
|   โ””โ”€โ”€ {etc}                    // <- I just have a few pages, no biggie
โ”œโ”€โ”€ static
|   โ””โ”€โ”€ images                   // <- All images 
โ””โ”€โ”€ themes\Mainroad              // <- This is the theme, which is a git submodule

Test how everything looks locally

One thing which I love love love about my static site is that I can do loads of work locally. All I need to do is run this command line in my project:

hugo server

This starts up a local webserver at http://localhost:1313/, which I can load in a browser and see exactly how my site looks now.

And even better, every time I save a change to a file in my Hugo project, it rebuilds and the change is immediately visible on the webserver. And it’s :boom: FAST :boom:. Having this super-fast local build and deploy made working on the conversion process really fun for me. I got to see things transform right before my eyes.

Groom my converted content, keep testing locally, and periodically push to GitHub

No conversion script will be perfect for every use case. I used a combination of manual editing and “find and replace in files” functionality in VSCode (at times with regular expressions), to get my content into a shape which I like.

Things which I fixed up include:

  • Replacing YouTube links with Hugo shortcodes. I had at least three different styles of YouTube links and embedding styles in my old posts so this took a little work.
  • Fixing up code samples in blog posts. I have a lot of posts with SQL scripts which had been formatted in WordPress by at least three different plugins over the years. These are now very simply styled in markdown. This was probably the most time consuming thing for me to fix given the amount of code I had, but it was very satisfying.
  • I had a few tables in posts which needed to be converted into markdown. I still had my old site available so this was just a matter of finding the posts by searching, loading up the old page, and using an online converter from HTML to markdown to generate the code.
  • Identifying those images I had which didn’t get converted and replacing them as needed. Often these had a strange issue with links to a site which no longer exists being wrapped around them which needed cleaning – almost certainly a remnant from having exported/imported them between WordPress sites before.
  • Cleaning up places where I had linked an image to the source file in WordPress (these links all fail now because the folder structure is different, and I’m not sure why I even used those links in the first place)
  • I had some large animated GIFs in posts. I moved these into Giphy and embedded them in Hugo as an image using the source URL from Giphy

Throughout the process of fixing things up, I constantly looked at my local webserver as I saved things.

I also committed fairly frequently, and periodically pushed a batch of commits up to my GitHub repo to make sure that the build and deployment from that point was still working.

Move my free training courses content over

After working with Hugo for a little while, I became ready to learn how to create a page structure alongside the blog to hold my courses and lessons.

I had used a plugin to manage my courses in WordPress, and happily the WordPress-export-to-markdown script did convert markdown pages for all my courses and lessons. I just needed to figure out how to get these to display in some coherent fashion without manually having to add a lot of links to list out lessons for each post and navigate between them– and ideally not change too many URLs.

It turns out that Hugo is very much suited to helping build out content like a “courses” structure with lessons. The main things I needed to learn about were:

  • Page bundles in Hugo - Branch bundles allow me to have a main course page in which all the lesson pages are auto-magically linked into the parent course page in order.
  • Taxonomies in Hugo enabled me to create a custom taxonomy for course categories, and drive navigation that way. There’s lots of documentation, but I found this post by Jake Wiesler really helpful in getting me going.
  • Templates to customize the look and feel of different pages

I did a lot of searching and employed a great deal of trial and error, but having a really fast and convenient local build and webserver experience made this fun to work out.

Re-point DNS and go live

My old site’s performance was so bad that I probably should have cut over nearly right away and just put a note on the new site that I was working on cleaning things up. But I was excited enough about what I was building that I wanted it to be a certain level of “done” before I showed it to people.

The process of cutting over is relatively simple, but I learned some important things about how to do this with an Azure Static Web App.

Microsoft has provided good documentation on how to setup a custom domain in Azure Static Web Apps Preview. Follow this, but note:

  • Azure Static Web Apps only support mapping a subdomain while in Preview. In other words, I could use The Preview does not support using a root domain like (but it sounds like this is coming later). <– Update May 18 2021: root domains are now available
  • If you’re not a DNS wizard (and I am not), this can be confusing to configure. For simplicity, I pointed my nameservers to Cloudflare and used their tooling to configure my DNS. In my experience they do a good job simplifying DNS management on their free plan.

If you are using Cloudflare, follow the steps in the documentation above to configure CNAMEs, but also know…

  • There is a step in the Azure portal setup where it needs to verify the CNAME. For this to work, you need to have the CNAMEs set up in cloudflare set to “DNS only”. After the verification is complete you may change them to proxying traffic.
  • I needed to go to “Rules” and create a Page Rule to handle existing links to pages at the root domain and send them to the www version. Here is the rule for my site:
    • If the url matches:*
    • Then the settings are:
      • Forwarding URL
      • 301 - Permanent Redirect

DNS takes time to propagate, and in this case waiting was the hardest part. I was so excited to go live with my revamped little site!

Tips and tricks for setting up a blog with Hugo

Here are some highlights of things I’ve learned and resources I’ve used while configuring my new Hugo site with the Mainroad theme. I found that there’s a really great community out there and that I could solve most of my problems with searching and testing out different options.

Mainroad theme

There are only two things I’ve found which I couldn’t easily override or modify outside of the Mainroad theme submodule. I’ve worked around both of these by forking the Mainroad repo in GitHub and making the forked repo my sub-project. This allowed me to:

  • Control thumbnail display. I hate it when the post’s thumbnail image is automatically displayed at the top of a blog post – I’ll add it in if I want! I copied the code for this from an existing PR on the Mainroad theme
  • Replace the default site favicon. This appears to simply not have been made easily configurable but it’s easy to replace the file in my forked copy of the Mainroad repo.

General styling

Contact me form

I spent a lot of time finding hard ways to do this before finding an incredibly easy way to do it. The easy way:

It’s absolutely possible that trolls or bots could strike and hit the form with a lot of garbage data – but this is honestly a risk for any contact form solution. If that happens it’s really easy for me to just delete the form. For a 1.0 this is just a really easy way to start.

Blog post comments

A lot of people seem to use Disqus for their comments. You can sync your older comments from WordPress into Disqus if you want an automated way to carry those over.

As a woman on the internet, I have had a mixed experience with comments. Some very positive, some… not.

I decided that for now I am going to use a loosely integrated system with comments implemented via GitHub issues, as described here. I migrated over comments for some of my most popular posts according to Google Analytics, but generally didn’t worry about the rest. This means that comments are effectively closed on a lot of my older posts and, well, I’m totally fine with that.

In the future, I may look at implementing GiTalk. Justin Bird has implemented them here and they take using GitHub issues for comments to another level.

RSS and blog post subscriptions

Some folks like to subscribe to blog posts – the two popular choices seem to be by RSS feed and by email.

It’s worth setting up both, if you have time. The email solution will depend on your RSS feed, so do that first.

Closing thoughts

I’ve had a great deal of fun working on this project. I’m more excited about blogging than I’ve been in a long time, and I’m looking forward to finding little ways to tweak and improve this site over time as well.

In a way, I guess this is all thanks to that terrible WordPress host. They ended up really improving my website, even if it was by convincing me to abandon WordPress and use static site generators for most of my personal website uses in the future. :woman-shrugging:

More genuine thanks go to the folks who helped me figure out this whole static site thing – everyone whose articles I’ve linked to above, plus:

  • My partner Jeremiah - for helping me curse at Git until it did what I wanted and letting me copy his DNS setup and forwarding rules
  • Justin Bird - for all the tips and tricks
  • Chrissy LeMaire for sharing her lessons learned