This is mostly here as a reference to remind myself of how this works on the rare occassions when I need to work on some Liquid templates and need to re-learn this. But it might be useful to someone else as well.

Liquid is a template language that was first developed for and is still used by Shopify, but it’s now open source and also used by Jekyll and many other projects. You can use Liquid templating in your Jekyll site’s layouts and includes, but you can also use it directly in posts or pages, which can be pretty handy (for example it’s used loads in the source code for this page).

Liquid is a bit of a weird template language. At least it is if you have a similar background to me: used to Python-world templating languages like Jinja2. As a result if you try to just use Liquid without taking the time out to properly learn it first, you’ll be frustrated.

The good news is that Liquid is a pretty small language, and more-or-less well-documented, so it is thoroughly learnable in an afternoon.

Weird Things about Liquid

Here’s a few of the unexpected things that threw me, before I took the time to learn Liquid properly:

  • Shopify’s Liquid docs contain a lot of Shopify-only stuff, and those are what often comes up in search results, which is confusing. To learn how to use Liquid effectively with GitHub Pages / Jekyll you need to read the Liquid docs, Jekyll’s docs and Liquid for Designers.

  • You can’t initialize your own arrays or hashes directly.

    You can create new variables in Liquid templates and assign string, number, boolean or nil values to them. For example: {% assign my_string = "foo" %}. But there are no array or hash literals and no way to initialize a new array or hash.

    There are oblique workarounds like splitting a string to create a new array of strings, or using various array processing filters to create new arrays, or using Jekyll’s front matter or data files to create your own arrays or hashes.

  • There’s no “not” (or !) operator. You do have unless (which is the opposite of if) and else, and !=, but there’s no not / !.

  • Truthiness doesn’t work how you expect it to: only false and nil are falsy, everything else is truthy including empty strings, empty arrays, and 0. If your if-statement doesn’t seem to be working this is probably why.

  • The order of operations in compound conditionals is fixed and you can’t use ( ... )’s to control it. (...)’s aren’t allowed in conditional expressions.
    (You can choose to use nested ifs instead of compound ifs, though).

  • An explanation of hashes and how to use them is mysteriously missing from the documentation, which doesn’t even list hashes as one of the available types. See my section on hashes instead.

  • Jekyll’s site.categories and site.tags are a bit difficult to work with.

Objects, Tags and Filters

There are only three types of thing in a Liquid template: objects (variables that have types), tags (control flow, iteration, and variable assignment), and filters (functions that return new objects).

1. Objects, a.k.a Variables: {{ ... }}

These are references to variables, or sub-parts of variables in the case of hash or array access, that print out the referenced variable’s value.

All object references print something out into the rendered page, unless the variable’s value is nil or the empty string.

For example given a variable foo whose value is "bar" (a string), this:

<p>{{ foo }}</p>

will output this:

<p>bar</p>

Each variable has a type. There are 6 types in Liquid:

  1. Strings: "..." or '...' (there’s no difference between double and single quotes)
  2. Numbers: 42 or 25.672
  3. Booleans: true and false
  4. nil
  5. Arrays: lists of objects. The special object empty is shorthand for an empty array. For example {% if my_array == empty %} is equivalent to {% if my_array.size == 0 %}
  6. Hashes: objects with multiple fields

2. Tags: {% ... %}

Tags implement control flow, iteration and variable assignments, among other things.

For example, {% if %} is one of the control flow tags. This if-statement will print out the value of the variable user if a variable named user exists and its value isn’t false or nil:

{% if user -%}
  The user is: {{ user }}
{%- else -%}
  There is no user.
{%- endif %}
Whitespace Control with {%- and -%}

The - characters in {%- and -%} are whitespace control. A {%- strips any leading whitespace from before the tag’s contents, in the rendered output. It strips spaces, tabs and newlines. -%} strips any trailing whitespace after the tag’s contents. They remove empty lines where the tags were from the rendered output. They can also remove the indentation (leading spaces) when the contents of tags are indented in the source file, but this only works when the tag only contains a single line, indentation from subsequent indented lines doesn’t seem to be stripped.

When there’s no user variable, or user’s value is false or nil, the {% if %} tag above outputs:

There is no user.

When there is a user variable whose value is "fred" it outputs:

The user is: fred

3. Filters: |

Filters are functions that return new objects based on a given object. For example: doing string manipulations and returning new strings; doing array operations and returning new arrays; doing math and returning new numbers; formatting dates; and more. Filters are pure functions that always just compute and return a new object, they never modify a given object and never have any side effects. (But you can use {% assign %} to assign the new object returned by a filter to an existing variable name.)

upcase is a simple string manipulation filter that returns an ALL CAPS copy of a given string. This:

{{ "apple" | upcase }}

Outputs:

APPLE

append is another string manipulation filter that takes an additional parameter: the suffix string to append to the first string. This:

The file is: {{ "index" | append: ".html" }}

Outputs:

The file is: index.html

Filters can take multiple positional parameters, but there are no named parameters:

{{ "A long string to be truncated" | truncate: 24, ", and so on" }}

Outputs:

A long string, and so on

Filter parameters can be optional. For example truncate’s second argument defaults to an ellipsis if omitted:

{{ "A long string to be truncated" | truncate: 24 }}

Outputs:

A long string to be t...

Finally, multiple filters can be chained together in a sequence like UNIX pipes:

{{ "adam!" | capitalize | prepend: "Hello " }}

Strings

String literals can use either 'single quotes' or "double quotes", there’s no difference.

There are lots of string manipulation filters:

See also: The complete list of all filters in the Liquid docs.

<STRING> | capitalize
a copy of a string with only the first character capitalized
<STRING> | downcase
an all-lowercase copy of a string
<STRING> | upcase
a copy of a string in ALL CAPS
<STRING> | lstrip
a copy of a string with leading whitespace removed
<STRING> | rstrip
a copy of a string with trailing whitespace removed
<STRING> | strip
a copy of a string with leading and trailing whitespace removed
<STRING> | strip_html
a copy of a string with HTML tags removed
<STRING> | strip_newlines
a copy of a string with line breaks removed
<STRING> | remove: "rain"
a copy of string with all occurrences of a substring removed
<STRING> | remove_first: "rain"
a copy of a string with the first occurrence of a substring removed
<STRING> | replace: "my", "your"
a copy of a string with all occurrences of the first argument replaced with the second argument
<STRING> | replace_first: "my", "your"
a copy of a string with the first occurrence of the first argument replaced with the second argument
<STRING> | newline_to_br
a copy of a string with all the \n’s replaced with <br />’s
<STRING> | escape
a URL-escaped copy of a string
<STRING> | escape_once
like escape but avoids double-escaping
<STRING> | url_encode
a copy of a string with any URL-unsafe characters percent-encoded
<STRING> | url_decode
a copy of a string with any percent-encoded characters decoded
<STRING> | append: ".html"
a new string by concatenating two strings
<STRING> | prepend: "prefix"
a copy of a string with a prefix string prepended onto the front
<STRING> | size
the number of characters in a string. Also works with arrays
<STRING> | slice: 0
<STRING> | slice: 2, 5
<STRING> | slice: -3, 2
a substring of a string
<STRING> | truncate: 25
<STRING> | truncate: 25, ", and so on"
a copy of a string truncated to the given number of characters, with an ellipsis appended if any characters had to be removed from the end. If the second argument is given it’s used instead of an ellipsis. Use "" for the second argument if you just don’t want any ellipsis.
<STRING> | truncatewords: 3
<STRING> | truncatewords: 3, "--"
a copy of a string truncated to the given number of words.
<STRING> | split: ", "
an array of substrings by splitting a string on the given substring. You can also join an array of substrings into a single string with the |join filter, which works on arrays

There’s no filter for reversing a string, but you can do it by first splitting it into a character array, then reversing the array, then joining the array back into a string: {{ <STRING> | split: "" | reverse | join: "" }}

<TIMESTAMP_STRING> | date: "<STRFTIME_FORMAT>"
a copy of a timestamp string, formatted according to the given strftime format string. Jekyll also adds a lot more date formatting filters

Numbers

Number literals can be either integers: 25, or floats: 39.756.

There are lots of filters for manipulating numbers:

See also: The complete list of all filters in the Liquid docs.

<NUMBER> | abs
the absolute value of a number
<NUMBER> | round
<NUMBER> | round: 2
a copy of a number, rounded to the nearest integer or to a given number of decimal places
<NUMBER> | at_least: 5
a copy of a number, clamped to a given minimum value
<NUMBER> | at_most: 5
a copy of a number, clamped to a given maximum value
<NUMBER> | floor
a copy of a number, rounded down to the nearest whole number
<NUMBER> | ceil
a copy of a number, rounded up to the nearest whole number
<NUMBER> | plus: 2
a copy of a number by adding a given number to it
<NUMBER> | minus: 2
a copy of a number by subtracting a given number from it
<NUMBER> | times: 2
a copy of a number by multiplying it by a given number
<NUMBER> | divided_by: 4
a copy of a number by dividing it by a given number
<NUMBER> | modulo: 2
a new number by taking the remainder from dividing a number by a given number

