Migrating from Jekyll to Hugo... or not
Source: Dev.to

Context
I started this blog on WordPress. After several years, I decided to migrate to Jekyll. I have been happy with Jekyll so far. It’s based on Ruby, and though I’m no Ruby developer, I was able to create a few plugins.
I’m hosting the codebase on GitLab, with GitLab CI, and I have configured Renovate to create a PR when a gem is outdated. This way, I pay technical debt every time, and I don’t accrue it over the years. Last week, I got a PR to update the parent Ruby Docker image from 3.4 to 4.0.
I checked if Jekyll was ready for Ruby 4. It isn’t, though there’s an open issue. However, it’s not only Jekyll: the Gemfile uses gems whose versions aren’t compatible with Ruby 4.
Worse, I checked the general health of the Jekyll project. The last commits were some weeks ago from the Continuous Integration bot. I thought perhaps it was time to look for an alternative.
Hugo
Just like Jekyll, Hugo is a static site generator.
Hugo is one of the most popular open‑source static site generators. With its amazing speed and flexibility, Hugo makes building websites fun again.
Contrary to Jekyll, Hugo builds upon Go. It touts itself as “amazingly fast”. Icing on the cake, the codebase sees much more activity than Jekyll. Though I’m not a Go fan, I decided Hugo was a good migration target.
Jekyll → Hugo
Migrating from Jekyll to Hugo follows the Pareto Law.
Migrating content
Hugo provides the following main folders:
| Folder | Purpose |
|---|---|
content | Content that needs to be processed |
static | Resources that are copied as‑is |
layouts | Templates |
data | Data sources |
See the full list for exhaustivity.
Jekyll distinguishes between posts (which have a date) and pages (which don’t). Posts are the foundation of a blog; pages are stable and structure the site. Hugo doesn’t make this distinction.
Mapping of Jekyll folders to Hugo folders:
| Jekyll | Hugo |
|---|---|
_posts | content/posts |
_pages/.md | content/posts/.md |
_data | data |
_layouts | layouts |
assets | static |
When mapping isn’t enough
Jekyll offers plugins. Plugins come in several categories:
- Generators – Create additional content on your site
- Converters – Change a markup language into another format
- Commands – Extend the
jekyllexecutable with subcommands - Tags – Create custom Liquid tags
- Filters – Create custom Liquid filters
- Hooks – Fine‑grained control to extend the build process
On Jekyll, I use generators, tags, filters, and hooks. Some I use through existing gems (e.g., the Twitter plugin), others are custom‑developed for my own needs.
Tags → Shortcodes
Jekyll tags translate to shortcodes in Hugo:
A shortcode is a template invoked within markup, accepting any number of arguments. They can be used with any content format to insert elements such as videos, images, and social‑media embeds into your content.
There are three types of shortcodes: embedded, custom, and inline. Hugo offers a solid collection of built‑in shortcodes, and you can roll out your own.
Generators → No direct equivalent
Unfortunately, generators have no direct equivalent in Hugo. I had developed generators to create newsletters and talk pages. The generator plugin automatically generated a page per year according to my data. In Hugo, I had to manually create one page per year.
Migrating the GitLab build
The Jekyll build consists of three steps:
- Detect if any of
Gemfile.lock,Dockerfile, or.gitlab-ci.ymlhas changed, and rebuild the Docker image if needed. - Use the Docker image to actually build the site.
- Deploy the site to GitLab Pages.
The main change obviously happens in the Dockerfile. Below is the new Hugo‑based version for reference:
FROM docker.io/hugomods/hugo:exts
ENV JAVA_HOME=/usr/lib/jvm/java-21-openjdk
ENV PATH=$JAVA_HOME/bin:$PATH
WORKDIR /builds/nfrankel/nfrankel.gitlab.io
RUN apk add --no-cache openjdk21-jre graphviz \ #1
&& gem install --no-document asciidoctor-diagram asciidoctor-diagram-plantuml rouge #2
- Packages for PlantUML.
- Gems for Asciidoctor diagrams and syntax highlighting.
At this point I should have smelled something fishy, but it worked, so I continued.
The deal breaker
I migrated with the help of Claude Code and Copilot CLI. It took me a few sessions, spread over a week, mostly during evenings and on weekends. During migration I regularly requested one‑to‑one comparisons to avoid regressions. My idea was to build the Jekyll and Hugo sites side‑by‑side, deploy them both on GitLab Pages, and compare the deployed versions for final gaps.
I updated the pipeline to do that and triggered a build:
- Jekyll build: a bit more than two minutes.
- Hugo build: more than ten minutes!
I couldn’t believe it, so I triggered the build again. The results were consistent.
Result: Hugo was dramatically slower than Jekyll in my CI environment, which was the decisive factor that stopped me from completing the migration.

I analyzed the logs to better understand the issue. Besides a couple of warnings, I saw nothing explaining where the slowness came from.
│ EN
──────────────────┼──────
Pages │ 2838
Paginator pages │ 253
Non-page files │ 5
Static files │ 2817
Processed images │ 0
Aliases │ 105
Cleaned │ 0
Total in 562962 ms
When I asked Claude Code, it pointed out my usage of Asciidoc in my posts. While Hugo perfectly supports Asciidoc (and other formats), it delegates formats other than Markdown to an external engine. For Asciidoc, that engine is asciidoctor. It turns out that this approach works well for a couple of Asciidoc documents, but not so much for more than 800. I quickly found that I wasn’t the first one to hit this wall: this thread spans five years.
Telling you I was disappointed is an understatement. I left the work on a branch and will probably delete it in the future, once I’ve cooled down.
Conclusion
Before working on the migration, I did my due diligence and asserted the technical feasibility of the work. I did that by reading the documentation and chatting with an LLM. Yet, I wasted time doing the work before rolling back.
I’m moderately angry toward the Hugo documentation for not clearly mentioning the behavior and the performance hit in bold red letters. Still, it’s a good lesson to remember to check for such issues before spending that much time, even on personal projects.
Go further:
Originally published at A Java Geek on February 15th, 2026