Logo
Logo

Programming by Stealth

A blog and podcast series by Bart Busschots & Allison Sheridan.

PBS 180 of X: Theming Jekyll (GitHub Pages)

24 May 2025

In the previous instalment, we learned the basics of how the Liquid templating language works within Jekyll. Because of our existing knowledge, we demonstrated how Liquid can be used within Markdown files in Jekyll sites, and while that is useful, it’s not actually Liquid’s most common use within Jekyll. So, what is the most common use for Liquid objects and tags in a Jekyll site? The definition of a Jekyll site’s theme, and that’s going to be the focus of this instalment.

Matching Podcast Episode

You can also Download the MP3

Read an unedited, auto-generated transcript with chapter marks: PBS_2025_05_24

Instalment Resources

What’s the Problem to be Solved?

The Markdown files that contain a Jekyll site’s contents only contain the contents. They don’t contain any of the other things you expect to see on a web page, like site-related branding, navigation bars, or footers. The obvious question is, how do those kinds of things get added? The answer is that the content in your files gets wrapped by pre-defined layouts (to use the correct Jekyll jargon), which are HTML files containing Liquid objects and tags.

Understanding Jekyll Theming

Jekyll theming is a rather loosely defined concept. There is no folder called _theme where the entire theme is contained. Instead, the HTML that gets wrapped around the Markdown is determined by two things:

  1. The layouts the site defines
  2. The assets referenced in the layouts
  3. The reusable snippets referenced in layouts

Note that we won’t be covering reusable snippets in this instalment; that will be the focus of our next instalment, so we’ll park those for now.

By assets, we mean the images, style sheets, and JavaScript files referenced in the HTML in the layouts. These assets are treated like regular files by Jekyll, so they don’t have an enforced location. However, there is a well-established convention of collecting the assets together into a top-level folder named assets, and that’s a convention we’ll be following in this series. For a more detailed discussion on static files and assets see the ‘Adding Static Files and Assets’ section in instalment 178.

When it comes to the layouts themselves, they are a new concept to us, and I like to think of them as being like Russian nesting dolls. The result of converting your Markdown to HTML is the smallest doll, and that doll gets wrapped with one or more layouts to build the final HTML web page saved to the output folder.

Note my use of the word wrap — this is the word the Jekyll documentation uses, and it’s the right way to think about how Jekyll sites get themed. Unlike assets, this is a very rigid rule.

We’re not going to to look into how this works until much later in the series, but it is important to mention that while Jekyll does not contain a site’s theme in any kind of single folder, it is possible to effectively import a theme from elsewhere into your site without actually copying any files into your site. This is done by specifying a theme location in _config.yml, but we’re going to ignore this option for now.

How Jekyll Layouts Work

Firstly, there is always a default layout, and if nothing tells Jekyll to use a different layout, then your page’s content will get wrapped by that default layout.

So where does the default layout come from? Jekyll looks for a file named default.html, and it tries the following locations, in the following order until it finds one:

  1. _layouts/default.html in your input folder
  2. _layouts/default.html in the theme specified in the site configuration, if there is one (remember, we’re ignoring this Jekyll feature for now)
  3. _layouts/default.html in the default theme Jekyll itself provides as a final fall-back to make sure even the most basic Jekyll site is at least functional!

Note: As of Jekyll 3.10.0 the default theme used to render sites that neither have their own default layout nor specify a theme in their config file is Minima version 2.5.1.

For now, we will be working entirely within our current site. We’ll define all our layouts, including our default layout, in the _layouts folder in our input folder (docs in our example site).

Layout files are just plain old HTML pages containing Liquid objects and liquid tags.

Since layouts are designed to wrap content, they use the Jekyll-specific Liquid variable content to insert the content they are wrapping. In other words, at the point in your layout that you want the HTML version of your Markdown to appear, you add the Liquid Object {{ content }}.

The simplest layout would simply be:

<html>
  <body>
    {{ content}}
  </body>
</html>

Specifying Layouts

Layouts are always specified with the basenames of the HTML files that define them, e.g., the layout front_page will always be defined by the file front_page.html. Jekyll decides on which front_page.html file to use based on the same rules described above for the default layout, that is to say:

  1. The site’s own _layouts folder (what we will be doing for now)
  2. The site’s theme, if one is specified (we are not specifying one at the moment)
  3. The default theme

There is no limit to how many layouts you can create, and each page can be rendered by any layout. Jekyll chooses the layout file to go looking for by checking for the following metadata elements in the following order:

  1. A front matter entry named layout
  2. A taxonomy-level default layout (we’ll be ignoring this feature for now since we’re only using the basic Pages taxonomy at the moment)
  3. The default layout (default.html)

