Home Blog Jekyll Variables Reference: site, page, layout, and More
Tutorial

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 Variables Reference: site, page, layout, and More

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.

Share LinkedIn