Jekyll Variables Reference: site, page, layout, and More
A complete reference for every Jekyll variable — site.*, page.*, layout.*, content, forloop, and paginator — with examples for each.
Jekyll makes a set of variables available in every template through Liquid. Knowing which variables exist and what they contain is essential for building and customising Jekyll themes. This is a complete reference for all of them.
site variables
site contains global data about your Jekyll site — configuration from _config.yml, collections, posts, and build information.
Configuration variables
Every key in _config.yml becomes a site.* variable:
# _config.yml
title: "JekyllHub"
author: "Marcus Webb"
description: "A Jekyll theme marketplace."
url: "https://jekyllhub.com"
baseurl: ""
google_analytics: "G-XXXXXXXXXX"
sendy_list_id: "abc123"
{{ site.title }} → "JekyllHub"
{{ site.description }} → "A Jekyll theme marketplace."
{{ site.url }} → "https://jekyllhub.com"
{{ site.baseurl }} → ""
{{ site.author }} → "Marcus Webb"
{{ site.google_analytics }}→ "G-XXXXXXXXXX"
{{ site.sendy_list_id }} → "abc123"
Built-in site variables
These are provided by Jekyll itself, not from _config.yml:
| Variable | Type | Description |
|---|---|---|
site.time |
DateTime | The time of the current build |
site.pages |
Array | All pages in the site |
site.posts |
Array | All posts, sorted newest first |
site.related_posts |
Array | Up to 10 related posts (for the current post) |
site.static_files |
Array | All static files (non-processed) |
site.html_pages |
Array | Pages with .html or .htm extension |
site.html_files |
Array | Static files with .html extension |
site.collections |
Array | All collections defined in config |
site.data |
Object | Data from all files in _data/ |
site.documents |
Array | All documents in all collections |
site.categories |
Object | Posts grouped by category |
site.tags |
Object | Posts grouped by tag |
site.posts
{% comment %} All posts, newest first {% endcomment %}
{% for post in site.posts %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
{% comment %} Post count {% endcomment %}
{{ site.posts | size }} posts
{% comment %} Latest post {% endcomment %}
{% assign latest = site.posts | first %}
{{ latest.title }}
site.pages
{% for page in site.pages %}
{% if page.title %}
<a href="{{ page.url }}">{{ page.title }}</a>
{% endif %}
{% endfor %}
Note: site.pages includes all pages — HTML files, Markdown files, and some generated files. Filter by page.layout or page.url if you need a subset.
site.data
Mirrors the _data/ directory structure:
_data/
├── navigation.yml
├── authors.yml
└── showcase/
└── sites.yml
{{ site.data.navigation }} → contents of navigation.yml
{{ site.data.authors }} → contents of authors.yml
{{ site.data.showcase.sites }} → contents of showcase/sites.yml
site.categories and site.tags
{% comment %} Loop over all categories {% endcomment %}
{% for category in site.categories %}
<h2>{{ category[0] }}</h2> ← category name
{% for post in category[1] %}
<li>{{ post.title }}</li> ← posts in this category
{% endfor %}
{% endfor %}
{% comment %} Posts in a specific category {% endcomment %}
{% assign tutorial_posts = site.categories["Tutorial"] %}
{{ tutorial_posts | size }} tutorials
Collection variables
For a collection named themes (defined in _config.yml):
{{ site.themes }} → array of all theme documents
{{ site.themes | size }} → number of themes
{% for theme in site.themes %}
{{ theme.title }}
{% endfor %}
page variables
page contains data about the current page, post, or collection document being rendered.
Built-in page variables
| Variable | Type | Description |
|---|---|---|
page.content |
String | Rendered HTML content of the page |
page.title |
String | Title from front matter |
page.excerpt |
String | Excerpt (first paragraph or custom) |
page.url |
String | URL of the page (e.g. /blog/my-post/) |
page.date |
DateTime | Post date |
page.id |
String | Unique identifier (e.g. /2026/08/06/my-post) |
page.categories |
Array | Categories from front matter |
page.tags |
Array | Tags from front matter |
page.path |
String | Source file path (e.g. _posts/2026-08-06-my-post.md) |
page.name |
String | Filename (e.g. 2026-08-06-my-post.md) |
page.next |
Object | Next post (chronologically) |
page.previous |
Object | Previous post (chronologically) |
Custom front matter variables
Every key in a page’s front matter becomes a page.* variable:
---
layout: post
title: "My Post"
author: Marcus Webb
featured: true
reading_time: 8
image: /assets/images/blog/cover.webp
difficulty: beginner
---
{{ page.author }} → "Marcus Webb"
{{ page.featured }} → true
{{ page.reading_time }} → 8
{{ page.image }} → "/assets/images/blog/cover.webp"
{{ page.difficulty }} → "beginner"
page.url vs page.id
{{ page.url }} → "/blog/jekyll-variables-reference/"
{{ page.id }} → "/2026/08/06/jekyll-variables-reference"
page.url is the clean URL visitors see. page.id is an internal identifier used by some plugins.
page.date
{{ page.date }} → 2026-08-06 00:00:00 +0000
{{ page.date | date: "%B %-d, %Y" }} → August 6, 2026
{{ page.date | date: "%Y-%m-%d" }} → 2026-08-06
{{ page.date | date: "%s" }} → Unix timestamp
page.excerpt
{{ page.excerpt }} → first paragraph (rendered HTML)
{{ page.excerpt | strip_html }} → plain text excerpt
{{ page.excerpt | strip_html | truncatewords: 30 }}
Override the default excerpt in front matter:
excerpt: "A custom summary for this post."
page.next and page.previous
Navigate between posts:
{% if page.next %}
<a href="{{ page.next.url }}">Next: {{ page.next.title }}</a>
{% endif %}
{% if page.previous %}
<a href="{{ page.previous.url }}">Previous: {{ page.previous.title }}</a>
{% endif %}
Note: page.next is the chronologically newer post; page.previous is the older one — counterintuitive but correct.
page.categories and page.tags
{% for category in page.categories %}
<a href="/category/{{ category | slugify }}/">{{ category }}</a>
{% endfor %}
{% for tag in page.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
layout variables
layout contains data from the current layout file’s front matter:
<!-- _layouts/post.html -->
---
layout: default
sidebar: true
show_related: true
---
{{ layout.sidebar }} → true
{{ layout.show_related }} → true
Useful when a layout needs configuration that individual pages can check:
{% if layout.sidebar %}
{% include sidebar.html %}
{% endif %}
content
Available only inside layout files. Contains the rendered HTML content being wrapped by the layout:
<!-- _layouts/default.html -->
<main>
{{ content }}
</main>
content is the output after all inner layouts have been applied. For a post using layout: post (which itself uses layout: default), by the time default.html sees content, it already contains the post’s HTML wrapped in post.html’s structure.
paginator variables
Available on paginated pages when using jekyll-paginate or jekyll-paginate-v2:
| Variable | Description |
|---|---|
paginator.page |
Current page number |
paginator.per_page |
Posts per page |
paginator.posts |
Posts on the current page |
paginator.total_posts |
Total post count |
paginator.total_pages |
Total page count |
paginator.previous_page |
Previous page number (or nil) |
paginator.previous_page_path |
URL of previous page |
paginator.next_page |
Next page number (or nil) |
paginator.next_page_path |
URL of next page |
{% for post in paginator.posts %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
</article>
{% endfor %}
<nav class="pagination">
{% if paginator.previous_page %}
<a href="{{ paginator.previous_page_path }}">← Newer</a>
{% endif %}
<span>Page {{ paginator.page }} of {{ paginator.total_pages }}</span>
{% if paginator.next_page %}
<a href="{{ paginator.next_page_path }}">Older →</a>
{% endif %}
</nav>
jekyll variables
Information about the Jekyll build environment:
{{ jekyll.environment }} → "development" or "production"
{{ jekyll.version }} → "4.3.2"
{% if jekyll.environment == "production" %}
{% include analytics.html %}
{% endif %}
Set JEKYLL_ENV=production before building to enable production-only features.
forloop variables
Inside any {% for %} loop:
{% for post in site.posts %}
{{ forloop.index }} → 1, 2, 3... (1-based)
{{ forloop.index0 }} → 0, 1, 2... (0-based)
{{ forloop.rindex }} → counts down from total (1-based)
{{ forloop.rindex0 }} → counts down from total (0-based)
{{ forloop.first }} → true on first iteration
{{ forloop.last }} → true on last iteration
{{ forloop.length }} → total items in array
{% endfor %}
Practical use — add a class to the first item and a divider between items:
{% for item in list %}
<div class="item{% if forloop.first %} item--first{% endif %}">
{{ item.name }}
</div>
{% unless forloop.last %}<hr>{% endunless %}
{% endfor %}
tablerow variables
Inside a {% tablerow %} loop:
{% tablerow item in list cols: 3 %}
{{ tablerowloop.index }}
{{ tablerowloop.col }} → current column (1-based)
{{ tablerowloop.col0 }} → current column (0-based)
{{ tablerowloop.col_first }} → true on first column
{{ tablerowloop.col_last }} → true on last column
{{ tablerowloop.row }} → current row number
{{ tablerowloop.first }} → true on first item
{{ tablerowloop.last }} → true on last item
{{ tablerowloop.length }} → total items
{% endtablerow %}
Quick reference: which variable to use
| You need | Use |
|---|---|
| Site name/URL from config | site.title, site.url |
| All blog posts | site.posts |
| All pages | site.pages |
Data from _data/nav.yml |
site.data.nav |
| Custom config value | site.your_key |
| Current page title | page.title |
| Current page URL | page.url |
| Custom front matter value | page.your_key |
| Post publish date | page.date |
| Post excerpt | page.excerpt |
| Next/previous post | page.next, page.previous |
| Rendered page content (in layouts) | content |
| Build environment | jekyll.environment |
| Pagination data | paginator.* |
| Loop position | forloop.index, forloop.first, etc. |
Using variables with Liquid filters
Jekyll variables become genuinely useful when combined with Liquid’s filter pipeline. Filters transform the raw variable output into something formatted for display or logic.
Formatting dates
{{ page.date | date: "%B %-d, %Y" }} → "January 29, 2026"
{{ page.date | date: "%Y-%m-%d" }} → "2026-01-29"
{{ page.date | date_to_xmlschema }} → "2026-01-29T00:00:00+00:00"
{{ page.date | date_to_rfc822 }} → RFC 822 format for RSS feeds
Generating slugs and URLs
{{ page.title | slugify }} → "jekyll-variables-reference"
{{ page.url | absolute_url }} → "https://example.com/blog/post/"
{{ page.url | relative_url }} → "/blog/post/"
Truncating and stripping content
{{ page.excerpt | strip_html }} → plain text excerpt
{{ page.excerpt | truncatewords: 30 }} → first 30 words
{{ page.content | strip_html | size }} → character count of plain text
{{ page.title | upcase }} → uppercase title
{{ page.title | downcase | slugify }} → normalised slug
Working with arrays
{{ site.posts | size }} → total post count
{{ site.posts | first }} → most recent post object
{{ site.posts | last }} → oldest post object
{{ page.tags | join: ", " }} → "jekyll, tutorial, sass"
{{ site.posts | sort: "title" }} → posts sorted alphabetically
{{ site.posts | where: "featured", true }} → only featured posts
{{ site.posts | where_exp: "post", "post.tags contains 'jekyll'" }}
Debugging variables with inspect
When a template produces unexpected output, the inspect filter reveals the raw structure of any variable:
{{ page | inspect }} → shows all page variables and their values
{{ site.data | inspect }} → shows the complete data structure
{{ page.tags | inspect }} → shows the tags array: ["jekyll", "tutorial"]
For complex nested objects, you can iterate to inspect individual keys:
{% for item in page %}
{{ item[0] }}: {{ item[1] | inspect }}
{% endfor %}
This prints every key-value pair in the page object, which is invaluable when a theme expects a front matter variable you are not providing or uses a variable name different from what you assumed.
Variables inside includes
When you call {% include file.html %}, you can pass parameters that become available as include.parameter_name inside the included file:
{% include card.html title=post.title url=post.url image=post.image %}
Inside _includes/card.html:
<div class="card">
<img src="{{ include.image }}" alt="{{ include.title }}">
<a href="{{ include.url }}">{{ include.title }}</a>
</div>
The include variable is scoped to the included file and is not accessible outside it. This scoping keeps includes self-contained — they do not bleed state into the calling template.
You can also pass Liquid expressions and variables as include parameters:
{% include card.html title=page.title url=page.url | relative_url %}
Checking whether a variable is defined
Use the nil check to guard against undefined front matter variables:
{% if page.image %}
<img src="{{ page.image }}" alt="{{ page.title }}">
{% endif %}
{% if page.author %}
<span>By {{ page.author }}</span>
{% else %}
<span>By {{ site.author }}</span>
{% endif %}
Use the default filter to provide a fallback value inline:
{{ page.author | default: site.author }}
{{ page.image | default: site.default_image }}
{{ page.description | default: site.description }}
The default filter returns the specified value when the original is nil, false, or an empty string. This is the cleanest way to implement fallback behaviour without {% if %} blocks.
Building a complete post template with variables
Putting variables together into a realistic post layout shows how they interact:
---
layout: default
---
<article
class="post"
itemscope
itemtype="https://schema.org/BlogPosting"
>
<header class="post-header">
<div class="post-meta">
<time datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished">
{{ page.date | date: "%B %-d, %Y" }}
</time>
{% if page.last_modified_at %}
<time datetime="{{ page.last_modified_at | date_to_xmlschema }}" itemprop="dateModified">
Updated {{ page.last_modified_at | date: "%B %-d, %Y" }}
</time>
{% endif %}
{% for category in page.categories %}
<a href="/category/{{ category | slugify }}/" class="category-link">
{{ category }}
</a>
{% endfor %}
</div>
<h1 itemprop="headline">{{ page.title }}</h1>
{% if page.description %}
<p class="post-description" itemprop="description">{{ page.description }}</p>
{% endif %}
{% if page.author %}
<div class="post-author" itemprop="author" itemscope itemtype="https://schema.org/Person">
<span itemprop="name">{{ page.author }}</span>
</div>
{% endif %}
{% if page.image %}
<img
src="{{ page.image | relative_url }}"
alt="{{ page.title }}"
itemprop="image"
class="post-hero"
>
{% endif %}
</header>
<div class="post-content" itemprop="articleBody">
{{ content }}
</div>
<footer class="post-footer">
{% if page.tags.size > 0 %}
<div class="post-tags">
{% for tag in page.tags %}
<a href="/tag/{{ tag | slugify }}/" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
<nav class="post-nav" aria-label="Post navigation">
{% if page.previous %}
<a href="{{ page.previous.url }}" class="post-nav__prev">
← {{ page.previous.title }}
</a>
{% endif %}
{% if page.next %}
<a href="{{ page.next.url }}" class="post-nav__next">
{{ page.next.title }} →
</a>
{% endif %}
</nav>
</footer>
</article>
This template uses page.date, page.last_modified_at, page.categories, page.title, page.description, page.author, page.image, content, page.tags, page.previous, and page.next — the full set of standard variables that any well-structured post file provides.
Common pitfalls with Jekyll variables
Forgetting that site.posts is sorted newest-first. If you want oldest-first, use {{ site.posts | reverse }}.
Confusing page.url with page.id. The URL is what you use in links; the ID is an internal reference used by some plugins. They look similar but are not interchangeable.
Using page.content inside the post layout. In layout files, use content (no page. prefix) to get the rendered HTML. page.content gives you the raw Markdown source, which is rarely what you want.
Accessing collection variables before declaring the collection. If site.projects returns nil, check that projects is declared under collections: in _config.yml and that the _projects/ directory exists.
Expecting forloop.first to detect the first post in site.posts. The forloop variable is scoped to the current {% for %} loop iteration. It does not know anything about the global list of posts — it only knows its position in the current loop run.
Keeping this variable reference bookmarked saves time whenever you are building or debugging a Jekyll template. The full list of built-in variables is also documented in the official Jekyll docs with additional detail on edge cases.
Variables in collection documents
Collection documents have all the standard page.* variables plus some collection-specific ones. When you loop over site.themes (a collection), each item exposes:
{{ theme.collection }} → "themes" (the collection name)
{{ theme.relative_path }} → "_themes/minimal-mistakes.md"
{{ theme.path }} → full filesystem path
{{ theme.url }} → "/themes/minimal-mistakes/"
{{ theme.id }} → "/themes/minimal-mistakes"
{{ theme.title }} → from front matter
{{ theme.content }} → rendered HTML body
All front matter fields on the document also become available as variables, so a theme document with stars: 27000 exposes theme.stars. This is the basis of filtering and sorting collection documents:
{% assign popular = site.themes | where_exp: "t", "t.stars > 10000" | sort: "stars" | reverse %}
{% for theme in popular limit: 6 %}
<a href="{{ theme.url }}">{{ theme.title }} — ★ {{ theme.stars }}</a>
{% endfor %}
Generating variables at build time
Some variables cannot come from a static Markdown file — they need to be computed at build time. Jekyll’s plugin system lets you generate data and expose it through site.* or front matter variables during the build.
For example, to add a site.build_time variable that contains the formatted build timestamp:
# _plugins/build_time.rb
Jekyll::Hooks.register :site, :after_init do |site|
site.config['build_time'] = Time.now.strftime('%Y-%m-%d %H:%M')
end
After adding this plugin, {{ site.build_time }} is available in every template.
A more common use case is reading data from external sources during the build — an API response, a JSON file, or a generated data file — and making it available as site.data.something. The _data/ directory supports this natively without plugins: any YAML, JSON, CSV, or TSV file in _data/ becomes accessible as site.data.filename.
The scope of each variable type
Understanding the scope of each variable type prevents a common class of bugs in Jekyll templates.
site variables are global — the same values are available in every layout, include, and page throughout the entire build. Changing a site.* variable in one template does not affect others (Liquid is stateless), but reading from it in any file always returns the same value.
page variables are page-scoped — they represent the current document being rendered. When Jekyll renders _posts/my-post.md using _layouts/post.html, the page variable inside both files refers to the same post document. When a layout includes a partial ({% include sidebar.html %}), the page variable inside the include still refers to the same document — not the include file itself.
layout variables are layout-scoped — they come from the front matter of the current layout file, not the page. This is a subtle distinction: if a page uses layout: post and post.html uses layout: default, then within default.html, the layout variable holds the front matter from default.html, and page still holds the page’s front matter.
include variables are include-scoped — parameters passed via {% include file.html param="value" %} are available as include.param only inside that include file, not in calling templates or other includes.
The mental model: site is the broadest scope (the whole build), page is mid-scope (the current document), layout is the current wrapper, and include parameters are the narrowest (one function call).
Keeping this hierarchy clear while reading or debugging templates makes it much easier to understand why a variable holds the value it does and where to go to change it.
Extending variables with custom plugins
For advanced use cases, Jekyll’s plugin system lets you add custom variables that are not available by default. This is useful for computed values, external API data, or dynamic configuration.
Adding custom site-level variables
# _plugins/site_extensions.rb
module Jekyll
class SiteExtensions
def self.extend(site)
# Add a formatted build date
site.config['build_date'] = Time.now.strftime('%B %-d, %Y')
# Count published posts
site.config['post_count'] = site.posts.docs.select(&:published?).size
# Count themes in a collection
themes = site.collections['themes']
site.config['theme_count'] = themes ? themes.docs.size : 0
end
end
end
Jekyll::Hooks.register :site, :post_read do |site|
SiteExtensions.extend(site)
end
After adding this plugin, you can use {{ site.build_date }}, {{ site.post_count }}, and {{ site.theme_count }} in any template — values computed once at build time and available everywhere.
Adding custom page-level variables
For per-page computed values, use a page hook that runs after front matter is read:
# _plugins/reading_time.rb
Jekyll::Hooks.register [:posts, :pages], :post_convert do |page|
word_count = page.content.split.size
reading_time = [(word_count / 200.0).ceil, 1].max
page.data['reading_time'] = reading_time
end
This makes {{ page.reading_time }} available on every post and page, computed from the actual word count rather than requiring you to set it manually in front matter.
Custom variables created by plugins follow the same scoping rules as front matter variables — site.your_variable for site-level, page.your_variable for page-level — so they work seamlessly alongside standard Jekyll variables in any template.
Understanding all available variables and how to extend them gives you complete control over what data is available in your Jekyll templates, making it possible to build sophisticated themes and content structures without reaching for a dynamic backend.
Variables as the backbone of Jekyll templates
Every Jekyll template you write is fundamentally a question: what data do I have, and how do I transform and display it? Variables are the answers to that question. The more fluent you become with site.*, page.*, layout.*, and content, the faster you can build new templates and debug existing ones. The best way to develop this fluency is to read the templates of well-maintained open-source Jekyll themes — see how they handle missing values with defaults, how they construct URLs using relative_url, and how they combine variables with Liquid filters to produce clean, reliable output across every possible page configuration. Variables are the vocabulary; templates are the sentences. Mastering one makes the other straightforward.
Variables are well-documented in Jekyll’s official documentation at jekyllrb.com/docs/variables/, and that reference is worth keeping open when building templates. The official docs and this reference together cover every variable you will encounter in day-to-day Jekyll development.