Skip to content
Go back

Moving My Blog from Hugo to Astro

5 min readEdit page

This blog has moved again. Back in 2016 I was writing about running Ghost in CPanel. In 2024, I wrote about deploying Hugo to Vercel and managing it with Terraform. Now the site runs on Astro, with static files served by Cloudflare Workers.

There are posts here that go back to my early days with R, web scraping, and Linux servers. Moving them meant bringing along almost a decade of links, dates, images, and formatting decisions. Some of those decisions were easier to carry over than others 😂.

Here is what changed, and a few things worth checking if you are doing a similar migration.

Starting with AstroPaper

The new site started from AstroPaper. From there, I changed the layout, typography, colours, and navigation, and added pages for my background and CV. The demo posts were removed once my own content was in place.

The site is still fairly small. Posts live in Markdown, the layouts and components live alongside them, and the build produces static files. There is a search page, tags, archives, and an RSS feed. Search uses Pagefind, which runs against the generated site during the build.

The directory structure now looks like this:

src/
  components/
  layouts/
  pages/
  content/
    posts/
    pages/
    cv/

The About page has its own Markdown file. The CV is structured data in a separate collection, and the credentials shown on the About page come from that same CV data. Updating a certification in two places would be an easy thing to forget, so it only needs to be edited once.

Bringing the posts over

The import brought over 50 posts. Three were drafts in the old site, and they stayed drafts here.

Astro’s content collections let me define the fields a post needs. In this site, those include a title, publication date, description, and tags. The old Hugo content used date; the new collection uses pubDatetime.

That sounds like a simple field rename. The dates needed a closer look, though. Some posts had a timezone offset, while others only had a date and time. Treating all of them the same way could shift the displayed date. During the import, the dates without an offset were given the configured site timezone, and the rendered dates were checked against the dates in the old URLs.

The descriptions needed attention too. None of the source posts had the description required by the new schema, so the import extracted one from the body. Some of my old R tutorials use indented code blocks. An extractor looking for the first paragraph can end up picking R commands as the description. Valid text, but not very helpful in a search result.

The generated descriptions got the content through the migration. Some still read like a paragraph cut off halfway, so they could use another pass.

The old URLs included the publication date:

/2016/09/20/setup-node.js-dan-mongodb-di-linux/

The new URL for that post is:

/posts/setup-nodejs-dan-mongodb-di-linux/

The redirect list came from the old site’s sitemap. This mattered because the filenames were not a reliable record of the public URLs. Hugo had generated slugs from the post titles, and those often differed from the filenames.

There were a couple of awkward cases as well. The Packer post had qemu/kvm in its old URL, which made it a nested path. The Node.js post had a dot in its slug. Both got simpler filenames during the migration, with redirects from their original addresses.

The current redirect file contains entries like this:

/2016/09/20/setup-node.js-dan-mongodb-di-linux/  /posts/setup-nodejs-dan-mongodb-di-linux/  301

There are 47 post redirects and another one from the old categories page to the tags page. Checking that each destination actually existed was part of the import. A redirect to a missing page would only make the broken link take an extra step.

Some images were already gone

This was an unfortunate part of the migration. Seventy image references across 17 posts pointed at an old DigitalOcean Spaces host. The migration checks found that those images were already returning 404s.

Those broken references were removed. A few sentences also needed editing because they referred to an image that was no longer there. The seven images available in the old site’s static/img/ directory were copied into public/img/.

The posts survived, but some of the illustrations did not. Keeping the Markdown in Git had only preserved part of the content. For the images that remain, having them in the repository at least makes it possible to move the text and its illustrations together next time.

Getting it onto Cloudflare

The Astro site initially had Netlify deployment configuration. Moving it to Cloudflare Workers brought another small set of problems.

One was the Cloudflare adapter. In this setup, the adapter’s prerender process failed when generating the social preview images, which use Satori and Sharp:

Error: No such module "chunks/sharp"

The final configuration leaves the adapter out. The pages and images are generated during the Node.js build, then Cloudflare serves the contents of dist/ as static assets. There is no Worker application entrypoint in this setup.

Redirects also needed a change. The old Netlify configuration included a rule with an absolute source URL for redirecting the www hostname. Carrying that rule over caused the deployment to fail with:

Only relative URLs are allowed. [code: 100324]

That hostname redirect was moved out of the asset redirect file. The file now contains path redirects, and the build script checks their source paths and status codes before deployment.

There is one more slightly odd detail in the repository: _redirects and _headers are stored with a .txt extension. In the build setup used during the migration, the extensionless redirect file was being parsed as a module. The workaround was to keep the source files as text and rename them after the build. That is why there is a prepare-cloudflare-assets.mjs script at the end of the build command.

Small things, but they are the sort of things I would forget by the next time I need to change the deployment.

Back to writing

The new site is up, and the old posts are still here. There is also a proper place for my CV and background now, instead of trying to fit everything into the blog itself.

There is still cleanup to do. Some descriptions need rewriting, and some old posts contain formatting from earlier versions of the site. A successful build does not mean every article reads properly. Opening the older posts and checking the content is still necessary.

For now, this is a good place to stop working on the blog’s infrastructure and actually write something for it. The homelab GPU follow-up is still waiting 😄.


Edit page
Share this post:

Previous Post
Building Poor-man Homelab to Learn AI and MLOps