Migrating WordPress blog to Jekyll

This year I migrated my blog from the famous WordPress blogging platform to Jekyll static site generator. The main reasons were the handling of code snippets, simplicity, and security. I think that WordPress is fine, but my own time with WordPress is certainly up.

Jekyll 101

Jekyll is a blog aware static site generator. It’s one of the oldest and most popular static site generators out there. The three pillars of Jekyll are:

Jekyll is not a content management system (CMS) so the comparison with WordPress might be unfair (both ways), however, both are widely used these days for blogging and Jekyll got a +1 this year from me.

Installation

Jekyll is written in Ruby. This is great for the ability to extend its core functionality with plugins but bad for the requirement of having Ruby installed on your system.

So first, we need to install Ruby. On Fedora you can install system Ruby with:

$ dnf install ruby -y

Then we can install Jekyll:

$ gem install bundler jekyll

Bundler, a Ruby gems manager, is optional but can help you manage all the site dependencies in the Gemfile.

Structure

Once installed, it’s easy to tell Jekyll to generate a new site to have a peek inside:

$ jekyll new my-awesome-site
$ tree my-awesome-site
my-awesome-site
├── 404.html
├── about.markdown
├── _config.yml
├── Gemfile
├── Gemfile.lock
├── index.markdown
└── _posts
    └── 2020-10-16-welcome-to-jekyll.markdown

Each project has a few key files and directories, Here are the main ones:

An important thing to keep in mind is that Jekyll requires post files to be named in the following format:

YEAR-MONTH-DAY-title.MARKUP

There are also other directories in a typical Jekyll site that might make their way in. Here are some that you might come across:

Configuration

The _config.yml YAML file is the source of configuration.

Here are some of its parts.

Basic name, description, author, layout:

name: Notes to self
title: Notes to self
tagline: Software engineering, ...and bootstrapping
description: Josef Strzibny's publication on software engineering...
author: strzibny
permalink: /:title/
layout: post

Markdown used:

markdown: kramdown
kramdown:
  input: GFM
  syntax_highlighter: rouge

Sass setup:

sass:
    sass_dir: _sass
    style: compressed

Default value:

defaults:
  - scope:
      path: ""
    values:
      image: /img/nts.png

Plugin definitions:

plugins:
  - jekyll-sitemap
  - jekyll-paginate
  - jekyll-seo-tag
  - jekyll-archives

And some of the settings for plugins:

jekyll-archives:
  enabled:
    - tags
    - month
  layouts:
    month: month
    tag: tag
  permalinks:
    month: '/:year/:month/'
    tag: '/tag/:name/'
  title_prefix: Archives
paginate: 5
paginate_path: "/page/:num"

I mention some of these settings below. Feel free to start with the default and incrementally add new settings with each added feature.

Templates

Jekyll uses the Liquid templating language from Shopify to process site templates.

Content is injected by using variables surrounded by two curly braces as showed on the following snippet of this blog’s _layouts/default.html layour:

<nav>
  <a title="Notes to self by Josef Strzibny" href="/"><strong>/home/nts</strong></a>
</nav>

{{ content }}

<footer>
  <a href="/about/">About</a>
</footer>

content above is a variable that is provided for us by Jekyll.

Filters can modify the provided values:

{{ "Hello world!" | number_of_words }}

Tags provide control flow and helpers. They are started with {% and ended with %}:

{% link /assets/files/doc.pdf %}

I recommend going through the generated files to get the feel for Liquid and to see official docs on tags and filters.

Front Matter

A regular Liquid template in Jekyll will start with a so-called Front Matter, a special meta YAML block.

Here is a start of one of the post on this blog:

---
title: Getting Vagrant with libvirt support on your Fedora 20
layout: post
permalink: /getting-vagrant-with-libvirt-support-on-your-fedora-20/
tags:
  - fedora
  - vagrant
---
I was ...

It defines the post title, layout, permalink and tags.

This block must come first in the template and be separate with three triple-dashed lines as you can see above.

Build and production

We can build our new site with jekyll build or use jekyll serve which will build the site and serve it immediately for preview:

$ bundle exec jekyll serve --drafts

By default, Jekyll will use http://127.0.0.1:4000, so just open it locally with any browser. If we need to preview our drafts as well, we should add --drafts option.

Once the site is ready for publishing we should build it with JEKYLL_ENV set to production:

$ JEKYLL_ENV=production jekyll build

This environment is useful for building up features we want to only see in production such as a snippet for tracking visitors. In any template we can use the following if statement to separate the production environment:

{% if jekyll.environment == "production" %}
  ...only in production...
{% endif %}

Publishing

The writing routine is certainly something I had to change. Before I would log in to my WordPress instance and write my posts entirely from the WordPress admin interface. I could schedule the publishing, save drafts, or see the number of clicks per posts.

