Goodbye WordPress, Hello Hugo

2 min read Scott

I switched this site from WordPress to Hugo. It’s now a static site sitting on S3.

That means no more servers to hack. No more PHP or Apache vulnerabilities to patch. No need for a database. It’s just files.

It’s simple, it still looks good, and it costs less. Less overall headache.

I also had Claude helping me build the site and take care of the infrastructure, so there’s less maintenance as well. I really love the simplicity.

How it works

If you want to skip WordPress too, here’s the setup.

Getting the old content out. WordPress has a public REST API (/wp-json/wp/v2), so there was no need to log in and export anything. A small Python script (wp_export.py) pulls every post, page, category and tag, converts the HTML to Markdown, and downloads every image from the media library, including all the resized versions. The images keep their original /wp-content/uploads/... paths and every post keeps its old URL, so nothing out there breaks. YouTube embeds became Hugo shortcodes, and a second script (check_links.py) confirms every old URL exists in the new build.

Hosting on S3. The site is just files in a private S3 bucket. Nobody can reach the bucket directly; only CloudFront can read from it (Origin Access Control). A tiny CloudFront Function handles the two things S3 can’t: turning /about/ into /about/index.html, and redirecting www to the bare domain.

SSL. The certificate comes from AWS Certificate Manager, validated through DNS records in Route 53, and attached to CloudFront. It’s free, and it renews itself. The domain points at CloudFront with Route 53 alias records.

Deployment. All the AWS pieces are defined in OpenTofu (the open source Terraform), so the whole setup is code. I brought it up on stage.scottpreston.com first, and once it looked right, flipping one variable switched the real domain over. Publishing is one script:

hugo --minify
aws s3 sync public/ s3://my-bucket --delete
aws cloudfront create-invalidation --distribution-id MY_DIST_ID --paths '/*'

Build, sync, clear the cache. That’s it.

Try it yourself. I put the export tooling up on GitHub at scottpreston/goodbye-wordpress. The README walks through the whole move: export, build with Hugo, host on S3 and CloudFront, deploy, and switch DNS. There’s also a list of gotchas I ran into along the way.

But to be honest…

I do miss the good old days. Writing some PHP, pushing a site up with jQuery and CSS, and having it be live. Good old includes for the headers and footers, and direct database connections live on the website.

Simple, direct, HTML.