This is a sort of meta-post describing how I set up this blog–with some Fedora Desktop specific advice.

TL;DR

~ $ sudo dnf install rubygem-bundler rubygem-jekyll
~ $ bundle config set path vendor/bundle
~ $ jekyll new my-site
~ $ cd my-site
~/my-site $ cat <<EOF > Gemfile
~/my-site > source "https://rubygems.org"
~/my-site > gem "github-pages", group: :jekyll_plugins
~/my-site > EOF
~/my-site $ bundle install
~/my-site $ bundle exec jekyll serve
# => Now browse to http://localhost:4000

The Full Story

I first stumbled on GitHub Pages and Jekyll while researching Asciidoctor and was intrigued. It seemed to occupy a sweet spot in the world of technical publishing:

  • A low bar of entry with free hosting via your existing GitHub account and automatic conversion of (the well known) GitHub Flavoured Markdown.

  • Being built around Git, content could be developed, reviewed, and deployed just as you would normal ‘code’. (No composing in clunky web editors or cut and paste into web forms).

  • A flexible, low-friction development environment, with Jekyll and Liquid providing attractive and flexible layout and themes, plus plugins for extra features and few constraints for advanced users.

This looked like much more fun than the Movable Type based blogs.perl.org I had previously used, and that rekindled my desire to write a technical blog again.

Learning about GitHub Pages

The starting point for this adventure was, well, a page that no longer exists. To be honest, I first started looking at this four months ago, but I got distracted by “life” for a while. By the time I had got back to it, things had changed significantly. Fortunately, these changes were for the better. The GitHub Pages front page now gives you a nice video introduction and interactive set up details, plus some links to various Working with GitHub Pages guides.

I quickly discovered that would have to choose a “site type” before I could get much further.

There are three types of GitHub Pages sites: project, user, and organisation. Project sites are connected to a specific project hosted on GitHub, such as a JavaScript library or a recipe collection. User and organisation sites are connected to a specific GitHub account.

This distinction is repeated constantly throughout the documentation, so once I figured out which one I was using I could skip over a lot of irrelevant instructions. For example, any references to a gh-pages branch only apply to “project” site types.

I was planning on building a site to host a professional blog, ostensibly for gratuitous self-promotion. This was not going to be tied to any particular project or organisation, so the “user” site type is definitely what I wanted.

The next step was to set up a “publishing source”. For my “user” site I simply needed to:

  • Create a GitHub repository called “tartansandal.github.io”
  • Browse to the “Settings” page and find the “GitHub Pages” section
  • Select “master branch” from the “Source” drop-down menu

Now any content that I commit to the master branch of that repository would be automatically published to https://tartansandal.github.io. For example, I could add a simple index.html file containing:

<html>
  <body>
    <h1>Hello World</h1>
  </body>
</html>

Then browsing to https://tartansandal.github.io would display “Hello World” in large bold text.

That was pretty easy, but it gets better. If I added a _config.yml file containing:

markdown: kramdown

GitHub would then automatically convert any Markdown files to appropriately formatted HTML. I could replace the index.html file with index.md file containing:

# Hello World

And get the same effect. Nice!

Now I just needed to learn how to use Jekyll to layout a technical blog.

Learning about Jekyll

GitHub Pages uses Jekyll as its static site generator and has extensive set up and usage documentation. After scanning that, I decided on running through the official Jekyll Docs, to get a better handle on its requirements and capabilities.

One key point—somewhat obscured by the above guides—was that pushing content to the master branch of my repository would cause the public website to be updated (almost) immediately. If I wanted to (privately) review any new content, major edits, or layout changes, I would have to install and run Jekyll locally. This was something I was definitely going to want to do.

Installing Jekyll on Fedora 31

Unfortunately the GitHub Guide and the Jekyll Guide did not strictly agree on how to install Jekyll, and some advice relating to Ruby, RubyGems, and Bundler did not seem appropriate for Fedora.

Some research and experimentation were required to find the best path forward.

Local RubyGems

The Jekyll Guide suggests you first adjust your environment so that running the gem command as a normal user operates on user files rather than the default system files. In particular,

  • Gems are installed under ~/.gems
  • Executable files are installed under ~/.gems/bin.

This is probably appropriate for Ubuntu and many other Linux systems.

However, Fedora has modified RubyGems (see operating_system.rb) so that the gem command automatically performs a “user install” if run by a normal user. In particular,

  • Gems are installed under ~/.gem/ruby
  • Executable files are installed under ~/bin