Arrays

Jekyll passes various arrays to your templates as variables, for example:

  • site.pages is an array of all your site’s pages
  • site.posts is an array of all your site’s posts in reverse chronoligical order
  • page.categories is a list of all this page’s categories, and page.tags is a list of all this page’s tags

Individual items in an array can be accessed with []’s, for example:

  • {{ site.posts[0] }} is the newest post
  • {{ site.posts[-1] }} is the oldest post

Arrays also have convenience attributes first and last, for example you could print the titles of the site’s first and last posts like this:

  • {{ site.posts.first.title }} is the newest post
  • {{ site.posts.last.title }} is the oldest post

You can loop over an array with a for loop:

{% for post in site.posts %}
  {{ post.title }}
{% endfor %}

Each array has a .size field that’s the number of items in the array. empty is a special object that’s equal to an empty array. For example this:

{% if site.pages == empty %}
  This site has no pages.
{% endif %}

is equivalent to this:

{% if site.pages.size == 0 %}
  This site has no pages.
{% endif %}

You can’t initialize your own arrays directly, but there are a few workarounds:

  • You can use the split filter to split a string into an array of substrings: {% assign my_array = "First String, Second String, Third String" | split: ", " %}

  • You can use ranges to create arrays of numbers for for loops. For example this:

    <h2>The 5 Most Recent Posts</h2>
    <ol>
    {%- for i in (0..4) %}
      <li>{{ site.posts[i].title }}</li>
    {%- endfor %}
    </ol>
    

    prints a list of the title’s of the site’s 5 most recent posts:

    <h2>The 5 Most Recent Posts</h2>
    <ol>
      <li>Compiling Python Requirements Files with GNU make</li>
      <li>How to Publish a Python Package from GitHub Actions</li>
      <li>How I Use Restic to Back up My Home Folders to Backblaze B2</li>
      <li>Mutt</li>
      <li>How to Backup Your Fastmail &amp; Gmail Accounts with isync</li>
    </ol>
    
  • You can initialize an array from an existing array or arrays, by using one of several filters that return new arrays from existing ones, such as concat, reverse, sort, map, and where (see below for the complete list). For example: {% assign draft_posts = site.posts | where: "draft", true %}

  • You can define an array in the YAML front matter at the top of your file. This is a Jekyll feature, not a Liquid feature. For example:

    ---
    my_array: [6, five, 3.2]
    ---
    

    my_array becomes available as an attribute of the page variable. For example this:

    {% for item in page.my_array %}
      {{ item }}
    {%- endfor %}
    

    Outputs:

      
      6
      five
      3.2
    

    An array in the layout’s front matter (available on the layout variable) or in _config.yml (available on the site variable) would also work.

  • Jekyll’s data files would be another way to create your own arrays.

There are lots of filters for manipulating arrays:

See also: The complete list of all filters in the Liquid docs.

<ARRAY> | size
the number of items in an array. Also works with strings. Arrays also have an <ARRAY>.size attribute
<ARRAY> | compact
a copy of an array with any nil’s removed
<ARRAY> | concat: other_array
a new array by concatenating an array witn another array (to join more than two arrays pipe multiple |concat’s together)
<ARRAY> | first
the first item of an array. Arrays also have an <ARRAY>.first attribute
<ARRAY> | last
the last item of an array. Arrays also have an <ARRAY>.last attribute
<ARRAY_OF_STRINGS> | join: " and "
a string by joining an array of strings into a single string
<ARRAY> | reverse
a copy of an array with the order of items reversed
<ARRAY> | sort
<ARRAY> | sort: "field"
a copy of an array sorted case-sensitively

Todo: It looks like Jekyll might replace sort with its own custom one that has an additional “nils order” parameter. See: https://jekyllrb.com/docs/liquid/filters/

