Jekyll Liquid Tags: The Complete Reference Guide
A comprehensive reference for all Jekyll Liquid tags β if, for, assign, capture, include, case, raw, comment, and more with real examples.
Liquid tags are the logic layer of Jekyll templates. Wrapped in {% %} delimiters, they control flow, create variables, loop over data, and embed content β without outputting anything themselves. This is a complete reference for every Liquid tag used in Jekyll.
assign
Creates a variable and assigns it a value. The variable is available for the rest of the template (or until overwritten).
{% assign title = "My Post" %}
{% assign count = site.posts | size %}
{% assign is_premium = false %}
{% assign tags_list = "jekyll,tutorial,liquid" | split: "," %}
Variables created with assign are available in the current scope and any includes called from it.
{% assign author = site.authors | where: "name", page.author | first %}
{% if author %}
<img src="{{ author.avatar }}" alt="{{ author.name }}">
{% endif %}
capture
Builds a string variable from a block of content β useful when the value spans multiple lines or includes Liquid expressions.
{% capture post_url %}{{ site.url }}{{ post.url }}{% endcapture %}
<meta property="og:url" content="{{ post_url }}">
{% capture author_bio %}
{{ page.author }} Β· {{ page.date | date: "%B %-d, %Y" }} Β· {{ page.reading_time }} min read
{% endcapture %}
<p class="post-meta">{{ author_bio | strip }}</p>
Unlike assign, capture captures everything between its opening and closing tags, including whitespace and newlines. Use | strip to remove leading/trailing whitespace.
if / elsif / else / endif
Conditional rendering. Outputs content only when the condition is true.
{% if page.price == 0 %}
<span class="badge">Free</span>
{% elsif page.price < 20 %}
<span class="badge">Budget</span>
{% else %}
<span class="badge">Premium</span>
{% endif %}
Conditions can use:
==equal to!=not equal to>>=<<=numeric comparisonscontainsstring/array containsandboth conditions trueoreither condition true
{% if page.tags contains "jekyll" and page.featured %}
This is a featured Jekyll post.
{% endif %}
{% if user.name == "admin" or user.role == "editor" %}
Show edit button
{% endif %}
Truthy/falsy: In Liquid, nil and false are falsy. Everything else β including 0, "", and [] β is truthy. This differs from many other languages.
{% if page.image %} β false only if image is nil (not set) or false
{% if page.count > 0 %} β more reliable check for zero
unless / endunless
The inverse of if β runs the block when the condition is false. Equivalent to {% if not condition %}.
{% unless page.hide_sidebar %}
{% include sidebar.html %}
{% endunless %}
{% unless forloop.last %}
<hr>
{% endunless %}
case / when / else / endcase
Multi-branch conditional, cleaner than a long if/elsif chain when checking a single variable against multiple values.
{% case page.category %}
{% when "Tutorial" %}
<span class="badge badge--blue">Tutorial</span>
{% when "Comparison" %}
<span class="badge badge--purple">Comparison</span>
{% when "Themes", "Design" %}
<span class="badge badge--green">Design</span>
{% else %}
<span class="badge">Article</span>
{% endcase %}
Multiple values for the same branch: {% when "Themes", "Design" %} matches either.
for / endfor
Loops over an array or range. One of the most-used tags in Jekyll templates.
{% for post in site.posts %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<p>{{ post.excerpt | strip_html | truncatewords: 25 }}</p>
</article>
{% endfor %}
Loop parameters
{% comment %} First 6 posts {% endcomment %}
{% for post in site.posts limit: 6 %}
{% comment %} Skip the first 3 {% endcomment %}
{% for post in site.posts offset: 3 %}
{% comment %} Reverse order {% endcomment %}
{% for post in site.posts reversed %}
{% comment %} Combine: posts 4-9 {% endcomment %}
{% for post in site.posts limit: 6 offset: 3 %}
forloop variables
Inside a loop, these special variables describe the current iteration:
{% for post in site.posts %}
{% if forloop.first %}<ul>{% endif %}
<li class="{% if forloop.last %}last{% endif %}">
{{ forloop.index }}. {{ post.title }}
</li>
{% if forloop.last %}</ul>{% endif %}
{% endfor %}
| Variable | Value |
|---|---|
forloop.index |
Current iteration, 1-based |
forloop.index0 |
Current iteration, 0-based |
forloop.rindex |
Reverse index, 1-based |
forloop.rindex0 |
Reverse index, 0-based |
forloop.first |
true on first iteration |
forloop.last |
true on last iteration |
forloop.length |
Total number of items |
else in for loops
Runs when the array is empty:
{% for theme in site.themes %}
<div class="theme-card">{{ theme.title }}</div>
{% else %}
<p>No themes available yet.</p>
{% endfor %}
Looping over a number range
{% for i in (1..5) %}
<span>Step {{ i }}</span>
{% endfor %}
Nested loops
{% for category in site.categories %}
<h2>{{ category[0] }}</h2>
{% for post in category[1] %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
{% endfor %}
break and continue
Control loop execution:
{% comment %} Stop after finding first featured post {% endcomment %}
{% for post in site.posts %}
{% if post.featured %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% break %}
{% endif %}
{% endfor %}
{% comment %} Skip drafts in a custom loop {% endcomment %}
{% for post in site.posts %}
{% unless post.published %}{% continue %}{% endunless %}
<li>{{ post.title }}</li>
{% endfor %}
include
Inserts the contents of a file from _includes/. One of the most-used tags in Jekyll layouts.
{% include nav.html %}
{% include footer.html %}
{% include components/card.html %}
include with parameters
Pass variables to an include using key=value pairs:
{% include components/card.html
title=theme.title
url=theme.url
price=theme.price
image=theme.card_image %}
Inside the include, access them with include.keyname:
<!-- _includes/components/card.html -->
<div class="card">
<h3>{{ include.title }}</h3>
<a href="{{ include.url }}">View</a>
</div>
include with a variable filename
{% assign template = "components/card.html" %}
{% include {{ template }} %}
Or use include_relative to include from a path relative to the current file:
{% include_relative ../shared/notice.html %}
raw / endraw
Prevents Liquid from processing the enclosed content. Essential when writing Liquid code in blog posts.
Use {{ variable }} syntax to output values.
{% if condition %}...{% endif %}
Everything inside ... is output literally, without Liquid processing.
comment / endcomment
Adds a Liquid comment β not rendered in output and not processed:
{% comment %}
TODO: add pagination here
This section is temporarily disabled
{% endcomment %}
Unlike HTML comments (<!-- -->), Liquid comments are completely removed from output and cannot be seen by users in the page source.
highlight / endhighlight
Syntax-highlights a code block using Rouge:
{% highlight ruby %}
def hello
puts "Hello, Jekyll!"
end
{% endhighlight %}
{% highlight javascript linenos %}
const site = "JekyllHub";
console.log(site);
{% endhighlight %}
The optional linenos adds line numbers. The language identifier must be one Rouge supports (ruby, javascript, python, bash, yaml, html, css, json, etc.).
link and post_url
Generate correct URLs for internal pages and posts, accounting for baseurl:
{% comment %} Link to a page (fails build if page not found) {% endcomment %}
<a href="{% link _pages/about.md %}">About</a>
<a href="{% link _posts/2026-08-03-jekyll-directory-structure.md %}">Read post</a>
{% comment %} Link to a post by filename {% endcomment %}
<a href="{% post_url 2026-08-03-jekyll-directory-structure %}">Read post</a>
Both link and post_url cause a build error if the target file does not exist β useful for catching broken internal links. They automatically apply baseurl.
tablerow
Like for, but generates an HTML table:
<table>
{% tablerow plugin in site.data.plugins cols: 3 %}
<td>{{ plugin.name }}</td>
{% endtablerow %}
</table>
cols: sets the number of columns per row. Less commonly used than for, but useful for tabular data.
jekyll_draft and jekyll_version
Available in Jekyll 4+:
{% comment %} Show content only when serving with --drafts flag {% endcomment %}
{% if jekyll.environment == "development" %}
<div class="draft-banner">Draft preview</div>
{% endif %}
Whitespace control
Liquid tags add blank lines to output. Strip whitespace with -:
{%- assign foo = "bar" -%}
{%- for item in list -%}
{{ item }}
{%- endfor -%}
The - inside the tag delimiters strips whitespace (including newlines) before or after that tag. Use this when generating clean HTML or JSON output.
Tags vs filters: the difference
People sometimes confuse tags and filters. The distinction:
- Tags (
{% %}) execute logic β they control flow, assign variables, loop, include files - Filters (
|) transform values β they modify the output of{{ }}expressions
{% assign count = site.posts | size %} β tag with filter in the value
{{ page.title | upcase }} β output with filter
{% for post in site.posts %} β loop tag
Knowing both tags and filters β and which is which β gives you the full toolkit for working with any Jekyll theme.
Why tags matter for Jekyll theme development
Every Jekyll theme is built on Liquid tags. When you open a layout file like _layouts/default.html or an include like _includes/header.html, you are reading a template that uses a combination of tags and filters to transform data into HTML. Understanding each tag at this level lets you read and modify any themeβs templates without guesswork.
The most common tags in production themes are if, for, include, assign, and capture. These five account for the majority of the logic in any Jekyll template. The others β unless, case, raw, comment, highlight, link, and post_url β appear regularly but less frequently.
A key insight for working with tags: they are processed at build time, not at runtime in the browser. Everything inside {% for %} loops and {% if %} conditions is evaluated once when Jekyll builds the site, and the resulting HTML is fixed. There is no re-execution in the browser. This is what makes Jekyll sites fast β no template processing happens when a visitor loads a page β and it is also what defines the boundary between Liquid (build-time) and JavaScript (runtime).
Debugging tag logic
When a Liquid tag produces unexpected output, a few diagnostic techniques are reliable.
The {{ variable | inspect }} output tag (which is not a tag but a filter, confusingly) prints the raw structure of any variable. Use it to confirm what a variable contains before passing it to a loop or condition:
{% assign author = site.authors | where: "name", page.author | first %}
<!-- Debug: -->
{{ author | inspect }}
<!-- Expected output: {"name"=>"Marcus Webb", "avatar"=>"/assets/...", ...} -->
For loop debugging, confirm the array is not empty before looping:
{% if site.themes == empty %}
<!-- No themes found -->
{% else %}
Found {{ site.themes | size }} themes
{% for theme in site.themes %}
{{ theme.title }}
{% endfor %}
{% endif %}
For assign debugging, check that the variable name does not conflict with a Liquid keyword or a variable already defined in the scope. Liquid variables are case-sensitive β author and Author are different variables.
For include debugging, verify the file path is relative to _includes/. {% include nav.html %} looks for _includes/nav.html. {% include components/nav.html %} looks for _includes/components/nav.html. File not found silently outputs nothing in older Jekyll versions; newer versions throw a build error.
Combining tags for common patterns
Real templates combine tags into patterns that appear repeatedly across themes.
Conditional include with data lookup:
{% if page.author %}
{% assign author_data = site.data.authors | where: "name", page.author | first %}
{% if author_data %}
{% include author-card.html author=author_data %}
{% endif %}
{% endif %}
Paginated loop with empty state:
{% assign posts = site.posts | where: "category", page.category %}
{% if posts == empty %}
<p>No posts in this category yet.</p>
{% else %}
{% for post in posts limit: 12 %}
{% include post-card.html post=post %}
{% endfor %}
{% endif %}
Accumulated string with capture:
{% capture breadcrumbs %}
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
{% for category in page.categories %}
<li><a href="/category/{{ category | slugify }}/">{{ category }}</a></li>
{% endfor %}
<li aria-current="page">{{ page.title }}</li>
</ol>
</nav>
{% endcapture %}
{{ breadcrumbs }}
The capture pattern is useful when you need to build HTML conditionally, store it in a variable, and either render it in a different part of the page or pass it as a parameter to an include.
Loop with separator (no trailing comma):
{% for tag in page.tags %}{{ tag }}{% unless forloop.last %}, {% endunless %}{% endfor %}
The unless forloop.last pattern adds a separator between items without a trailing one β cleaner than using join when you need control over the formatting.
Liquid tags are not the most exciting part of Jekyll, but they are the most essential for theme development and customisation. Fluency with the full tag set β knowing when to reach for capture instead of assign, when unless is cleaner than if not, and how to combine for with where and sort β makes the difference between reading a themeβs templates with comprehension and reading them with confusion. Bookmark this reference and consult it whenever a tagβs behaviour is unclear.
The assign tag in depth
assign is the most-used tag in real Jekyll templates, and understanding its scoping rules prevents a common class of bugs.
Variables assigned with {% assign %} exist for the remainder of the current template file and are passed into any {% include %} calls made after the assignment. However, they do not leak back up from includes into the calling template. An assign inside _includes/card.html does not affect variables in the layout file that included it.
Variables do not persist between pages. Each page renders with a fresh Liquid scope β there is no shared global state between page renders (other than site.* variables, which are set before rendering begins and are read-only).
This scoping model is what allows Jekyll to build pages in parallel: each pageβs Liquid execution is completely independent of every other page.
Common assign patterns:
{% comment %} Computed value used multiple times {% endcomment %}
{% assign post_count = site.posts | size %}
{% assign half_count = post_count | divided_by: 2 %}
{% comment %} Cached filter result {% endcomment %}
{% assign sorted_themes = site.themes | sort: "stars" | reverse %}
{% comment %} Boolean flag {% endcomment %}
{% assign show_cta = false %}
{% if page.template == "landing" %}
{% assign show_cta = true %}
{% endif %}
{% comment %} Data lookup result {% endcomment %}
{% assign current_author = site.data.authors | where: "name", page.author | first %}
The for tag: all options
The for tag has several modifiers that are easy to forget:
{% comment %} Basic loop {% endcomment %}
{% for post in site.posts %}
{% endfor %}
{% comment %} Limit: only first N items {% endcomment %}
{% for post in site.posts limit: 6 %}
{% endfor %}
{% comment %} Offset: skip first N items {% endcomment %}
{% for post in site.posts offset: 3 %}
{% endfor %}
{% comment %} Combined: items 4-9 (offset 3, then take 6) {% endcomment %}
{% for post in site.posts limit: 6 offset: 3 %}
{% endfor %}
{% comment %} Reversed {% endcomment %}
{% for post in site.posts reversed %}
{% endfor %}
{% comment %} Number range {% endcomment %}
{% for i in (1..site.posts.size) %}
{{ i }}
{% endfor %}
{% comment %} Empty state {% endcomment %}
{% for post in site.posts %}
{{ post.title }}
{% else %}
No posts found.
{% endfor %}
The reversed modifier combined with limit and offset gives you the ability to page through content, though for production pagination the jekyll-paginate-v2 plugin is a better approach.
Practical tag combination patterns
These patterns solve real problems in Jekyll templates and appear regularly across production themes:
Navigation with active state:
{% for item in site.data.navigation %}
<a href="{{ item.url | relative_url }}"
{% if page.url == item.url %}aria-current="page"{% endif %}>
{{ item.title }}
</a>
{% endfor %}
Related posts by category:
{% assign related = site.posts | where: "category", page.category %}
{% assign related_filtered = "" | split: "" %}
{% for post in related %}
{% unless post.url == page.url %}
{% assign related_filtered = related_filtered | push: post %}
{% endunless %}
{% endfor %}
{% for post in related_filtered limit: 3 %}
{% include post-card.html post=post %}
{% endfor %}
Comma-separated tag list without trailing comma:
{% for tag in page.tags %}<a href="/tag/{{ tag | slugify }}/">{{ tag }}</a>{% unless forloop.last %}, {% endunless %}{% endfor %}
First post with different layout:
{% for post in site.posts limit: 6 %}
{% if forloop.first %}
{% include post-card-featured.html post=post %}
{% else %}
{% include post-card.html post=post %}
{% endif %}
{% endfor %}
These are the patterns that most Jekyll themes are built from. Recognising them in unfamiliar themes makes reading and adapting other peopleβs templates much faster β the same patterns appear across Minimal Mistakes, Chirpy, Hyde, and every other popular theme, just with different variable names and class names wrapped around the same underlying Liquid logic.
Reading Liquid with confidence
Liquid tags are the control structures of Jekyll templating β the if statements, loops, and includes that turn data into HTML. Once you can read a Liquid template fluently, you can adapt any Jekyll theme to your needs, debug rendering problems quickly, and build your own layouts from scratch without guesswork. The most effective way to build this reading fluency is to open the template files of a theme you admire and trace through the logic: follow the {% for %} loops, check what the {% if %} conditions are testing, and look for {% include %} calls to find the sub-components. After doing this with two or three themes, the patterns become second nature. Liquid is deliberately simple β it was designed to be readable by non-programmers β and with the tags in this reference you have everything you need to write professional-quality Jekyll templates.
Bookmark this reference page and return to it when you encounter an unfamiliar tag in a theme you are adapting. The Jekyll documentation at jekyllrb.com and the Liquid documentation at shopify.github.io/liquid are also excellent companion references for edge cases and advanced usage patterns not covered here.