Since the build and management are more local, I recommend leveraging your preferred Markdown editor. I moved entirely to Sublime Text 3 where I already program and write. Here is a post on jekyll-sublime plugin that helps me with this.

Some other people might still want to use some kind of admin. There is Jekyll Admin, Netlify CMS or Prose.io that are worth checking out.

If you will host your site git repository on GitHub, you can also leverage their online editor, which is handy for quick fixes. I also started to leverage GitHub issues for my post ideas and notes.

Hosting

I am done with WordPress hosting. While I can imagine a project that I would start on WordPress, I don’t want to handle the hosting part of it. This is because I don’t want to deal with PHP hosting anymore. Some hosting providers have a one -click WordPress install, but then leave you on an old version of PHP and some plugins won’t install, etc. Managed WordPress would be my choice today, but it’s quite expensive.

Hosting is one of the best reasons to migrate off WordPress. By building the blog with Jekyll I don’t have to manage the PHP backend nor do I have to care about running a MySQL database. It’s also a reason I specifically evaluated only static site generators. It’s liberating.

And the cherry on top? There are many completely free static site hosting providers and many of them even integrate directly with Jekyll. They usually handle TLS certificates and offer a CDN as well.

GitHub Pages and Netflify integrate with Jekyll nicely. Render is another option for completely free static site hosting with CDN.

Assets

One thing to consider for blogs with a lot of images and media would be separate hosting for your assets. Doing a lot of images inside the main git repository does not scale well. Object storage like S3 or Digital Ocean spaces could be an option. I am still thinking of a good setup for the future.

Error pages

Error pages redirects depend on your hosting.

If you decide to use Netlify like me, see their redirects guide.

WordPress migration

Almost sold on Jekyll? Curious about how much work it takes to migrate a WP site?

There is an official importer that supports WordPress. The importer only converts posts with YAML front-matter. It does not import any layouts, styling, or external files (images, CSS, etc.). If your site is assets heavy, I would not recommend it.

There are however WordPress plugins that can get you started with the migration. One of them is WordPress to Jekyll Exporter.

It should create drafts and posts directories. Assets will be referenced from the wp-content directory that you either leave as it is or optionally migrate the files out of it.

It does not migrate your design or comments though. Personally, I took this as an opportunity to create a new design for my blog from scratch and simply remove comments. I direct people to drop me a note on Twitter instead.

Building blocks

Let’s see how to implement and work with the main bits and pieces on a typical Jekyll programmer blog.

This section covers:

Excerpts

Each post can define its own excerpt separator with excerpt_separator in the Front Matter:

excerpt_separator: <!--more-->

This can be useful when migrating from systems such as WordPress.

Other than that, excerpt will be the first paragraph. If you are building a post listing you can edit the excerpt using Liquid filters.

For example, you can strip HTML and truncate to a designated length with the following:

{{ post.excerpt | strip_html | truncatewords:75 }}

Tags and archives

I recommend jekyll-archives plugin that can handle both archive and tag pages.

Add to Gemfile

I wrote previously:
gem "jekyll-archives"

and _config.yml:

plugins:
  - jekyll-archives

And you can start with the similar settings I use on this blog (also in _config.yml):

jekyll-archives:
  enabled:
    - tags
    - month
  layouts:
    month: month
    tag: tag
  permalinks:
    month: '/:year/:month/'
    tag: '/tag/:name/'
  title_prefix: Archives

What these little settings do?

If you are curious, here is the simple tag layout I use:

---
layout: default
---

<h1><span>{{ page.title }}{{ page.date | date: "%B %Y" }}</span></h1>

<section class="main">
  <ul>
    {% for post in page.posts %}
      <li><a href="{{ post.url }}">{{ post.title }}</a></li>
    {% endfor %}
  </ul>
</section>

And archive template:

---
layout: default
---
<h1><span>{{ page.date | date: "%B %Y" }}</span></h1>

<section class="main">
  <ul>
    {% for post in page.posts %}
      <li><a href="{{ post.url }}">{{ post.title }}</a></li>
    {% endfor %}
  </ul>
</section>

Code snippets

One of the main reasons for leaving WordPress was the handling of code snippets. I never ever made them to work to my satisfaction on WordPress.

Luckily this is actually very straightforward on Jekyll.

Rouge is a pure-ruby syntax highlighter that Jekyll will happily use with a little configuration in _config.yml:

markdown: kramdown

kramdown:
  input: GFM
  syntax_highlighter: rouge

Make sure the syntax_highlighter is set to rouge.

Then you need to pick a color theme. You can start with github or monokai.

Generate the styles you want with rougify:

$ rougify style monokai > /path/to/css/file.css

And include the generated styles in your <head> as:

<link href="/path/to/css/file.css" rel="stylesheet">