This make the gem command “just work” without the user having to change their environment, but it uses different paths to those suggested by the Jekyll Guide.

I’m running Fedora and already have some “user installed” ruby gems, so if I blindly follow the Jekyll Guide, those gems are going to break. Not good.

Using Bundler

Managing local gem installations is a well known path to Dependency Hell and one I’d rather avoid.

Both the GitHub Guide and the Jekyll Guide suggest using Bundler to manage your sites dependencies. Bundler allows you to safely install multiple, potentially conflicting, versions of gems, by using a special command wrapper to ensure that only the correct versions are used by your project. In short:

  • A Gemfile is used to specify the required gems and perhaps their minimal versions.

  • The bundle install command installs those gems, plus any dependencies, whilst automatically recording the required versions in Gemfile.lock.

  • The bundle exec command is used to run any programs under the version constraints specified in Gemfile.lock.

Unfortunately this does not work well with the Fedora modifications to RubyGems mentioned above. We can get mutually compatible behaviour, however, by adding the following to our bash configuration:

export GEM_HOME=$HOME/.gem/ruby
if [[ ! -e $HOME/.gem/ruby/bin ]]
then
    ln -s $HOME/bin $HOME/.gem/ruby/
fi

Bundler also has the option of installing gems inside your project directory, just like Python’s vitualenv or Node’s node_modules. You can make this the default behaviour with the following:

bundle config set path "vendor/bundle"

One advantage of localising installations this way is that you don’t end up polluting your environment with executables from random dependencies. These could shadow system executables and cause unexpected problems. This does cost some disk space (gems are not shared between projects) and some time (duplicated gem downloads), but for me, the effect was small.

Different installation methods

The Jekyll Guide suggests installing some system dependencies:

sudo dnf install ruby ruby-devel @development-tools

Then installing both Bundler and Jekyll as a normal user:

gem install bundler jekyll

Using the configuration tweaks mentioned above, this is safe and straight-forward.

On the other hand the GitHub Guide suggests using Bundler to install Jekyll in order to ensure the version is compatible with the one used by GitHub. The details of how exactly to do this were not explained though and, at this point, the exposition starts to get a bit muddy.

The key to unravelling all this is that GitHub maintains and provides a special github-pages gem:

A simple Ruby Gem to bootstrap dependencies for setting up and maintaining a local Jekyll environment in sync with GitHub Pages

So if we have Bundler installed and a Gemfile containing:

gem 'github-pages', group: :jekyll_plugins

We can install Jekyll and its dependencies at the correct versions:

bundle install

We can keep this in sync with the current GitHub Pages by regularly running:

bundle update

And we can generate and serve our content locally:

bundle exec jekyll serve

The final wrinkle is the following useful jekyll sub-command:

jekyll new PATH

This generates a simple skeleton that can be used as a starting point for your site:

$ tree -a
.
├── 404.html
├── about.md
├── _config.yml
├── Gemfile
├── .gitignore
├── index.md
└── _posts
    └── 2020-01-20-welcome-to-jekyll.markdown

Note that the Gemfile even contains (commented out) instructions for github-pages gem. Using this requires having Jekyll installed first though.

The key take away from all this is that the version of Jekyll used by our project can (and probably will) be different from the version installed using the gem command. If this is the case, then we could just install the versions of Bundler and Jekyll that come with the Fedora distribution:

sudo dnf install rubygems-bundler rubygems-jekyll

This would automatically take care of any dependencies, and we wouldn’t have to worry about managing updates. The Fedora versions are relatively modern and (after reading the respective Changelogs) seem to be appropriately stable.

Bootstrapping GitHub Pages

At this point I felt I had done enough research and experimentation to formulate a reasonable plan.

First, I cloned the GitHub Pages repository I set up earlier in Learning about GitHub Pages:

git clone git@github.com:tartansandal/tartansandal.github.io.git
cd tartansandal.github.io

Next, I installed the Fedora packages:

sudo dnf install rubygem-bundler rubygem-jekyll

This gave me access to reasonably up-to-date versions of the bundle and jekyll commands.

Next, I configured Bundler to always install gems into a local vendor/bundle directory so any projects would be self-contained:

bundle config set path "vendor/bundle"

I used the path vendor/bundle here to be consistent with standard bundle deployments and because, that path is ignored by default when Jekyll processes files. Note: this was a global setting and would affect all future uses of the bundle command.

I could now generate some basic scaffolding:

jekyll new .

The generated Gemfile contained a lot of comments and Windows specific tweaks, so I stripped that back to:

source "https://rubygems.org"
gem "github-pages", group: :jekyll_plugins

I could now safely install all the required gems:

bundle install

And verify that they were all installed under vendor/bundle/ruby. I didn’t want those files tracked by Git, so I added an appropriate line to the .gitignore file.

Generating and serving the default content was straight-forward:

bundle exec jekyll serve

And I could ensure development environment was in sync with GitHub Pages by running:

bundle update

Now I was ready to start working through the Jekyll Docs and developing content for my blog.

Blogging with Jekyll

The Jekyll Docs walked me through the details of building a Jekyll based site from scratch. This was not strictly necessary, but gave me a bit more insight into what was going on.

Processing files

The basic operation of Jekyll is to recursively process all the files in the project and place the results in the _site/ directory:

  • Files or directories beginning with a . and _ are excluded from direct processing. Additional exclusions, and exceptions to those exclusions, are defined in the project’s exclude, include, and keep_files lists.

  • HTML, Markdown, and SCCS files containing YAML front matter are processed using the Liquid templating engine. This provides additional markup for the interpolation of “objects”, “tags”, and “filters”, as well as flow control constructs.

  • If a layout name is set in a file’s front matter, then the corresponding layout template is used to wrap the file’s content during processing.

  • The content of Markdown files are converted to HTML (using kramdown) and the suffix of the resulting file is changed to .html

  • The content of SCCS files are converted to CSS (using sassc) and the suffix of the resulting file is changed to .css

  • All other files and directories are copied over verbatim.

Many aspects of this process can be modified via the project’s jekyll configuration file _config.yml.

Files under directories beginning with _ provide additional “data” for use during processing. For example,

  • _include/ provides content fragments that can included in other files.

  • _layout/ provides layout templates (which make extensive use of content fragments).

  • _sass/ provides style definitions for converting SCCS files.

These three _ directories, plus an additional assets directory, form the project’s “theme”.

Themes generally make heavy use of Jekyll Variables via Liquid markup. For example:

  • the site variable for site wide information configurations settings.
  • the page variable for page specific information and variables set in a page’s front matter.
  • the content variable for the rendered content being wrapped by a layout.

While themes can be defined locally, they are more often packaged and imported as gems. In this case, files from the theme gem are (right) merged with files from the project, before the combined set of files are processed. This allows you to selectively override parts of a theme adding files with the same relative path to your project–the project files shadow the gem files. This helps to keep your project uncluttered and makes it easy to use sophisticated 3rd party themes. If the theme has been published on RubyGems, then you can simply add it to your Gemfile, set the corresponding theme name in _config.yml, then all those files in that gem will become part of your next build.

The default theme

The default gem-based theme used by Jekyll is “minima”:

$ tree vendor/bundle/ruby/2.6.0/gems/minima-2.5.1/
vendor/bundle/ruby/2.6.0/gems/minima-2.5.1/
├── assets
│   ├── main.scss
│   └── minima-social-icons.svg
├── _includes
│   ├── disqus_comments.html
│   ├── footer.html
│   ├── google-analytics.html
│   ├── header.html
│   ├── head.html
│   ├── icon-github.html
│   ├── icon-github.svg
│   ├── icon-twitter.html
│   ├── icon-twitter.svg
│   └── social.html
├── _layouts
│   ├── default.html
│   ├── home.html
│   ├── page.html
│   └── post.html
├── LICENSE.txt
├── README.md
└── _sass
    ├── minima
    │   ├── _base.scss
    │   ├── _layout.scss
    │   └── _syntax-highlighting.scss
    └── minima.scss

These are merged with the scaffolding files generated by the jekyll new command:

$ tree
.
├── 404.html
├── about.md
├── _config.yml
├── Gemfile
├── index.md
└── _posts
    └── 2020-01-20-welcome-to-jekyll.markdown

The combined set of files are processed as follows:

  • assets/main.scss is compiled using the “minima” style defined in the themes _sass directory. This produces the _site/assets/main.css file which is referenced in the head.html fragment.

  • 404.html uses the “default” layout (which includes the head.html, header.html and footer.html fragments) to produce a well-formed HTML page at _site/404.html

  • All the other layouts inherit the “default” layout to produce a consistent well-formed HTML pages.

  • All Markdown files are compiled into well-formed HTML fragments before being wrapped in their respective layouts.

  • about.md uses the “page” layout and sets the permalink property. The latter changes the destination file to _site/about/index.html so it can be linked to with the path /about/.

  • index.md uses the “home” layout and the content of the _post directory (via a site.posts object) to form a listing of links to posts. This produces _site/index.html.

  • 2020-01-20-welcome-to-jekyll.markdown uses the “post” layout and variables set in the front matter (via the page object) to produce /_site/jekyll/update/2020/01/20/welcome-to-jekyll.html.

This can be personalised by setting the configuration variables title, author, email, description, and github_username, and by replacing the content of the about.md file.

The result is a simple, attractive, and functional blogging site–all from simple jekyll new command.

Posts and drafts

While Jekyll is great for generating many types of static sites, the default theme and scaffolding are focused on producing blogs. This was what I was really after.

The section above introduced the _posts data directory which is handled specially. Files in this directory have a special naming convention:

YEAR-MONTH-DAY-title.MARKUP

The YEAR-MONTH-DAY prefix corresponds to the intended publication date. Posts are sorted by this date and allows for authoring posts with a planned future publication date.

The title is going to be a slugified version of the post’s title which we set in the pages front matter.

I plan to use Markdown for most of my posts, so I’ll use the md suffix for MARKUP. However, I may write some posts in Asciidoctor and manually convert them to HTML, so the html suffix is also a possibility.

Individual post files require some minimal front matter:

---
layout: posts
title: "Welcome to Jekyll!"
categories: jekyll update
---

The categories setting allows us to group related posts. In particular, it changes the path part of the post’s URL, for example, in the above the path is changed to /jekyll/update/2020/01/20/welcome-to-jekyll.html. This can be useful for SEO, but does can make linking between posts more complicated. The trick is to use the post_url tag to generate the correct path:

{% post_url 2020-01-20-welcome-to-jekyll %}

What about drafts?

Most of my posts are going to take a while to polish and I expect be working on more than one post at a time. In this case I can use a _drafts/ directory to store draft posts. Drafts are not visible in production and are only shown locally if you pass the --drafts option:

bundle exec jekyll serve --drafts

The naming convention is simpler as well:

title.MARKUP

The post.date is automatically set to when the file was last changed on the file. Once I’m happy with the post, I can move it to and appropriately named file under _posts.

Wrapping things up

I was pretty happy with the default minima theme and it was simple enough to edit the _config.yml file to change the blog title, description, and various user details. I also made some minor customisation:

  • Changing the date format to Australian standard:

    minima:
      date_format: "%-d %B %Y"
    
  • Enabling display of post “excerpts” in the main listing:

    show_excerpts: true
    
  • Setting up a Disqus account:

    disqus:
      shortname: tartansandal
    
  • Setting the sites URL so Disqus would work:

    url: "https://tartansandal.github.io"
    
  • Using https://realfavicongenerator.net/ to build my own favicons.

Updating the about.md content initially got out of hand, but I eventually decided to cut it back to something simple.

Finally, I wrote this story and used it to replace the introductory ‘welcome-to-jekyll’ post.

Now it was time to deploy.

I had already been using git to track the development of this project and had merged all my changes into the master branch. All I had to do was attach the project to the tartansandal.github.io repository and push my changes:

git add remote origin git@github.com:tartansandal/tartansandal.github.io.git
git push --force

Conclusions

Setting up this blog has been fun and relatively easy. I managed to achieve excellent results with minimum effort. There were some Fedora specific wrinkles with setting up Jekyll, but I managed to find appropriate tweaks to the Quick-start instructions.

The principle GitHub Pages documentation had improved significantly, but there are still some issues. The main contents page suggested a reasonable narrative, however, the individual articles lacked the navigation links to make that narrative easy to follow. There were many internal links pointing to similar looking pages and I had to keep going back to the contents page to make sure I hadn’t missed anything. This may all be due to an accumulation of features over time (GitHub Pages is not new) and I found some older guides to be more straight-forward.

The Jekyll documentation was especially good–simple and direct–although some of the more useful pages were a little hard to track down.

I hope you have found this post useful. Any mistakes or omission are my own. Feel free to let me know about them in the comments.

Guides

Some other useful guides I found a long the way: