Migrating from Octopress to Hugo
Octopress is a great blogging engine/framework, and I was happy with it for several years, but recently I discovered a new one - Hugo.
I liked it so much that I decided to move my blog to it.
First time I heard about Hugo was this 1Password’s blog post telling about their migrating to Hugo from WordPress. And then I heard about some other teams started using Hugo too, so I decided to try it myself.
Octopress vs Hugo
Mostly I wanted to compare Hugo with Octopress as the latter has some problems:
- Octopress is no longer being developed;
- The environment/infrastructure (Ruby and gems) is quite complicated and fragile. Every time I was moving between computers, I had problems setting up the environment for building and deploying: all of those gems dependencies and their mysterious errors, my god;
- Live rebuilding takes 8+ seconds, even if you just changed one character;
- Essentially, Octopress is a fork of someone else’s blog, which might be a bit confusing and not very convenient to work with;
- The community is rather small, and sometimes it’s quite challenging to find a solution for various problems.
And as it turned out after a brief evaluation, Hugo is indeed better than Octopress:
- It is actively developed and updated;
- The whole thing is just a single self-contained executable file;
- Live rebuilding takes fractions of a second;
- Nice and helpful community.
So I started the migration process.
Migration from Octopress to Hugo
I won’t describe the whole process of setting up Hugo from scratch, so refer to the official documentation and other sources (like this one about directory structure).
Instead I will list the concrete steps I took and share the solution for problems I encountered.
Keeping the old URLs
Of course I wanted all the URLs to remain available as my blog is referenced from quite a few places on the internet.
The URLs have the following structure:
/blog/2014/07/30/first-post/
So I added this value to config.toml
:
[permalinks]
blog = "/blog/:year/:month/:day/:filename/"
Renaming the posts
That’s actually just an intermediate step for organizing posts into bundles.
Two things to be done here:
- Change the extension from
.markdown
to.md
; - Delete the date from the name.
So instead of
2014-07-30-first-post.markdown
it should be
first-post.md
You can automatically do that with almost any decent file manager. Since I’m on Mac, I used ForkLift.
Changes in posts files
There are several things that need to be deleted or changed in Octopress post files in order for those to be parsed correctly by Hugo:
- I decided not to differentiate posts by layouts (
layout: post
), so that line has to go; - The line that enables/disables the comments (
comments: true
) is not needed either; - I want categories from Octopress to become tags in Hugo, so those need to be replaced;
- Summary divider in Hugo can only be this hardcoded value -
<!--more-->
- no customization possible, so I had to replace the divider from Octopress (<!-- more -->
) with it.
I used sed
to automate the process:
find . -type f -iname "*.md" -exec sed -i '' -e '/layout: post/d' {} \;
find . -type f -iname "*.md" -exec sed -i '' -e '/comments: true/d' {} \;
find . -type f -iname "*.md" -exec sed -i '' -e 's/^categories:/tags:/' {} \;
find . -type f -iname "*.md" -exec sed -i '' -e 's/<!-- more -->/<!--more-->/' {} \;
Reorganizing the posts
Hugo has this nice feature - Page Bundles - when the post file and all its media are stored in the same folder. Unfortunately you cannot have HTML pages within bundles, which is a bit frustrating, so I had to put those into the /static
folder (replicating the corresponding post path).
For example, here’s how a post file is named and organized in Octopress:
...
├── source
│ ├── _posts
│ │ ├── 2018-02-17-build-qt-statically.markdown
│ ├── images
│ │ ├── build-qt-statically
│ │ │ ├── dynamic-vs-static.png
│ │ │ ├── install-qt-msvc.png
│ │ │ ├── qt-configure.png
...
And here is a Page Bundle for the same post in Hugo:
├── content
│ ├── blog
│ ├── build-qt-statically
│ │ ├── images
│ │ │ ├── dynamic-vs-static.png
│ │ │ ├── install-qt-msvc.png
│ │ │ ├── qt-configure.png
│ │ └── index.md
For that posts need to be renamed and reorganized accordingly:
- Create a new folder for every post;
- Name the folder after the post name;
- Put the post file inside that folder and then rename the file to
index.md
; - Put the post images to the
images
folder within this folder.
To automate that I created the following Python script.
But I couldn’t automate moving images as I had those stored in folders with different names. For the same reason I couldn’t automate changing references to those images in post files, which resulted into several days of a manual work.
Syntax highlight
Syntax highlight comes out of the box and I recommend using the default shortcode for it.
I didn’t risk to automate the process of replacing the syntax highlighting I used in Octopress, which meant yet again several days of manual labor.
Tags
So categories from Octopress are tags in Hugo. Actually, you can have both categories and tags, but I decided that tags are just enough for me.
For tag pages to work properly I had to specify the following in config.toml
:
disableKinds = ["taxonomyTerm"]
preserveTaxonomyNames = true
[taxonomies]
tag = "tags"
And here’s how to list all the tags with post counters (/themes/YOUR-THEME/layouts/_default/baseof.html
):
<div class="sidebar-section">
<h3>Tags</h3>
<ul style="list-style-type:none; padding:0; margin:0;">
{{ range $.Site.Taxonomies.tags.ByCount }}
<li><a href="/tags/{{ .Name }}">{{ .Name }} ({{ .Count }})</a></li>
{{ end }}
</ul>
</div>
Working with Hugo
Here I will tell about a couple of useful things I discovered about Hugo.
Shortcodes
Shortcodes is a super convenient Hugo feature. It is kinda mini templates for certain things like images or syntax highlight blocks. Relying on shortcodes you control all their occurrences in your posts, so when you change the style or structure of a shortcode it will change in all the posts.
For example, here’s my shortcode for images:
{{ $altText := .Get "alt"}}
{{ with $.Page.Resources.GetMatch (.Get "name") }}
<img class="image-post" src="{{ .RelPermalink }}" alt="{{$altText}}"/>
{{ else }}
<code style="color:red;">Error: could not find image "{{ .Get "name" }}"</code>
{{ end }}
Usage:
{{< image name="images/name.png" alt="Description" >}}
And here’s one for videos:
{{ with $.Page.Resources.GetMatch (.Get "name") }}
<video controls loop class="video">
<source src="{{ .RelPermalink }}" type="video/mp4">
</video>
<p class="video-fallback">If video doesn’t play in your browser, you can download it <a href="{{ .RelPermalink }}">here</a></p>
{{ else }}
<code style="color:red">Error: could not find video "{{ .Get "name" }}"</code>
{{ end }}
Usage:
{{< video name="video/name.mp4" >}}
Auto-generated table of contents
Yes, it just woks (almost) out of the box. Simply put {{< toc >}}
into your post and it will generate a table of contents with all the anchors and links automatically.
For that to work you need to create a shortcode /layouts/shortcodes/toc.html
with the following content:
{{ .Page.TableOfContents }}
Archetypes
Archetypes help with creating new posts. Here’s an example of my post bundle archetype:
archetypes/
├── default.md
└── post-bundle
├── images
│ └── .gitkeep
└── index.md
.gitkeep
is just an empty file to force creating of the images
folder.
The contents of index.md
:
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
tags: [some]
draft: true
---
Intro
<!--more-->
[ here goes the shortcode for {< toc >} which I cannot put here as it is because it will be parsed into the actual table of contents ]
# Header
Now I can create new posts with the following command (from the root folder):
hugo new --kind post-bundle blog/ololo-some-post
Custom 404 page
Custom 404 page can be specified via /layouts/404.html
:
{{ define "main"}}
<main id="main">
<div>
<img class="image-post" src="/404.jpg" />
<p style="text-align: center;">No such page, mate!</p>
</div>
</main>
{{ end }}
By the way, /404.jpg
means that local image path is /static/404.jpg
.
RSS
For generating RSS you need to add a special layout for that, which can be customized as well.
A bit sad news is that the link to your RSS feed will be /index.xml
and apparently there is no way to make it to be /rss.xml
.
Another problem is that it’s not trivial to make use of CDATA, so you could provide a markup for your post summaries for feed readers to parse it into something nice. But thanks to this guy I’ve managed to do that.
Here’s my /layouts/rss.xml
:
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:georss="http://www.georss.org/georss">
<channel>
<title>{{ .Site.Title }}</title>
<link>{{ .Permalink }}</link>
<description>Recent content of {{ .Site.Title }}</description>
{{ with .Site.LanguageCode }}<language>{{.}}</language>{{end}}
{{ with .Site.Copyright }}<copyright>{{.}}</copyright>{{end}}
{{ if not .Date.IsZero }}
<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>
{{ end }}
{{ with .OutputFormats.Get "RSS" }}
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
{{ end }}
{{ range .Pages }}
<item>
<title>{{ .Title }}</title>
<link>{{ .Permalink }}</link>
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
<guid>{{ .Permalink }}</guid>
{{ `<description><![CDATA[` | safeHTML }}New post: {{ .Title }}]]></description>
{{ `<content:encoded><![CDATA[` | safeHTML }}<h2>{{ .Title }}</h2>{{ .Summary }}]]></content:encoded>
</item>
{{ end }}
</channel>
</rss>
Yet another problem is that images and videos in my posts have relative URLs which are not allowed in RSS feeds. I haven’t figured out yet how to transform those into absolute ones in summaries.
Robots.txt and sitemap
Sitemap is generated automatically and robots.txt
is controlled via /layouts/robots.txt
:
User-agent: *
Sitemap: {{ .Site.BaseURL }}sitemap.xml
Posting
To start a local server for previewing run:
hugo server
Note that if some of your posts have draft: true
then those won’t be rendered. To include those too add the option -D
:
hugo server -D
As I said, pages regeneration happens almost instantly, which is a killer-feature in comparison to Octopress.
To generate your blog/website pages for deployment simply run the the command:
hugo
Results will be generated to the /public/
folder. And in case of GitHub Pages that’s the place where you need to create a repository. But unlike Octopress there is no deploy
command, so you will be the one performing the actual deployment via regular git push
.
Oh, in order for links and styles to work properly you need to set the baseURL
parameter in config.toml
:
baseURL = "https://retifrav.github.io/"
Social networks
Zuck: Just ask
Zuck: I have over 4,000 emails, pictures, addresses, SNS
smb: What? How'd you manage that one?
Zuck: People just submitted it.
Zuck: I don't know why.
Zuck: They "trust me"
Zuck: Dumb fucks