Code snippets will be automatically highlighted if the programming language used is specified:

```python
def function():
  print('Yes')
...

SEO

There are few basic things to set up for a decent search engine optimization. Most of it can be handled by jekyll-seo-tag. It takes care of the following (taken from the project README.md):

Add as any other plugin in Gemfile:

gem "jekyll-seo-tag", "~> 2.6"

And to _config.yml:

plugins:
  - jekyll-seo-tag

Then add the following just before </head>:

{% seo %}

Finally, adjust as you like. On this blog, I am using the following in the config file:

social:
  name: Josef Strzibny
  links:
    - https://twitter.com/strzibnyj
    - https://www.linkedin.com/in/strzibny
    - https://github.com/strzibny
    - https://gitlab.com/strzibny
twitter:
  username: strzibnyj
  card: summary

I also recommend to do a Google site verification if you didn’t do it previously.

Social images

With jekyll-seo-tag plugin mentioned above we can easily specify social images per post or page in Front Matter with:

image: /path/to/post/image.img

And adjust its properties by providing the whole object if needed:

image:
  path: /img/twitter.png
  height: 100
  width: 100

You can also set a default image as a placeholder with the following snippet in the _config.yml file:

defaults:
  - scope:
      path: ""
    values:
      image: /assets/images/default-card.png

If you want to automate the whole generation of social media images, you can look into the open-source Open Graph Image as a Service.

Sitemap

There is a jekyll-sitemap plugin to handle the sitemap generation for you.

Add to Gemfile:

gem "jekyll-sitemap"

And to _config.yaml:

url: "https://hostname.com"
plugins:
  - jekyll-sitemap

Read the plugin documentation if you need to exclude some pages.

Post updates

Post updates can be managed by updating last_modified_at directive in Front Matter:

date: 2016-02-13T20:47:31+08:00
last_modified_at: 2016-02-14T00:00:00+08:00
---

Then in the template you can include the date within the HTML <time> tag:

<p class="post-meta">
  <time datetime="2020-10-16T00:00:00+00:00" itemprop="datePublished">Oct 16, 2020</time>
  
  ...
</p>

You can define custom permalinks with permalink directive in Front Matter:

permalink: /about/

Here is how to use many of the variables available such as ::categories, :year, :month, and :day.

If you need to tweak the permalinks even further, I have previously written a post Changing URLs in Jekyll.

Favicon

If you need a new favicon, you can generate one with Favic-o-Matic.

RSS and Atom feeds

With jekyll-feed plugin you can have an Atom feed available in no time.

Add to Gemfile:

gem "jekyll-feed", "~> 0.12"

And to the _config file:

plugins:
  - jekyll-feed

The plugin will generate the feed at /feed.xml. The path is configurable.

Then you can include the link to the feed as:

<link rel="alternate" type="application/rss+xml" title="RSS feed" href="http://your-site.com/feed.xml" />

One thing that is not possible with a typical Jekyll site is to have a database-powered search. Your option is to implement a separate backend service (that would need hosting management), go with a search engine like Google (you could redirect to search with site: scope), or do pure pre-generated front-end search.

For Fedora Developer we successfully used jekyll-lunr-js-search plugin. Unfortunately it’s unmaintained by now. You can look into Simple-Jekyll-Search as an alternative, but I don’t have a personal experience with it.

In my case, I decided not to implement a search function for now. I am not sure it’s needed for a typical blog. Nobody will miss it.

Analytics

With WordPress, I used a server-side analytics plugin which was very easy to setup. It gave me a simple overview of how I am doing. While I got a free hosting for my static blog with Netflify, I would have to pay $10 every month with the most basic plan to have access to analytics.

So I started with exploring my options which I summarized in Privacy-oriented alternatives to Google Analytics for 2020. The article mentions a lot of client-side services and open source tools from which I tried Fathom and Plausible in the last couple of months.

I am staying with Plausible for now, but I also recommend you to read my experience with Netlify analytics which I gave two months to compare. Choosing between client-side and server-side analytics is not easy, you might as well run both.

Testing

Most people won’t test their static sites the same way they won’t test their WordPress installation. And mostly you don’t need to. But I would actually recommend doing some simple testing.

Personally, I see value in checking that relative links work. You could then extend it to test that your references work as well (to delete dead links).

Here is my approach to testing static sites.

Is Jekyll worth it?

I spent a few days on the migration, but I am so relieved and happy with it! It also made me create a new minimal theme which I am improving as time goes. It’s light, fast, and easy to manage. I am not missing WordPress a bit.

As a Rubyist and someone that used Jekyll before, the choice was obvious. I want to be able to tweak my sites with Ruby. If you don’t like Jekyll in particular, try Hugo.

Any comments? Write me a DM on Twitter.