<ARRAY> | sort_natural
<ARRAY> | sort_natural: "field"
a copy of an array sorted case-insensitively
<ARRAY> | map: "foo"
an array of the "foo" attribute values from each object in the original array
<ARRAY> | uniq
a copy of an array with duplicate items removed
<ARRAY> | where: "available"
<ARRAY> | where: "type", "kitchen"
a filtered copy of an array containing only those objects from the original array where the value of a named property (the first argument) matches a given value (the second argument). The second argument defaults to “truthy”

Todo: I think Jekyll overrides Liquid’s built-in where with its own different version, and it also adds where_exp, group_by and group_by_exp. See: https://jekyllrb.com/docs/liquid/filters/

Hashes

They’re missing from the documentation, but Liquid also has hashes: objects with multiple named fields. Jekyll passes several hashes into your templates, for example:

  • site is an object containing site-wide information and settings from your _config.yml file
  • page is an object containing page-specific information and metadata from the page’s YAML front matter
  • layout is an object containing layout-specific information and metadata from the layout’s YAML front matter
  • site.pages is an array of objects, one for each page on your site
  • site.posts is an array of objects, one for each post on your site
  • On GitHub Pages site.github is an object containing all sorts of GitHub Pages-specific metadata about the site

The main thing you can do with hashes is access their attributes using . or [] notation. For example here’s some code that prints out some common Jekyll site attributes:

Site title: {{ site.title }}
Site description: {{ site.description }}
Site base URL: {{ site.baseurl }}
Site last updated at: {{ site.time }}

This outputs:

Site title: seanh.cc
Site description: 
Site base URL: 
Site last updated at: 2023-08-05 13:59:13 +0000

You can also access object attributes using ["string"] notation, which works even if the key has a space in it. For example here’s some code that prints out a bunch of Jekyll’s page attributes:

Page title: {{ page["title"] }}
Beginning of page content: {{ page["content"] | truncate }}
Page URL: {{ page["url"] }}

This outputs:

Page title: Liquid Templating Crash Course
Beginning of page content: This is mostly here as a reference to remind my...
Page URL: /2019/09/29/liquid/

You can also put a string variable or an expression that resolves to a string in []’s, for example:

{% assign tag = "Jekyll" %}
Number of posts tagged "{{ tag }}": {{ site.tags[tag].size }}

You can loop over a hash with {% for %} and it loops over the hash’s keys:

{% for key in site.github limit:3 -%}
  {{ key }}: {{ site.github[key] }}
{% endfor %}

Output:

api_url: https://api.github.com
archived: false
baseurl: 

Trying to access an attribute that doesn’t exist returns nil, which is falsey. For example this page has an attribute does_exist: true in its Jekyll front matter, but it doesn’t have any attribute named doesnt_exist. Given this code:

{% if page.does_exist -%}
  The does_exist attribute does exist
{%- endif %}

{% if page.doesnt_exist -%}
  This won't be printed out
{%- else -%}
  The doesnt_exist attribute doesn't exist
{%- endif %}

{% if page.doesnt_exist == nil -%}
  The doesnt_exist attribute is nil
{%- endif %}

You get this output:

The does_exist attribute does exist

The doesnt_exist attribute doesn't exist

The doesnt_exist attribute is nil

As far as I know there’s no way to create your own hashes in pure Liquid. But you can create them in the YAML front matter at the top of the file. This is a Jekyll feature, not a Liquid feature. For example:

---
my_hash:
  some_attr: some_value
  another_attr: 23
---

Jekyll then makes my_hash available as page.my_hash. For example {{ page.my_hash.some_attr }} would render some_value.

A hash in the layout’s front matter (available on the layout variable) or in _config.yml (available on the site variable) would also work.

Jekyll’s data files would be another way to create your own hashes.

Variable Assignment Tags: {% assign %}, {% capture %}, {% increment %} and {% decrement %}

There are four ways to create a new variable in Liquid: the {% assign %}, {% capture %}, {% increment %} and {% decrement %} tags:1

With {% assign %} you can assign a string, number, boolean, nil or empty value to a variable (you can’t assign arrays or hashes):

{% assign my_variable = false %}

You can then use the newly assigned variable in objects and tags just like any other variable, for example: {{ my_variable }} or {% if my_variable != true %}.

{% capture %} works for string variables only, it lets you assign a multiline string and use objects and tags as part of the string.

{% capture about_me %}
  I am {{ age }}
  {% if favorite_food %}
    and my favorite food is {{ favorite_food }}
  {% endif %}
{% endcapture %}

Once captured the variable can be used in objects and tags like any other:

{{ about_me }}

{% increment %} and {% decrement %} are conveniences that create special number variables that automatically increment by one or decrement by one each time they’re printed. The variable has to be referenced using the {% increment %} or {% decrement %} each time you want to print it out. Unlike {% assign %}, {% capture %}, and most Liquid tags, the {% increment %} and {% decrement %} tags print out the variable’s value in and of themselves, like an object reference would.

For example this:

{% increment my_counter %}
{% increment my_counter %}
{% increment my_counter %}

prints out this:

0
1
2

{% increment %} counters start from 0. {% decrement %} counters work the same way but they start from -1 and go down rather than up.

Control Flow Tags: {% if %}, {% unless %} and {% case %}

{% if %}, {% else %} and {% elsif %}

{% if %} is the basic control flow tag in Liquid, along with its helper tags {% else %} (which also works with for loops, see below) and {% elsif %}. Examples:

{% if page.categories == empty -%}
  This page has no categories.
{%- endif %}

{% if page.draft == true -%}
  This page is a draft.
{%- else -%}
  This page is not a draft.
{%- endif %}

{% assign num_apples = 4 -%}
{% if num_apples > 5 -%}
  There are lots of apples.
{%- elsif num_apples > 3 -%}
  There are several apples.
{%- elsif num_apples > 0 -%}
  There are a few apples.
{%- else -%}
  There are no apples.
{%- endif %}

Outputs:

This page has no categories.

This page is not a draft.

There are several apples.

Control Flow Operators

Several operators can be used with {% if %} and other control flow tags:

  • == and != for comparing equality: {% if post.published == true %} (also works with strings, numbers, nil and empty, even arrays and hashes).

  • <, <=, > and >= for comparing numbers: {% if post.tags.size > 0 %}

  • contains for checking for a substring in a string, or for a string in an array of strings: {% if post.tags contains "Python" %}. contains only works with strings.

  • and and or can be used to create compound expressions:

    {% if post.category == "liquid" and post.draft != true %}
    

Note: there’s no not or ! operator!

{% unless %}

{% unless %} is a convenience that’s the same as using {% if %} with != instead of ==. {% unless product.title == "Awesome Shoes" %} ... {% endunless %} is equivalent to {% if product.title != "Awesome Shoes" %} ... {% endunless %}. {% else %} and {% elsif %} work with {% unless %} the same as they work with {% if %}.

{% case %} and {% when %}

{% case %}/{% when %} is a convenience for when you’d otherwise have to write a repetitive {% if %}/{% elsif %} block testing multiple different values of the same variable. A {% case %} can have an {% else %} on the end just like an {% if %}/{% elsif %} can. Here’s the example from the Liquid docs:

{% case handle %}
  {% when "cake" %}
     This is a cake
  {% when "cookie" %}
     This is a cookie
  {% else %}
     This is not a cake nor a cookie
{% endcase %}

Iteration Tags: {% for %}, {% cycle %} and {% tablerow %}

Iteration in Liquid is done using the {% for %} tag, its helpers {% else %}, {% break %}, {% continue %}, and {% cycle %}, and its parameters limit, offset, and reversed. There’s also a special (n..m) syntax for defining ranges of numbers to loop through.

Here’s some examples:

For loop over an array

To loop over an array of strings:

{%- assign items = "first item, second item, third item" | split: ", " %}
<ol>
{%- for item in items %}
  <li>{{ item }}</li>
{%- endfor %}
</ol>

Outputs:

<ol>
  <li>first item</li>
  <li>second item</li>
  <li>third item</li>
</ol>

For loop over a hash

Looping over a hash seems to loop over the keys of the hash. For example in Jekyll the page variable is a hash of page-specific information and YAML front matter. Looping over page like this:

<ol>
{%- for item in page %}
  <li>{{ item }}: {{ page[item] | truncate: 20 }}</li>
{%- endfor %}
</ol>

Outputs this:

<ol>
  <li>content: This is mostly he...</li>
  <li>output: </li>
  <li>id: /2019/09/29/liquid</li>
  <li>url: /2019/09/29/liquid/</li>
  <li>next: Breadcrumbs help ...</li>
  <li>previous: <!DOCTYPE html>
<...</li>
  <li>path: _posts/2019-09-29...</li>
  <li>relative_path: _posts/2019-09-29...</li>
  <li>excerpt: <p>This is mostly...</li>
  <li>collection: posts</li>
  <li>draft: false</li>
  <li>categories: []</li>
  <li>subtitle: My Liquid templat...</li>
  <li>does_exist: true</li>
  <li>foo: bar</li>
  <li>my_array: [6, "five", 3.2]</li>
  <li>my_hash: {"some_attr"=>"so...</li>
  <li>tags: ["Jekyll"]</li>
  <li>date: 2019-09-29 00:00:...</li>
  <li>title: Liquid Templating...</li>
  <li>slug: liquid</li>
  <li>ext: .md</li>
  <li>layout: post</li>
</ol>

For loop over a range of numbers

(n..m) is a special syntax for creating a range of integers for a for loop. n and m can either be literal numbers or variables whose values are numbers. Examples:

{% for i in (3..5) %}
  {{ i }}
{% endfor %}

{% assign num = 4 %}
{% for i in (1..num) %}
  {{ i }}
{% endfor %}

{% else %} tags on for loops

An {% else %} tag in a {% for %} loop specifies an alternative block to be rendered if the array is empty:

{%- assign items = empty %}
<ol>
{%- for item in items %}
  <li>{{ item }}</li>
{%- else %}
  The list is empty.
{%- endfor %}
</ol>

Outputs:

<ol>
  The list is empty.
</ol>

{% break %} and {% continue %}

A {% break %} tag breaks out of a for loop, and a {% continue %} jumps to the next iteration, as you would expect. You’d normally use these in an {% if %} within a {% for %}, of course:

{% for item in items %}
  {% if should_stop_looping %}
    {% break %}
  {% elsif should_skip_item %}
    {% continue %}
  {% else %}
    ...
  {% endif %}
{% endfor %}

limit, offset and reversed parameters

{% for %} can accept three parameters limit, offset and reversed. This:

{%- for i in (1..10) reversed limit:3 offset:2 %}
{{ i }}
{%- endfor %}

Outputs this:

5
4
3

When passing multiple parameters to a tag like {% for %} the ordering of the parameters can be tricky: flags have to come before parameters with values. reversed is a flag: a parameter that doesn’t take a value, whereas limit and offset are parameters that require values. When passing multiple parameters to a tag, any flag parameters have to be listed before any parameters with values. Other than that the order of parameters doesn’t make any difference. These are equivalent:

  • {% for i in (1..10) reversed limit:3 offset:2 %}
  • {% for i in (1..10) reversed offset:2 limit:3 %}

But these don’t work because reversed has no effect if it isn’t listed before the parameters with values:

  • {% for i in (1..10) limit:3 reversed offset:2 %}
  • {% for i in (1..10) limit:3 offset:2 reversed %}

for loop helper variables

See also: The forloop object in Shopify’s Liquid docs.

Several special helper variables are available inside a for loop:

forloop.length
The total number of items being looped over.
forloop.index
The current index of the for loop. Starts at 1.
forloop.index0
The current index of the for loop. Starts at 0.
forloop.rindex
forloop.index but in reverse order.
forloop.rindex0
forloop.index0 but in reverse order.
forloop.first
true if this is the first iteration.
forloop.last
true if this is the last iteration.

{% cycle %} and {% tablerow %}

Finally, the {% cycle %} tag is a convenience for generating alternating or looping values within a loop (for example alternating or cycling between two or more colours or odd/even classes in a list), and the {% tablerow %} tag is a convenience for generating HTML tables. See the Liquid docs for how to use these.

Miscellaneous Filters

Filters that don’t fit under strings, numbers or arrays:

See also: The complete list of all filters in the Liquid docs.

<OBJECT> | default: <FALLBACK_VALUE>
Either returns <OBJECT> or, if <OBJECT> is false, nil or empty, returns <FALLBACK_VALUE> instead

Comments

Comments can be entered using the {% comment %} tag:

{% comment %}
This is a comment. It will be omitted from the rendered output.
{% endcomment %}

Kramdown comments also work on GitHub Pages.

Jekyll Liquid

Jekyll adds custom filters, custom tags, global variables, a very important {% include %} tag, and template inheritance (“layouts”) on top of standard Liquid:

Jekyll’s {% link %} and {% post_url %}

See also: Jekyll’s docs on Links.

Jekyll’s {% link %} tag, for generating URLs to pages and posts of your site, is particularly useful. {% link news/index.html %} returns the URL to the news/index.html page. {% link _posts/2016-07-26-name-of-post.md %} returns the URL to the 2016-07-26-name-of-post.md post.

The {% post_url %} tag is a similar but more convenient tag for generating URLs to posts only: {% post_url 2010-07-21-name-of-post %} or {% post_url /subdir/2010-07-21-name-of-post %}.

The jekyll-relative-links plugin is enabled by default on GitHub Pages and provides an even easier way to link to posts and pages: any normal Markdown link that points to a Markdown file (relative to the location of the current file) gets turned into a link to that Markdown file’s page or post’s permalink URL. These kind of links also work when the Markdown is rendered and previewed on GitHub.com: [foo](bar.md) gets converted to [foo](/bar/) (bar.md’s permalink according to your permalink style setting).

To enable these kind of relative links everywhere put this in your _config.yml:

relative_links:
  enabled:     true
  collections: true

Jekyll’s includes

See also: Jekyll’s docs on Includes and Liquid’s docs on capture.

Includes are a way of compartmentalizing your templates into lots of small, reusable templates. It’s a good idea to use as many of these as possible. The basic idea is that you create a snippet of Liquid, HTML and Markdown (Kramdown) code in an _includes/foo.html file and then include that code in another template, page or post whenever you want it with {% include foo.html %}.

There’s also {% include_relative %} which searches for an include file relative to the location of the current file, instead of looking in the global _includes/ directory.

You can pass one or more parameters into an include:

{% include my_include.html foo="foo" bar="bar" %}

These are then available in my_include.html as {{ include.foo }} and {{ include.bar }}.

Optional include parameters can be implemented by using if-statements like {% if include.foo %} ... {% else %} ... {% endif %}, or by using the |default filter.

Captures are often useful when passing parameters into includes.

Jekyll’s site.categories and site.tags

See also: Jekyll’s docs on Categories and Tags (includes a for-loop similar to the one below for listing all posts by category).

Jekyll’s site.categories and site.tags are arrays of arrays, one subarray for each category or tag, where each category or tag subarray contains two elements: [0]: the name of the category or tag (a string), and [1]: a subsubarray containing all the posts (post objects with various fields) with that category or tag:

The structure of site.categories looks something like this (and site.tags is the same):

[
  [
    "<name of first category>",
    [
      {
        "title": "<title of first post in category>",
        "url": "<URL of first post in category>",
        ...
      },
      {
        "title": "<title of second post in category>",
        "url": "<URL of third post in category>",
        ...
      },
      ...
    ],
  ],
  [
    "<name of second category>",
    [
      {<first post in category>},
      {<second post in category>},
    ],
  ],
  ...
]

For example here’s some code to loop over all of a site’s tags and, for each tag, print out a list of links to all of the posts with that tag:

<ul>
  {% for tag in site.tags %}
    <li>
      <p>{{ tag[0] }}:</p> <!-- The name of the tag. -->
      <ol>
        {% for post in tag[1] %} <!-- Loop over all of the tag's posts. -->
          <li>
            <a href="{{ post.url | absolute_url }}">
              {{ post.title }}
            </a>
          </li>
        {% endfor %}
      </ol>
    </li>
  {% endfor %}
</ul>

As well as looping over them, you can also access individual tags or categories by name:

The "Jekyll" tag has {{ site.tags.Jekyll.size }} posts.

{% for post in site.tags.Jekyll limit:3 %}
  {{ post.title }}
{% endfor %}

Arbitrary front matter are available as variables

See also: Front Matter in Jekyll’s docs.

You can put arbitrary key/value pairs in the YAML front matter at the top of a page or post file and Jekyll makes them available as attributes of the page object.

For example given this YAML front matter at the top of a page or post file:

---
foo: bar
---

You could do this to render “bar”:

{{ page.foo }}

You can also put key/value pairs in the front matter of layout files, and Jekyll makes them available as attributes of the global variable layout.

Arbitrary config settings are available as variables

See also: Configuration in Jekyll’s docs.

You can put arbitrary key/value pairs in your site’s _config.yml file and Jekyll makes them available as attributes of the global variable site. For example given this line on _config.yml:

foo: bar

You could do this to render “bar”:

{{ site.foo }}

YAML, JSON, CSV and TSV files in _data are accessible as variables

You can put YAML, JSON, CSV and TSV files in a _data folder and Jekyll makes them all available as attributes of a site.data object. See Data Files in the Jekyll docs.

  1. With Jekyll you can also create variables for templates to use in YAML front matter, in the config file, or in data files. 

All posts tagged “Jekyll”: