After the first part dedicated to installing and configuring Jekyll, this second part of these annotated notes dives into developing a Jekyll site using a pre-existing theme. On the other hand, developing a theme for Jekyll from scratch is far beyond my expertise, but those who can do it don’t need to read these notes.
What do we want?
First, we need to have a clear idea of what the site should look like graphically and what features it should support. Do we want a personal blog with a simple list of posts? Do we want a more elaborate magazine-style site? Do we need a showcase site to present our business or a specific project? Do we want a photography or documentation site?
Once we’ve clarified the site’s “specifications,” we can look for the Jekyll themes in this site for Jekyll, or here, here and here (I can’t tell them apart by name because they are all called Jekyll Themes). The most patient among us can go through the themes one by one, while others may prefer to filter by the type of site they’re interested in and only look at those themes. In any case, it’s good to take note of the themes we like the most and try out the demo sites, when available. It’s useful to keep an open mind – we could find an interesting theme that doesn’t fit our original specifications, and it’s worth noting that down, too.1
How a theme is structured
Now, it’s time to try some theme out. To avoid being too generic, I’ll describe the installation of the Mundana theme which, coincidentally, is the one I used for melabit.com.
Like Mundana, most Jekyll themes that are not gem-based are installed by cloning their GitHub repository or downloading a compressed file (in which case, all the following parts related to git
can be skipped). In both cases, what we install is both the theme and the actual site because, in Jekyll, there is no strict separation between design and content.
The first thing to do is install git
on the computer. On macOS, it comes preinstalled, but it’s usually an outdated version, and as I mentioned last time, it’s always better to keep system tools separate from development tools. With Homebrew, installing the latest (or nearly latest) version of git
on the Mac is a matter of seconds,
$ brew install git
On Linux, the process is similar, but this time we must use the package manager of our distribution. This means we need to execute one of the following commands,
$ sudo apt install git
$ sudo dnf install git
$ sudo pacman -S git
depending on whether we use Debian/Ubuntu, Fedora/CentOS, Arch, or one of their many derivatives.
As already mentioned, installing Mundana is done by cloning its repository,
$ git clone https://github.com/wowthemesnet/mundana-theme-jekyll.git
Cloning into 'mundana-theme-jekyll'...
remote: Enumerating objects: 324, done.
remote: Total 324 (delta 0), reused 0 (delta 0), pack-reused 324 (from 1)
Receiving objects: 100% (324/324), 4.12 MiB | 24.52 MiB/s, done.
Resolving deltas: 100% (116/116), done.
or by downloading and extracting the project’s zip
file.
The developers suggest making first a fork on our GitHub account and then cloning from there. This makes sense if we plan to contribute to the code; otherwise, it seems like an unnecessary step. Anyway, those who know what a fork is don’t need further instructions.
Once the installation is complete, we can navigate to the Mundana directory and explore a bit (for clarity, all directory names end with a /
).
$ cd mundana-theme-jekyll
% ls -nhp
total 72
-rw-r--r-- 1 501 20 398B Feb 12 19:58 404.html
-rw-r--r-- 1 501 20 510B Feb 12 19:58 Gemfile
-rw-r--r-- 1 501 20 1.7K Feb 12 19:58 Gemfile.lock
-rw-r--r-- 1 501 20 2.0K Feb 12 19:58 README.md
-rw-r--r-- 1 501 20 1.7K Feb 12 19:58 _config.yml
drwxr-xr-x 11 501 20 352B Feb 12 19:58 _includes/
drwxr-xr-x 6 501 20 192B Feb 12 19:58 _layouts/
drwxr-xr-x 11 501 20 352B Feb 12 19:58 _pages/
drwxr-xr-x 20 501 20 640B Feb 12 19:58 _posts/
drwxr-xr-x 5 501 20 160B Feb 12 19:58 assets/
-rw-r--r-- 1 501 20 149B Feb 12 19:58 docker-compose.yml
-rw-r--r-- 1 501 20 9.2K Feb 12 19:58 index.html
The Mundana directory contains two service files for RubyGems
, Gemfile
and Gemfile.lock
, as well as the docker-compose.yml
file, which is only needed if we run Jekyll inside a Docker container (which is not relevant here). Everything else is related to Jekyll.
Static pages go into the _pages
folder, while all the more dynamic content, such as the blog posts that are updated more frequently, goes into the _posts
folder. The content of static pages and posts can be written in either HTML
or Markdown
.
The _layouts
folder contains files that generate the main parts of the site, such as static pages, blog post templates, or sidebars, while the _includes
folder contains snippets of code that can be included in other pages, more or less like library functions.
These four folders, all prefixed with an underscore _
, are processed by the Jekyll engine, which uses them to generate the actual HTML
pages for the static site.
The assets
folder, on the other hand, holds site content that Jekyll does not process, such as the CSS
files that define the site’s design, the JavaScript
files for interactive features,2 and all site images. When Jekyll generates a site, this folder is copied as is, and the system automatically creates links to the various files.
Finally, the _config.yml
file contains all the site configuration parameters, such as the site name and description, the links to the site’s logo and favicon, the required plugins, the pagination settings, and a list of files and service folders that Jekyll should ignore. The _config.yml
file can become quite long and complex, so for simplicity, here’s just a small portion of the default _config.yml
file for Mundana.
# Site
name: 'Mundana'
description: 'Mundana is a free Jekyll theme for awesome people like you, Medium like.'
logo: 'assets/images/logo.png'
favicon: 'assets/images/favicon.ico'
baseurl: '/mundana-theme-jekyll'
[...]
# Plugins
plugins:
- jekyll-feed
- jekyll-sitemap
- jekyll-paginate
- jekyll-seo-tag
[...]
# Paginate
paginate: 10
# Exclude metadata and development time dependencies (like Grunt plugins)
exclude: [README.markdown, package.json, grunt.js, Gruntfile.js, Gruntfile.coffee, node_modules]
Since we’re at it, let’s make our future work easier with a small modification to the _config.yml
file, replacing the line baseurl: '/mundana-theme-jekyll'
with baseurl: ''
.
Almost forgot: plugins (and their dependencies) are also Ruby gems and are not installed by default, so we need to install them. First run
$ rm Gemfile.lock
to prevent conflicts between plugin versions listed in the Gemfile.lock
file and newer versions that Ruby suggests to install. After that, run
$ bundle install
[...]
Fetching gem metadata from https://rubygems.org/............
Resolving dependencies...
Fetching jekyll-paginate 1.1.0
Installing jekyll-paginate 1.1.0
Fetching jekyll-sitemap 1.4.0
Installing jekyll-sitemap 1.4.0
Bundle complete! 4 Gemfile dependencies, 39 gems now installed.
Bundled gems are installed into `[...]/.gems`
that installs the actual plugins (and automatically regenerates the Gemfile.lock
file with the newly installed versions of the plugins).
Testing Jekyll again
All that is left now is to test Jekyll again, this time with the theme we just installed.
$ bundle exec jekyll serve --host=0.0.0.0
The site will be quickly regenerated and will be available on our real machine at the URL http://localhost:4000
. If we are using a virtual machine or a cloud server the port will still be :4000
, but the URL will depend on the configuration of the machine (for cloud servers, the URL usually has a label like reverse DNS name
or something similar).
Compared to the simple gem-based themes covered in the previous post, Mundana is clearly on a different level. At the top of the page are the four most recent posts, along with their images. In the center, a featured post is prominently highlighted against a contrasting background (for example, it could be the most recently published post). Below that, all other posts are arranged in reverse chronological order. On the side, a sidebar contains posts that we want to highlight.
If we replace Mundana’s default posts with our own, add the images, make two or three modifications to the _config.yml
file, and within minutes we could have a site ready to go online, with all the posts neatly organized into well-structured pages (as you can see here).
When the default theme is not enough
A well-designed theme is very convenient and, as we’ve just seen, allows you to get online quickly with minimal effort. However, it may not fully meet everyone’s needs as it is.
For example, I wanted to create a multilingual site in both Italian and English – something that Mundana does not support by default.
Additionally, I wanted the site’s Home
page to display a summary of the latest posts with thumbnail previews of the featured images, but didn’t want the thumbnails to appear in the individual post pages. Mundana handles the first requirement correctly, but not the second.
Another issue concerns pagination: the default pagination looks nice, but it displays a single horizontal row of links to all the generated pages. When there are too many pages, this row expands excessively, overlapping other graphical elements on the page or extending beyond the browser margins.
Lastly, Mundana natively supports the Disqus commenting system, but I preferred a self-hosted solution, to avoid reliance on third-party services that could change their terms of use or, worse, shut down unexpectedly.
Let’s get to work!
All the modified theme code is available on GitHub, at https://github.com/sabinomaggi/mundana-theme-jekyll-multilang.
Pagination
Let’s start with pagination. Since Jekyll version 3 (we are now at 4.4.1), pagination has been managed by the jekyll-paginate-v2 plugin. In Mundana, the pagination code is integrated into the index.html
file and is quite simple, which can cause issues when the number of pages grows.
I moved the pagination logic to _includes/custom/paginator.html
,3 making sure that the links only point to a subset of the available pages: those adjacent to the current page, the first and last pages, and an intermediate page calculated based on the distance of the current page from the first (or last) page. This approach allows pagination to work correctly even on very narrow windows or mobile devices.
There isn’t much to say about the code: starting from the current page, all other pagination elements are determined, dynamically generating the HTML code for each link. All the logic is written in Liquid
, the templating language used by Jekyll. Liquid’s syntax is a bit peculiar and requires some workarounds with variables, but the important thing is that it works.
Actually, the most interesting part of the code is the first line, which dynamically changes the text strings based on the active language. But that brings us to the next section.
Multilingual support
Jekyll can handle multilingual sites using the jekyll-polyglot plugin, but it does not interact well with the jekyll-paginate-v2
plugin,4 so I needed a different solution.
Polyglot is very easy to use: you just need to add the plugin under the relevant section in the _config.yml
configuration file and define two new variables for all the languages you want to support.
[...]
# Plugins
plugins:
[...]
- jekyll-polyglot
[...]
# Polyglot
languages: [en, it, de, fr]
default_lang: en
To set the language of a post, simply add the lang:
variable (e.g., lang: en
or lang: it
) to its front matter, and you’re done.
The front matter in Jekyll is a block of metadata in YAML format placed at the beginning of a Markdown (.md
) or HTML (.html
) file. It is enclosed by three dashes (---
) and provides information about the page or post, such as the title, language, layout, and settings for customizing the page behavior. For example:
---
title: "Welcome"
layout: default
permalink: /it/
lang: it
---
When Jekyll processes the site, it uses the front matter to:
- Define the template to use for rendering the page (
layout: default
). - Customize the URL associated with the page (
permalink: /it/
). - Set specific variables for that file, which can be used in layouts and themes (
lang: it
).
If a page or post includes the lang: it
variable in the front matter, Polyglot can use it to apply content or styles specific to the Italian version of the site.
My idea was to manually emulate the behavior of polyglot
. The first step was to define two new variables in _config.yml
, that are similar to those used by Polyglot:
#--- Custom localization variables ---
# Polyglot does not work well with Paginate-v2
# define these two variables instead
locales: [en, it]
default_locale: en
I also added a new variable to the front matter of each page or post, which, following the same pattern as _config.yml
, could be either locale: en
or locale: it
.
To simplify language management, I created two subfolders, en
and it
, inside the main _posts
directory to store posts in their respective languages. This way, it is very easy to add the correct locale
variable to each group of posts with sed
.
Lastly, I leveraged a Jekyll feature: when Jekyll finds a file with a language indicator in its name, it treats it as another version of the original file (without the indicator), inheriting all its content but using the specific variables defined in its front matter.
In other words, if I have an index.html
file that contains all the code for the Home
page of the site, and I create a new empty index_it.html
file, adding the locale: it
variable to its front matter, the index_it.html
file will inherit all the content from index.html
, but will use the locale
variable to localize the page for Italian. This mechanism is well documented in some pages I consulted, but unfortunately I can’t find the links anymore.
One last detail: when using pagination, the locale: it
variable must be defined inside the pagination:
variable in the front matter:
pagination:
enabled: true
locale: it
Therefore, all code that dynamically determines the language of a page must always check both the page.locale
and the page.pagination.locale
variables.
Of course, all text strings on the site must also be adapted to the selected language. To achieve this, there is a _data/translations.yml
file containing English and Italian translations for all the strings used.
Needless to say, setting up the localization mechanism correctly took the most time during the development of melabit.com
. However, the way it’s done allows to add a new language in just a few steps: add the new language code to the locales
variable in _config.yml
, add another folder inside _posts
, and include the language flag image in assets/images
. Almost makes me want to try it… 😂
Thumbnails and comments
After dealing with localization, removing the thumbnail from post pages was easy. I just had to find the relevant lines of code and comment them out. Problem solved.
Comments, on the other hand, were a completely different story, even more complicated than localization. But that will be the topic of the next post.
-
For example, while writing this post, I came across this theme, which doesn’t match my original idea for a site but might have worked just as well (but I’m not changing now). ↩
-
Jekyll is a static site generator, but even static sites today require a certain level of dynamism to handle things like search functionality, comments, and more. ↩
-
I placed all the new code inside the
custom
folder to physically separate it from the original theme files. ↩ -
Since several posts from 4-5 years ago report successfully using both
jekyll-polyglot
andjekyll-paginate-v2
, this may be a Mundana-specific issue. Alternatively, it could be an incompatibility that has emerged in the latest versions of Jekyll or of the two plugins. ↩