As a practical example, if we assume we have created a special layout for the home page and saved it as _layouts/front_page.html, then we would specify its use by expanding the front matter in our index.md file to specify our layout like so:

---
title: Home
layout: front_page
---

Nesting Layouts

To avoid confusion as to how this works, I suggest avoiding thinking of layouts as inheriting from each other, and instead think of nested layouts — yes, back to our Russian Nesting Dolls metaphor.

When Jekyll builds the HTML for a page, it starts by converting the Markdown to HTML. Think of that initial HTML as the inner-most doll.

This initial HTML will always be wrapped by at least one layout. That first layout is the second doll in our analogy. Jekyll decides which layout to use as the initial layout with the following steps:

  1. Does the page specify a layout in its front matter? If so, use it.
  2. Does the site’s config file specify a layout for this page? (We’re going to ignore this feature for now, but we will circle back to it in a future instalment.) Again, if it does, use it.
  3. Use the default layout.

We now have our initial HTML wrapped with additional HTML from one layout. Often, this is the end of the process, we now have our finished HTML file.

But this is where Jekyll’s layout nesting feature comes into play. Every layout can use its front matter to instruct Jekyll to further wrap the output it produced with another layout.

So, if the first layout used specifies another layout, then a third bigger doll gets added, and if that layout also specifies another layout then a fourth doll gets added, and so on.

This ability for a layout to specify that its output should be wrapped by another layout leads to a recursive structure which some people find hard to wrap their heads around.

To try make things a little clearer, let’s look at a common example of how this nesting feature gets used in real sites.

In the previous section we imagined that we wanted to use a different layout for the home page to set it apart from the regular pages making up the site. We assumed there was a layout named front_page defined in our site, and we instructed the home page to use by adding some front matter to index.md.

In reality, it’s a near certainty that every single page on a site needs at least some common HTML code at the top and bottom of every page, usually for loading standard style sheets and JavaScript, and adding a some kind of standard container around every page’s content. Nesting gives us a nice easy way of avoiding duplicating the same code across multiple files.

For our custom home page example we would have a default theme that only provided the completely generic basics that will be present on every page. We then add only the additional things we want on the only on the home page to the front_page theme, and add front matter to the top of that layout to instruct Jekyll to further wrap the output with the default theme. To specify this nested structure we just need to add one line to the front matter in _layouts/front_page.html:

---
layout: default
---

This arrangement gives us three proverbial nested dolls for index.md:

  1. The inner most part of the final HTML file produced by Jekyll (index.html in the output folder) will be the Markdown in index.md converted to HTML.
  2. The initial HTML will be wrapped with a little more HTML by the front_page layout.
  3. This intermediate-stage HTML will be wrapped with yet more HTML by the default layout.

Liquid Variables within Layouts

Remember that Jekyll layouts are simply HTML files with Liquid objects and tags used to insert content and metadata. Where does Liquid get the content and the metadata? It gets it from Liquid variables, so we need to look at the Liquid variables Jekyll makes available while layouts are being applied.

As a quick reminder from the previous instalment, Jekyll uses dictionaries for just about all the variables it provides, and we included a high-level description of the most important of those variables in the section title ‘Jekyll’s Liquid Variables’.

Let’s have a closer look at those variables now.

Before we look at specific variables, it’s worth noting that the dictionaries all have something in common — they contain a combination of metadata gathered by Jekyll during the build process, and data specified in the front matter of various files.

Firstly, we have the site dictionary, which holds information related to the site as a whole. This variable is completely global within Jekyll, so it can be used in any context, including in layouts.

Because layouts are always invoked to convert a specific Markdown file to HTML, there is always the concept of a current page, so layouts also have access to the page dictionary.

Liquid tags and objects within layout files also have access to the layout dictionary which is only accessible in this context.

The Jekyll documentation on variables gives a full descriptions of all these variables, but here’s a quick summary of what they contain:

Some Important Liquid Filters when Designing Layouts

We met some Liquid filters in the previous instalment, and we did include links to the full list of available filters, but I want to highlight a few that are particularly important or useful when building layouts.

