What can bite you moving from WordPress to Hugo

I had recently migrated this site to Hugo static site generator, after 11+ years run on WordPress. I am happy with the result and didn’t want to write an obligatory migration post for the sake of it.

There had been several interesting nuances for me as experienced WP dev. This means to give you heads up on some challenges, not cover WordPress/Hugo completely.

No native WordPress import

WordPress has a target on its back in regards to other systems eager to import its content. Hugo isn’t concerned though. It suggests third party solutions or migrating WP to Jekyll first, then to import that.

As an experienced wheel reinventor, I ended up rolling own wprss2hugo importer. That took care of everything I needed and PHP implementation ensures I can reuse and adjust.

No categories in post URLs

The /category-name/post-name link structure is very popular in WordPress sites. Unfortunately, Hugo does not support it.

There are two possible approaches to handle it.

Split into sections

Hugo sections are a level of file organization and establish content types, an analogue of WP post types.

It might make sense to split posts from categories into sections and get URLs working that way.

But if the posts are similar or identical in nature, many different content types would be pointless.

There might be a way to find a good balance with subsections here, but I hadn’t found it yet.

Set URL per post

A strong Hugo feature is the ability to assign any page to any arbitrary URL in the front matter. This makes it trivial to import existing posts and keep URLs working.

For new posts, you can start using a different link structure or continue to specify URL in the front matter.

I decided to stick with manual approach and use archetypes to pre–fill it.

No hierarchical taxonomies

You can nest terms in WordPress taxonomies, creating comprehensive layered organization.

Terms in Hugo are flat, they all live on a single level.

I got lucky that I only used terms in a flat way, this could get messy with manual URL assignments.

No hierarchical post types

This one got a little mind–bendy for me. The concepts do not completely align between WordPress and Hugo.

WordPress post types can be hierarchical, with posts nested within each other. WordPress post archives only contain posts.

Hugo content sections determine, among other things, the content type of the pages. You can nest pages within sections, but sections can also contain other sections.

Said in WordPress terminology, you cannot nest posts in Hugo, but you can nest post archives.

It is possible to convert this. Would not be a trivial operation. The logic of nesting levels will change from post–like to archive–like. That impacts templates and more.

No autoembeds (out of the box)

In WordPress, a raw link to some online services in content turns into embedded content.

Hugo does not do this, but it will be possible with Markdown hooks when the feature works for autolinks.

[updated 2022-06-10] This got ironed out and embeds can now be implemented with combination of autolinks and markdown render hooks, see example.

Taxonomy and term templates named backwards

Taxonomy templates naming is a small thing, that nearly fried my brain.

In WordPress, a taxonomy archive template shows posts in a term of a taxonomy. There is no archive showing terms in a taxonomy.

In Hugo, taxonomy list template shows pages in a term of a taxonomy. Taxonomy terms template shows terms in a taxonomy.

Problem is — a list of pages in a term is taxonomy kind and list of terms in a taxonomy is taxonomyTerm kind. taxonomyTerm templates apply to taxonomies (not terms) and taxonomy templates apply to terms (not taxonomies).

I repeatedly wailed “why” before, during, and after figuring this out.

Coming from WordPress you use taxonomy template for your terms. Consider disabling taxonomyTerm via disableKinds, since there is no analogue in the WP site structure.

Go templates are a challenge

Hugo is written in Go programming language and uses html/template package for its templating.

The syntax is not at all C–like, conventions common to PHP and JavaScript are out of the window.

It closer to logic–less Mustache templates and meant for display of passed context data. Problem is — in Hugo you do not have direct control over the passed context. You do not have control over “back–end” implementation at all.

I use Twig and Mustache over native WordPress templates, so I did not expect the learning curve to be that steep. Coming from WordPress, expect this part to hurt.


It was a great experience, moving from a very open system like WordPress to a very closed like Hugo. I see constraints as important as goals, both in the design of my code and projects I use.

At the moment Hugo is viable alternative for WordPress content site. It aligns better with my current priorities. The learning curve was worth it, for the results I achieved.

Be ready to roll your eyes quite a few times, and have your mind blown a few more.

Related Posts