Tobias Erdle's Blog

Development Engineer working with different technologies. Former Jakarta MVC & Eclipse Krazo Committer. All views are my own.

Github: erdlet | E-Mail: blog (at) erdlet (punkt) de

Migrating my blog from Jekyll to Pelican

After using Jekyll and GitHub Pages to generate my blog for a few years now, more and more major and minor code maintenance challenges arose over time.

A simple example of this was updating my operating system to Ubuntu 22.04, which ships Ruby 3 by default. However, the version of Jekyll required by GitHub Pages must be built using Ruby 2.7, which no longer compiled on my system. The workaround for this was too much work for me, though, since I'm not a pro for Ruby and its ecosystem. Besides, it was a pain every time to set up Jekyll in such a way that easy development was possible.

Of course there are solutions for all these and upcoming problems, but I don't want to invest time again and again just to generate some HTML. So I started looking for an alternative to this setup, which is easier for me and less work intensive.

What is this article

This article is a kind of personal story of why and how I migrated from Jekyll to Pelican without showing each technical step in detail, as most of those can be read in the various links to Pelican's documentation. The articles demonstrates the main actions I took and what the result looked like. The whole code can be viewed at the erdlet.de GitHub repository.

What is this article not?

This article is no step by step introduction to Pelican. Please refer to the documentation for detailed information on this topic.

Migration to my own server

First I wanted to solve the problem of outdated versions caused by GitHub Pages. The simple solution: move to my own server. For this I used an already installed virtual server, which provides a web server. This was extended by the corresponding domain and the generated content was stored there.

The result: it works, but requires manual effort for deployment.

Automation of the deployment - a failed attempt

After deploying the site manually a few times, I wanted to automate this task. This should be done by a simple shell job in my Jenkins instance.

For this Ruby 3.0 should be installed via RVM. The operating system of the server was Ubuntu 22.04, so I thought to encounter no problems. However, the installation failed immediately, because Ruby 3.0 is based on an old OpenSSL version. There would be possibilities for a downgrade of the library (see RVM GitHub Issue 5209), but this can again lead to problems with other packages, also on system level. It would definitely not be worth it to me. Likewise, although a manual upgrade of the RVM index would have been possible, this information simply escaped me in the crowd of possibilities.

Thus, the attempt to set up a simple deployment already failed when installing the underlying programming language.

The search for a successor for Jekyll

After this "experience" I decided to look for another static page generator and to realize my blog in it.

To keep the migration effort within limits, the new generator should have the following features:

Since my main used programming languages are Java, Python and, from time to time, Go, I tried to find a corresponding generator in these languages. The search then resulted in the following hits:

After looking at all of these generators a little closer, my choice fell pretty quickly on Pelican. From the usage point of view this was the closest to what I had in mind, Python 3 runs without problems on Ubuntu and I already have experience with the tools used there. Java seemed too complex from a setup point of view and Go would have required too much effort in my eyes.

Migration to Pelican

With the choice of Pelican as the new tool for the homepage, the conversion began immediately. But before I dive into the real migration, I'll show some Pelican fundamentals.

Pelican installation

First a virtual environment for Python was set up in the project directory to install the dependencies. Therefore I created the project directory an navigated into it. Inside the directory I ran following bash commands, assuming I'm working on Ubuntu 22.04. This may differ on other operating systems.

# Create virtual environment in project directory
python3 -m venv ./venv

# Activate virtual environment
. ./venv/bin/activate

# Install pelican with Markdown support
python -m pip install "pelican[markdown]"

Then a new Pelican project was generated directly in the project directory by running pelican-quickstart. This command will ask you several questions I'll skip here for brevity. The resulting directory structure looks like this.

erdlet.de/
├── content   └── (pages)
├── output
├── tasks.py
├── Makefile
├── pelicanconf.py " contains the basic Pelican configuration
└── publishconf.py " contains configuration for publishing the page (if done by Pelican)

By running pelican --listen inside the project directory, the content will be generated and served on localhost:8000. Also, pelican supports auto-reload by adding --autoreload to the command mentioned before.

Pelican concepts

Now that the Pelican project is up and running, let's start looking into the concepts of Pelican. Pelican is based on "themes", which provide specific Jinja2 templates for the respective types of content. The topic "Themes" will be discussed later.

In addition, Pelican distinguishes between "articles" and "pages". An "article" are chronological entries, e.g. blog entries, which are provided with a date. The "pages" on the other hand are static in nature and independent of a specific date, such as an imprint.

Each content can be enriched with certain metadata, which can and will then be used for processing in the templates. The following example shows the possible metadata taken tags delivered by Pelican see Writing content topic. You can also add your own data, but it must not overlap with the existing data.

Metadata Description
title Title of the article or page
date Publication date (e.g., YYYY-MM-DD HH:SS)
modified Modification date (e.g., YYYY-MM-DD HH:SS)
tags Content tags, separated by commas
keywords Content keywords, separated by commas (HTML content only)
category Content category (one only — not multiple)
slug Identifier used in URLs and translations
author Content author, when there is only one
authors Content authors, when there are multiple
summary Brief description of content for index pages
lang Content language ID (en, fr, etc.)
translation If content is a translation of another (true or false)
status Content status: draft, hidden, or published
template Name of template to use to generate content (without extension)
save_as Save content to this relative file path
url URL to use for this article/page

An example for using this metadata in an article is the source of this one:

Title: Migrating my blog from Jekyll to Pelican
Date: 2022-07-05
Category: python
Tags: python, pelican, migration
Slug: migrating-blog-from-jekyll-to-pelican
Authors: Tobias Erdle
Status: draft

Those tags add the title, which is used for generating headlines, browser tab titles, et cetera by the most themes. The other tags are described above, so there won't be further explanation.

Pelican themes

The last concept explained are the "themes" which are used for rendering the content. A Pelican theme has a fix directory structure like it is shown below (taken from Pelican Documentation, topic themes).

themename
├── static   ├── css   └── images
└── templates
    ├── archives.html         // to display archives
    ├── period_archives.html  // to display time-period archives
    ├── article.html          // processed for each article
    ├── author.html           // processed for each author
    ├── authors.html          // must list all the authors
    ├── categories.html       // must list all the categories
    ├── category.html         // processed for each category
    ├── index.html            // the index (list all the articles)
    ├── page.html             // processed for each page
    ├── tag.html              // processed for each tag
    └── tags.html             // must list all the tags. Can be a tag cloud.

Those files are mandatory to have within a theme, thus it is not necessary to implement them all. If e.g. categories or tags aren't used, the templates /templates/category, /templates/categories/ et cetera can be kept empty.

The static/css contains all necessary styling and static/images assets like logos or icons. You're free to add custom directories like static/fonts in case a site specific font is used. Personally I added this directory, as using Google Fonts via CDN is problematic in Germany due to data privacy laws.

There are already a lot of themes if someone wants to use them instead of a custom implementation. Those themes can be found in the pelican-themes GitHub repository.

Migrating files to their corresponding directory

At beginning of the migration, the Jekyll repository looked like the following directory structure.

.
├── _layouts/
│   ├── layout.html
│   └── base.html
├── _posts/
│   ├── 2019-08-07-jsr371-multi-language-support.md
│   ├── 2019-08-23-jsr371-custom-locale-resolver.md
│   └── 2019-09-10-jsr371-talk-at-jug-in.md
│   └── ...
├── assets/
│   └── css/
│       └── styles.css
├── .gitignore
├── .ruby-version
├── .rvmrc
├── 404.html
├── Gemfile
├── Gemfile.lock
├── _config.yml
├── about_me.html
├── impress.html
├── index.html
└── privacy.html

And, as described above, the new Pelican project looks like this:

erdlet.de/
├── content
│   └── (pages)
├── output
├── tasks.py
├── Makefile
├── pelicanconf.py
└── publishconf.py

In the old project, the _layouts/layout.html contained the basic layout, whereas _layouts/post.html contained all the posts (or articles). Static files, like CSS, were stored in the assets/ directory. Last but not least, the static pages, like the imprint, were simply laying around in the projects root directory.

Now to bring this in the pelican directory structure, at first all files from _posts/ were moved into the erdlet.de/content/ directory, as well as the static files into erdlet.de/content/pages/. The result looked like this structure:

erdlet.de
├── content/
│   ├── pages/
│   │   ├── imprint.html
│   │   ├── privacy.html
│   │   └── about_me.html
│   ├── 2019-08-07-jsr371-multi-language-support.md
│   ├── 2019-08-23-jsr371-custom-locale-resolver.md
│   ├── 2019-09-10-jsr371-talk-at-jug-in.md
│   └── ...
├── output
├── tasks.py
├── Makefile
├── pelicanconf.py
└── publishconf.py