Filter Description
relative_url (important reminder) For a Jekyll site to work reliably both locally and when published to GitHub Pages, all internal URLs used to link to assets and site pages within layouts need to be passed through this filter!
uri_escape & cgi_escape This pair of related filters is vital for safely handling URL encoding of data coming from Liquid variables. uri_escape is used to sanitise entire URLs, while cgi_escape is used to sanitise individual values in query strings.
markdownify This tag is used to convert Markdown from a Liquid variable to HTML. The naming choice is potentially confusing, so to be clear, this filter converts Markdown → HTML!
normalize_whitespace This filter cleans up strings with strange spacing issues, possibly caused by concatenating lots of variables together, or from poorly formatted front matter. All consecutive white space characters get collapsed into a single space.
smartify When writing in Markdown, we generally use vanilla single and double quotes (non-directional ones), but there are separate glyphs in modern fonts for explicitly directional single and double quotes. Most content management systems automatically convert vanilla quotes in the content to so-called smart quotes on published pages. Jekyll will do this for the content in your Markdown files, but if you need this filter to apply the same functionality to strings coming from variables.
array_to_sentence_string When rendering lists in your layouts, you may want to convert them into human-friendly sentences by adding commas between all but the last two pairs, and a different word or symbol between those last two. That’s what this filter does, defaulting to and, with support for an optional argument to specify another word like or.
sort This filter sorts a list alphabetically, or, if it’s a list of dictionaries, by the key or keys specified with the named argument :sort.
sample Something many content management systems do is highlight a randomly chosen piece of content somewhere, usually the front page. The sample filter takes a list as its input and outputs a single randomly chosen element. The filter can support returning more than one random item by adding a number as the optional argument.
inspect This filter is useful for debugging, it converts any variable into a string so you can see what it contains.

These are all Jekyll-specific Liquid filters, so you’ll find their full documentation (though it is a little terse) on the Jekyll site.

Worked Example 1 — Explaining our Current Bootstrap 5 Default Layout

From the start of our exploration of Jekyll, we’ve been using a very basic layout that loads Bootstrap 5, wraps our content in some appropriate HTML 5 tags, and inserts a header at the top of all pages. Up until this point, I’ve asked you to indulge me by simply accepting the content of _layouts/default.html as magic sauce on the promise that we would explain how it actually works later. Well, now we’ve learned enough to do just that, so let’s get stuck in 🙂

If you’d like to play along, our starting point will be our demo site as we left it at the end of the previous instalment. You can download a snapshot of the site at the appropriate commit here, or you can clone the repo and checkout the commit with the tag pbs180-startingPoint.

Because I wanted to start with as simple a demo site as possible, the site only has one layout at the moment — the default layout — which is defined in _layouts/default.html in our site’s input folder, i.e. docs. Here’s the content of that file as it stands at this point in the series:

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ page.title }} | {{ site.title }}</title>
  <link rel="stylesheet" href="{{ '/assets/css/style.css' | relative_url }}">
</head>
<body>
<main class="container pt-3">
    {{ content }}
</main>
<script src="{{ '/assets/js/bootstrap.bundle.min.js' | relative_url }}"></script>
</body>
</html>

For the most part, you’ll recognise this as the standard HTML 5 page structure as recommended in the Bootstrap documentation, but it contains five Liquid objects.

The first two set the window/tab title for pages to the page’s title as read from the page.title Liquid variable followed by a pipe, followed by the site’s title as read from the site.title variable.

Note that page.title is loaded from the front matter in the current Markdown file. If you look at the front matter in, say, about.md you’ll see it says:

---
title: About
---

The rule is that all page metadata provided by Jekyll and all front matter defined at the top of the page are made available via the page dictionary, so title in a page’s front matter become page.title in Liquid.

Similarly, the values defined in _config.yml are merged with the site-level metadata provided by Jekyll into the site variable, so the following line from the top of _config.yml defines site.title:

title: PBS Jekyll Demo Site

The third Liquid object inserts the Bootstrap 5 style sheet converted to standard CSS from Bootstrap’s SASS source code by Jekyll. I want to remind you of two things; firstly, the source file for /assets/css/style.css is /assets/css/style.scss. When Jekyll converts the file from SASS to CSS, it also changes the file extension. And secondly, this is a relative URL, so we must filter it with the relative_url filter.

The third Liquid object inserts the special variable content — this variable holds the page’s content converted from Markdown to HTML by Jekyll. This is where we inject the content to be wrapped by the layout.

Finally, we use another Liquid object with the relative_url filter to include the Bootstrap 5 JavaScript.

Our entire demo site theme currently consists of just:

  1. The Bootstrap SASS source code
  2. The bundled Bootstrap JavaScript code
  3. A default layout consisting of just 15 lines of HTML code with just five Liquid objects making use of just three Liquid variables and one Liquid filter.

There’s not even a single Liquid tag used yet!

Worked Example 2 — Creating a Custom Layout for our Front Page

To demonstrate how layouts are nested, let’s update our demo site to make the front page a little different from the others. We still want the front page to be Bootstrap 5, etc., so we don’t want to lose anything provided by the default layout. But we want to move some of our hard-coded content from the Markdown into a custom layout. The only thing that should remain in the Markdown is the actual content for the front page. Specifically, we want our custom layout to handle two things:

  1. The logo at the top should become a theme asset and be added automatically by the layout
  2. The list of pages should become a site map and also be added automatically by the layout

We want to wrap our home page content between two things provided by the custom layout, which we want wrapped by the default layout.

As a first step, let’s clean index.md and remove everything but the content, so you’ll just be left with a very simple file:

---
title: Home
---
This is a [Jekyll](https://jekyllrb.com)-powered website designed to be tested locally and deployed on [GitHub Pages](https://pages.github.com).

It serves as the basis for the examples in the [Programming by Stealth](https://pbs.bartificer.net) mini-series on using Jekyll as a Content Management System compatible with GitHub Pages.

Next, let’s follow our convention and move the now theme-related jumbotron image from the illustrations folder to the assets folder. We want it to become /docs/assets/siteIllustration.png within the repo as a whole.

If we build the site locally at this point, we’ll now see that our front page has lost its banner and the list of pages, and only contains the content now left in index.md.

The next step is to create our custom layout in the _layouts folder. We’ll call this layout front_page, so the file to create is _layouts/front_page.html, and it should have the following content:

---
layout: default
---
<img src="{{ 'assets/siteIllustration.png' | relative_url }}" alt="An Illustration showing the PBS logo over the GitHub and Jekyll Logos with a plus between them" class="img-fluid">

{{ content}}

<h2>Site Map</h2>
<ul>
    {%- assign list_pages = site.html_pages | where_exp: "item", "item.title != 'Home'" %}
    {%- for p in list_pages %}
    <li><a href="{{ p.url | relative_url }}">{{ p.title }}</a></li>
    {%- else %}
    <li class="text-muted">No pages yet</li>
    {%- endfor %}
</ul>

You’ll find a copy of this file in the resources ZIP as layouts_front_page.html (remember to rename it!).

Let’s have a look at the contents of this file to understand what it does.

Firstly, notice that we specify the fact that we want this layout to get wrapped by the default layout by specifying the default layout in the front matter:

---
layout: default
---

Next, note that we are working in HTML here, not Markdown!

When the image is inserted we use the relative_url filter to sanitise a string literal rather than the content of a variable ('assets/siteIllustration.png' | relative_url).

We use the special content variable to specify the insertion point for the converted Markdown.

Next, we use the same Liquid looping construct that was originally in index.md, but now we’re looping over an HTML unordered list rather than a Markdown list, and we’re adding HTML <a> tags rather than Markdown links.

Again, note the all-important use of the relative_url filter!

With that our layout is ready to be applied to our home page. We do that by adding a layout definition to the front matter in index.md:

---
title: Home
layout: front_page
---

The entire index.md file is now just 7 lines! Also, notice it’s now pure Markdown and YAML, with no weird Liquid syntax cluttering our content anymore.

---
title: Home
layout: front_page
---
This is a [Jekyll](https://jekyllrb.com)-powered website designed to be tested locally and deployed on [GitHub Pages](https://pages.github.com).

It serves as the basis for the examples in the [Programming by Stealth](https://pbs.bartificer.net) mini-series on using Jekyll as a Content Management System compatible with GitHub Pages.

For your convenience, you’ll find this version of the file in the instalment ZIP.

If you rebuild your site, you’ll see we’ve succeeded in our task 🎉

A challenge — Add a Navigation Bar to the Default Theme

We’ve now learned enough Jekyll to be able to resume our tradition of ending each instalment with a challenge. In this case, the intention is to both put what we’ve learned so far about Jekyll into practice, but also to blow the cobwebs off our Bootstrap 5 skills if they’ve gone unused for a while.

Using the code for our sample site as it stands at the end of the worked example above as your starting point, update the default layout to add a Bootstrap 5 nav bar to the top of every page that gives quick access to all the site’s pages.

When it’s correctly implemented, the nav bar should appear on all pages, both regular ones which use the default layout like /about, and the front page which now uses a dedicated layout.

Hint: You’ll need a Liquid loop similar to the one used to print the list of pages in the layout for the front page.

For your convenience, you can quickly access the starting point for this challenge by downloading a snapshot of the demo site at the appropriate commit here, or you can clone the repo and checkout the commit with the tag pbs180-challenge-startingPoint.

Final Thoughts

Learning how to nest layouts is very powerful, and simply understanding that principle opens many doors, enabling sufficient theming for basic sites. However, to really get the most out of Jekyll, we need to learn a related concept — reusable HTML snippets that can be added to multiple themes. We can even put them into our content to facilitate features that Markdown can’t handle natively, like the use of <figure> tags to add images with captions. We’ll dedicate the next instalment to learning about the Jekyll includes, which are implemented using the include Liquid tag, often with the help of another powerful Liquid tag, capture.

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack