For a while, I’ve been uncomfortable with Astro. I’m not a big fan of npm’s policy on security which is basically “yeah, just upload it here.” Every time I upgrade npm packages, they break. There are workarounds to some security issues with configurations like npm in a box but the developer experience for npm is still kind of like having a rash.
So I decided to give Hugo a shot.
Moving there was surprisingly easy. There were a few quirks. Here’s what I ended up doing:
Set up Hugo#
I installed Hugo from the .deb that’s linked from gohugo.io/installation/linux. I’d have preferred to have installed from the Ubuntu package manager, but its version was… less than recent. I’ll have to rememer to update this manually.
Hugo doesn’t come with a default theme, so I had to pick one (from themes.gohugo.io/) or roll my own. I picked Blowfish because it has light and dark modes, has good docs, tags, and search.
I walked through the Blowfish installation instructions. I was very careful to avoid the npm tools that they provided — my whole reason for doing this was to get away from that.
Incidentally, I get a warning:
WARN Module "blowfish" is not compatible with this Hugo version: 0.141.0/0.152.2 extended; run "hugo mod graph" for more information.It doesn’t seem to affect things.
Copy the old posts over#
Next, I needed to copy my old posts into the new structure. That wasn’t too bad:
cp -R ~/andrewmemory-astromicroacademic/src/content/blog/ ~/andrewmemory-hugo/content/posts/Fix the posts#
- Fix the time. After copying the posts over, I started to get errors left and right. Hugo is pickier about time formats than Astro. By default, Hugo uses
2025-12-14T00:08:05-0700. Astro was OK with2025-12-14 00:08:05 GMT-7. Hugo doesn’t really care about theT, I discovered, so all I had to do was:
find . -name index.md -print0 | xargs -0 sed -i 's/ GMT-7/-0700/1'- Change from description to summary. I discovered that Hugo uses
summary:where Astro usesdescription:in the front matter. So…
find . -name index.md -print0 | xargs -0 sed -i 's/description:/summary:/1'- Update internal links. Since Astro wants its blog posts in /blog/ and Hugo wants them in /posts/, I needed to update. I could probably have renamed posts to blog, but I figured, let’s go with the Hugo way. You might not want to do this. I had to undo it. Scroll down a few sections to see why.
find . -name index.md -print0 | xargs -0 sed -i 's|/blog/|/posts/|g'- Fix links to static files. Astro requires that you put static files in /public for them to be visible. Hugo apparently lets you keep them in their associated directories! So my post with a Perl script can just say
[convertcsv.pl](convertcsv.pl). Nice! I copied the static assets into their appropriate directories.
Tweak code blocks to allow filenames#
I had modified Astro to enable filenames on code blocks. I needed to do the same thing for Hugo. I found a few good references, particularly Roger Steve Ruiz’s blog. There were a few steps here:
Create directories
~/andrewmemory-hugo/layouts/_defaultand~/andrewmemory-hugo/layouts/_default/_markup/.Add a
render-codeblock.htmlfile there. I wanted the titles to stick out a little more, so I abused the<mark>tag. I also decided to setisVerbatimto false because I prefer it that way. I ended up with:
{{/* this file is exists at `layouts/_default/_markup/` in your Hugo project */}}
{{- $isVerbatim := false -}}
{{- if isset .Attributes "verbatim" -}}
{{- $isVerbatim = .Attributes.verbatim -}}
{{- end -}}
<figure class="highlight">
{{- with .Attributes.title }}
<figcaption>
{{- if $isVerbatim -}}
<span><mark>{{ . }}</mark></span> {{/* As a file name */}}
{{- else -}}
<span><mark>{{ . | markdownify }}</mark></span> {{/* As a code description */}}
{{- end -}}
</figcaption>
{{- end }}
{{- if transform.CanHighlight .Type }}
<pre tabindex="0" class="chroma"><code class="language-{{ .Type }}" data-lang="{{ .Type }}">
{{- with transform.HighlightCodeBlock . -}}
{{ .Inner }}
{{- end -}}
</code></pre>
{{- else }}
<pre tabindex="0"><code class="language-{{ .Type }}" data-lang="{{ .Type }}">{{ .Inner }}</code></pre>
{{- end }}
</figure>- Finally, I needed to change the references from the old Astro syntax:
```bash:/usr/local/bin/file
My file contents
```to:
```bash {title = "/usr/local/bin/file"}
My file contents
```I didn’t have enough code blocks with filenames to script this, so I this by hand. I used this one-liner to find out where I needed to make changes:
find . -name index.md -print0 | xargs -0 grep '```' | grep '`.*:'Move things to match the old site#
Ok, it looks as if Hugo doesn’t have an easy way to alias a section, so I can’t define a global /blog/ that redirects to /posts/. If I put everything in /posts/ it will break existing links. I have to undo it. So much for the Hugo way…
- Undo the sed that I did above:
find . -name index.md -print0 | xargs -0 sed -i 's|/posts/|/blog/|g'- Move the posts/ directory under content/ to blog/:
mv posts blog- Update the Blog link
...
[[main]]
name = "Blog"
pageRef = "blog" # was posts
weight = 10
...- I’d love to add a link for the old RSS. But I can’t figure out how to rename it. Boo. I guess I can add that when I build the site.
Prettify the RSS feed#
By default, the RSS that Hugo/Blowfish has is pretty spartan. I found a great blog post called Hugo RSS Improvements that got me started. Here’s what I did:
- Create a file in /assets/img/ called
rss_image.png. Mine is 475 by 475 pixels. I read on rss.com that Apple wants a minimum of 3000 by 3000. 🙄 - Copy the theme’s XML from
themes/blowfish/layouts/_default/rss.xmlintolayouts/_default/rss.xml. - Change the RSS to include the image under the
<generator>line:
...
<image>
<url>{{ $.Site.BaseURL }}img/rss_image.png</url>
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
<link>{{ $.Site.BaseURL }}</link>
</image>
...- Change the RSS
<description>to include content rather than summaries:
...
<description>{{ .Content | html }}</description>
...Now my RSS feed is 600 K and only going to grow, but I can read the blog from the RSS feed in AntennaPod.
Build the static site#
On to building and testing. That’s easy:
killall hugo
cd ~/andrewmemory-hugo
rm -rf public resources
hugo --gc --minify
cd public
cp index.xml rss.xml # Yuck
python3 -m http.server &Hey, it looks good! Everything works… except search. Hmm… there’s a lot of questions in the Hugo community about that. The RSS file is just copied, which means I’ll have to do that every time.
Is it possible that search isn’t working because I’m not under my domain? Close your eyes, take a breath, and try it out.
Install the static site#
- Blow away the existing site.
- Upload everything in the new public/ to the site.
Images are broken#
Hmm… that appears to be something weird with HostGator. I changed Hotlink Protection Management to block links to .png, but allowed direct URL requests. Don’t ask, that just has to happen. Websites -> Settings -> Hotlink Protection -> Manage -> Click .png, Click Allow direct requests
Reply by Email