monogram with initials UKR

Migrating my blog, again (Hugo)

Updated: Under: technology Tags: #hugo #web-dev

Time goes by fast, the last time I posted on this site was in 2022. I’m alive and doing well, more than well to be honest, a lot of good news1.

But today let’s talk about the state of this website. In the last week there has been a considerable change to the makeup and hosting of this website. It wen’t from being a Next.js website hosted on Netlify to being a Hugo SSG(static site generator) on a Debian VPS on Linode. This site has always been an SSG, even with Next.JS it was using the static html export feature.

Negambo, Sri Lanka, 2024
Negambo, Sri Lanka, 2024

Why am I doing this?

Before getting into specifics, looking back there has been a lot of changes in the way I approach these problems. I must accept my naiveté and short sighted when setting my website four years back.

I’m looking for simplicity and having control of my website in this new implementation.

  1. I’m tired of keeping the dependencies up to date of a JavaScript project2.
  2. The website was one of my first Next.JS projects therefore it has some questionable code and questionable DX.

Migrating to Hugo

Hugo is a static site generator written in Go, I’ve never been interested in learning Go have always been aware of all the tools that I used daily that are written and maintained with it. Also a year back I took up a new job which uses Go as the primary backend language, but I digress.

There is no customisation here, I downloaded a copy of Hugo and chucked in my content and bootstrapped a custom theme using simple.css3.

I was able to hit the ground running fairly quickly by dropping my markdown files with YAML frontmatter.

title: RSS Feed for next.js blog
description: >-
  On creating an RSS feed for this blog, and how it's not as tedious as you think.  
date: '2021-06-19T17:12:26.723Z'
thumbnail: 'cmb_np.jpg'
    - projects
    - web-dev
    - nextjs
type: post
slug: rss_feed_nextjs

Configuring multiple taxonomies based on the attributes was straightforward.

  category = 'category'
  tag = 'tags'

But one of my biggest pain points was asset management, before there was a singular folder which had all of my images and it was a mess. I wanted to switch over to use page resources instead, which will have all the assets needed on the content folder itself.

With my laziness, coaxing chat-gpt and spending more time bug fixing resulted in this script, which migrated all my blog posts to their own folders with relative links to images.

import os
import shutil
import re

def migrate_markdown(source_dir, destination_dir, base_img_path):
    # Ensure destination directory exists
    if not os.path.exists(destination_dir):

    # Iterate through Markdown files in source directory
    for filename in os.listdir(source_dir):
        if filename.endswith(".md"):
            # Read Markdown file
            with open(os.path.join(source_dir, filename), "r") as file:
                markdown_content =

            # Extract folder name
            folder_name = os.path.splitext(filename)[0]

            # Create folder
            new_folder_path = os.path.join(destination_dir, folder_name)

            # Move Markdown file
            new_markdown_path = os.path.join(new_folder_path, "")
            shutil.copy(os.path.join(source_dir, filename), new_markdown_path)

            # Copy images to folder
            img_paths = re.findall(r"/img/([\w-]+.\w{1,5})", markdown_content)
            for img_path in img_paths:
                img_filename = os.path.basename(img_path)
                shutil.copy(os.path.join(base_img_path, img_path), os.path.join(new_folder_path, img_filename))

            # Update image paths to be relative
            markdown_content = re.sub(r"/img/([^'\"\(\)]+)", r"\1", markdown_content)

            # Write updated Markdown content
            with open(new_markdown_path, "w") as new_file:

# Example usage
source_dir = "./posts"
destination_dir = "./content/posts"
base_img_path = "./static/img"  # Base path for images
migrate_markdown(source_dir, destination_dir, base_img_path)

Tips I wish I’d knew days back

Yes to RTFM, but these are the customisations that were done.

Customize markdown image rendering

It’s best that the image tags in markdown be rendered in a figure tag and have compression applied you can do that using an hook in the layouts directory. layouts/_default/_markup/render-image.html.

{{- $imgURL := .Destination -}}
{{- $altText := .Text -}}
{{ $image := .Page.Resources.Get $imgURL }}
	{{ if $image }}
		{{  $image = $image.Process "resize 1000x webp q50" }}
		<img src="{{ $image.RelPermalink }}" class="summary-image">
	{{ end }}
  <figcaption>{{ $altText }}</figcaption>

This also has image processing, where I requested that the images be resized to 1000px on width and exported as webp with 50% quality.

Minifying CSS

I found the documentation on this, but what I missed was the fact that the css files should be in the assets directory not the static directory.

{{ $style := resources.Get "style.css" | minify | fingerprint }}
{{ $custom := resources.Get "custom.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $style.Permalink }}">
<link rel="stylesheet" href="{{ $custom.Permalink }}">

Minifying HTML content

I’d guessed this is done via configuration, but it’s done using during the build stage using a cli flag

hugo --minify


Migrating to hugo only took me a couple of days, and compared to the custom Next.js code base, this is much better DX and updating my toolchain is only a single step to replace the hugo binary. This is one of the reasons not choosing to opt into using Tailwind as well.

  1. Shall post about that soon ↩︎

  2. Did use dependbot, but then proceeded to forget about it ↩︎

  3. Do enjoyed my old website theme made with Tailwind, but wanted to get started quickly. Might re-visit it later ↩︎