After this, the next step is to migrate the content/pages/*html files into markdown files. This could be done easily by changing the file ending, as HTML tags are allowed within markdown. This way, a lot of time can be saved by avoiding the full migration of bigger pages. After this, the repository looks like this:

erdlet.de
├── content/
│   ├── pages/
│   │   ├── imprint.md
│   │   ├── privacy.md
│   │   └── about_me.md
│   ├── 2019-08-07-jsr371-multi-language-support.md
│   ├── 2019-08-23-jsr371-custom-locale-resolver.md
│   ├── 2019-09-10-jsr371-talk-at-jug-in.md
│   └── ...
├── output
├── tasks.py
├── Makefile
├── pelicanconf.py
└── publishconf.py

After running pelican --autoreload --listen a bunch of errors occured, as the metadata of my articles and pages was wrong. Fixing this took the most time of the migration. As it is a straigth forward action, as the necessary metadata was explained above, it won't be covered in detail. A short example of the 2019-08-07-jsr371-multi-language-support.md metadata migration shall be enough:

# Before

---
layout: post
title:  "MVC API: I18n fundamentals"
date:   2019-08-07 10:00:00 +0200
categories: jakarta-ee
author: Tobias Erdle
---


# After
Title:  Jakarta MVC: I18n fundamentals
Date:   2019-08-07
Modified: 2022-07-04
Category: jakartaee
Tags: jakartaee, jakarta-mvc, java
Slug: jakarta-mvc-i18n
Authors: Tobias Erdle
Status: published

One important thing to mention here is, that Status is necessary to control if the article is loaded into the articles collection when implementing templates or not. With setting the slug, the generated URL can be set.

Creating custom theme

After the default theme worked like intended, it was time to create the custom theme, as the original style and templating shall be the same. Therefore, the directory structure was extended to match the following diagram.

erdlet.de
├── content/
│   ├── pages/
│   │   ├── imprint.md
│   │   ├── privacy.md
│   │   └── about_me.md
│   ├── 2019-08-07-jsr371-multi-language-support.md
│   ├── 2019-08-23-jsr371-custom-locale-resolver.md
│   ├── 2019-09-10-jsr371-talk-at-jug-in.md
│   └── ...
├── output
├── themes/
│   └── erdlet/
│       ├── static/
│       │   ├── css
│       │   ├── fonts
│       │   └── images
│       └── templates/
│           ├── article.html
│           ├── authors.html
│           ├── author.html
│           ├── base.html
│           ├── categories.html
│           ├── category.html
│           ├── index.html
│           ├── page.html
│           ├── periodic_archives.html
│           ├── tag.html
│           └── tags.html
├── tasks.py
├── Makefile
├── pelicanconf.py
└── publishconf.py

The assets/css/styles.css was copied directly into the themes/erdlet/static/css/ directory and the necessary fonts downloaded into themes/erdlet/static/fonts/. Adding the fonts into the CSS file won't be covered here. Afterwards the necessary templates need to be implemented. In this blog's case, only article.html, index.html and page.html are filled, as well as base.html which contains the overall layout. It will be necessary to change some variables to match Pelican's data structures and use the Jinja2 syntax to extend the pages from base.html.

To activate the custom theme, the setting THEME = 'themes/erdlet' had to be set in pelicanconf.py. Now running the page worked nearly like expected.

The last thing to do was to set PAGE_SAVE_AS = 'pages/{slug}.html' in pelicanconf.py, so the files of the pages in output/ will be named bei their slug instead of their title. Those names are shorter and easier to link.

Now running pelican --autoreload --listen worked and showed the pages like intended.

Automation of the deployment - an successful attempt

The trigger for this migration was, as mentioned at the beginning, the dissatisfaction with the installation and configuration of the automatic deployment on the Jenkins server.

Python 3 is already installed on the server and runs without problems. To be able to use venv, the package python3.10-venv has to be installed. This works without problems. After that a simple Jenkins job can be created with a script like this one and, if the rights of the user fit, executed.

# Create virtual environment
python3 -m venv ./venv

# Activate virtual environment
. ./venv/bin/activate

# Installiere die Abhängigkeiten mit pip
pip install -r requirements.txt

# Generiere `output/`
make html

# Kopiere generierten Inhalt in Webserververzeichnis (wenn auf gleichem Server)
cp -r output/* /var/www/foobar/

The automatic deployment works as expected and no futher actions must be taken.

Conclusion

Would I do this migration again? I am sure: yes.

The whole migration from Jekyll to Pelican took me about 4 hours, so probably less than rewriting everything in another language. I've also always had problems keeping Ruby and Jekyll running, which has never happened to me with Python and its frameworks (never say never - haha). Besides the fact that I find it easier to run with Pelican, I also like the clear structure of the repository much better and the clear guidelines on themes complete the picture for me.

Would I recommend Pelican?

Here, too, I clearly say: yes. If I had known Pelican before, I would have used it directly.