Migrating From Kirby to Hugo

Despite working with the PHP based content management systems TYPO3 and Neos on a daily base for over 15 years, about 10 years ago when I last “relaunched” my personal website, I decided to use a different approach. Back then I decided to use Kirby that has evolved a lot since then. I never really upgraded Kirby and rarely wrote any blog posts anyway.

Fast forward to end of 2022 I wanted to do some more with my website again and looked into a new static site generator so I can build the site locally or in a Gitlab CI build pipeline and just publish the resulting static files from there on. The team from the TYPO3 Camp Switzerland just started to use Hugo for their event site and while I looked at their git repository I just started playing around with Hugo. Having Hugo up and running that quickly was motivating and well, I just tried how far I can get with it.

How to replace the “CMS”

As both Kirby and Hugo are file-based, the overall process was pretty straight forward:

  • install Hugo (see quickstart guide below)
  • move the content files (markdown + images) to a temporary directory and out of the way for the moment
  • remove the Kirby config + code files + the git submodule dependency if present from the existing git repository
  • chose a nice Hugo Theme of your choice
  • locally install Hugo + kickstart a new site by following the great Hugo Quick-Start Guide
  • if not done while kickstarting, include the theme of your liking to be used
  • configure the theme’s details if needed

Then just run

hugo server

and open http://localhost:1313 to preview your new site.

With this you should have a basic website up and running locally, ready to start writing fresh content - or migrate the existing content.

How to migrate content

This is basically just moving the existing article.text.txt files to the new directory structure and renaming them to index.md.

In the same moment, the “header” or config of each single post needed to be converted. Since I was confronted with <30 pages/posts, I did that manually as it was less work than automating this step with a one-time script that would be threwn away later on.

In Kirby it was like:

Title: New Website


Description: Something about this new website - welcome btw!


Date: 2013-02-18


Categories: Website



(... the real content comes below ...)

And in Hugo this became the following format - basically the same bits of information, just differently formatted (and tags added):

title: New Website
description: Something about this new website - welcome btw!
date: 2013-02-18
slug: new-website
- Website
- website
- kirby
- php
(... the real content comes below ...)

As far as I remember, I played around with the directory-/filename structure until it matched what I had with Kirby - basically so I don’t need to create a lot of old-to-new-URL redirects.

How to replace images in Markdown files with a Regex

Kirby uses a different syntax to embed images into markdown files as hugo does. With the following regex, I could easily switch all of them (did that in PhpStorm):

Search for:

(\(image: )(.*)( alt: )(.*)(\))

Replace with:


This replaces e.g. (image: foo.jpg alt: Alt-Text) to ![Alt-Text](foo.jpg)

Manual tweaks

I ran through the single pages and posts and fine-tuned manually some things I noticed here and there.


Kirby and Hugo (or the involved themes, don’t know where it comes from) seem to deliver the RSS feed for subscribing to my blog under different URLs. In Kirby it was just /feed while in Hugo it’s /index.xml

I was not able to configure that path in Hugo, so I just fixed it with a redirect on Nginx so that both the old and the new URL will work:

location = /feed {
    try_files $uri /index.xml;

If there is a file named feed in the document root, that one is served, if not, the content of the index.xml file is delivered as a fallback.

How to deploy the Hugo output

I already automatically deployed the Kirby site, but that “ran” and was executied on the target server using PHP to render the output from the plain-text / markdown files. It was basically just an automated rsync to the target server.

Now with Hugo, I want the git-repo to hold just the config + sources - and let the build automatism actually “build” the site (as in render the final HTML etc.) and then deploy the result of that to the target server. The pipeline is thus split into two steps: build + deploy.

I use Gitlab CI with the following .gitlab-ci.yml file and the container image from the Gitlab Pages with Hugo example for this:

  - build
  - deploy

  stage: build
  image: registry.gitlab.com/pages/hugo/hugo_extended
    # do this to let Gitlab fetch/update the git submodules *before* starting the pipeline
    - hugo -v --cleanDestinationDir --source . --destination public
      - public

  stage: deploy
  image: ubuntu:jammy
    - hugo-build
    # prepare things so we can SSH into the target server
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - ssh-add <(echo "$SSH_PRIVATE_KEY")
    - mkdir -p ~/.ssh
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
    - apt-get install rsync -y

    # rsync generated files to the webserver
    - rsync -avz -e ssh --exclude=.git ./public/ USER@TARGET-SERVER.DOMAIN.TLD:/var/www/vhosts/TARGET-DOMAIN.TLD/httpdocs/
    - main

This does the following:

  • using the Hugo container image as the base (the extended version that contains the SCSS compiler used for this template)
  • fetch the submodule(s) of our repo (e.g. the theme in use)
  • execute hugo to generate and write out the resulting files to deploy to public/
  • store the public/ directory and it’s content as a build artifact so it can be used in the next step
  • mark the deploy job as dependent from the build step - so that the artifacts will be linked and usable in this step
  • install rsync and stuff in this stock ubuntu image (this part could be simplified I guess, but I’m lazy)
  • then rsync the public/ directory’s content to the target-server (you probably need to adopt that for your own setup)

I guess that was it. And as you are reading this page, it looks like it worked out and Hugo and the build-pipeline are doing a great job :-)

(I’ve started to write this blog-post in december 2022, then finalized + published my new website in February 2023 and with that also finished this blog-post)