<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://jekyllhub.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jekyllhub.com/" rel="alternate" type="text/html" /><updated>2026-07-08T06:11:31+00:00</updated><id>https://jekyllhub.com/feed.xml</id><title type="html">JekyllHub</title><subtitle>The premier marketplace for Jekyll themes. Browse, preview, and download stunning themes for your next project.</subtitle><author><name>JekyllHub</name><email>hello@jekyllhub.com</email></author><entry><title type="html">Jekyll vs Gatsby: Which Static Site Generator Should You Choose in 2026?</title><link href="https://jekyllhub.com/comparison/2026/07/03/jekyll-vs-gatsby/" rel="alternate" type="text/html" title="Jekyll vs Gatsby: Which Static Site Generator Should You Choose in 2026?" /><published>2026-07-03T00:00:00+00:00</published><updated>2026-07-03T00:00:00+00:00</updated><id>https://jekyllhub.com/comparison/2026/07/03/jekyll-vs-gatsby</id><content type="html" xml:base="https://jekyllhub.com/comparison/2026/07/03/jekyll-vs-gatsby/"><![CDATA[<p>Jekyll and Gatsby both produce fast, static websites — but they come from completely different worlds. Jekyll is a mature, Ruby-based generator built for simplicity. Gatsby is a React-based framework built for power. Choosing the wrong one can mean weeks of unnecessary complexity or hitting a ceiling too early.</p>

<p>Here is an honest, practical comparison.</p>

<h2 id="what-is-jekyll">What is Jekyll?</h2>

<p>Jekyll is a static site generator written in Ruby and officially supported by GitHub. You write content in Markdown, define layouts in Liquid templates, and Jekyll outputs a folder of plain HTML files. No JavaScript framework, no GraphQL, no build pipeline required.</p>

<p>It has been around since 2008 and powers hundreds of thousands of sites — including the official GitHub Pages platform.</p>

<p><strong>Best for:</strong> blogs, documentation sites, portfolios, small business sites, and anyone who wants simplicity over features.</p>

<h2 id="what-is-gatsby">What is Gatsby?</h2>

<p>Gatsby is a React-based static site framework that uses GraphQL as its data layer. It pulls content from anywhere — Markdown files, headless CMSs, REST APIs, databases — and compiles everything into optimised static files with automatic code splitting, image optimisation, and prefetching baked in.</p>

<p>Netlify acquired Gatsby in 2023. The framework is still actively maintained but is no longer the default choice for new React-based static projects, with Astro and Next.js (static export) taking significant market share.</p>

<p><strong>Best for:</strong> content-heavy sites, e-commerce storefronts, sites pulling from multiple data sources, and teams already working in React.</p>

<h2 id="build-speed">Build speed</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Jekyll</th>
      <th>Gatsby</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Small site (&lt; 100 pages)</td>
      <td>&lt; 5 seconds</td>
      <td>15–45 seconds</td>
    </tr>
    <tr>
      <td>Medium site (500 pages)</td>
      <td>10–30 seconds</td>
      <td>60–120 seconds</td>
    </tr>
    <tr>
      <td>Large site (5,000+ pages)</td>
      <td>2–5 minutes</td>
      <td>5–15 minutes</td>
    </tr>
    <tr>
      <td>Incremental builds</td>
      <td>Yes (with flag)</td>
      <td>Yes (experimental)</td>
    </tr>
  </tbody>
</table>

<p>Jekyll is significantly faster to build, especially on small and medium sites. Gatsby’s build times grow steeply with content volume because of its GraphQL data layer and image processing pipeline.</p>

<h2 id="learning-curve">Learning curve</h2>

<p>Jekyll requires knowledge of Liquid templating and YAML front matter — both are simple and learnable in an afternoon. No JavaScript knowledge required beyond optional enhancements.</p>

<p>Gatsby requires React, GraphQL, and an understanding of Gatsby’s plugin ecosystem and data layer. If you already know React it is approachable. If you do not, it is a steep entry point just to publish a blog.</p>

<p><strong>Winner for beginners:</strong> Jekyll by a wide margin.</p>

<h2 id="themes-and-design">Themes and design</h2>

<p>Jekyll has a large ecosystem of free themes on GitHub, RubyGems, and marketplaces like JekyllHub. Themes are plain HTML/CSS/Liquid — easy to read, modify, and extend without tooling.</p>

<p>Gatsby themes are React components. They are more powerful (you can compose and shadow components) but require React knowledge to customise meaningfully. The Gatsby theme ecosystem is smaller and less active than it was in 2020–2022.</p>

<p><strong>Winner for theme selection:</strong> Jekyll.</p>

<h2 id="plugins-and-ecosystem">Plugins and ecosystem</h2>

<p>Gatsby’s plugin ecosystem was once its biggest advantage — thousands of source and transformer plugins connect to every conceivable data source. That ecosystem is still there but growth has slowed since the Netlify acquisition.</p>

<p>Jekyll’s plugin ecosystem is smaller but covers all common needs: SEO, sitemaps, feeds, pagination, image optimisation, and syntax highlighting. The <code class="language-plaintext highlighter-rouge">jekyll-*</code> gem namespace has over 1,000 plugins.</p>

<p><strong>Winner for integrations:</strong> Gatsby (still), though the gap has narrowed.</p>

<h2 id="hosting">Hosting</h2>

<p>Both deploy well to Netlify, Cloudflare Pages, and Vercel. Jekyll has one unique advantage: GitHub Pages hosts it natively, for free, with zero configuration. Push to a repository and your site is live.</p>

<p>Gatsby does not run on GitHub Pages natively (you need a GitHub Actions workflow to build and deploy).</p>

<p><strong>Winner for free hosting:</strong> Jekyll.</p>

<h2 id="performance">Performance</h2>

<p>Both generators produce static HTML, which means excellent performance out of the box. Gatsby adds automatic image optimisation via its <code class="language-plaintext highlighter-rouge">gatsby-image</code> / <code class="language-plaintext highlighter-rouge">gatsby-plugin-image</code> plugin, lazy loading, and link prefetching, which can push Lighthouse scores even higher.</p>

<p>For typical sites, both score 90+ on Core Web Vitals. Gatsby has a slight edge for image-heavy sites where its built-in processing pipeline matters.</p>

<p><strong>Winner for out-of-the-box performance:</strong> Gatsby (marginally).</p>

<h2 id="when-to-choose-jekyll">When to choose Jekyll</h2>

<ul>
  <li>You want a blog, portfolio, or documentation site with minimal setup</li>
  <li>You are not a JavaScript developer</li>
  <li>You want to host for free on GitHub Pages</li>
  <li>Your site has fewer than a few thousand pages</li>
  <li>You want themes you can understand and modify without a build step</li>
</ul>

<h2 id="when-to-choose-gatsby">When to choose Gatsby</h2>

<ul>
  <li>You are already building in React and want static output</li>
  <li>Your content comes from multiple sources (CMS + API + Markdown)</li>
  <li>You need advanced image optimisation built into the framework</li>
  <li>You are building a content-heavy e-commerce or news site</li>
  <li>Your team has React expertise and needs component composition</li>
</ul>

<h2 id="the-bottom-line">The bottom line</h2>

<p>For most developers starting a blog, portfolio, or documentation site, Jekyll is the right choice. It is simpler, faster to set up, and cheaper to host. The themes are better, the GitHub Pages integration is seamless, and you will spend your time on content — not tooling.</p>

<p>If you are building something complex in React — pulling from a headless CMS, composing content from multiple APIs, or need advanced image processing — Gatsby earns its complexity. But in 2026, Astro is a strong contender for that use case too.</p>

<p>Browse <a href="/themes/">free and premium Jekyll themes on JekyllHub</a> to get started.</p>

<h2 id="the-react-dependency-what-it-means-in-practice">The React dependency: what it means in practice</h2>

<p>Gatsby’s React dependency is its most consequential practical characteristic. Every Gatsby site is a React application — pages are React components, layouts are React components, even navigation and footers are React components. The benefit is access to React’s component model, hooks, and the broader npm ecosystem. The cost is a build pipeline that involves Babel, Webpack, React, GraphQL, and Gatsby’s own plugin and API layers.</p>

<p>For a developer fluent in React, this stack is comfortable. For a developer who wants to write Markdown and publish content, it is significant overhead. Installing Gatsby, understanding its GraphQL data layer, configuring plugins through <code class="language-plaintext highlighter-rouge">gatsby-config.js</code>, and debugging Webpack issues are all meaningfully more complex than the Jekyll equivalent (<code class="language-plaintext highlighter-rouge">bundle install</code>, write Markdown, <code class="language-plaintext highlighter-rouge">bundle exec jekyll serve</code>).</p>

<p>The complexity has a maintenance dimension too. Gatsby sites accumulate dependencies — gatsby plugins for every feature, npm packages for every utility — that require ongoing updates. A Gatsby site unattended for a year typically has dozens of outdated dependencies and potential breaking changes from Gatsby’s own version updates. A Jekyll site unattended for a year is typically still functional with a single <code class="language-plaintext highlighter-rouge">bundle update</code>.</p>

<h2 id="graphql-as-a-content-layer-powerful-but-heavy">GraphQL as a content layer: powerful but heavy</h2>

<p>Gatsby’s GraphQL layer is genuinely innovative. Every data source — local files, CMS APIs, remote JSON, image metadata — is unified into a single queryable graph. A page component queries exactly the data it needs; Gatsby’s data layer fetches it during the build. The resulting pages are fast because they include only the data they need, not a full response from an API.</p>

<p>Jekyll’s data model is simpler and less abstract. Content comes from front matter, Markdown files, <code class="language-plaintext highlighter-rouge">_data/</code> YAML files, and <code class="language-plaintext highlighter-rouge">site.*</code> variables — all accessed with straightforward Liquid syntax without querying. For sites with complex content relationships across many data sources, Gatsby’s unified data layer is genuinely more powerful. For sites with a standard blog structure — posts with metadata, categories, tags — Jekyll’s simpler model requires far less configuration to achieve the same result.</p>

<p>The GraphQL dependency also affects debugging. A Gatsby data error that traces back through a plugin’s GraphQL resolver can be difficult to diagnose without understanding Gatsby’s internal data layer architecture. A Jekyll template error that references a missing front matter field produces a clear, readable error message.</p>

<h2 id="image-processing-gatsbys-genuine-strength">Image processing: Gatsby’s genuine strength</h2>

<p>Gatsby’s image processing pipeline is a legitimate advantage for image-heavy sites. The <code class="language-plaintext highlighter-rouge">gatsby-plugin-image</code> component automatically generates multiple image sizes, converts to WebP, applies lazy loading, prevents layout shift with placeholder images, and selects the optimal size for the visitor’s viewport — all without manual configuration. The result is excellent Core Web Vitals scores on pages with many images, without the image optimisation work that Jekyll requires.</p>

<p>Jekyll’s image story requires more manual effort. <code class="language-plaintext highlighter-rouge">jekyll-picture-tag</code> provides a similar pipeline but requires configuration and is not enabled by default. Many Jekyll sites skip responsive images entirely, serving a single image size that is larger than necessary on mobile. The gap between Gatsby’s automatic image optimisation and Jekyll’s default “copy image unchanged” approach is real and significant for photographic sites.</p>

<p>For a photography portfolio, a recipe blog with multiple images per post, or a design showcase where image quality and loading performance are paramount, Gatsby’s image pipeline is worth the overhead. For a text-primarily blog or a technical documentation site where images are incidental, Jekyll’s image story is adequate.</p>

<h2 id="migration-from-gatsby-to-jekyll">Migration from Gatsby to Jekyll</h2>

<p>The shift from Gatsby to Jekyll — often motivated by maintenance fatigue or a desire for simpler tooling — is a manageable but real migration. Your Markdown content files port directly; Jekyll and Gatsby both use Markdown with front matter, and the front matter conventions are similar enough that minimal renaming is needed. GraphQL queries in Gatsby page components have no Jekyll equivalent, so any page that fetches data from a CMS API requires reconfiguration against a Jekyll-compatible data source.</p>

<p>Component-based layouts in React become Liquid templates and includes. The logic is equivalent — conditional rendering, iteration, slot-based content injection — but the syntax is different. A developer comfortable with both React and Liquid can typically port a Gatsby theme to Jekyll in a week; a developer who has only used Gatsby takes longer.</p>

<p>The reverse migration (Jekyll to Gatsby) is similarly feasible for content; the main investment is building or adapting a Gatsby theme to match your existing design and configuring the GraphQL data layer for your content structure.</p>

<h2 id="the-verdict-for-2026">The verdict for 2026</h2>

<p>Gatsby’s best years were 2018-2022, when it pioneered the concept of a React-based static site generator and built a large ecosystem around the idea. In 2023-2024, Astro emerged with a similar value proposition but lighter weight and the island architecture model. By 2026, developers choosing between a JavaScript-ecosystem static site generator and Jekyll are increasingly choosing Astro rather than Gatsby because Astro’s developer experience is better and its output is lighter.</p>

<p>For Jekyll vs Gatsby specifically: if you are building a content site, Jekyll is the simpler, lower-maintenance, lower-cost choice. If you are committed to React and need Gatsby-specific features (its image pipeline, its plugin ecosystem, GraphQL data layer), Gatsby remains the right tool for that use case — but those requirements are uncommon in purely content-driven sites.</p>

<p>The practical guidance: start with Jekyll unless you have a specific reason for Gatsby. You can always migrate to a React-based framework if the project’s requirements evolve to require it — and having a clean, well-structured Jekyll site as the starting point makes that migration easier than starting with a complex Gatsby configuration that you later want to simplify.</p>]]></content><author><name>Marcus Webb</name></author><category term="Comparison" /><summary type="html"><![CDATA[A straight comparison of Jekyll and Gatsby — build speed, themes, learning curve, hosting, and which one is right for your next project in 2026.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/jekyll-vs-gatsby.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/jekyll-vs-gatsby.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyll Static Files and Assets: How to Manage Images, CSS, and JS</title><link href="https://jekyllhub.com/tutorial/2026/07/02/jekyll-static-files-assets/" rel="alternate" type="text/html" title="Jekyll Static Files and Assets: How to Manage Images, CSS, and JS" /><published>2026-07-02T00:00:00+00:00</published><updated>2026-07-02T00:00:00+00:00</updated><id>https://jekyllhub.com/tutorial/2026/07/02/jekyll-static-files-assets</id><content type="html" xml:base="https://jekyllhub.com/tutorial/2026/07/02/jekyll-static-files-assets/"><![CDATA[<p>Jekyll processes some files (Markdown, Liquid templates, Sass) and copies others unchanged. Understanding which is which — and how to reference assets correctly in your templates — is fundamental to building Jekyll sites that work properly across all environments.</p>

<h2 id="processed-files-vs-static-files">Processed files vs static files</h2>

<p>Jekyll handles two categories of files:</p>

<p><strong>Processed files</strong> — files with YAML front matter, or files in special directories (<code class="language-plaintext highlighter-rouge">_posts/</code>, <code class="language-plaintext highlighter-rouge">_layouts/</code>, <code class="language-plaintext highlighter-rouge">_includes/</code>, <code class="language-plaintext highlighter-rouge">_sass/</code>). Jekyll runs these through its build pipeline: Liquid templating, Markdown conversion, Sass compilation.</p>

<p><strong>Static files</strong> — everything else. Jekyll copies them to <code class="language-plaintext highlighter-rouge">_site/</code> unchanged. Images, fonts, JavaScript files, PDFs, videos — all copied as-is.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my-jekyll-site/
├── _posts/           ← processed (has front matter)
├── _sass/            ← processed (Sass compilation)
├── assets/
│   ├── images/       ← static (copied unchanged)
│   ├── fonts/        ← static (copied unchanged)
│   ├── js/           ← static (copied unchanged)
│   └── css/
│       └── main.scss ← processed (has front matter → compiled to CSS)
</code></pre></div></div>

<h2 id="the-assets-folder-convention">The assets/ folder convention</h2>

<p>While Jekyll has no hard requirement for folder naming, <code class="language-plaintext highlighter-rouge">assets/</code> is the universal convention for static files. Most themes use:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>assets/
├── css/
│   └── main.scss        ← entry point for Sass (has front matter)
├── js/
│   ├── main.js
│   ├── bookmarks.js
│   └── search.js
├── images/
│   ├── logo.png
│   ├── social-card.png
│   ├── favicon.ico
│   └── blog/
│       ├── post-cover.webp
│       └── another-cover.webp
└── fonts/
    └── inter.woff2
</code></pre></div></div>

<p>Jekyll copies everything in <code class="language-plaintext highlighter-rouge">assets/</code> to <code class="language-plaintext highlighter-rouge">_site/assets/</code> during build. A file at <code class="language-plaintext highlighter-rouge">assets/images/logo.png</code> is served at <code class="language-plaintext highlighter-rouge">https://example.com/assets/images/logo.png</code>.</p>

<h2 id="referencing-assets-in-templates">Referencing assets in templates</h2>

<h3 id="the-relative_url-filter">The relative_url filter</h3>

<p>Always use <code class="language-plaintext highlighter-rouge">relative_url</code> when referencing assets in Liquid templates:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;link rel="stylesheet" href="<span class="p">{{</span><span class="w"> </span><span class="s1">'/assets/css/main.css'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>"&gt;
&lt;script src="<span class="p">{{</span><span class="w"> </span><span class="s1">'/assets/js/main.js'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>" defer&gt;&lt;/script&gt;
&lt;img src="<span class="p">{{</span><span class="w"> </span><span class="s1">'/assets/images/logo.png'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>" alt="Logo"&gt;

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">relative_url</code> prepends <code class="language-plaintext highlighter-rouge">site.baseurl</code> to the path. If your site has <code class="language-plaintext highlighter-rouge">baseurl: ""</code> (root domain), it changes nothing. If your site lives at a subdirectory (<code class="language-plaintext highlighter-rouge">baseurl: "/my-project"</code>), it correctly produces <code class="language-plaintext highlighter-rouge">/my-project/assets/images/logo.png</code>.</p>

<p>Without <code class="language-plaintext highlighter-rouge">relative_url</code>, assets break on sites with a non-empty <code class="language-plaintext highlighter-rouge">baseurl</code>.</p>

<h3 id="the-absolute_url-filter">The absolute_url filter</h3>

<p>For Open Graph tags, sitemaps, or any place that needs a full URL:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;meta property="og:image" content="<span class="p">{{</span><span class="w"> </span><span class="s1">'/assets/images/social-card.png'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">absolute_url</span><span class="w"> </span><span class="p">}}</span>"&gt;

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">absolute_url</code> prepends both <code class="language-plaintext highlighter-rouge">site.url</code> and <code class="language-plaintext highlighter-rouge">site.baseurl</code>.</p>

<h3 id="in-markdown-content">In Markdown content</h3>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">![</span><span class="nv">Hero image</span><span class="p">](</span><span class="sx">/assets/images/blog/hero.webp</span><span class="p">)</span>

<span class="p">[</span><span class="nv">Download PDF</span><span class="p">](</span><span class="sx">/assets/files/guide.pdf</span><span class="p">)</span>
</code></pre></div></div>

<p>Note: Markdown links do not go through <code class="language-plaintext highlighter-rouge">relative_url</code>. If you have a <code class="language-plaintext highlighter-rouge">baseurl</code>, use the HTML <code class="language-plaintext highlighter-rouge">&lt;img&gt;</code> tag inside your Markdown, or ensure your permalink structure accounts for the base URL.</p>

<h3 id="in-scsscss">In SCSS/CSS</h3>

<p>Reference assets from CSS using relative paths from the CSS file’s location. Since <code class="language-plaintext highlighter-rouge">assets/css/main.scss</code> is in <code class="language-plaintext highlighter-rouge">assets/css/</code>, fonts and images are reached with <code class="language-plaintext highlighter-rouge">../</code>:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@font-face</span> <span class="p">{</span>
  <span class="nl">font-family</span><span class="p">:</span> <span class="s2">"Inter"</span><span class="p">;</span>
  <span class="nl">src</span><span class="p">:</span> <span class="sx">url("../fonts/inter.woff2")</span> <span class="nf">format</span><span class="p">(</span><span class="s2">"woff2"</span><span class="p">);</span>
<span class="p">}</span>

<span class="nc">.hero</span> <span class="p">{</span>
  <span class="nl">background-image</span><span class="p">:</span> <span class="sx">url("../images/hero-bg.webp")</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="working-with-images">Working with images</h2>

<h3 id="file-formats-in-2026">File formats in 2026</h3>

<ul>
  <li><strong>WebP</strong> — best default choice. 25–35% smaller than JPEG at equivalent quality. Supported by all modern browsers.</li>
  <li><strong>AVIF</strong> — even smaller than WebP but slower to encode. Good for images you generate offline.</li>
  <li><strong>JPEG</strong> — use for photographs when WebP is not an option.</li>
  <li><strong>PNG</strong> — use for images requiring transparency or exact colours (logos, icons).</li>
  <li><strong>SVG</strong> — use for logos, icons, and illustrations. Infinitely scalable, tiny file size.</li>
</ul>

<h3 id="converting-images-to-webp">Converting images to WebP</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Single file</span>
cwebp <span class="nt">-q</span> 85 image.jpg <span class="nt">-o</span> image.webp

<span class="c"># Batch convert all JPEGs</span>
find assets/images <span class="nt">-name</span> <span class="s2">"*.jpg"</span> <span class="nt">-exec</span> sh <span class="nt">-c</span>   <span class="s1">'cwebp -q 85 "$1" -o "${1%.jpg}.webp"'</span> _ <span class="o">{}</span> <span class="se">\;</span>

<span class="c"># Using ImageMagick</span>
mogrify <span class="nt">-format</span> webp <span class="nt">-quality</span> 85 assets/images/blog/<span class="k">*</span>.jpg
</code></pre></div></div>

<h3 id="responsive-images-with-srcset">Responsive images with srcset</h3>

<p>For images that appear at different sizes across screen widths:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span>
  <span class="na">src=</span><span class="s">"/assets/images/hero-800.webp"</span>
  <span class="na">srcset=</span><span class="s">"
    /assets/images/hero-400.webp 400w,
    /assets/images/hero-800.webp 800w,
    /assets/images/hero-1200.webp 1200w
  "</span>
  <span class="na">sizes=</span><span class="s">"(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 800px"</span>
  <span class="na">alt=</span><span class="s">"Hero image"</span>
  <span class="na">width=</span><span class="s">"800"</span>
  <span class="na">height=</span><span class="s">"450"</span>
  <span class="na">loading=</span><span class="s">"lazy"</span>
<span class="nt">&gt;</span>
</code></pre></div></div>

<p>Generate multiple sizes during your build process (using Pillow, ImageMagick, or a Jekyll plugin like <code class="language-plaintext highlighter-rouge">jekyll-responsive-image</code>).</p>

<h3 id="always-specify-width-and-height">Always specify width and height</h3>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"{{ post.image | relative_url }}"</span> <span class="na">alt=</span><span class="s">"{{ post.title }}"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"450"</span><span class="nt">&gt;</span>

</code></pre></div></div>

<p>Width and height attributes prevent Cumulative Layout Shift (CLS) — a Core Web Vitals metric — by letting the browser reserve the correct space before the image loads.</p>

<h3 id="lazy-loading">Lazy loading</h3>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- Below-the-fold images — lazy load --&gt;</span>
<span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"..."</span> <span class="na">alt=</span><span class="s">"..."</span> <span class="na">loading=</span><span class="s">"lazy"</span><span class="nt">&gt;</span>

<span class="c">&lt;!-- Above-the-fold images (hero, LCP element) — eager load --&gt;</span>
<span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"..."</span> <span class="na">alt=</span><span class="s">"..."</span> <span class="na">loading=</span><span class="s">"eager"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>Use <code class="language-plaintext highlighter-rouge">loading="lazy"</code> on images that are not visible when the page loads. Never use it on your LCP (Largest Contentful Paint) element — the hero image or main post image.</p>

<h2 id="the-sitestatic_files-variable">The site.static_files variable</h2>

<p>Jekyll provides a <code class="language-plaintext highlighter-rouge">site.static_files</code> variable that lists all static files (files copied without processing):</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">file</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.static_files</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">file</span><span class="p">.</span><span class="nv">path</span><span class="w"> </span><span class="p">}}</span>          → /assets/images/logo.png
  <span class="p">{{</span><span class="w"> </span><span class="nv">file</span><span class="p">.</span><span class="nv">basename</span><span class="w"> </span><span class="p">}}</span>      → logo
  <span class="p">{{</span><span class="w"> </span><span class="nv">file</span><span class="p">.</span><span class="nv">name</span><span class="w"> </span><span class="p">}}</span>          → logo.png
  <span class="p">{{</span><span class="w"> </span><span class="nv">file</span><span class="p">.</span><span class="nv">extname</span><span class="w"> </span><span class="p">}}</span>       → .png
  <span class="p">{{</span><span class="w"> </span><span class="nv">file</span><span class="p">.</span><span class="nv">modified_time</span><span class="w"> </span><span class="p">}}</span> → last modified date
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Filter by extension or path:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">images</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">static_files</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where_exp</span><span class="p">:</span><span class="w"> </span><span class="s2">"f"</span><span class="p">,</span><span class="w"> </span><span class="s2">"f.extname == '.webp'"</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">image</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">images</span><span class="w"> </span><span class="p">%}</span>
  &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">image</span><span class="p">.</span><span class="nv">path</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>" alt="<span class="p">{{</span><span class="w"> </span><span class="nv">image</span><span class="p">.</span><span class="nv">basename</span><span class="w"> </span><span class="p">}}</span>"&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>This is useful for auto-generating image galleries from files in a folder.</p>

<h2 id="javascript-files">JavaScript files</h2>

<p>Jekyll copies JavaScript files unchanged — no bundling, no transpilation. For simple sites, this is fine:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>assets/js/
├── main.js         ← copied as-is
├── bookmarks.js    ← copied as-is
└── search.js       ← copied as-is
</code></pre></div></div>

<p>Reference in your layout:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"{{ '/assets/js/main.js' | relative_url }}"</span> <span class="na">defer</span><span class="nt">&gt;&lt;/script&gt;</span>

</code></pre></div></div>

<h3 id="using-liquid-in-javascript-files">Using Liquid in JavaScript files</h3>

<p>If you need Jekyll variables in JavaScript (e.g. site data), add front matter to the JS file:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="o">---</span>
<span class="o">---</span>
<span class="c1">// assets/js/search-data.js</span>

<span class="kd">var</span> <span class="nx">SITE_DATA</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ site.url }}</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">posts</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span><span class="o">%</span> <span class="k">for</span> <span class="nx">post</span> <span class="k">in</span> <span class="nx">site</span><span class="p">.</span><span class="nx">posts</span> <span class="o">%</span><span class="p">}</span>
    <span class="p">{</span>
      <span class="na">title</span><span class="p">:</span> <span class="p">{{</span> <span class="nx">post</span><span class="p">.</span><span class="nx">title</span> <span class="o">|</span> <span class="nx">jsonify</span> <span class="p">}},</span>
      <span class="na">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">{{ post.url | relative_url }}</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">excerpt</span><span class="p">:</span> <span class="p">{{</span> <span class="nx">post</span><span class="p">.</span><span class="nx">excerpt</span> <span class="o">|</span> <span class="nx">strip_html</span> <span class="o">|</span> <span class="na">truncatewords</span><span class="p">:</span> <span class="mi">30</span> <span class="o">|</span> <span class="nx">jsonify</span> <span class="p">}}</span>
    <span class="p">}{</span><span class="o">%</span> <span class="nx">unless</span> <span class="nx">forloop</span><span class="p">.</span><span class="nx">last</span> <span class="o">%</span><span class="p">},{</span><span class="o">%</span> <span class="nx">endunless</span> <span class="o">%</span><span class="p">}</span>
    <span class="p">{</span><span class="o">%</span> <span class="nx">endfor</span> <span class="o">%</span><span class="p">}</span>
  <span class="p">]</span>
<span class="p">};</span>

</code></pre></div></div>

<p>Jekyll processes this file through Liquid (because of the front matter) and outputs JavaScript with the values baked in at build time.</p>

<h3 id="minifying-javascript">Minifying JavaScript</h3>

<p>Jekyll does not minify JS. Options:</p>

<ol>
  <li><strong>Minify manually</strong> and commit the minified file</li>
  <li><strong>Use a build step</strong> (esbuild, rollup, or webpack) before Jekyll runs</li>
  <li><strong>Use a CDN</strong> for third-party libraries and serve your own JS unminified</li>
</ol>

<p>For most Jekyll blogs, unminified JS is fine — the files are small and HTTP/2 handles multiple small requests efficiently.</p>

<h2 id="favicon-files">Favicon files</h2>

<p>Place favicon files in your project root or <code class="language-plaintext highlighter-rouge">assets/</code> and reference in <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>assets/
├── favicon.ico          ← legacy IE fallback
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png  ← 180×180px for iOS
└── site.webmanifest     ← PWA manifest
</code></pre></div></div>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c">&lt;!-- _includes/head.html --&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"icon"</span> <span class="na">type=</span><span class="s">"image/x-icon"</span> <span class="na">href=</span><span class="s">"{{ '/favicon.ico' | relative_url }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"icon"</span> <span class="na">type=</span><span class="s">"image/png"</span> <span class="na">sizes=</span><span class="s">"32x32"</span> <span class="na">href=</span><span class="s">"{{ '/assets/favicon-32x32.png' | relative_url }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"icon"</span> <span class="na">type=</span><span class="s">"image/png"</span> <span class="na">sizes=</span><span class="s">"16x16"</span> <span class="na">href=</span><span class="s">"{{ '/assets/favicon-16x16.png' | relative_url }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"apple-touch-icon"</span> <span class="na">sizes=</span><span class="s">"180x180"</span> <span class="na">href=</span><span class="s">"{{ '/assets/apple-touch-icon.png' | relative_url }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"manifest"</span> <span class="na">href=</span><span class="s">"{{ '/assets/site.webmanifest' | relative_url }}"</span><span class="nt">&gt;</span>

</code></pre></div></div>

<h2 id="excluding-assets-from-the-build">Excluding assets from the build</h2>

<p>Use <code class="language-plaintext highlighter-rouge">exclude:</code> in <code class="language-plaintext highlighter-rouge">_config.yml</code> to prevent source files (like uncompressed originals) from appearing in <code class="language-plaintext highlighter-rouge">_site/</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">exclude</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">assets/images/originals/</span>   <span class="c1"># source files, not served</span>
  <span class="pi">-</span> <span class="s">assets/fonts/source/</span>       <span class="c1"># font source files</span>
  <span class="pi">-</span> <span class="s">tools/</span>                     <span class="c1"># build scripts</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">*.psd"</span>                    <span class="c1"># Photoshop files</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">*.ai"</span>                     <span class="c1"># Illustrator files</span>
</code></pre></div></div>

<h2 id="cache-busting">Cache busting</h2>

<p>Because browsers cache static assets aggressively, changing a CSS or JS file may not update for returning visitors. Options:</p>

<p><strong>Query string versioning</strong> — append a version number:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;link rel="stylesheet" href="<span class="p">{{</span><span class="w"> </span><span class="s1">'/assets/css/main.css'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>?v=<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">version</span><span class="w"> </span><span class="p">}}</span>"&gt;

</code></pre></div></div>

<p>Update <code class="language-plaintext highlighter-rouge">version: "1.2.3"</code> in <code class="language-plaintext highlighter-rouge">_config.yml</code> after significant changes.</p>

<p><strong>Filename hashing</strong> — include a hash in the filename (<code class="language-plaintext highlighter-rouge">main.abc123.css</code>). Requires a build pipeline; not supported natively by Jekyll.</p>

<p><strong>Long cache headers with immutable assets</strong> — in <code class="language-plaintext highlighter-rouge">_headers</code> (Cloudflare/Netlify):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/assets/*
  Cache-Control: public, max-age=31536000, immutable
</code></pre></div></div>

<p>Use very long cache on assets with versioned filenames; use no-cache on HTML.</p>

<h2 id="summary-what-goes-where">Summary: what goes where</h2>

<table>
  <thead>
    <tr>
      <th>File type</th>
      <th>Location</th>
      <th>How Jekyll handles it</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Blog posts</td>
      <td><code class="language-plaintext highlighter-rouge">_posts/</code></td>
      <td>Processed (Markdown → HTML)</td>
    </tr>
    <tr>
      <td>Page Markdown</td>
      <td><code class="language-plaintext highlighter-rouge">_pages/</code> or root</td>
      <td>Processed (Markdown → HTML)</td>
    </tr>
    <tr>
      <td>Layout HTML</td>
      <td><code class="language-plaintext highlighter-rouge">_layouts/</code></td>
      <td>Used as template, not output</td>
    </tr>
    <tr>
      <td>Include fragments</td>
      <td><code class="language-plaintext highlighter-rouge">_includes/</code></td>
      <td>Used as template, not output</td>
    </tr>
    <tr>
      <td>Sass partials</td>
      <td><code class="language-plaintext highlighter-rouge">_sass/</code></td>
      <td>Compiled (via entry point)</td>
    </tr>
    <tr>
      <td>Sass entry point</td>
      <td><code class="language-plaintext highlighter-rouge">assets/css/</code></td>
      <td>Compiled to CSS</td>
    </tr>
    <tr>
      <td>JavaScript</td>
      <td><code class="language-plaintext highlighter-rouge">assets/js/</code></td>
      <td>Copied unchanged</td>
    </tr>
    <tr>
      <td>Images</td>
      <td><code class="language-plaintext highlighter-rouge">assets/images/</code></td>
      <td>Copied unchanged</td>
    </tr>
    <tr>
      <td>Fonts</td>
      <td><code class="language-plaintext highlighter-rouge">assets/fonts/</code></td>
      <td>Copied unchanged</td>
    </tr>
    <tr>
      <td>Data files</td>
      <td><code class="language-plaintext highlighter-rouge">_data/</code></td>
      <td>Loaded as <code class="language-plaintext highlighter-rouge">site.data.*</code>, not output</td>
    </tr>
  </tbody>
</table>

<p>Understanding this table — what Jekyll processes vs what it copies — explains nearly every “why is my file not showing up” or “why is my template not working” issue you will encounter.</p>

<h2 id="organising-your-assets-folder-for-maintainability">Organising your assets folder for maintainability</h2>

<p>A well-organised <code class="language-plaintext highlighter-rouge">assets/</code> directory makes finding and updating files fast. The conventional structure is: <code class="language-plaintext highlighter-rouge">assets/css/</code> for compiled stylesheets, <code class="language-plaintext highlighter-rouge">assets/js/</code> for JavaScript files, <code class="language-plaintext highlighter-rouge">assets/images/</code> for site-wide images, <code class="language-plaintext highlighter-rouge">assets/fonts/</code> for self-hosted typefaces, and <code class="language-plaintext highlighter-rouge">assets/icons/</code> for SVG icons or favicon variants. Post-specific images often go in a post-images subfolder organised by year or post slug to prevent the root images directory from becoming unmanageable.</p>

<p>Naming conventions matter more as the asset library grows. Use lowercase hyphenated names for all files (<code class="language-plaintext highlighter-rouge">hero-background.webp</code> rather than <code class="language-plaintext highlighter-rouge">HeroBackground.WebP</code>). Include dimensions in image filenames when you serve multiple sizes (<code class="language-plaintext highlighter-rouge">logo-200w.webp</code>, <code class="language-plaintext highlighter-rouge">logo-400w.webp</code>). Prefix JavaScript files that are page-specific rather than sitewide (<code class="language-plaintext highlighter-rouge">post.js</code>, <code class="language-plaintext highlighter-rouge">home.js</code>, <code class="language-plaintext highlighter-rouge">theme-detail.js</code>) to distinguish them from libraries. These conventions make the assets directory self-documenting and reduce the time spent hunting for a specific file.</p>

<p>Version your asset files explicitly when they change significantly. Jekyll does not have a built-in asset fingerprinting system (unlike Webpack or Vite), so browser caching can cause visitors to see stale CSS or JavaScript after an update. The simplest workaround is a version query string in <code class="language-plaintext highlighter-rouge">_config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">asset_version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2025-12"</span>
</code></pre></div></div>

<p>Reference it in your layout: <code class="language-plaintext highlighter-rouge">main.css?v={{ site.asset_version }}</code>. When you update CSS significantly, bump the version string and the new filename breaks the browser cache for all visitors. This is not as elegant as hash-based fingerprinting but is simple, reliable, and sufficient for most Jekyll sites.</p>

<h2 id="handling-images-efficiently-in-jekyll">Handling images efficiently in Jekyll</h2>

<p>Images are the largest assets in most Jekyll sites and the primary target for performance optimisation. The goal is to serve each image at the smallest file size that maintains acceptable visual quality, in the correct format, at exactly the dimensions needed for the display context.</p>

<p>WebP is the correct format for photographs and complex images in 2025. It is smaller than JPEG at equivalent quality and supported by all modern browsers. PNG remains appropriate for logos, icons, and graphics with transparency or hard edges where lossless compression matters. Convert JPEG originals to WebP using ImageMagick or Squoosh before adding them to the assets folder — do not rely on on-the-fly conversion during builds, which is slow and increases build times.</p>

<p>For images that appear in multiple sizes — a cover image shown as a thumbnail on the blog index and full-width on the post page — provide multiple source files and use the <code class="language-plaintext highlighter-rouge">srcset</code> attribute in your HTML to let the browser select the appropriate size. The <code class="language-plaintext highlighter-rouge">&lt;picture&gt;</code> element with multiple <code class="language-plaintext highlighter-rouge">&lt;source&gt;</code> children gives you full control: WebP for modern browsers, JPEG fallback for older ones, and different crop ratios for different breakpoints if needed.</p>

<p>Store images in a dedicated folder rather than at the root of <code class="language-plaintext highlighter-rouge">_posts/</code>. Images co-located with post files work (Jekyll processes files in <code class="language-plaintext highlighter-rouge">_posts/</code> subdirectories) but create clutter and make bulk operations on all images harder. A single <code class="language-plaintext highlighter-rouge">assets/images/posts/</code> directory or an organisation by year (<code class="language-plaintext highlighter-rouge">assets/images/2025/</code>) keeps images findable and manageable.</p>

<h2 id="javascript-in-jekyll-the-right-amount">JavaScript in Jekyll: the right amount</h2>

<p>Jekyll’s JavaScript story is deliberate minimalism. The framework provides no JavaScript of its own — your theme and your customisations provide whatever JavaScript the site needs. This is a feature, not a limitation: it means you only load JavaScript you have consciously chosen to include.</p>

<p>Audit your theme’s JavaScript dependencies before using it. Open the network tab in browser DevTools and load the live demo. Check what scripts are loaded, how large they are, and whether they are render-blocking. A theme that loads jQuery (87KB gzipped) for dropdown menus that could be handled with 15 lines of vanilla JavaScript is carrying avoidable weight. A theme that loads a 50KB animation library for a hero section that animates only on first load is optimisable.</p>

<p>Modularise your JavaScript into page-specific files and load them conditionally. A post page should load <code class="language-plaintext highlighter-rouge">post.js</code> (table of contents, back-to-top button, copy-to-clipboard). The homepage loads <code class="language-plaintext highlighter-rouge">home.js</code> (carousel, testimonial rotation). The theme listing page loads <code class="language-plaintext highlighter-rouge">themes.js</code> (filtering, sorting, search). Loading all JavaScript on every page increases parse and execution time on pages that do not need it. Conditional loading — <code class="language-plaintext highlighter-rouge">{% if page.layout == 'post' %}&lt;script src="/assets/js/post.js" defer&gt;&lt;/script&gt;{% endif %}</code> in your layout — is a structural performance improvement that compounds across every page visit.</p>

<h2 id="static-files-as-a-deployment-advantage">Static files as a deployment advantage</h2>

<p>One of Jekyll’s most underappreciated properties is that every output file — HTML, CSS, JavaScript, images, fonts — is a static file that can be cached aggressively at the CDN edge. There are no API calls, no database queries, no server-side rendering that must complete before a response can be sent. Every resource the browser requests is pre-computed, pre-cached, and pre-positioned at an edge node near the visitor.</p>

<p>This architecture means a Jekyll site’s performance does not degrade under traffic spikes. The thousandth concurrent visitor gets the same response time as the first, because the response is a cached file delivered from a CDN node, not a dynamically generated page competing for server resources. For content that occasionally goes viral — a post that gets linked from a popular newsletter, a theme that gets featured on a high-traffic developer site — the static file architecture handles the traffic surge without additional cost or configuration.</p>

<p>Treat this architectural advantage as a reason to build your site correctly the first time: choose the right CDN host, configure caching headers properly, and optimise your build pipeline. The investment in static file architecture pays dividends every time a visitor loads a page, every time a search crawler indexes a URL, and every time traffic spikes beyond what a dynamic hosting tier would handle comfortably.</p>]]></content><author><name>Marcus Webb</name></author><category term="Tutorial" /><summary type="html"><![CDATA[How Jekyll handles static files and assets — the assets/ folder, referencing files in templates, image management, static_files, and optimising assets for production.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/jekyll-static-files-assets.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/jekyll-static-files-assets.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyll Drafts and Publishing Workflow: How to Manage Content</title><link href="https://jekyllhub.com/tutorial/2026/07/01/jekyll-drafts-publishing-workflow/" rel="alternate" type="text/html" title="Jekyll Drafts and Publishing Workflow: How to Manage Content" /><published>2026-07-01T00:00:00+00:00</published><updated>2026-07-01T00:00:00+00:00</updated><id>https://jekyllhub.com/tutorial/2026/07/01/jekyll-drafts-publishing-workflow</id><content type="html" xml:base="https://jekyllhub.com/tutorial/2026/07/01/jekyll-drafts-publishing-workflow/"><![CDATA[<p>Jekyll gives you several ways to keep content out of your public site while you work on it: the <code class="language-plaintext highlighter-rouge">_drafts/</code> folder, <code class="language-plaintext highlighter-rouge">published: false</code> front matter, and future-dated posts. Understanding these tools lets you build a proper editorial workflow without a CMS.</p>

<h2 id="the-_drafts-folder">The _drafts folder</h2>

<p>The simplest way to keep work-in-progress posts off your live site. Create a <code class="language-plaintext highlighter-rouge">_drafts/</code> directory at your project root and put unfinished posts there:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_drafts/
├── jekyll-drafts-publishing-workflow.md
├── ideas-for-q3.md
└── half-finished-tutorial.md
</code></pre></div></div>

<p>Unlike <code class="language-plaintext highlighter-rouge">_posts/</code>, draft filenames do not need a date prefix:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># _posts/ — date required
_posts/2026-08-09-my-post.md

# _drafts/ — no date
_drafts/my-post.md
</code></pre></div></div>

<h3 id="previewing-drafts-locally">Previewing drafts locally</h3>

<p>Normal <code class="language-plaintext highlighter-rouge">jekyll serve</code> ignores <code class="language-plaintext highlighter-rouge">_drafts/</code> entirely. To preview your drafts:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>jekyll serve <span class="nt">--drafts</span>
</code></pre></div></div>

<p>When <code class="language-plaintext highlighter-rouge">--drafts</code> is active, Jekyll assigns today’s date to draft posts and includes them in the site. You can see exactly how a draft will look when published.</p>

<h3 id="drafts-in-ciproduction">Drafts in CI/production</h3>

<p>Your CI/CD build command should never include <code class="language-plaintext highlighter-rouge">--drafts</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Correct — drafts excluded</span>
<span class="nv">JEKYLL_ENV</span><span class="o">=</span>production bundle <span class="nb">exec </span>jekyll build

<span class="c"># Wrong — would publish drafts</span>
<span class="nv">JEKYLL_ENV</span><span class="o">=</span>production bundle <span class="nb">exec </span>jekyll build <span class="nt">--drafts</span>
</code></pre></div></div>

<p>Drafts are never published accidentally as long as you do not pass <code class="language-plaintext highlighter-rouge">--drafts</code> to your production build.</p>

<h2 id="published-false">published: false</h2>

<p>An alternative to <code class="language-plaintext highlighter-rouge">_drafts/</code> — set <code class="language-plaintext highlighter-rouge">published: false</code> in any post’s front matter to exclude it from the build:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Work</span><span class="nv"> </span><span class="s">in</span><span class="nv"> </span><span class="s">Progress"</span>
<span class="na">date</span><span class="pi">:</span> <span class="s">2026-08-09</span>
<span class="na">published</span><span class="pi">:</span> <span class="no">false</span>
<span class="nn">---</span>

<span class="s">Content here will not appear on the live site.</span>
</code></pre></div></div>

<p>This works for posts in <code class="language-plaintext highlighter-rouge">_posts/</code> and pages anywhere on your site. Useful when:</p>
<ul>
  <li>You want to keep the file in <code class="language-plaintext highlighter-rouge">_posts/</code> (with a date) but not publish it yet</li>
  <li>You want to temporarily hide a published post without deleting it</li>
  <li>You want to keep old content for reference but remove it from the site</li>
</ul>

<p>Preview unpublished content locally:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>jekyll serve <span class="nt">--unpublished</span>
</code></pre></div></div>

<h2 id="future-dated-posts">Future-dated posts</h2>

<p>Jekyll excludes posts whose date is in the future by default. Write a post today, set a future date, and it will automatically appear on your site on that date — the next time your site builds.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">Scheduled</span><span class="nv"> </span><span class="s">Post"</span>
<span class="na">date</span><span class="pi">:</span> <span class="s">2026-09-01</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>This post will not appear in <code class="language-plaintext highlighter-rouge">site.posts</code> until September 1, 2026 (or whenever you trigger a build after that date).</p>

<p>Preview future posts locally:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>jekyll serve <span class="nt">--future</span>
</code></pre></div></div>

<h3 id="scheduling-posts-with-cicd">Scheduling posts with CI/CD</h3>

<p>Future posts are only published when your site rebuilds after their date. If you do not rebuild frequently, a future post will sit in your repository unpublished.</p>

<p>Set up a scheduled CI/CD build to rebuild daily. On Netlify:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Netlify → Site settings → Build hooks
# Add a build hook and schedule it with a cron service (cron-job.org)
# to trigger the hook daily at midnight
</code></pre></div></div>

<p>On GitHub Actions:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/scheduled-build.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Scheduled Build</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">schedule</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0</span><span class="nv"> </span><span class="s">0</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*"</span>   <span class="c1"># midnight UTC every day</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">ruby/setup-ruby@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">ruby-version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.2"</span>
          <span class="na">bundler-cache</span><span class="pi">:</span> <span class="no">true</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">JEKYLL_ENV=production bundle exec jekyll build</span>
      <span class="c1"># add your deploy step here</span>
</code></pre></div></div>

<h2 id="combining-methods">Combining methods</h2>

<p>The three approaches can be combined:</p>

<table>
  <thead>
    <tr>
      <th>Situation</th>
      <th>Method</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Early draft, no date yet</td>
      <td><code class="language-plaintext highlighter-rouge">_drafts/</code> folder</td>
    </tr>
    <tr>
      <td>Finished but not ready</td>
      <td><code class="language-plaintext highlighter-rouge">published: false</code> in <code class="language-plaintext highlighter-rouge">_posts/</code></td>
    </tr>
    <tr>
      <td>Ready but timed for later</td>
      <td>Future date in <code class="language-plaintext highlighter-rouge">_posts/</code></td>
    </tr>
    <tr>
      <td>Temporarily hidden live post</td>
      <td><code class="language-plaintext highlighter-rouge">published: false</code></td>
    </tr>
  </tbody>
</table>

<h2 id="a-practical-editorial-workflow-for-jekyll">A practical editorial workflow for Jekyll</h2>

<p>Here is a workflow that works well for solo writers and small teams:</p>

<h3 id="stage-1-idea-capture">Stage 1: Idea capture</h3>

<p>Create a file in <code class="language-plaintext highlighter-rouge">_drafts/</code> with just a title and outline:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">&lt;!-- _drafts/jekyll-pagination-guide.md --&gt;
---
</span>title: "Jekyll Pagination: The Complete Guide"
<span class="gh">tags: [jekyll, pagination, tutorial]
---
</span>
<span class="gu">## Outline</span>
<span class="p">
-</span> What is pagination
<span class="p">-</span> jekyll-paginate vs jekyll-paginate-v2
<span class="p">-</span> Setup
<span class="p">-</span> Customising the paginator
<span class="p">-</span> SEO considerations
</code></pre></div></div>

<h3 id="stage-2-writing">Stage 2: Writing</h3>

<p>Fill in the content in <code class="language-plaintext highlighter-rouge">_drafts/</code>. Preview with <code class="language-plaintext highlighter-rouge">jekyll serve --drafts</code> at any point.</p>

<h3 id="stage-3-review-and-polish">Stage 3: Review and polish</h3>

<p>Run a local preview and check:</p>
<ul>
  <li>All code examples work</li>
  <li>Images are in place</li>
  <li>Internal links resolve</li>
  <li>SEO: title, description, image set in front matter</li>
</ul>

<h3 id="stage-4-schedule-or-publish">Stage 4: Schedule or publish</h3>

<p><strong>Publish immediately:</strong> Move the file from <code class="language-plaintext highlighter-rouge">_drafts/</code> to <code class="language-plaintext highlighter-rouge">_posts/</code> and add today’s date:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mv </span>_drafts/jekyll-pagination-guide.md    _posts/2026-08-09-jekyll-pagination-guide.md
</code></pre></div></div>

<p><strong>Schedule for later:</strong> Move to <code class="language-plaintext highlighter-rouge">_posts/</code> with a future date:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mv </span>_drafts/jekyll-pagination-guide.md    _posts/2026-09-15-jekyll-pagination-guide.md
</code></pre></div></div>

<p><strong>Keep hidden while finalising:</strong> Move to <code class="language-plaintext highlighter-rouge">_posts/</code> with today’s date but <code class="language-plaintext highlighter-rouge">published: false</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">published</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">date</span><span class="pi">:</span> <span class="s">2026-08-09</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>Set <code class="language-plaintext highlighter-rouge">published: true</code> (or remove the line) when ready.</p>

<h3 id="stage-5-push-and-deploy">Stage 5: Push and deploy</h3>

<p>Commit and push. Your CI/CD pipeline builds and deploys the updated site.</p>

<h2 id="working-with-collaborators">Working with collaborators</h2>

<p>For teams using GitHub:</p>

<p><strong>Use pull requests for drafts.</strong> Writers work on a branch. Open a PR when the post is ready for review. Reviewers see a preview deployment (Netlify/Cloudflare/Vercel create these automatically). Merge when approved.</p>

<p><strong>Use <code class="language-plaintext highlighter-rouge">_drafts/</code> for long-running work.</strong> Content that spans multiple sessions lives safely in <code class="language-plaintext highlighter-rouge">_drafts/</code> in its own branch.</p>

<p><strong>Use <code class="language-plaintext highlighter-rouge">published: false</code> for quick feedback.</strong> Push to main with <code class="language-plaintext highlighter-rouge">published: false</code> to get a staging URL the reviewer can share without it going live.</p>

<h2 id="adding-a-headless-cms-for-non-technical-editors">Adding a headless CMS for non-technical editors</h2>

<p>If your team includes non-developers who are not comfortable with Git and Markdown, add Decap CMS (formerly Netlify CMS) for a visual editing interface:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># admin/config.yml</span>
<span class="na">collections</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">drafts</span>
    <span class="na">label</span><span class="pi">:</span> <span class="s">Drafts</span>
    <span class="na">folder</span><span class="pi">:</span> <span class="s">_drafts</span>
    <span class="na">create</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">slug</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{slug}}"</span>
    <span class="na">fields</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">{</span> <span class="nv">label</span><span class="pi">:</span> <span class="nv">Title</span><span class="pi">,</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">title</span><span class="pi">,</span> <span class="nv">widget</span><span class="pi">:</span> <span class="nv">string</span> <span class="pi">}</span>
      <span class="pi">-</span> <span class="pi">{</span> <span class="nv">label</span><span class="pi">:</span> <span class="nv">Body</span><span class="pi">,</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">body</span><span class="pi">,</span> <span class="nv">widget</span><span class="pi">:</span> <span class="nv">markdown</span> <span class="pi">}</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">posts</span>
    <span class="na">label</span><span class="pi">:</span> <span class="s">Posts</span>
    <span class="na">folder</span><span class="pi">:</span> <span class="s">_posts</span>
    <span class="na">create</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">slug</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{year}}-{{month}}-{{day}}-{{slug}}"</span>
    <span class="na">fields</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">{</span> <span class="nv">label</span><span class="pi">:</span> <span class="nv">Title</span><span class="pi">,</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">title</span><span class="pi">,</span> <span class="nv">widget</span><span class="pi">:</span> <span class="nv">string</span> <span class="pi">}</span>
      <span class="pi">-</span> <span class="pi">{</span> <span class="nv">label</span><span class="pi">:</span> <span class="nv">Publish Date</span><span class="pi">,</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">date</span><span class="pi">,</span> <span class="nv">widget</span><span class="pi">:</span> <span class="nv">datetime</span> <span class="pi">}</span>
      <span class="pi">-</span> <span class="pi">{</span> <span class="nv">label</span><span class="pi">:</span> <span class="nv">Published</span><span class="pi">,</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">published</span><span class="pi">,</span> <span class="nv">widget</span><span class="pi">:</span> <span class="nv">boolean</span><span class="pi">,</span> <span class="nv">default</span><span class="pi">:</span> <span class="nv">false</span> <span class="pi">}</span>
      <span class="pi">-</span> <span class="pi">{</span> <span class="nv">label</span><span class="pi">:</span> <span class="nv">Body</span><span class="pi">,</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">body</span><span class="pi">,</span> <span class="nv">widget</span><span class="pi">:</span> <span class="nv">markdown</span> <span class="pi">}</span>

</code></pre></div></div>

<p>Non-developers can write in a rich text editor and save as drafts. The <code class="language-plaintext highlighter-rouge">published: false</code> toggle keeps posts off the live site until ready.</p>

<h2 id="useful-command-reference">Useful command reference</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Normal serve (no drafts, no future posts)</span>
bundle <span class="nb">exec </span>jekyll serve

<span class="c"># Show all drafts</span>
bundle <span class="nb">exec </span>jekyll serve <span class="nt">--drafts</span>

<span class="c"># Show future-dated posts</span>
bundle <span class="nb">exec </span>jekyll serve <span class="nt">--future</span>

<span class="c"># Show unpublished posts (published: false)</span>
bundle <span class="nb">exec </span>jekyll serve <span class="nt">--unpublished</span>

<span class="c"># Show everything</span>
bundle <span class="nb">exec </span>jekyll serve <span class="nt">--drafts</span> <span class="nt">--future</span> <span class="nt">--unpublished</span>

<span class="c"># Production build (nothing extra)</span>
<span class="nv">JEKYLL_ENV</span><span class="o">=</span>production bundle <span class="nb">exec </span>jekyll build
</code></pre></div></div>

<h2 id="tips-for-a-smooth-workflow">Tips for a smooth workflow</h2>

<p><strong>Name draft files descriptively.</strong> Even without dates, <code class="language-plaintext highlighter-rouge">jekyll-pagination-complete-guide.md</code> is much easier to find than <code class="language-plaintext highlighter-rouge">draft-post-3.md</code>.</p>

<p><strong>Keep drafts short at first.</strong> Write the outline and key points, then expand. A complete outline in <code class="language-plaintext highlighter-rouge">_drafts/</code> is better than a blank file in <code class="language-plaintext highlighter-rouge">_posts/</code>.</p>

<p><strong>Commit drafts to Git.</strong> <code class="language-plaintext highlighter-rouge">_drafts/</code> should be in your repository, not gitignored. This gives you version history, backup, and branch-based collaboration.</p>

<p><strong>Set a date in front matter while drafting.</strong> Even if you are not ready to publish, set an estimated date in front matter so you can preview how it will sort among your posts.</p>

<p>Jekyll’s draft and publishing system is simple but complete. Once you have a consistent workflow, writing and scheduling content becomes as smooth as any CMS — with the added benefit of full version control.</p>

<h2 id="collaborative-workflows-with-jekyll-drafts">Collaborative workflows with Jekyll drafts</h2>

<p>For blogs with more than one author or editor, the drafts workflow needs structure beyond simply keeping files in <code class="language-plaintext highlighter-rouge">_drafts/</code>. Git branches provide the structure: each article in progress lives on its own branch, and the merge to main is the publishing action. This approach has several advantages over shared <code class="language-plaintext highlighter-rouge">_drafts/</code> folders — authors work independently without seeing each other’s work-in-progress, editors can review by checking out the branch or viewing a pull request diff, and the history of each article is cleanly isolated.</p>

<p>Configure GitHub Actions to deploy preview branches automatically. With Netlify or Cloudflare Pages connected to your repository, every push to any branch creates a preview URL. An author writing on a branch named <code class="language-plaintext highlighter-rouge">post/jekyll-seo-tips</code> gets a preview at <code class="language-plaintext highlighter-rouge">post-jekyll-seo-tips.yoursite.pages.dev</code>. They can share this link for editorial review without requiring the reviewer to run Jekyll locally. The merge to main triggers a production deployment. This workflow is robust, version-controlled, and requires no CMS software.</p>

<p>For teams where authors are not comfortable with Git branching, Decap CMS layered on top of Jekyll provides a graphical interface for the same workflow. Decap CMS creates a GitHub PR for each draft when the editorial workflow is enabled — non-technical authors write in a rich text editor, and the underlying mechanism is the same branch-per-draft Git model. The technical implementation is identical; only the interface changes.</p>

<h2 id="scheduling-posts-in-jekyll">Scheduling posts in Jekyll</h2>

<p>Jekyll does not have native post scheduling — it is a build tool, not a runtime server. Posts with future dates are excluded from the default build, which means scheduling requires triggering a build at the right time.</p>

<p>The cleanest scheduling approach is a GitHub Actions workflow with a cron trigger. Set up a workflow file that runs <code class="language-plaintext highlighter-rouge">bundle exec jekyll build</code> on a schedule — daily at midnight, for instance. Posts with dates up to and including that day are included in the build and thus appear on the site. Authors set the desired publication date in the post’s front matter and commit the file to the main branch; the scheduled build picks it up when the date arrives.</p>

<p>A sample GitHub Actions schedule trigger looks like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
  <span class="na">schedule</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s1">'</span><span class="s">0</span><span class="nv"> </span><span class="s">0</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*'</span>  <span class="c1"># runs at midnight UTC daily</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">main</span><span class="pi">]</span>
</code></pre></div></div>

<p>This runs the build on every push (for immediate publishing) and once daily at midnight (to publish scheduled posts). The combination ensures immediate deployment for posts dated today or earlier, and automatic publication for future-dated posts as their dates arrive.</p>

<p>Cloudflare Pages and Netlify both support build hooks — URLs you can POST to trigger a rebuild. Integrating a build hook with a CRON service (like EasyCron or GitHub Actions) gives you the same scheduled publishing capability on any host.</p>

<h2 id="handling-editorial-review-before-publishing">Handling editorial review before publishing</h2>

<p>For high-stakes content — sponsored posts, legal content, press releases, guest contributions — a formal editorial review step before publishing adds quality assurance that informal drafts workflows lack. The Git pull request model provides this naturally: the author submits a PR from their draft branch, the editor reviews the diff, leaves comments, requests changes, and approves when satisfied. The merge to main publishes the post.</p>

<p>Within the PR, reviewers can leave line-level comments on specific sentences or paragraphs in the Markdown file. Authors address each comment, push revisions, and the reviewer re-reads. This iterative review process is identical to code review — and it works remarkably well for content because the diff format makes changes clear and the history is preserved permanently.</p>

<p>For teams uncomfortable with GitHub’s PR interface, Prose.io provides a graphical editor for Jekyll repositories that creates and edits files through the GitHub API. Authors write in a visual editor, save to a branch, and the tech-comfortable editor reviews and merges via the GitHub PR interface. The two groups never need to use the same tool; the Git repository is the shared layer.</p>

<h2 id="from-draft-to-evergreen-maintaining-published-content">From draft to evergreen: maintaining published content</h2>

<p>Publishing is not the end of a post’s lifecycle. Evergreen technical content — how-to guides, comparison articles, reference posts — benefits from periodic updates to keep examples current, statistics accurate, and recommendations relevant. An update strategy prevents posts from becoming outdated and keeps rankings stable.</p>

<p>Add a <code class="language-plaintext highlighter-rouge">last_updated</code> field to post front matter when you make meaningful content changes. Configure your post layout to show both the original publication date and the last updated date when they differ — this signals to readers that the content is maintained, which increases trust and reduces bounce rates from readers who notice a date from three years ago and assume the content is stale.</p>

<p>Set a content review calendar: choose a frequency (quarterly for most posts, monthly for fast-moving topics) and review each published post against that schedule. The review questions are: Is the information still accurate? Have any links broken? Are there new alternatives or tools worth mentioning? Has the recommended approach changed? A thirty-minute review session per post, twice a year, keeps a blog of fifty posts in excellent condition without feeling overwhelming.</p>

<p>Jekyll’s Git history makes content archaeology easy — you can see exactly what changed in a post at any point in its history, which is useful for understanding why something was written a certain way or when a specific claim was added. This transparency is one of the underrated long-term benefits of a Git-based content workflow over a database-driven CMS where content history is often opaque or unavailable.</p>]]></content><author><name>Marcus Webb</name></author><category term="Tutorial" /><summary type="html"><![CDATA[How to use Jekyll drafts, future posts, unpublished pages, and a full editorial workflow — from idea to published post.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/jekyll-drafts-publishing-workflow.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/jekyll-drafts-publishing-workflow.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to Use Sass and SCSS with Jekyll (Complete Guide)</title><link href="https://jekyllhub.com/tutorial/2026/06/30/jekyll-sass-scss-guide/" rel="alternate" type="text/html" title="How to Use Sass and SCSS with Jekyll (Complete Guide)" /><published>2026-06-30T00:00:00+00:00</published><updated>2026-06-30T00:00:00+00:00</updated><id>https://jekyllhub.com/tutorial/2026/06/30/jekyll-sass-scss-guide</id><content type="html" xml:base="https://jekyllhub.com/tutorial/2026/06/30/jekyll-sass-scss-guide/"><![CDATA[<p>Jekyll has built-in Sass processing — no Node.js, no build tools, no webpack required. Write SCSS files, Jekyll compiles them to CSS automatically. This guide covers everything from basic setup to advanced patterns used in production Jekyll themes.</p>

<h2 id="sass-vs-scss">Sass vs SCSS</h2>

<p>Sass has two syntaxes:</p>

<p><strong>SCSS</strong> (Sassy CSS) — superset of CSS. Valid CSS is valid SCSS. Uses curly braces and semicolons. The most common syntax.</p>

<p><strong>Sass</strong> (indented syntax) — uses indentation instead of braces, no semicolons. Older syntax, less commonly used.</p>

<p>Jekyll supports both. This guide uses SCSS (the <code class="language-plaintext highlighter-rouge">.scss</code> extension).</p>

<h2 id="jekylls-sass-directory-structure">Jekyll’s Sass directory structure</h2>

<p>Jekyll processes Sass files with this convention:</p>

<ul>
  <li>Files in <code class="language-plaintext highlighter-rouge">_sass/</code> starting with <code class="language-plaintext highlighter-rouge">_</code> are <strong>partials</strong> — they are imported by other files, never compiled directly</li>
  <li>Files in <code class="language-plaintext highlighter-rouge">assets/css/</code> (or anywhere outside <code class="language-plaintext highlighter-rouge">_sass/</code>) with <code class="language-plaintext highlighter-rouge">.scss</code> extension and front matter are <strong>entry points</strong> — Jekyll compiles these to CSS</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_sass/                     ← partials live here
├── _variables.scss
├── _base.scss
├── _nav.scss
├── _cards.scss
└── _post.scss

assets/
└── css/
    └── main.scss          ← entry point — compiled to main.css
</code></pre></div></div>

<h2 id="creating-the-entry-point">Creating the entry point</h2>

<p>The entry point file needs front matter (even if empty) to tell Jekyll to process it:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* assets/css/main.scss */</span>
<span class="nt">---</span>
<span class="nt">---</span>

<span class="o">@</span><span class="nt">import</span> <span class="s2">"variables"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"base"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"nav"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"cards"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"post"</span><span class="p">;</span>
</code></pre></div></div>

<p>The empty <code class="language-plaintext highlighter-rouge">---</code> block is required. Without it, Jekyll copies the file as-is without processing.</p>

<p>Import partials without the leading <code class="language-plaintext highlighter-rouge">_</code> or <code class="language-plaintext highlighter-rouge">.scss</code> extension — Sass resolves them automatically.</p>

<h2 id="scss-partials-in-_sass">SCSS partials in _sass/</h2>

<h3 id="_variablesscss--design-tokens">_variables.scss — design tokens</h3>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// _sass/_variables.scss</span>

<span class="c1">// Colours</span>
<span class="nv">$color-primary</span><span class="p">:</span>    <span class="mh">#2563eb</span><span class="p">;</span>
<span class="nv">$color-primary-dark</span><span class="p">:</span> <span class="mh">#1d4ed8</span><span class="p">;</span>
<span class="nv">$color-text</span><span class="p">:</span>       <span class="mh">#1a1a2e</span><span class="p">;</span>
<span class="nv">$color-muted</span><span class="p">:</span>      <span class="mh">#6b7280</span><span class="p">;</span>
<span class="nv">$color-border</span><span class="p">:</span>     <span class="mh">#e5e7eb</span><span class="p">;</span>
<span class="nv">$color-bg</span><span class="p">:</span>         <span class="mh">#ffffff</span><span class="p">;</span>
<span class="nv">$color-bg-alt</span><span class="p">:</span>     <span class="mh">#f9fafb</span><span class="p">;</span>

<span class="c1">// Typography</span>
<span class="nv">$font-sans</span><span class="p">:</span>        <span class="s2">"Inter"</span><span class="o">,</span> <span class="n">system-ui</span><span class="o">,</span> <span class="o">-</span><span class="n">apple-system</span><span class="o">,</span> <span class="nb">sans-serif</span><span class="p">;</span>
<span class="nv">$font-mono</span><span class="p">:</span>        <span class="s2">"Fira Code"</span><span class="o">,</span> <span class="s2">"Cascadia Code"</span><span class="o">,</span> <span class="nb">monospace</span><span class="p">;</span>
<span class="nv">$font-size-base</span><span class="p">:</span>   <span class="m">1rem</span><span class="p">;</span>
<span class="nv">$line-height-base</span><span class="p">:</span> <span class="m">1</span><span class="mi">.6</span><span class="p">;</span>

<span class="c1">// Spacing</span>
<span class="nv">$spacing-xs</span><span class="p">:</span>   <span class="m">0</span><span class="mi">.25rem</span><span class="p">;</span>
<span class="nv">$spacing-sm</span><span class="p">:</span>   <span class="m">0</span><span class="mi">.5rem</span><span class="p">;</span>
<span class="nv">$spacing-md</span><span class="p">:</span>   <span class="m">1rem</span><span class="p">;</span>
<span class="nv">$spacing-lg</span><span class="p">:</span>   <span class="m">1</span><span class="mi">.5rem</span><span class="p">;</span>
<span class="nv">$spacing-xl</span><span class="p">:</span>   <span class="m">2rem</span><span class="p">;</span>
<span class="nv">$spacing-2xl</span><span class="p">:</span>  <span class="m">3rem</span><span class="p">;</span>

<span class="c1">// Layout</span>
<span class="nv">$container-max</span><span class="p">:</span> <span class="m">1200px</span><span class="p">;</span>
<span class="nv">$sidebar-width</span><span class="p">:</span> <span class="m">280px</span><span class="p">;</span>

<span class="c1">// Borders</span>
<span class="nv">$radius-sm</span><span class="p">:</span>  <span class="m">4px</span><span class="p">;</span>
<span class="nv">$radius-md</span><span class="p">:</span>  <span class="m">10px</span><span class="p">;</span>
<span class="nv">$radius-lg</span><span class="p">:</span>  <span class="m">16px</span><span class="p">;</span>
<span class="nv">$radius-full</span><span class="p">:</span> <span class="m">9999px</span><span class="p">;</span>

<span class="c1">// Shadows</span>
<span class="nv">$shadow-sm</span><span class="p">:</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">2px</span> <span class="nf">rgba</span><span class="p">(</span><span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="mi">.05</span><span class="p">);</span>
<span class="nv">$shadow-md</span><span class="p">:</span> <span class="m">0</span> <span class="m">4px</span> <span class="m">6px</span> <span class="nf">rgba</span><span class="p">(</span><span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="mi">.07</span><span class="p">);</span>
<span class="nv">$shadow-lg</span><span class="p">:</span> <span class="m">0</span> <span class="m">10px</span> <span class="m">15px</span> <span class="nf">rgba</span><span class="p">(</span><span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="mi">.1</span><span class="p">);</span>

<span class="c1">// Transitions</span>
<span class="nv">$transition-fast</span><span class="p">:</span>   <span class="m">150ms</span> <span class="n">ease</span><span class="p">;</span>
<span class="nv">$transition-base</span><span class="p">:</span>   <span class="m">250ms</span> <span class="n">ease</span><span class="p">;</span>
<span class="nv">$transition-slow</span><span class="p">:</span>   <span class="m">400ms</span> <span class="n">ease</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="_basescss--reset-and-global-styles">_base.scss — reset and global styles</h3>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// _sass/_base.scss</span>
<span class="k">@import</span> <span class="s2">"variables"</span><span class="p">;</span>

<span class="err">*,
*</span><span class="p">:</span><span class="o">:</span><span class="n">before</span><span class="o">,</span>
<span class="o">*::</span><span class="n">after</span> <span class="p">{</span>
  <span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">html</span> <span class="p">{</span>
  <span class="nl">font-size</span><span class="p">:</span> <span class="m">16px</span><span class="p">;</span>
  <span class="na">-webkit-text-size-adjust</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">body</span> <span class="p">{</span>
  <span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
  <span class="nl">font-family</span><span class="p">:</span> <span class="nv">$font-sans</span><span class="p">;</span>
  <span class="nl">font-size</span><span class="p">:</span> <span class="nv">$font-size-base</span><span class="p">;</span>
  <span class="nl">line-height</span><span class="p">:</span> <span class="nv">$line-height-base</span><span class="p">;</span>
  <span class="nl">color</span><span class="p">:</span> <span class="nv">$color-text</span><span class="p">;</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nv">$color-bg</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">img</span> <span class="p">{</span>
  <span class="nl">max-width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
  <span class="nl">height</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">a</span> <span class="p">{</span>
  <span class="nl">color</span><span class="p">:</span> <span class="nv">$color-primary</span><span class="p">;</span>
  <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>

  <span class="k">&amp;</span><span class="nd">:hover</span> <span class="p">{</span>
    <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nt">h1</span><span class="o">,</span> <span class="nt">h2</span><span class="o">,</span> <span class="nt">h3</span><span class="o">,</span> <span class="nt">h4</span><span class="o">,</span> <span class="nt">h5</span><span class="o">,</span> <span class="nt">h6</span> <span class="p">{</span>
  <span class="nl">line-height</span><span class="p">:</span> <span class="m">1</span><span class="mi">.3</span><span class="p">;</span>
  <span class="nl">font-weight</span><span class="p">:</span> <span class="m">700</span><span class="p">;</span>
  <span class="nl">margin-top</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">code</span> <span class="p">{</span>
  <span class="nl">font-family</span><span class="p">:</span> <span class="nv">$font-mono</span><span class="p">;</span>
  <span class="nl">font-size</span><span class="p">:</span> <span class="m">0</span><span class="mi">.875em</span><span class="p">;</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nv">$color-bg-alt</span><span class="p">;</span>
  <span class="nl">padding</span><span class="p">:</span> <span class="m">0</span><span class="mi">.15em</span> <span class="m">0</span><span class="mi">.4em</span><span class="p">;</span>
  <span class="nl">border-radius</span><span class="p">:</span> <span class="nv">$radius-sm</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="scss-features-used-in-jekyll-themes">SCSS features used in Jekyll themes</h2>

<h3 id="variables">Variables</h3>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$color-primary</span><span class="p">:</span> <span class="mh">#2563eb</span><span class="p">;</span>

<span class="nc">.btn</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nv">$color-primary</span><span class="p">;</span>
  
  <span class="k">&amp;</span><span class="nd">:hover</span> <span class="p">{</span>
    <span class="nl">background</span><span class="p">:</span> <span class="nf">darken</span><span class="p">(</span><span class="nv">$color-primary</span><span class="o">,</span> <span class="m">10%</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="nesting">Nesting</h3>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.card</span> <span class="p">{</span>
  <span class="nl">border-radius</span><span class="p">:</span> <span class="nv">$radius-md</span><span class="p">;</span>
  <span class="nl">overflow</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span>
  
  <span class="k">&amp;</span><span class="nt">__image</span> <span class="p">{</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
    <span class="na">aspect-ratio</span><span class="p">:</span> <span class="m">16</span> <span class="o">/</span> <span class="m">9</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="k">&amp;</span><span class="nt">__body</span> <span class="p">{</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="nv">$spacing-lg</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="k">&amp;</span><span class="nt">__title</span> <span class="p">{</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">1</span><span class="mi">.125rem</span><span class="p">;</span>
    <span class="nl">font-weight</span><span class="p">:</span> <span class="m">600</span><span class="p">;</span>
    <span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="nv">$spacing-sm</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="k">&amp;</span><span class="nt">--featured</span> <span class="p">{</span>
    <span class="nl">border</span><span class="p">:</span> <span class="m">2px</span> <span class="nb">solid</span> <span class="nv">$color-primary</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="k">&amp;</span><span class="nd">:hover</span> <span class="p">{</span>
    <span class="nl">box-shadow</span><span class="p">:</span> <span class="nv">$shadow-md</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This BEM-style nesting (<code class="language-plaintext highlighter-rouge">&amp;__element</code>, <code class="language-plaintext highlighter-rouge">&amp;--modifier</code>) generates classes like <code class="language-plaintext highlighter-rouge">.card__image</code>, <code class="language-plaintext highlighter-rouge">.card__body</code>, <code class="language-plaintext highlighter-rouge">.card--featured</code>.</p>

<h3 id="mixins">Mixins</h3>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// _sass/_mixins.scss</span>

<span class="k">@mixin</span> <span class="nf">flex-center</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
  <span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
  <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">@mixin</span> <span class="nf">truncate</span> <span class="p">{</span>
  <span class="nl">overflow</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span>
  <span class="nl">text-overflow</span><span class="p">:</span> <span class="n">ellipsis</span><span class="p">;</span>
  <span class="nl">white-space</span><span class="p">:</span> <span class="nb">nowrap</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">@mixin</span> <span class="nf">responsive</span><span class="p">(</span><span class="nv">$breakpoint</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">@if</span> <span class="nv">$breakpoint</span> <span class="o">==</span> <span class="n">mobile</span> <span class="p">{</span>
    <span class="k">@media</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">640px</span><span class="p">)</span> <span class="p">{</span> <span class="k">@content</span><span class="p">;</span> <span class="p">}</span>
  <span class="p">}</span> <span class="k">@else</span> <span class="n">if</span> <span class="nv">$breakpoint</span> <span class="o">==</span> <span class="n">tablet</span> <span class="p">{</span>
    <span class="k">@media</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">1024px</span><span class="p">)</span> <span class="p">{</span> <span class="k">@content</span><span class="p">;</span> <span class="p">}</span>
  <span class="p">}</span> <span class="k">@else</span> <span class="n">if</span> <span class="nv">$breakpoint</span> <span class="o">==</span> <span class="n">desktop</span> <span class="p">{</span>
    <span class="k">@media</span> <span class="p">(</span><span class="n">min-width</span><span class="o">:</span> <span class="m">1025px</span><span class="p">)</span> <span class="p">{</span> <span class="k">@content</span><span class="p">;</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Usage</span>
<span class="nc">.nav</span> <span class="p">{</span>
  <span class="k">@include</span> <span class="nd">flex-center</span><span class="p">;</span>
  
  <span class="k">@include</span> <span class="nd">responsive</span><span class="p">(</span><span class="n">mobile</span><span class="p">)</span> <span class="p">{</span>
    <span class="nl">flex-direction</span><span class="p">:</span> <span class="n">column</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nc">.card__title</span> <span class="p">{</span>
  <span class="k">@include</span> <span class="nd">truncate</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="functions">Functions</h3>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Convert px to rem</span>
<span class="k">@function</span> <span class="nf">rem</span><span class="p">(</span><span class="nv">$px</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">@return</span> <span class="p">(</span><span class="nv">$px</span> <span class="o">/</span> <span class="m">16</span><span class="p">)</span> <span class="o">*</span> <span class="m">1rem</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.heading</span> <span class="p">{</span>
  <span class="nl">font-size</span><span class="p">:</span> <span class="nf">rem</span><span class="p">(</span><span class="m">24</span><span class="p">);</span>  <span class="c1">// → 1.5rem</span>
<span class="p">}</span>

<span class="c1">// Darken a colour by percentage</span>
<span class="nc">.btn</span><span class="nd">:hover</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nf">darken</span><span class="p">(</span><span class="nv">$color-primary</span><span class="o">,</span> <span class="m">8%</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Generate a colour with opacity</span>
<span class="nc">.overlay</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nf">rgba</span><span class="p">(</span><span class="nv">$color-text</span><span class="o">,</span> <span class="m">0</span><span class="mi">.5</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="extends--placeholders">Extends / placeholders</h3>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">%visually-hidden</span> <span class="p">{</span>
  <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span>
  <span class="nl">width</span><span class="p">:</span> <span class="m">1px</span><span class="p">;</span>
  <span class="nl">height</span><span class="p">:</span> <span class="m">1px</span><span class="p">;</span>
  <span class="nl">padding</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
  <span class="nl">margin</span><span class="p">:</span> <span class="m">-1px</span><span class="p">;</span>
  <span class="nl">overflow</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span>
  <span class="nl">clip</span><span class="p">:</span> <span class="nf">rect</span><span class="p">(</span><span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="p">);</span>
  <span class="nl">white-space</span><span class="p">:</span> <span class="nb">nowrap</span><span class="p">;</span>
  <span class="nl">border</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.sr-only</span> <span class="p">{</span>
  <span class="k">@extend</span> <span class="nv">%visually-hidden</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.skip-link</span><span class="nd">:focus</span> <span class="p">{</span>
  <span class="k">@extend</span> <span class="nv">%visually-hidden</span><span class="p">;</span>
  <span class="nl">clip</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
  <span class="nl">width</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
  <span class="nl">height</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="dark-mode-with-sass">Dark mode with Sass</h2>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// _sass/_variables.scss</span>

<span class="c1">// Light mode defaults (also used as CSS custom properties)</span>
<span class="nd">:root</span> <span class="p">{</span>
  <span class="na">--color-bg</span><span class="p">:</span>     <span class="si">#{</span><span class="nv">$color-bg</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--color-text</span><span class="p">:</span>   <span class="si">#{</span><span class="nv">$color-text</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--color-border</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$color-border</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--color-bg-alt</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$color-bg-alt</span><span class="si">}</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Dark mode overrides</span>
<span class="nd">:root</span><span class="o">[</span><span class="nt">data-theme</span><span class="o">=</span><span class="s2">"dark"</span><span class="o">],</span>
<span class="nc">.dark</span> <span class="p">{</span>
  <span class="na">--color-bg</span><span class="p">:</span>     <span class="mh">#0f172a</span><span class="p">;</span>
  <span class="na">--color-text</span><span class="p">:</span>   <span class="mh">#f1f5f9</span><span class="p">;</span>
  <span class="na">--color-border</span><span class="p">:</span> <span class="mh">#1e293b</span><span class="p">;</span>
  <span class="na">--color-bg-alt</span><span class="p">:</span> <span class="mh">#1e293b</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Use CSS custom properties throughout</span>
<span class="nt">body</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">color-bg</span><span class="p">);</span>
  <span class="nl">color</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">color-text</span><span class="p">);</span>
<span class="p">}</span>

<span class="nc">.card</span> <span class="p">{</span>
  <span class="nl">border-color</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">color-border</span><span class="p">);</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">color-bg-alt</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This approach — setting CSS custom properties from Sass variables — gives you the best of both worlds: Sass for authoring, CSS variables for runtime dark mode toggling with JavaScript.</p>

<h2 id="sass-compilation-settings-in-_configyml">Sass compilation settings in _config.yml</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml</span>
<span class="na">sass</span><span class="pi">:</span>
  <span class="na">sass_dir</span><span class="pi">:</span> <span class="s">_sass</span>          <span class="c1"># where partials live (default: _sass)</span>
  <span class="na">style</span><span class="pi">:</span> <span class="s">compressed</span>        <span class="c1"># compressed | expanded | nested | compact</span>
  <span class="na">load_paths</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">_sass</span>
    <span class="pi">-</span> <span class="s">node_modules</span>         <span class="c1"># if importing npm packages</span>
</code></pre></div></div>

<p><strong>style options:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">compressed</code> — removes all whitespace, one-line output. Use for production.</li>
  <li><code class="language-plaintext highlighter-rouge">expanded</code> — each rule and property on its own line. Default in development.</li>
  <li><code class="language-plaintext highlighter-rouge">nested</code> — rules nested to reflect the SCSS structure.</li>
  <li><code class="language-plaintext highlighter-rouge">compact</code> — one rule per line.</li>
</ul>

<p>Most setups use <code class="language-plaintext highlighter-rouge">compressed</code> in production builds:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">sass</span><span class="pi">:</span>
  <span class="na">style</span><span class="pi">:</span> <span class="s">compressed</span>
</code></pre></div></div>

<h2 id="importing-npm-sass-packages">Importing npm Sass packages</h2>

<p>If you install Sass libraries via npm, add the path to <code class="language-plaintext highlighter-rouge">load_paths</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install </span>sass-mq normalize.css
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">sass</span><span class="pi">:</span>
  <span class="na">load_paths</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">_sass</span>
    <span class="pi">-</span> <span class="s">node_modules</span>
</code></pre></div></div>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// assets/css/main.scss</span>
<span class="nt">---</span>
<span class="nt">---</span>
<span class="o">@</span><span class="nt">import</span> <span class="s2">"normalize.css/normalize"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"sass-mq/mq"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"variables"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"base"</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="organising-a-production-ready-_sass-directory">Organising a production-ready _sass/ directory</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_sass/
├── _variables.scss      ← design tokens
├── _mixins.scss         ← reusable mixins
├── _functions.scss      ← Sass functions
├── _reset.scss          ← CSS reset/normalise
├── _base.scss           ← global styles (body, a, h1-h6, img)
├── _typography.scss     ← prose/content typography
│
├── layout/
│   ├── _container.scss
│   ├── _grid.scss
│   └── _sections.scss
│
├── components/
│   ├── _nav.scss
│   ├── _footer.scss
│   ├── _cards.scss
│   ├── _badges.scss
│   ├── _buttons.scss
│   ├── _forms.scss
│   └── _modals.scss
│
├── pages/
│   ├── _home.scss
│   ├── _blog.scss
│   ├── _theme-detail.scss
│   └── _authors.scss
│
└── utilities/
    ├── _helpers.scss    ← .sr-only, .clearfix, etc.
    └── _dark-mode.scss
</code></pre></div></div>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* assets/css/main.scss */</span>
<span class="nt">---</span>
<span class="nt">---</span>

<span class="o">//</span> <span class="nt">Tokens</span> <span class="nt">and</span> <span class="nt">tools</span>
<span class="o">@</span><span class="nt">import</span> <span class="s2">"variables"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"mixins"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"functions"</span><span class="p">;</span>

<span class="c1">// Base</span>
<span class="k">@import</span> <span class="s2">"reset"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"base"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"typography"</span><span class="p">;</span>

<span class="c1">// Layout</span>
<span class="k">@import</span> <span class="s2">"layout/container"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"layout/grid"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"layout/sections"</span><span class="p">;</span>

<span class="c1">// Components</span>
<span class="k">@import</span> <span class="s2">"components/nav"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"components/footer"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"components/cards"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"components/badges"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"components/buttons"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"components/forms"</span><span class="p">;</span>

<span class="c1">// Pages</span>
<span class="k">@import</span> <span class="s2">"pages/home"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"pages/blog"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"pages/theme-detail"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"pages/authors"</span><span class="p">;</span>

<span class="c1">// Utilities</span>
<span class="k">@import</span> <span class="s2">"utilities/helpers"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"utilities/dark-mode"</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="common-mistakes">Common mistakes</h2>

<p><strong>Missing front matter on the entry point:</strong> Without the <code class="language-plaintext highlighter-rouge">---</code> block, Jekyll copies <code class="language-plaintext highlighter-rouge">main.scss</code> as a plain text file instead of compiling it. Always include the empty front matter.</p>

<p><strong>Importing from the wrong path:</strong> Partials in <code class="language-plaintext highlighter-rouge">_sass/</code> are imported without the leading <code class="language-plaintext highlighter-rouge">_</code> or the directory path. If your partial is <code class="language-plaintext highlighter-rouge">_sass/components/_nav.scss</code>, import it as <code class="language-plaintext highlighter-rouge">@import "components/nav"</code>.</p>

<p><strong>Using <code class="language-plaintext highlighter-rouge">@use</code> instead of <code class="language-plaintext highlighter-rouge">@import</code>:</strong> Jekyll’s built-in Sass processor uses <code class="language-plaintext highlighter-rouge">libsass</code> which supports <code class="language-plaintext highlighter-rouge">@import</code> but has limited support for the newer <code class="language-plaintext highlighter-rouge">@use</code> syntax. Stick with <code class="language-plaintext highlighter-rouge">@import</code> for Jekyll’s native Sass processing. If you need <code class="language-plaintext highlighter-rouge">@use</code>, switch to a Node.js PostCSS pipeline.</p>

<p><strong>Not compressing in production:</strong> Add <code class="language-plaintext highlighter-rouge">sass: style: compressed</code> to <code class="language-plaintext highlighter-rouge">_config.yml</code> or use a production build command to minimise CSS output.</p>

<p>Built-in Sass support is one of Jekyll’s most useful features. No Node.js, no build pipeline, no configuration — just write SCSS and Jekyll compiles it. For most Jekyll sites, the built-in processor is all you need.</p>

<hr />

<h2 id="why-jekylls-built-in-sass-is-enough-for-most-sites">Why Jekyll’s built-in Sass is enough for most sites</h2>

<p>The built-in Sass processor handles the vast majority of use cases without any additional tooling. You get variables, nesting, mixins, functions, partials, and the ability to import npm packages via <code class="language-plaintext highlighter-rouge">load_paths</code>. For blogs, documentation sites, portfolios, and small business sites, this is everything you need.</p>

<p>The case for adding a Node.js build pipeline (PostCSS, Webpack, Vite) only becomes compelling when you need features the built-in processor cannot provide: Tailwind CSS utilities, PostCSS plugins like autoprefixer for vendor prefixes at scale, or the newer Sass <code class="language-plaintext highlighter-rouge">@use</code>/<code class="language-plaintext highlighter-rouge">@forward</code> module system. For most Jekyll sites, the complexity of adding a Node.js pipeline outweighs the benefits.</p>

<p>Start with Jekyll’s built-in Sass. Add a Node.js pipeline only when you have a specific, concrete reason that the built-in processor cannot address.</p>

<h2 id="writing-maintainable-scss-for-jekyll-themes">Writing maintainable SCSS for Jekyll themes</h2>

<p>SCSS is a powerful tool, but it is also one that is easy to misuse. A few discipline practices keep Jekyll theme stylesheets maintainable over time.</p>

<p><strong>Use CSS custom properties for runtime values, Sass variables for build-time values.</strong> Sass variables compile away — you cannot change them with JavaScript. CSS custom properties (<code class="language-plaintext highlighter-rouge">--color-primary: blue</code>) are accessible at runtime, making them essential for dark mode toggles, user preference systems, and theme switchers. The best practice is to define your design tokens as Sass variables and then assign them to CSS custom properties in <code class="language-plaintext highlighter-rouge">:root</code>:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$blue-600</span><span class="p">:</span> <span class="mh">#2563eb</span><span class="p">;</span>
<span class="nv">$blue-700</span><span class="p">:</span> <span class="mh">#1d4ed8</span><span class="p">;</span>

<span class="nd">:root</span> <span class="p">{</span>
  <span class="na">--color-primary</span><span class="p">:</span>      <span class="si">#{</span><span class="nv">$blue-600</span><span class="si">}</span><span class="p">;</span>
  <span class="na">--color-primary-dark</span><span class="p">:</span> <span class="si">#{</span><span class="nv">$blue-700</span><span class="si">}</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Use the custom property everywhere (not the Sass variable)</span>
<span class="nc">.btn</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">color-primary</span><span class="p">);</span>
  <span class="k">&amp;</span><span class="nd">:hover</span> <span class="p">{</span> <span class="nl">background</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">color-primary-dark</span><span class="p">);</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This gives you the authoring convenience of Sass variables for defining the palette, and the runtime flexibility of CSS custom properties for applying them.</p>

<p><strong>Avoid deep nesting.</strong> Nesting beyond three levels creates high-specificity selectors that are hard to override and indicate overly coupled HTML and CSS. A good rule: if you cannot read the compiled selector at a glance, the nesting is too deep. BEM naming with one level of nesting (<code class="language-plaintext highlighter-rouge">&amp;__element</code>, <code class="language-plaintext highlighter-rouge">&amp;--modifier</code>) produces clear, flat selectors without sacrificing the readability benefits of nesting.</p>

<p><strong>Keep files focused.</strong> A <code class="language-plaintext highlighter-rouge">_nav.scss</code> file should contain only nav-related styles. When a file grows beyond 150–200 lines, consider splitting it. A <code class="language-plaintext highlighter-rouge">_nav-desktop.scss</code> and <code class="language-plaintext highlighter-rouge">_nav-mobile.scss</code> approach is sometimes cleaner than one large file with breakpoints scattered throughout.</p>

<p><strong>Comment at the section level, not the line level.</strong> A comment explaining what a block of CSS achieves is valuable. A comment explaining what a single property does is usually not. Exception: non-obvious values like magic numbers, z-index values, and vendor-specific hacks benefit from inline comments explaining why they are there.</p>

<h2 id="the-use-and-forward-system-dart-sass">The @use and @forward system (Dart Sass)</h2>

<p>Jekyll’s built-in Sass processor uses LibSass, which supports the older <code class="language-plaintext highlighter-rouge">@import</code> syntax. Dart Sass (the reference implementation) introduces <code class="language-plaintext highlighter-rouge">@use</code> and <code class="language-plaintext highlighter-rouge">@forward</code> as replacements that offer better encapsulation:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @use namespaces modules automatically</span>
<span class="k">@use</span> <span class="s2">"variables"</span> <span class="nt">as</span> <span class="nt">vars</span><span class="p">;</span>
<span class="k">@use</span> <span class="s2">"mixins"</span><span class="p">;</span>

<span class="nc">.btn</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="n">vars</span><span class="o">.</span><span class="nv">$color-primary</span><span class="p">;</span>
  <span class="k">@include</span> <span class="nd">mixins</span><span class="o">.</span><span class="n">flex-center</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@use</code> prevents global namespace pollution by scoping module members under a namespace. <code class="language-plaintext highlighter-rouge">@forward</code> re-exports module members, useful for building a single entry point that exposes multiple partials.</p>

<p>If you want to use <code class="language-plaintext highlighter-rouge">@use</code> with Jekyll, you need to replace the built-in Sass processor with a Node.js-based pipeline using <code class="language-plaintext highlighter-rouge">sass</code> (the Dart Sass npm package) and PostCSS. The setup is more complex but enables the full modern Sass feature set.</p>

<p>For most Jekyll projects, the added complexity is not worth it. The <code class="language-plaintext highlighter-rouge">@import</code> system, while deprecated in Dart Sass, works perfectly well with Jekyll’s LibSass processor and will continue to do so for the foreseeable future.</p>

<h2 id="performance-how-much-css-is-too-much">Performance: how much CSS is too much?</h2>

<p>The impact of your CSS file size on performance depends on how it is delivered. Jekyll’s built-in Sass with <code class="language-plaintext highlighter-rouge">style: compressed</code> produces minified CSS — no whitespace, comments stripped. This is important for production builds.</p>

<p>A rough guide: CSS under 50kb (uncompressed) has negligible performance impact on modern connections. Between 50kb and 150kb, the file size is noticeable on slower connections but unlikely to cause Lighthouse score problems. Above 150kb, you are probably including unused styles and should consider audit tooling.</p>

<p>For Jekyll themes, CSS bloat typically comes from including large third-party stylesheets (Bootstrap, Foundation, Bulma) without purging unused rules. If you import Bootstrap via npm and use only a fraction of its utilities, you are shipping tens of kilobytes of unused CSS.</p>

<p>The solution for third-party framework CSS is either to import only the specific component files you need:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Import only Bootstrap's grid and buttons, not everything</span>
<span class="k">@import</span> <span class="s2">"bootstrap/scss/grid"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"bootstrap/scss/buttons"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"bootstrap/scss/utilities/api"</span><span class="p">;</span>
</code></pre></div></div>

<p>Or to use a tool like PurgeCSS to remove unused rules from the production build. For Jekyll with a PostCSS pipeline, <code class="language-plaintext highlighter-rouge">@fullhuman/postcss-purgecss</code> scans your HTML and template files and removes any CSS rules that do not match used class names.</p>

<p>For most Jekyll sites that write their own SCSS rather than importing a large framework, CSS file size is not a meaningful concern. Write clear, modular stylesheets using the conventions in this guide, compress them in production, and focus on the variables and patterns that make your site easy to maintain and customise over time.</p>

<p>Explore the <a href="/themes/">JekyllHub theme collection</a> to see SCSS-driven Jekyll themes in action — the best themes demonstrate these conventions cleanly and serve as excellent references for your own SCSS architecture.</p>

<hr />

<h2 id="debugging-sass-compilation-errors">Debugging Sass compilation errors</h2>

<p>When Jekyll’s Sass processor encounters an error, it stops the build and reports the file and line number. The error messages from LibSass are generally clear, but a few common patterns trip up beginners.</p>

<p><strong>“Undefined variable”</strong> — you referenced a variable (<code class="language-plaintext highlighter-rouge">$color-primary</code>) before declaring it or in a file that does not import <code class="language-plaintext highlighter-rouge">_variables.scss</code>. Fix: add <code class="language-plaintext highlighter-rouge">@import "variables"</code> at the top of the partial that uses the variable.</p>

<p><strong>“File to import not found”</strong> — the partial path in the <code class="language-plaintext highlighter-rouge">@import</code> statement does not match the actual filename. Check for typos and remember that partial filenames start with <code class="language-plaintext highlighter-rouge">_</code> but are imported without it: <code class="language-plaintext highlighter-rouge">@import "nav"</code> imports <code class="language-plaintext highlighter-rouge">_sass/_nav.scss</code>.</p>

<p><strong>“Invalid CSS after”</strong> — a syntax error in your SCSS. LibSass’s error messages point to a line but the actual error is sometimes a few lines earlier — an unclosed bracket or missing semicolon from a previous rule.</p>

<p><strong>No error but CSS unchanged</strong> — Jekyll cached the previous build. Run <code class="language-plaintext highlighter-rouge">bundle exec jekyll clean</code> then <code class="language-plaintext highlighter-rouge">bundle exec jekyll serve</code> to force a fresh compilation.</p>

<p>For faster debugging cycles, use <code class="language-plaintext highlighter-rouge">bundle exec jekyll serve</code> with the <code class="language-plaintext highlighter-rouge">--incremental</code> flag during active CSS development — Jekyll only rebuilds changed files, dramatically reducing the time between saving a SCSS change and seeing it in the browser. When using <code class="language-plaintext highlighter-rouge">--incremental</code>, you may need to run a full clean build periodically to ensure all files are in sync.</p>

<h2 id="moving-forward-with-jekyll-scss">Moving forward with Jekyll SCSS</h2>

<p>Once you have the fundamentals working — imports, variables, mixins, and a sensible file structure — you have everything you need to write maintainable, scalable styles for any Jekyll project. The key habits to develop are keeping your SCSS partials small and focused, using variables for anything that appears more than once, and compiling locally so you catch errors before pushing to production.</p>]]></content><author><name>Marcus Webb</name></author><category term="Tutorial" /><summary type="html"><![CDATA[A complete guide to using Sass and SCSS with Jekyll — directory structure, importing partials, variables, nesting, theming with custom properties, and compilation settings.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/jekyll-sass-scss-guide.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/jekyll-sass-scss-guide.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyll Permalinks and URL Structure: The Complete Guide</title><link href="https://jekyllhub.com/tutorial/2026/06/29/jekyll-permalinks-url-structure/" rel="alternate" type="text/html" title="Jekyll Permalinks and URL Structure: The Complete Guide" /><published>2026-06-29T00:00:00+00:00</published><updated>2026-06-29T00:00:00+00:00</updated><id>https://jekyllhub.com/tutorial/2026/06/29/jekyll-permalinks-url-structure</id><content type="html" xml:base="https://jekyllhub.com/tutorial/2026/06/29/jekyll-permalinks-url-structure/"><![CDATA[<p>Jekyll gives you precise control over the URL structure of every page on your site. Understanding how permalinks work is important for clean URLs, SEO, and ensuring internal links stay consistent when you reorganise content.</p>

<h2 id="how-jekyll-builds-urls-by-default">How Jekyll builds URLs by default</h2>

<p>By default, Jekyll mirrors your file structure. A file at <code class="language-plaintext highlighter-rouge">_posts/2026-08-07-my-post.md</code> generates a URL like <code class="language-plaintext highlighter-rouge">/2026/08/07/my-post/</code>. A page at <code class="language-plaintext highlighter-rouge">about.md</code> generates <code class="language-plaintext highlighter-rouge">/about/</code>.</p>

<p>But the default is rarely what you want for production. The <code class="language-plaintext highlighter-rouge">permalink</code> setting in <code class="language-plaintext highlighter-rouge">_config.yml</code> (and in individual file front matter) lets you control this precisely.</p>

<h2 id="the-permalink-setting">The permalink setting</h2>

<p>Set a global permalink pattern in <code class="language-plaintext highlighter-rouge">_config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">permalink</span><span class="pi">:</span> <span class="s">/blog/:title/</span>
</code></pre></div></div>

<p>This tells Jekyll: every post’s URL should be <code class="language-plaintext highlighter-rouge">/blog/</code> followed by the post’s title-slug.</p>

<h2 id="built-in-permalink-styles">Built-in permalink styles</h2>

<p>Jekyll ships with several named permalink patterns:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">permalink</span><span class="pi">:</span> <span class="s">date</span>      <span class="c1"># /year/month/day/title.html</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">pretty</span>    <span class="c1"># /year/month/day/title/  (trailing slash, no .html)</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">ordinal</span>   <span class="c1"># /year/ordinal/title.html</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">weekdate</span>  <span class="c1"># /year/week/short_day/title/</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">none</span>      <span class="c1"># /title.html</span>
</code></pre></div></div>

<p>Most sites use <code class="language-plaintext highlighter-rouge">pretty</code> or a custom pattern. <code class="language-plaintext highlighter-rouge">date</code> (the historical default) produces cluttered URLs. <code class="language-plaintext highlighter-rouge">none</code> creates flat URLs with <code class="language-plaintext highlighter-rouge">.html</code> extensions.</p>

<h2 id="permalink-placeholders">Permalink placeholders</h2>

<p>Build custom patterns using these placeholders:</p>

<table>
  <thead>
    <tr>
      <th>Placeholder</th>
      <th>Value</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:year</code></td>
      <td>4-digit year</td>
      <td><code class="language-plaintext highlighter-rouge">2026</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:month</code></td>
      <td>2-digit month</td>
      <td><code class="language-plaintext highlighter-rouge">08</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:day</code></td>
      <td>2-digit day</td>
      <td><code class="language-plaintext highlighter-rouge">07</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:hour</code></td>
      <td>2-digit hour (24h)</td>
      <td><code class="language-plaintext highlighter-rouge">14</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:minute</code></td>
      <td>2-digit minute</td>
      <td><code class="language-plaintext highlighter-rouge">30</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:second</code></td>
      <td>2-digit second</td>
      <td><code class="language-plaintext highlighter-rouge">00</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:title</code></td>
      <td>Slugified title</td>
      <td><code class="language-plaintext highlighter-rouge">my-post-title</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:slug</code></td>
      <td>Slug from front matter (fallback to title)</td>
      <td><code class="language-plaintext highlighter-rouge">custom-slug</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:categories</code></td>
      <td>Categories joined by <code class="language-plaintext highlighter-rouge">/</code></td>
      <td><code class="language-plaintext highlighter-rouge">tutorial/jekyll</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:name</code></td>
      <td>Filename without date and extension</td>
      <td><code class="language-plaintext highlighter-rouge">my-post-title</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:path</code></td>
      <td>Path relative to site root</td>
      <td><code class="language-plaintext highlighter-rouge">_posts/my-post.md</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">:output_ext</code></td>
      <td>Output file extension</td>
      <td><code class="language-plaintext highlighter-rouge">.html</code></td>
    </tr>
  </tbody>
</table>

<h3 id="common-custom-patterns">Common custom patterns</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Clean blog URL — most common for blog sites</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/blog/:title/</span>

<span class="c1"># With date — good for news sites</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/:year/:month/:title/</span>

<span class="c1"># Category-based</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/:categories/:title/</span>

<span class="c1"># Flat — no nesting</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/:title/</span>

<span class="c1"># With date and category</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/:categories/:year/:month/:day/:title/</span>
</code></pre></div></div>

<h2 id="recommended-permalink-for-most-jekyll-blogs">Recommended permalink for most Jekyll blogs</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">permalink</span><span class="pi">:</span> <span class="s">/blog/:title/</span>
</code></pre></div></div>

<p>This gives you:</p>
<ul>
  <li>Clean, readable URLs: <code class="language-plaintext highlighter-rouge">/blog/jekyll-front-matter-guide/</code></li>
  <li>No date in the URL (posts stay relevant even when old)</li>
  <li>Consistent <code class="language-plaintext highlighter-rouge">/blog/</code> prefix separating blog content from pages</li>
  <li>Easy to remember and share</li>
</ul>

<p>Avoid including dates in permalinks unless you publish time-sensitive content (news, changelogs) where date context adds value.</p>

<h2 id="per-page-permalink-override">Per-page permalink override</h2>

<p>Override the global setting in any file’s front matter:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">page</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">About"</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/about/</span>
<span class="nn">---</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">Special</span><span class="nv"> </span><span class="s">Post"</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/featured/my-special-post/</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>The front matter <code class="language-plaintext highlighter-rouge">permalink</code> always wins over the global setting in <code class="language-plaintext highlighter-rouge">_config.yml</code>.</p>

<h2 id="permalinks-for-pages">Permalinks for pages</h2>

<p>Pages (in <code class="language-plaintext highlighter-rouge">_pages/</code> or the root directory) use the same <code class="language-plaintext highlighter-rouge">permalink</code> front matter key:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">page</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Browse</span><span class="nv"> </span><span class="s">Themes"</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/themes/</span>
<span class="nn">---</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">page</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Submit</span><span class="nv"> </span><span class="s">a</span><span class="nv"> </span><span class="s">Theme"</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/submit/</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>Without a <code class="language-plaintext highlighter-rouge">permalink</code>, a page at <code class="language-plaintext highlighter-rouge">_pages/about.md</code> generates <code class="language-plaintext highlighter-rouge">/about</code> (no trailing slash). Set <code class="language-plaintext highlighter-rouge">permalink: /about/</code> explicitly for consistency.</p>

<h2 id="permalinks-for-collections">Permalinks for collections</h2>

<p>Collections get their permalink pattern in <code class="language-plaintext highlighter-rouge">_config.yml</code> under the collection definition:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">collections</span><span class="pi">:</span>
  <span class="na">themes</span><span class="pi">:</span>
    <span class="na">output</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">permalink</span><span class="pi">:</span> <span class="s">/themes/:name/</span>
  <span class="na">authors</span><span class="pi">:</span>
    <span class="na">output</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">permalink</span><span class="pi">:</span> <span class="s">/authors/:name/</span>
</code></pre></div></div>

<p>For a file <code class="language-plaintext highlighter-rouge">_themes/minimal-mistakes.md</code>, this generates <code class="language-plaintext highlighter-rouge">/themes/minimal-mistakes/</code>.</p>

<p>Available placeholders for collections: <code class="language-plaintext highlighter-rouge">:name</code> (filename without extension), <code class="language-plaintext highlighter-rouge">:path</code>, <code class="language-plaintext highlighter-rouge">:output_ext</code>, <code class="language-plaintext highlighter-rouge">:title</code>, <code class="language-plaintext highlighter-rouge">:categories</code>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Custom collection permalink using title from front matter</span>
<span class="na">collections</span><span class="pi">:</span>
  <span class="na">themes</span><span class="pi">:</span>
    <span class="na">output</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">permalink</span><span class="pi">:</span> <span class="s">/themes/:title/</span>
</code></pre></div></div>

<h2 id="the-title-placeholder-in-detail">The :title placeholder in detail</h2>

<p><code class="language-plaintext highlighter-rouge">:title</code> uses the post or page title, converted to a URL-safe slug:</p>

<ul>
  <li>Lowercase</li>
  <li>Spaces replaced with hyphens</li>
  <li>Special characters removed</li>
  <li>Accented characters transliterated (é → e)</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Title: "Jekyll Front Matter: The Complete Guide!"
:title → "jekyll-front-matter-the-complete-guide"
</code></pre></div></div>

<p>If you want a different slug than the auto-generated one, set <code class="language-plaintext highlighter-rouge">slug</code> in front matter:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Jekyll</span><span class="nv"> </span><span class="s">Front</span><span class="nv"> </span><span class="s">Matter:</span><span class="nv"> </span><span class="s">The</span><span class="nv"> </span><span class="s">Complete</span><span class="nv"> </span><span class="s">Guide!"</span>
<span class="na">slug</span><span class="pi">:</span> <span class="s">jekyll-front-matter-guide</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/blog/:slug/</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>This gives <code class="language-plaintext highlighter-rouge">/blog/jekyll-front-matter-guide/</code> instead of the long auto-generated version.</p>

<h2 id="the-categories-placeholder">The :categories placeholder</h2>

<p>If posts have categories, <code class="language-plaintext highlighter-rouge">:categories</code> generates a nested URL:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">categories</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">Tutorial</span><span class="pi">,</span> <span class="nv">Jekyll</span><span class="pi">]</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/:categories/:title/</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>Generates: <code class="language-plaintext highlighter-rouge">/tutorial/jekyll/my-post/</code></p>

<p>If a post has no categories, <code class="language-plaintext highlighter-rouge">:categories</code> is omitted from the URL (Jekyll does not include the empty slash).</p>

<p><strong>Warning:</strong> Using <code class="language-plaintext highlighter-rouge">:categories</code> in your permalink means changing a post’s category changes its URL — which breaks links and SEO. Avoid <code class="language-plaintext highlighter-rouge">:categories</code> in permalinks for blog posts. Use it only for intentional category-based URL structures.</p>

<h2 id="trailing-slashes">Trailing slashes</h2>

<p>Jekyll generates <code class="language-plaintext highlighter-rouge">index.html</code> inside a folder for trailing-slash URLs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permalink: /about/
→ _site/about/index.html
→ served at https://example.com/about/
</code></pre></div></div>

<p>Without a trailing slash:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permalink: /about
→ _site/about.html
→ served at https://example.com/about
</code></pre></div></div>

<p>Use trailing slashes consistently. Mixing <code class="language-plaintext highlighter-rouge">/about/</code> and <code class="language-plaintext highlighter-rouge">/contact</code> causes inconsistency and potential duplicate content. Most modern Jekyll sites use trailing slashes.</p>

<h2 id="redirect-old-urls-after-changing-permalinks">Redirect old URLs after changing permalinks</h2>

<p>If you change a permalink on an existing post, the old URL breaks. Redirect it using <code class="language-plaintext highlighter-rouge">jekyll-redirect-from</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Gemfile</span>
<span class="n">gem</span> <span class="s2">"jekyll-redirect-from"</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">Post"</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/blog/my-new-url/</span>
<span class="na">redirect_from</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">/2026/08/07/my-old-url/</span>
  <span class="pi">-</span> <span class="s">/blog/my-old-url/</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>Jekyll generates redirect pages at the old URLs pointing to the new one. Essential for maintaining SEO when restructuring content.</p>

<h2 id="checking-generated-urls">Checking generated URLs</h2>

<p>To see what URL Jekyll generates for each file, run a build and check the <code class="language-plaintext highlighter-rouge">_site/</code> directory structure — it mirrors your URL structure exactly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>jekyll build
find _site <span class="nt">-name</span> <span class="s2">"index.html"</span> | <span class="nb">head</span> <span class="nt">-20</span>
</code></pre></div></div>

<p>Or use <code class="language-plaintext highlighter-rouge">jekyll serve</code> and browse to check each URL manually.</p>

<h2 id="internal-linking-with-the-link-tag">Internal linking with the link tag</h2>

<p>When linking between pages internally, use the <code class="language-plaintext highlighter-rouge">{% link %}</code> or <code class="language-plaintext highlighter-rouge">{% post_url %}</code> tag instead of hardcoding URLs — they account for <code class="language-plaintext highlighter-rouge">baseurl</code> and raise a build error if the target file does not exist:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;a href="<span class="p">{%</span><span class="w"> </span><span class="nt">link</span><span class="w"> </span>_posts/2026-08-07-my-post.md<span class="w"> </span><span class="p">%}</span>"&gt;My Post&lt;/a&gt;
&lt;a href="<span class="p">{%</span><span class="w"> </span><span class="nt">post_url</span><span class="w"> </span><span class="mi">2026</span><span class="o">-</span><span class="mi">08</span><span class="o">-</span><span class="mi">07</span><span class="o">-</span>my-post<span class="w"> </span><span class="p">%}</span>"&gt;My Post&lt;/a&gt;

</code></pre></div></div>

<p>Both resolve to the post’s actual URL, whatever the permalink setting.</p>

<h2 id="seo-implications-of-permalink-structure">SEO implications of permalink structure</h2>

<p><strong>Keyword in URL:</strong> Shorter URLs that include the post’s main keyword perform slightly better. <code class="language-plaintext highlighter-rouge">/blog/jekyll-permalinks/</code> is better than <code class="language-plaintext highlighter-rouge">/blog/jekyll-permalinks-and-url-structure-the-complete-guide-2026/</code>.</p>

<p><strong>Avoid dates unless meaningful:</strong> <code class="language-plaintext highlighter-rouge">/blog/2026/08/07/my-post/</code> makes content look dated. <code class="language-plaintext highlighter-rouge">/blog/my-post/</code> is evergreen.</p>

<p><strong>Be consistent:</strong> Changing URL structure after publishing harms SEO even with redirects. Choose a structure you can live with long-term before publishing.</p>

<p><strong>Use hyphens, not underscores:</strong> Google treats hyphens as word separators in URLs. <code class="language-plaintext highlighter-rouge">my-post</code> is two words; <code class="language-plaintext highlighter-rouge">my_post</code> is one. Use hyphens.</p>

<p><strong>Short is better:</strong> The shorter and more descriptive the URL, the better. Remove stop words (the, a, an, and, or) from slugs when they add no meaning.</p>

<h2 id="a-complete-permalink-setup">A complete permalink setup</h2>

<p>Here is a solid permalink configuration for a Jekyll blog/marketplace:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml</span>

<span class="c1"># Blog posts</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/blog/:title/</span>

<span class="c1"># Collections</span>
<span class="na">collections</span><span class="pi">:</span>
  <span class="na">themes</span><span class="pi">:</span>
    <span class="na">output</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">permalink</span><span class="pi">:</span> <span class="s">/themes/:name/</span>
  <span class="na">authors</span><span class="pi">:</span>
    <span class="na">output</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">permalink</span><span class="pi">:</span> <span class="s">/authors/:name/</span>
</code></pre></div></div>

<p>Pages set their own <code class="language-plaintext highlighter-rouge">permalink</code> in front matter:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _pages/themes.md</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/themes/</span>

<span class="c1"># _pages/about.md</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/about/</span>

<span class="c1"># _pages/blog.md (blog index)</span>
<span class="na">permalink</span><span class="pi">:</span> <span class="s">/blog/</span>
</code></pre></div></div>

<p>This gives a clean, consistent URL structure where <code class="language-plaintext highlighter-rouge">/blog/</code> contains posts, <code class="language-plaintext highlighter-rouge">/themes/</code> contains theme pages, and top-level paths handle static pages.</p>

<h2 id="permalink-strategies-for-seo">Permalink strategies for SEO</h2>

<p>Your permalink structure is one of the most consequential SEO decisions you make for a Jekyll site. URLs are visible in search results, shared on social media, cited in external links, and remembered by returning visitors. A well-designed permalink structure is clean, descriptive, and stable — changing it after your site has inbound links or search rankings is expensive.</p>

<p>The most SEO-friendly permalink format for blog posts is the title-only slug: <code class="language-plaintext highlighter-rouge">/blog/jekyll-seo-guide/</code> rather than <code class="language-plaintext highlighter-rouge">/blog/2025/12/16/jekyll-seo-guide/</code>. Title-based slugs communicate the topic of the page to both users and search engines, they do not expose the publication date (which can make older content feel stale), and they are significantly shorter which makes them easier to share and remember. In <code class="language-plaintext highlighter-rouge">_config.yml</code>, set this with <code class="language-plaintext highlighter-rouge">permalink: /blog/:title/</code>.</p>

<p>Date-based URLs have legitimate uses for news sites and blogs where publication date is a meaningful signal — a political news site where “today’s coverage” matters is better served by <code class="language-plaintext highlighter-rouge">/news/2025/12/16/story-title/</code> than by <code class="language-plaintext highlighter-rouge">/news/story-title/</code>. For evergreen technical content, tutorial sites, and theme marketplaces, date-based URLs are usually a liability rather than an asset.</p>

<p>Include the primary keyword in the slug where it appears naturally. A post titled “How to Install a Jekyll Theme on macOS” has the slug <code class="language-plaintext highlighter-rouge">how-to-install-a-jekyll-theme-on-macos</code> by default — which is appropriate. Do not stuff additional keywords into slugs; the post title’s natural keywords are sufficient. Very long slugs (over 60 characters) can be shortened by removing stop words: <code class="language-plaintext highlighter-rouge">how-to-install-a-jekyll-theme-on-macos</code> becomes <code class="language-plaintext highlighter-rouge">install-jekyll-theme-macos</code> without losing keyword meaning.</p>

<h2 id="managing-permalink-changes-and-redirects">Managing permalink changes and redirects</h2>

<p>When you change a permalink — whether for a single post or by changing the global <code class="language-plaintext highlighter-rouge">permalink:</code> format — you must redirect the old URL to the new one to preserve inbound links and avoid 404 errors. Each 404 on a URL that previously had inbound links is a lost backlink; over time, accumulating 404s from permalink changes can meaningfully reduce your domain authority.</p>

<p>For Netlify-hosted Jekyll sites, the <code class="language-plaintext highlighter-rouge">_redirects</code> file in your repository root handles redirects at the CDN level with zero latency: add <code class="language-plaintext highlighter-rouge">old/url/ new/url/ 301</code> for a permanent redirect. For Cloudflare Pages, the same syntax works. For GitHub Pages, a <code class="language-plaintext highlighter-rouge">jekyll-redirect-from</code> plugin adds a redirect HTML page at the old URL that meta-refreshes to the new one — less clean than a true server-side redirect, but functional.</p>

<p>Set up Google Search Console and monitor the Coverage report after any permalink change. Search Console shows 404 errors with the inbound links that were trying to reach them — a direct map of which old URLs need redirects. Resolving these 404s promptly, within days of the permalink change, minimises the time during which inbound link authority is not being passed to the new URL.</p>

<h2 id="canonical-urls-and-duplicate-content">Canonical URLs and duplicate content</h2>

<p>Jekyll sites can inadvertently create duplicate content if multiple URLs serve the same or similar content. A blog post accessible at both <code class="language-plaintext highlighter-rouge">/blog/post-title/</code> and <code class="language-plaintext highlighter-rouge">/blog/post-title</code> (with and without trailing slash) is a duplicate content risk if both URLs are indexed. Configure your Jekyll site to consistently redirect one format to the other — most hosts handle this with a redirect rule, and the <code class="language-plaintext highlighter-rouge">jekyll-seo-tag</code> plugin’s <code class="language-plaintext highlighter-rouge">canonical_url</code> output in the <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> tells search engines which version is authoritative.</p>

<p>Paginated archives are another potential duplicate content source. The first page of your blog at <code class="language-plaintext highlighter-rouge">/blog/</code> and the paginated version at <code class="language-plaintext highlighter-rouge">/blog/page/1/</code> may serve identical content. Configure your paginator to only generate <code class="language-plaintext highlighter-rouge">/blog/page/2/</code> onwards, making <code class="language-plaintext highlighter-rouge">/blog/</code> itself the first page — this is the default behaviour of <code class="language-plaintext highlighter-rouge">jekyll-paginate-v2</code> but worth verifying in your specific theme.</p>

<p>Tag and category pages can create near-duplicate content when a single post appears on multiple archive pages with the same excerpt. Use <code class="language-plaintext highlighter-rouge">noindex</code> carefully — you generally want category and tag pages indexed so they rank for their topic keywords — but ensure your theme generates distinct, topic-appropriate titles and descriptions for each archive page rather than repeating generic metadata.</p>

<h2 id="verifying-your-permalink-structure-works">Verifying your permalink structure works</h2>

<p>After configuring your permalink structure, test it before publishing extensively. Build your site locally with <code class="language-plaintext highlighter-rouge">bundle exec jekyll build</code> and browse the <code class="language-plaintext highlighter-rouge">_site/</code> directory to verify that posts are being generated at the expected paths. Check a few edge cases: a post title with special characters (apostrophes, colons, question marks) that might produce unexpected slugs; posts in subdirectories of <code class="language-plaintext highlighter-rouge">_posts/</code>; and any posts with custom <code class="language-plaintext highlighter-rouge">permalink:</code> in their front matter that should override the global setting.</p>

<p>Use the <code class="language-plaintext highlighter-rouge">jekyll-link-checker</code> gem or an HTML proofer tool to scan your built site for internal broken links before deploying. Internal 404s from misconfigured links or incorrect <code class="language-plaintext highlighter-rouge">link</code> tags are easy to introduce and easy to miss without automated checking. A clean link check before each major deployment — especially after changing the permalink structure — is a low-cost insurance policy against broken internal navigation.</p>

<p>Permalink design is a low-visibility decision with long-lasting consequences — once you publish URLs, changing them breaks inbound links, social shares, and search rankings built up over time. Spend an hour deciding on your permalink structure before you publish your first post rather than months later dealing with the fallout of a necessary restructure. A date-free, category-organised slug like <code class="language-plaintext highlighter-rouge">/blog/jekyll-collections-guide/</code> is the structure most likely to remain appropriate as your site grows and your content strategy evolves.</p>]]></content><author><name>Marcus Webb</name></author><category term="Tutorial" /><summary type="html"><![CDATA[How Jekyll builds URLs — permalink patterns, built-in styles, custom permalinks for posts, pages, and collections, and best practices for SEO-friendly URLs.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/jekyll-permalinks-url-structure.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/jekyll-permalinks-url-structure.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyll Variables Reference: site, page, layout, and More</title><link href="https://jekyllhub.com/tutorial/2026/06/28/jekyll-variables-reference/" rel="alternate" type="text/html" title="Jekyll Variables Reference: site, page, layout, and More" /><published>2026-06-28T00:00:00+00:00</published><updated>2026-06-28T00:00:00+00:00</updated><id>https://jekyllhub.com/tutorial/2026/06/28/jekyll-variables-reference</id><content type="html" xml:base="https://jekyllhub.com/tutorial/2026/06/28/jekyll-variables-reference/"><![CDATA[<p>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.</p>

<h2 id="site-variables">site variables</h2>

<p><code class="language-plaintext highlighter-rouge">site</code> contains global data about your Jekyll site — configuration from <code class="language-plaintext highlighter-rouge">_config.yml</code>, collections, posts, and build information.</p>

<h3 id="configuration-variables">Configuration variables</h3>

<p>Every key in <code class="language-plaintext highlighter-rouge">_config.yml</code> becomes a <code class="language-plaintext highlighter-rouge">site.*</code> variable:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">JekyllHub"</span>
<span class="na">author</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Marcus</span><span class="nv"> </span><span class="s">Webb"</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">A</span><span class="nv"> </span><span class="s">Jekyll</span><span class="nv"> </span><span class="s">theme</span><span class="nv"> </span><span class="s">marketplace."</span>
<span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://jekyllhub.com"</span>
<span class="na">baseurl</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
<span class="na">google_analytics</span><span class="pi">:</span> <span class="s2">"</span><span class="s">G-XXXXXXXXXX"</span>
<span class="na">sendy_list_id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">abc123"</span>
</code></pre></div></div>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>           → "JekyllHub"
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">description</span><span class="w"> </span><span class="p">}}</span>     → "A Jekyll theme marketplace."
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>             → "https://jekyllhub.com"
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">baseurl</span><span class="w"> </span><span class="p">}}</span>         → ""
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">}}</span>          → "Marcus Webb"
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">google_analytics</span><span class="w"> </span><span class="p">}}</span>→ "G-XXXXXXXXXX"
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">sendy_list_id</span><span class="w"> </span><span class="p">}}</span>   → "abc123"

</code></pre></div></div>

<h3 id="built-in-site-variables">Built-in site variables</h3>

<p>These are provided by Jekyll itself, not from <code class="language-plaintext highlighter-rouge">_config.yml</code>:</p>

<table>
  <thead>
    <tr>
      <th>Variable</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.time</code></td>
      <td>DateTime</td>
      <td>The time of the current build</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.pages</code></td>
      <td>Array</td>
      <td>All pages in the site</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.posts</code></td>
      <td>Array</td>
      <td>All posts, sorted newest first</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.related_posts</code></td>
      <td>Array</td>
      <td>Up to 10 related posts (for the current post)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.static_files</code></td>
      <td>Array</td>
      <td>All static files (non-processed)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.html_pages</code></td>
      <td>Array</td>
      <td>Pages with <code class="language-plaintext highlighter-rouge">.html</code> or <code class="language-plaintext highlighter-rouge">.htm</code> extension</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.html_files</code></td>
      <td>Array</td>
      <td>Static files with <code class="language-plaintext highlighter-rouge">.html</code> extension</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.collections</code></td>
      <td>Array</td>
      <td>All collections defined in config</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.data</code></td>
      <td>Object</td>
      <td>Data from all files in <code class="language-plaintext highlighter-rouge">_data/</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.documents</code></td>
      <td>Array</td>
      <td>All documents in all collections</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.categories</code></td>
      <td>Object</td>
      <td>Posts grouped by category</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">site.tags</code></td>
      <td>Object</td>
      <td>Posts grouped by tag</td>
    </tr>
  </tbody>
</table>

<h3 id="siteposts">site.posts</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> All posts, newest first </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Post count </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">}}</span> posts

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Latest post </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">latest</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">first</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">latest</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<h3 id="sitepages">site.pages</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">page</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.pages</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">%}</span>
    &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Note: <code class="language-plaintext highlighter-rouge">site.pages</code> includes all pages — HTML files, Markdown files, and some generated files. Filter by <code class="language-plaintext highlighter-rouge">page.layout</code> or <code class="language-plaintext highlighter-rouge">page.url</code> if you need a subset.</p>

<h3 id="sitedata">site.data</h3>

<p>Mirrors the <code class="language-plaintext highlighter-rouge">_data/</code> directory structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_data/
├── navigation.yml
├── authors.yml
└── showcase/
    └── sites.yml
</code></pre></div></div>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="p">.</span><span class="nv">navigation</span><span class="w"> </span><span class="p">}}</span>         → contents of navigation.yml
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="p">.</span><span class="nv">authors</span><span class="w"> </span><span class="p">}}</span>            → contents of authors.yml
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="p">.</span><span class="nv">showcase</span><span class="p">.</span><span class="nv">sites</span><span class="w"> </span><span class="p">}}</span>     → contents of showcase/sites.yml

</code></pre></div></div>

<h3 id="sitecategories-and-sitetags">site.categories and site.tags</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Loop over all categories </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.categories</span><span class="w"> </span><span class="p">%}</span>
  &lt;h2&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">category</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="w"> </span><span class="p">}}</span>&lt;/h2&gt;          ← category name
  <span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">category[1]</span><span class="w"> </span><span class="p">%}</span>
    &lt;li&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/li&gt;          ← posts in this category
  <span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Posts in a specific category </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">tutorial_posts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">categories</span><span class="p">[</span><span class="s2">"Tutorial"</span><span class="p">]</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">tutorial_posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">}}</span> tutorials

</code></pre></div></div>

<h3 id="collection-variables">Collection variables</h3>

<p>For a collection named <code class="language-plaintext highlighter-rouge">themes</code> (defined in <code class="language-plaintext highlighter-rouge">_config.yml</code>):</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="p">}}</span>                 → array of all theme documents
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">}}</span>          → number of themes
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">theme</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.themes</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="page-variables">page variables</h2>

<p><code class="language-plaintext highlighter-rouge">page</code> contains data about the current page, post, or collection document being rendered.</p>

<h3 id="built-in-page-variables">Built-in page variables</h3>

<table>
  <thead>
    <tr>
      <th>Variable</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.content</code></td>
      <td>String</td>
      <td>Rendered HTML content of the page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.title</code></td>
      <td>String</td>
      <td>Title from front matter</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.excerpt</code></td>
      <td>String</td>
      <td>Excerpt (first paragraph or custom)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.url</code></td>
      <td>String</td>
      <td>URL of the page (e.g. <code class="language-plaintext highlighter-rouge">/blog/my-post/</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.date</code></td>
      <td>DateTime</td>
      <td>Post date</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.id</code></td>
      <td>String</td>
      <td>Unique identifier (e.g. <code class="language-plaintext highlighter-rouge">/2026/08/06/my-post</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.categories</code></td>
      <td>Array</td>
      <td>Categories from front matter</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.tags</code></td>
      <td>Array</td>
      <td>Tags from front matter</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.path</code></td>
      <td>String</td>
      <td>Source file path (e.g. <code class="language-plaintext highlighter-rouge">_posts/2026-08-06-my-post.md</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.name</code></td>
      <td>String</td>
      <td>Filename (e.g. <code class="language-plaintext highlighter-rouge">2026-08-06-my-post.md</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.next</code></td>
      <td>Object</td>
      <td>Next post (chronologically)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">page.previous</code></td>
      <td>Object</td>
      <td>Previous post (chronologically)</td>
    </tr>
  </tbody>
</table>

<h3 id="custom-front-matter-variables">Custom front matter variables</h3>

<p>Every key in a page’s front matter becomes a <code class="language-plaintext highlighter-rouge">page.*</code> variable:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">Post"</span>
<span class="na">author</span><span class="pi">:</span> <span class="s">Marcus Webb</span>
<span class="na">featured</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">reading_time</span><span class="pi">:</span> <span class="m">8</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">/assets/images/blog/cover.webp</span>
<span class="na">difficulty</span><span class="pi">:</span> <span class="s">beginner</span>
<span class="nn">---</span>
</code></pre></div></div>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">}}</span>         → "Marcus Webb"
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">featured</span><span class="w"> </span><span class="p">}}</span>       → true
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">reading_time</span><span class="w"> </span><span class="p">}}</span>   → 8
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">}}</span>          → "/assets/images/blog/cover.webp"
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">difficulty</span><span class="w"> </span><span class="p">}}</span>     → "beginner"

</code></pre></div></div>

<h3 id="pageurl-vs-pageid">page.url vs page.id</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>   → "/blog/jekyll-variables-reference/"
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">id</span><span class="w"> </span><span class="p">}}</span>    → "/2026/08/06/jekyll-variables-reference"

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">page.url</code> is the clean URL visitors see. <code class="language-plaintext highlighter-rouge">page.id</code> is an internal identifier used by some plugins.</p>

<h3 id="pagedate">page.date</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">}}</span>                          → 2026-08-06 00:00:00 +0000
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%B %-d, %Y"</span><span class="w"> </span><span class="p">}}</span>    → August 6, 2026
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%Y-%m-%d"</span><span class="w"> </span><span class="p">}}</span>      → 2026-08-06
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%s"</span><span class="w"> </span><span class="p">}}</span>            → Unix timestamp

</code></pre></div></div>

<h3 id="pageexcerpt">page.excerpt</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">excerpt</span><span class="w"> </span><span class="p">}}</span>              → first paragraph (rendered HTML)
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">excerpt</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">strip_html</span><span class="w"> </span><span class="p">}}</span> → plain text excerpt
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">excerpt</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">strip_html</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">truncatewords</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<p>Override the default excerpt in front matter:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">excerpt</span><span class="pi">:</span> <span class="s2">"</span><span class="s">A</span><span class="nv"> </span><span class="s">custom</span><span class="nv"> </span><span class="s">summary</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">this</span><span class="nv"> </span><span class="s">post."</span>
</code></pre></div></div>

<h3 id="pagenext-and-pageprevious">page.next and page.previous</h3>

<p>Navigate between posts:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">next</span><span class="w"> </span><span class="p">%}</span>
  &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">next</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;Next: <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">next</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">previous</span><span class="w"> </span><span class="p">%}</span>
  &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">previous</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;Previous: <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">previous</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Note: <code class="language-plaintext highlighter-rouge">page.next</code> is the chronologically newer post; <code class="language-plaintext highlighter-rouge">page.previous</code> is the older one — counterintuitive but correct.</p>

<h3 id="pagecategories-and-pagetags">page.categories and page.tags</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">page.categories</span><span class="w"> </span><span class="p">%}</span>
  &lt;a href="/category/<span class="p">{{</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">slugify</span><span class="w"> </span><span class="p">}}</span>/"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">page.tags</span><span class="w"> </span><span class="p">%}</span>
  &lt;span class="tag"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="p">}}</span>&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="layout-variables">layout variables</h2>

<p><code class="language-plaintext highlighter-rouge">layout</code> contains data from the current layout file’s front matter:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">&lt;!-- _layouts/post.html --&gt;</span>
<span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">default</span>
<span class="na">sidebar</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">show_related</span><span class="pi">:</span> <span class="no">true</span>
<span class="nn">---</span>
</code></pre></div></div>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">layout</span><span class="p">.</span><span class="nv">sidebar</span><span class="w"> </span><span class="p">}}</span>      → true
<span class="p">{{</span><span class="w"> </span><span class="nv">layout</span><span class="p">.</span><span class="nv">show_related</span><span class="w"> </span><span class="p">}}</span> → true

</code></pre></div></div>

<p>Useful when a layout needs configuration that individual pages can check:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">layout</span><span class="p">.</span><span class="nv">sidebar</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>sidebar.html<span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="content">content</h2>

<p>Available only inside layout files. Contains the rendered HTML content being wrapped by the layout:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c">&lt;!-- _layouts/default.html --&gt;</span>
<span class="nt">&lt;main&gt;</span>
  {{ content }}
<span class="nt">&lt;/main&gt;</span>

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">content</code> is the output after all inner layouts have been applied. For a post using <code class="language-plaintext highlighter-rouge">layout: post</code> (which itself uses <code class="language-plaintext highlighter-rouge">layout: default</code>), by the time <code class="language-plaintext highlighter-rouge">default.html</code> sees <code class="language-plaintext highlighter-rouge">content</code>, it already contains the post’s HTML wrapped in <code class="language-plaintext highlighter-rouge">post.html</code>’s structure.</p>

<h2 id="paginator-variables">paginator variables</h2>

<p>Available on paginated pages when using <code class="language-plaintext highlighter-rouge">jekyll-paginate</code> or <code class="language-plaintext highlighter-rouge">jekyll-paginate-v2</code>:</p>

<table>
  <thead>
    <tr>
      <th>Variable</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">paginator.page</code></td>
      <td>Current page number</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">paginator.per_page</code></td>
      <td>Posts per page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">paginator.posts</code></td>
      <td>Posts on the current page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">paginator.total_posts</code></td>
      <td>Total post count</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">paginator.total_pages</code></td>
      <td>Total page count</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">paginator.previous_page</code></td>
      <td>Previous page number (or nil)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">paginator.previous_page_path</code></td>
      <td>URL of previous page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">paginator.next_page</code></td>
      <td>Next page number (or nil)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">paginator.next_page_path</code></td>
      <td>URL of next page</td>
    </tr>
  </tbody>
</table>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">paginator.posts</span><span class="w"> </span><span class="p">%}</span>
  &lt;article&gt;
    &lt;h2&gt;&lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;&lt;/h2&gt;
  &lt;/article&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

&lt;nav class="pagination"&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">paginator</span><span class="p">.</span><span class="nv">previous_page</span><span class="w"> </span><span class="p">%}</span>
    &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">paginator</span><span class="p">.</span><span class="nv">previous_page_path</span><span class="w"> </span><span class="p">}}</span>"&gt;← Newer&lt;/a&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
  &lt;span&gt;Page <span class="p">{{</span><span class="w"> </span><span class="nv">paginator</span><span class="p">.</span><span class="nv">page</span><span class="w"> </span><span class="p">}}</span> of <span class="p">{{</span><span class="w"> </span><span class="nv">paginator</span><span class="p">.</span><span class="nv">total_pages</span><span class="w"> </span><span class="p">}}</span>&lt;/span&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">paginator</span><span class="p">.</span><span class="nv">next_page</span><span class="w"> </span><span class="p">%}</span>
    &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">paginator</span><span class="p">.</span><span class="nv">next_page_path</span><span class="w"> </span><span class="p">}}</span>"&gt;Older →&lt;/a&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
&lt;/nav&gt;

</code></pre></div></div>

<h2 id="jekyll-variables">jekyll variables</h2>

<p>Information about the Jekyll build environment:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">jekyll</span><span class="p">.</span><span class="nv">environment</span><span class="w"> </span><span class="p">}}</span>   → "development" or "production"
<span class="p">{{</span><span class="w"> </span><span class="nv">jekyll</span><span class="p">.</span><span class="nv">version</span><span class="w"> </span><span class="p">}}</span>       → "4.3.2"

</code></pre></div></div>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">jekyll</span><span class="p">.</span><span class="nv">environment</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"production"</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>analytics.html<span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Set <code class="language-plaintext highlighter-rouge">JEKYLL_ENV=production</code> before building to enable production-only features.</p>

<h2 id="forloop-variables">forloop variables</h2>

<p>Inside any <code class="language-plaintext highlighter-rouge">{% for %}</code> loop:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.index</span><span class="w"> </span><span class="p">}}</span>    → 1, 2, 3... (1-based)
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.index0</span><span class="w"> </span><span class="p">}}</span>   → 0, 1, 2... (0-based)
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.rindex</span><span class="w"> </span><span class="p">}}</span>   → counts down from total (1-based)
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.rindex0</span><span class="w"> </span><span class="p">}}</span>  → counts down from total (0-based)
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.first</span><span class="w"> </span><span class="p">}}</span>    → true on first iteration
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.last</span><span class="w"> </span><span class="p">}}</span>     → true on last iteration
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.length</span><span class="w"> </span><span class="p">}}</span>   → total items in array
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Practical use — add a class to the first item and a divider between items:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">item</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">list</span><span class="w"> </span><span class="p">%}</span>
  &lt;div class="item<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nb">forloop.first</span><span class="w"> </span><span class="p">%}</span> item--first<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>"&gt;
    <span class="p">{{</span><span class="w"> </span><span class="nv">item</span><span class="p">.</span><span class="nv">name</span><span class="w"> </span><span class="p">}}</span>
  &lt;/div&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">unless</span><span class="w"> </span><span class="nb">forloop.last</span><span class="w"> </span><span class="p">%}</span>&lt;hr&gt;<span class="p">{%</span><span class="w"> </span><span class="kr">endunless</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="tablerow-variables">tablerow variables</h2>

<p>Inside a <code class="language-plaintext highlighter-rouge">{% tablerow %}</code> loop:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">tablerow</span><span class="w"> </span><span class="nv">item</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">list</span><span class="w"> </span><span class="na">cols</span><span class="o">:</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">tablerowloop</span><span class="p">.</span><span class="nv">index</span><span class="w"> </span><span class="p">}}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">tablerowloop</span><span class="p">.</span><span class="nv">col</span><span class="w"> </span><span class="p">}}</span>       → current column (1-based)
  <span class="p">{{</span><span class="w"> </span><span class="nv">tablerowloop</span><span class="p">.</span><span class="nv">col0</span><span class="w"> </span><span class="p">}}</span>      → current column (0-based)
  <span class="p">{{</span><span class="w"> </span><span class="nv">tablerowloop</span><span class="p">.</span><span class="nv">col_first</span><span class="w"> </span><span class="p">}}</span> → true on first column
  <span class="p">{{</span><span class="w"> </span><span class="nv">tablerowloop</span><span class="p">.</span><span class="nv">col_last</span><span class="w"> </span><span class="p">}}</span>  → true on last column
  <span class="p">{{</span><span class="w"> </span><span class="nv">tablerowloop</span><span class="p">.</span><span class="nv">row</span><span class="w"> </span><span class="p">}}</span>       → current row number
  <span class="p">{{</span><span class="w"> </span><span class="nv">tablerowloop</span><span class="p">.</span><span class="nf">first</span><span class="w"> </span><span class="p">}}</span>     → true on first item
  <span class="p">{{</span><span class="w"> </span><span class="nv">tablerowloop</span><span class="p">.</span><span class="nf">last</span><span class="w"> </span><span class="p">}}</span>      → true on last item
  <span class="p">{{</span><span class="w"> </span><span class="nv">tablerowloop</span><span class="p">.</span><span class="nv">length</span><span class="w"> </span><span class="p">}}</span>    → total items
<span class="p">{%</span><span class="w"> </span><span class="nt">endtablerow</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="quick-reference-which-variable-to-use">Quick reference: which variable to use</h2>

<table>
  <thead>
    <tr>
      <th>You need</th>
      <th>Use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Site name/URL from config</td>
      <td><code class="language-plaintext highlighter-rouge">site.title</code>, <code class="language-plaintext highlighter-rouge">site.url</code></td>
    </tr>
    <tr>
      <td>All blog posts</td>
      <td><code class="language-plaintext highlighter-rouge">site.posts</code></td>
    </tr>
    <tr>
      <td>All pages</td>
      <td><code class="language-plaintext highlighter-rouge">site.pages</code></td>
    </tr>
    <tr>
      <td>Data from <code class="language-plaintext highlighter-rouge">_data/nav.yml</code></td>
      <td><code class="language-plaintext highlighter-rouge">site.data.nav</code></td>
    </tr>
    <tr>
      <td>Custom config value</td>
      <td><code class="language-plaintext highlighter-rouge">site.your_key</code></td>
    </tr>
    <tr>
      <td>Current page title</td>
      <td><code class="language-plaintext highlighter-rouge">page.title</code></td>
    </tr>
    <tr>
      <td>Current page URL</td>
      <td><code class="language-plaintext highlighter-rouge">page.url</code></td>
    </tr>
    <tr>
      <td>Custom front matter value</td>
      <td><code class="language-plaintext highlighter-rouge">page.your_key</code></td>
    </tr>
    <tr>
      <td>Post publish date</td>
      <td><code class="language-plaintext highlighter-rouge">page.date</code></td>
    </tr>
    <tr>
      <td>Post excerpt</td>
      <td><code class="language-plaintext highlighter-rouge">page.excerpt</code></td>
    </tr>
    <tr>
      <td>Next/previous post</td>
      <td><code class="language-plaintext highlighter-rouge">page.next</code>, <code class="language-plaintext highlighter-rouge">page.previous</code></td>
    </tr>
    <tr>
      <td>Rendered page content (in layouts)</td>
      <td><code class="language-plaintext highlighter-rouge">content</code></td>
    </tr>
    <tr>
      <td>Build environment</td>
      <td><code class="language-plaintext highlighter-rouge">jekyll.environment</code></td>
    </tr>
    <tr>
      <td>Pagination data</td>
      <td><code class="language-plaintext highlighter-rouge">paginator.*</code></td>
    </tr>
    <tr>
      <td>Loop position</td>
      <td><code class="language-plaintext highlighter-rouge">forloop.index</code>, <code class="language-plaintext highlighter-rouge">forloop.first</code>, etc.</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="using-variables-with-liquid-filters">Using variables with Liquid filters</h2>

<p>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.</p>

<h3 id="formatting-dates">Formatting dates</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%B %-d, %Y"</span><span class="w"> </span><span class="p">}}</span>      → "January 29, 2026"
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%Y-%m-%d"</span><span class="w"> </span><span class="p">}}</span>         → "2026-01-29"
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date_to_xmlschema</span><span class="w"> </span><span class="p">}}</span>         → "2026-01-29T00:00:00+00:00"
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date_to_rfc822</span><span class="w"> </span><span class="p">}}</span>            → RFC 822 format for RSS feeds

</code></pre></div></div>

<h3 id="generating-slugs-and-urls">Generating slugs and URLs</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">slugify</span><span class="w"> </span><span class="p">}}</span>                  → "jekyll-variables-reference"
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">absolute_url</span><span class="w"> </span><span class="p">}}</span>               → "https://example.com/blog/post/"
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>               → "/blog/post/"

</code></pre></div></div>

<h3 id="truncating-and-stripping-content">Truncating and stripping content</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">excerpt</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">strip_html</span><span class="w"> </span><span class="p">}}</span>             → plain text excerpt
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">excerpt</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">truncatewords</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="w"> </span><span class="p">}}</span>      → first 30 words
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">content</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">strip_html</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">}}</span>      → character count of plain text
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">upcase</span><span class="w"> </span><span class="p">}}</span>                   → uppercase title
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">downcase</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">slugify</span><span class="w"> </span><span class="p">}}</span>       → normalised slug

</code></pre></div></div>

<h3 id="working-with-arrays">Working with arrays</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">}}</span>                     → total post count
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">first</span><span class="w"> </span><span class="p">}}</span>                    → most recent post object
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">last</span><span class="w"> </span><span class="p">}}</span>                     → oldest post object
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">tags</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">join</span><span class="p">:</span><span class="w"> </span><span class="s2">", "</span><span class="w"> </span><span class="p">}}</span>               → "jekyll, tutorial, sass"
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">sort</span><span class="p">:</span><span class="w"> </span><span class="s2">"title"</span><span class="w"> </span><span class="p">}}</span>            → posts sorted alphabetically
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"featured"</span><span class="p">,</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">}}</span>  → only featured posts
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where_exp</span><span class="p">:</span><span class="w"> </span><span class="s2">"post"</span><span class="p">,</span><span class="w"> </span><span class="s2">"post.tags contains 'jekyll'"</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<h2 id="debugging-variables-with-inspect">Debugging variables with inspect</h2>

<p>When a template produces unexpected output, the <code class="language-plaintext highlighter-rouge">inspect</code> filter reveals the raw structure of any variable:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">inspect</span><span class="w"> </span><span class="p">}}</span>       → shows all page variables and their values
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">inspect</span><span class="w"> </span><span class="p">}}</span>  → shows the complete data structure
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">tags</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">inspect</span><span class="w"> </span><span class="p">}}</span>  → shows the tags array: ["jekyll", "tutorial"]

</code></pre></div></div>

<p>For complex nested objects, you can iterate to inspect individual keys:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">item</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">page</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">item</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="w"> </span><span class="p">}}</span>: <span class="p">{{</span><span class="w"> </span><span class="nv">item</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">inspect</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>This prints every key-value pair in the <code class="language-plaintext highlighter-rouge">page</code> 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.</p>

<h2 id="variables-inside-includes">Variables inside includes</h2>

<p>When you call <code class="language-plaintext highlighter-rouge">{% include file.html %}</code>, you can pass parameters that become available as <code class="language-plaintext highlighter-rouge">include.parameter_name</code> inside the included file:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>card.html<span class="w"> </span><span class="na">title</span><span class="o">=</span>post.title<span class="w"> </span><span class="na">url</span><span class="o">=</span>post.url<span class="w"> </span><span class="na">image</span><span class="o">=</span>post.image<span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Inside <code class="language-plaintext highlighter-rouge">_includes/card.html</code>:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;div class="card"&gt;
  &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">include</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">}}</span>" alt="<span class="p">{{</span><span class="w"> </span><span class="nv">include</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>"&gt;
  &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">include</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">include</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
&lt;/div&gt;

</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">include</code> 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.</p>

<p>You can also pass Liquid expressions and variables as include parameters:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>card.html<span class="w"> </span><span class="na">title</span><span class="o">=</span>page.title<span class="w"> </span><span class="na">url</span><span class="o">=</span>page.url<span class="w"> </span>|<span class="w"> </span><span class="nv">relative_url</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="checking-whether-a-variable-is-defined">Checking whether a variable is defined</h2>

<p>Use the <code class="language-plaintext highlighter-rouge">nil</code> check to guard against undefined front matter variables:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">%}</span>
  &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">}}</span>" alt="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>"&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">%}</span>
  &lt;span&gt;By <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">}}</span>&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
  &lt;span&gt;By <span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">}}</span>&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Use the <code class="language-plaintext highlighter-rouge">default</code> filter to provide a fallback value inline:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">default</span><span class="p">:</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">default</span><span class="p">:</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">default_image</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">description</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">default</span><span class="p">:</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">description</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">default</code> filter returns the specified value when the original is <code class="language-plaintext highlighter-rouge">nil</code>, <code class="language-plaintext highlighter-rouge">false</code>, or an empty string. This is the cleanest way to implement fallback behaviour without <code class="language-plaintext highlighter-rouge">{% if %}</code> blocks.</p>

<h2 id="building-a-complete-post-template-with-variables">Building a complete post template with variables</h2>

<p>Putting variables together into a realistic post layout shows how they interact:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
---
layout: default
---

&lt;article
  class="post"
  itemscope
  itemtype="https://schema.org/BlogPosting"
&gt;
  &lt;header class="post-header"&gt;
    &lt;div class="post-meta"&gt;
      &lt;time datetime="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date_to_xmlschema</span><span class="w"> </span><span class="p">}}</span>" itemprop="datePublished"&gt;
        <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%B %-d, %Y"</span><span class="w"> </span><span class="p">}}</span>
      &lt;/time&gt;
      <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">last_modified_at</span><span class="w"> </span><span class="p">%}</span>
        &lt;time datetime="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">last_modified_at</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date_to_xmlschema</span><span class="w"> </span><span class="p">}}</span>" itemprop="dateModified"&gt;
          Updated <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">last_modified_at</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%B %-d, %Y"</span><span class="w"> </span><span class="p">}}</span>
        &lt;/time&gt;
      <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
      <span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">page.categories</span><span class="w"> </span><span class="p">%}</span>
        &lt;a href="/category/<span class="p">{{</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">slugify</span><span class="w"> </span><span class="p">}}</span>/" class="category-link"&gt;
          <span class="p">{{</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="p">}}</span>
        &lt;/a&gt;
      <span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
    &lt;/div&gt;

    &lt;h1 itemprop="headline"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/h1&gt;

    <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">description</span><span class="w"> </span><span class="p">%}</span>
      &lt;p class="post-description" itemprop="description"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">description</span><span class="w"> </span><span class="p">}}</span>&lt;/p&gt;
    <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

    <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">%}</span>
      &lt;div class="post-author" itemprop="author" itemscope itemtype="https://schema.org/Person"&gt;
        &lt;span itemprop="name"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">}}</span>&lt;/span&gt;
      &lt;/div&gt;
    <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

    <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">%}</span>
      &lt;img
        src="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>"
        alt="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>"
        itemprop="image"
        class="post-hero"
      &gt;
    <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
  &lt;/header&gt;

  &lt;div class="post-content" itemprop="articleBody"&gt;
    <span class="p">{{</span><span class="w"> </span><span class="nv">content</span><span class="w"> </span><span class="p">}}</span>
  &lt;/div&gt;

  &lt;footer class="post-footer"&gt;
    <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">tags</span><span class="p">.</span><span class="nf">size</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">%}</span>
      &lt;div class="post-tags"&gt;
        <span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">page.tags</span><span class="w"> </span><span class="p">%}</span>
          &lt;a href="/tag/<span class="p">{{</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">slugify</span><span class="w"> </span><span class="p">}}</span>/" class="tag"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
        <span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
      &lt;/div&gt;
    <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

    &lt;nav class="post-nav" aria-label="Post navigation"&gt;
      <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">previous</span><span class="w"> </span><span class="p">%}</span>
        &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">previous</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>" class="post-nav__prev"&gt;
          ← <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">previous</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>
        &lt;/a&gt;
      <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
      <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">next</span><span class="w"> </span><span class="p">%}</span>
        &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">next</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>" class="post-nav__next"&gt;
          <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">next</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span> →
        &lt;/a&gt;
      <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
    &lt;/nav&gt;
  &lt;/footer&gt;
&lt;/article&gt;

</code></pre></div></div>

<p>This template uses <code class="language-plaintext highlighter-rouge">page.date</code>, <code class="language-plaintext highlighter-rouge">page.last_modified_at</code>, <code class="language-plaintext highlighter-rouge">page.categories</code>, <code class="language-plaintext highlighter-rouge">page.title</code>, <code class="language-plaintext highlighter-rouge">page.description</code>, <code class="language-plaintext highlighter-rouge">page.author</code>, <code class="language-plaintext highlighter-rouge">page.image</code>, <code class="language-plaintext highlighter-rouge">content</code>, <code class="language-plaintext highlighter-rouge">page.tags</code>, <code class="language-plaintext highlighter-rouge">page.previous</code>, and <code class="language-plaintext highlighter-rouge">page.next</code> — the full set of standard variables that any well-structured post file provides.</p>

<h2 id="common-pitfalls-with-jekyll-variables">Common pitfalls with Jekyll variables</h2>

<p><strong>Forgetting that <code class="language-plaintext highlighter-rouge">site.posts</code> is sorted newest-first.</strong> If you want oldest-first, use <code class="language-plaintext highlighter-rouge">{{ site.posts | reverse }}</code>.</p>

<p><strong>Confusing <code class="language-plaintext highlighter-rouge">page.url</code> with <code class="language-plaintext highlighter-rouge">page.id</code>.</strong> 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.</p>

<p><strong>Using <code class="language-plaintext highlighter-rouge">page.content</code> inside the post layout.</strong> In layout files, use <code class="language-plaintext highlighter-rouge">content</code> (no <code class="language-plaintext highlighter-rouge">page.</code> prefix) to get the rendered HTML. <code class="language-plaintext highlighter-rouge">page.content</code> gives you the raw Markdown source, which is rarely what you want.</p>

<p><strong>Accessing collection variables before declaring the collection.</strong> If <code class="language-plaintext highlighter-rouge">site.projects</code> returns nil, check that <code class="language-plaintext highlighter-rouge">projects</code> is declared under <code class="language-plaintext highlighter-rouge">collections:</code> in <code class="language-plaintext highlighter-rouge">_config.yml</code> and that the <code class="language-plaintext highlighter-rouge">_projects/</code> directory exists.</p>

<p><strong>Expecting <code class="language-plaintext highlighter-rouge">forloop.first</code> to detect the first post in <code class="language-plaintext highlighter-rouge">site.posts</code>.</strong> The <code class="language-plaintext highlighter-rouge">forloop</code> variable is scoped to the current <code class="language-plaintext highlighter-rouge">{% for %}</code> loop iteration. It does not know anything about the global list of posts — it only knows its position in the current loop run.</p>

<p>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 <a href="https://jekyllrb.com/docs/variables/">official Jekyll docs</a> with additional detail on edge cases.</p>

<hr />

<h2 id="variables-in-collection-documents">Variables in collection documents</h2>

<p>Collection documents have all the standard <code class="language-plaintext highlighter-rouge">page.*</code> variables plus some collection-specific ones. When you loop over <code class="language-plaintext highlighter-rouge">site.themes</code> (a collection), each item exposes:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">collection</span><span class="w"> </span><span class="p">}}</span>   → "themes" (the collection name)
<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">relative_path</span><span class="w"> </span><span class="p">}}</span> → "_themes/minimal-mistakes.md"
<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">path</span><span class="w"> </span><span class="p">}}</span>          → full filesystem path
<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>           → "/themes/minimal-mistakes/"
<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">id</span><span class="w"> </span><span class="p">}}</span>            → "/themes/minimal-mistakes"
<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>         → from front matter
<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">content</span><span class="w"> </span><span class="p">}}</span>       → rendered HTML body

</code></pre></div></div>

<p>All front matter fields on the document also become available as variables, so a theme document with <code class="language-plaintext highlighter-rouge">stars: 27000</code> exposes <code class="language-plaintext highlighter-rouge">theme.stars</code>. This is the basis of filtering and sorting collection documents:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">popular</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where_exp</span><span class="p">:</span><span class="w"> </span><span class="s2">"t"</span><span class="p">,</span><span class="w"> </span><span class="s2">"t.stars &gt; 10000"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">sort</span><span class="p">:</span><span class="w"> </span><span class="s2">"stars"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">reverse</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">theme</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">popular</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">6</span><span class="w"> </span><span class="p">%}</span>
  &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span> — ★ <span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">stars</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="generating-variables-at-build-time">Generating variables at build time</h2>

<p>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 <code class="language-plaintext highlighter-rouge">site.*</code> or front matter variables during the build.</p>

<p>For example, to add a <code class="language-plaintext highlighter-rouge">site.build_time</code> variable that contains the formatted build timestamp:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _plugins/build_time.rb</span>
<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">:after_init</span> <span class="k">do</span> <span class="o">|</span><span class="n">site</span><span class="o">|</span>
  <span class="n">site</span><span class="p">.</span><span class="nf">config</span><span class="p">[</span><span class="s1">'build_time'</span><span class="p">]</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s1">'%Y-%m-%d %H:%M'</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>After adding this plugin, <code class="language-plaintext highlighter-rouge">{{ site.build_time }}</code> is available in every template.</p>

<p>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 <code class="language-plaintext highlighter-rouge">site.data.something</code>. The <code class="language-plaintext highlighter-rouge">_data/</code> directory supports this natively without plugins: any YAML, JSON, CSV, or TSV file in <code class="language-plaintext highlighter-rouge">_data/</code> becomes accessible as <code class="language-plaintext highlighter-rouge">site.data.filename</code>.</p>

<h2 id="the-scope-of-each-variable-type">The scope of each variable type</h2>

<p>Understanding the scope of each variable type prevents a common class of bugs in Jekyll templates.</p>

<p><code class="language-plaintext highlighter-rouge">site</code> variables are global — the same values are available in every layout, include, and page throughout the entire build. Changing a <code class="language-plaintext highlighter-rouge">site.*</code> variable in one template does not affect others (Liquid is stateless), but reading from it in any file always returns the same value.</p>

<p><code class="language-plaintext highlighter-rouge">page</code> variables are page-scoped — they represent the current document being rendered. When Jekyll renders <code class="language-plaintext highlighter-rouge">_posts/my-post.md</code> using <code class="language-plaintext highlighter-rouge">_layouts/post.html</code>, the <code class="language-plaintext highlighter-rouge">page</code> variable inside both files refers to the same post document. When a layout includes a partial (<code class="language-plaintext highlighter-rouge">{% include sidebar.html %}</code>), the <code class="language-plaintext highlighter-rouge">page</code> variable inside the include still refers to the same document — not the include file itself.</p>

<p><code class="language-plaintext highlighter-rouge">layout</code> 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 <code class="language-plaintext highlighter-rouge">layout: post</code> and <code class="language-plaintext highlighter-rouge">post.html</code> uses <code class="language-plaintext highlighter-rouge">layout: default</code>, then within <code class="language-plaintext highlighter-rouge">default.html</code>, the <code class="language-plaintext highlighter-rouge">layout</code> variable holds the front matter from <code class="language-plaintext highlighter-rouge">default.html</code>, and <code class="language-plaintext highlighter-rouge">page</code> still holds the page’s front matter.</p>

<p><code class="language-plaintext highlighter-rouge">include</code> variables are include-scoped — parameters passed via <code class="language-plaintext highlighter-rouge">{% include file.html param="value" %}</code> are available as <code class="language-plaintext highlighter-rouge">include.param</code> only inside that include file, not in calling templates or other includes.</p>

<p>The mental model: <code class="language-plaintext highlighter-rouge">site</code> is the broadest scope (the whole build), <code class="language-plaintext highlighter-rouge">page</code> is mid-scope (the current document), <code class="language-plaintext highlighter-rouge">layout</code> is the current wrapper, and <code class="language-plaintext highlighter-rouge">include</code> parameters are the narrowest (one function call).</p>

<p>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.</p>

<hr />

<h2 id="extending-variables-with-custom-plugins">Extending variables with custom plugins</h2>

<p>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.</p>

<h3 id="adding-custom-site-level-variables">Adding custom site-level variables</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _plugins/site_extensions.rb</span>
<span class="k">module</span> <span class="nn">Jekyll</span>
  <span class="k">class</span> <span class="nc">SiteExtensions</span>
    <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">extend</span><span class="p">(</span><span class="n">site</span><span class="p">)</span>
      <span class="c1"># Add a formatted build date</span>
      <span class="n">site</span><span class="p">.</span><span class="nf">config</span><span class="p">[</span><span class="s1">'build_date'</span><span class="p">]</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s1">'%B %-d, %Y'</span><span class="p">)</span>
      
      <span class="c1"># Count published posts</span>
      <span class="n">site</span><span class="p">.</span><span class="nf">config</span><span class="p">[</span><span class="s1">'post_count'</span><span class="p">]</span> <span class="o">=</span> <span class="n">site</span><span class="p">.</span><span class="nf">posts</span><span class="p">.</span><span class="nf">docs</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:published?</span><span class="p">).</span><span class="nf">size</span>
      
      <span class="c1"># Count themes in a collection</span>
      <span class="n">themes</span> <span class="o">=</span> <span class="n">site</span><span class="p">.</span><span class="nf">collections</span><span class="p">[</span><span class="s1">'themes'</span><span class="p">]</span>
      <span class="n">site</span><span class="p">.</span><span class="nf">config</span><span class="p">[</span><span class="s1">'theme_count'</span><span class="p">]</span> <span class="o">=</span> <span class="n">themes</span> <span class="p">?</span> <span class="n">themes</span><span class="p">.</span><span class="nf">docs</span><span class="p">.</span><span class="nf">size</span> <span class="p">:</span> <span class="mi">0</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">:post_read</span> <span class="k">do</span> <span class="o">|</span><span class="n">site</span><span class="o">|</span>
  <span class="no">SiteExtensions</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="n">site</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>After adding this plugin, you can use <code class="language-plaintext highlighter-rouge">{{ site.build_date }}</code>, <code class="language-plaintext highlighter-rouge">{{ site.post_count }}</code>, and <code class="language-plaintext highlighter-rouge">{{ site.theme_count }}</code> in any template — values computed once at build time and available everywhere.</p>

<h3 id="adding-custom-page-level-variables">Adding custom page-level variables</h3>

<p>For per-page computed values, use a page hook that runs after front matter is read:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _plugins/reading_time.rb</span>
<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="p">[</span><span class="ss">:posts</span><span class="p">,</span> <span class="ss">:pages</span><span class="p">],</span> <span class="ss">:post_convert</span> <span class="k">do</span> <span class="o">|</span><span class="n">page</span><span class="o">|</span>
  <span class="n">word_count</span> <span class="o">=</span> <span class="n">page</span><span class="p">.</span><span class="nf">content</span><span class="p">.</span><span class="nf">split</span><span class="p">.</span><span class="nf">size</span>
  <span class="n">reading_time</span> <span class="o">=</span> <span class="p">[(</span><span class="n">word_count</span> <span class="o">/</span> <span class="mf">200.0</span><span class="p">).</span><span class="nf">ceil</span><span class="p">,</span> <span class="mi">1</span><span class="p">].</span><span class="nf">max</span>
  <span class="n">page</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'reading_time'</span><span class="p">]</span> <span class="o">=</span> <span class="n">reading_time</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This makes <code class="language-plaintext highlighter-rouge">{{ page.reading_time }}</code> available on every post and page, computed from the actual word count rather than requiring you to set it manually in front matter.</p>

<p>Custom variables created by plugins follow the same scoping rules as front matter variables — <code class="language-plaintext highlighter-rouge">site.your_variable</code> for site-level, <code class="language-plaintext highlighter-rouge">page.your_variable</code> for page-level — so they work seamlessly alongside standard Jekyll variables in any template.</p>

<p>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.</p>

<h2 id="variables-as-the-backbone-of-jekyll-templates">Variables as the backbone of Jekyll templates</h2>

<p>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 <code class="language-plaintext highlighter-rouge">site.*</code>, <code class="language-plaintext highlighter-rouge">page.*</code>, <code class="language-plaintext highlighter-rouge">layout.*</code>, and <code class="language-plaintext highlighter-rouge">content</code>, 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 <code class="language-plaintext highlighter-rouge">relative_url</code>, 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.</p>

<p>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.</p>]]></content><author><name>Marcus Webb</name></author><category term="Tutorial" /><summary type="html"><![CDATA[A complete reference for every Jekyll variable — site.*, page.*, layout.*, content, forloop, and paginator — with examples for each.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/jekyll-variables-reference.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/jekyll-variables-reference.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">What Makes a Good Jekyll Theme? (Checklist Before You Buy)</title><link href="https://jekyllhub.com/themes/2026/06/27/what-makes-a-good-jekyll-theme/" rel="alternate" type="text/html" title="What Makes a Good Jekyll Theme? (Checklist Before You Buy)" /><published>2026-06-27T00:00:00+00:00</published><updated>2026-06-27T00:00:00+00:00</updated><id>https://jekyllhub.com/themes/2026/06/27/what-makes-a-good-jekyll-theme</id><content type="html" xml:base="https://jekyllhub.com/themes/2026/06/27/what-makes-a-good-jekyll-theme/"><![CDATA[<p>A Jekyll theme can look polished in a screenshot and be a nightmare to work with in practice. Bad themes have hard-coded values scattered across templates, SCSS that cannot be overridden without editing source files, and JavaScript that blocks rendering on every page load.</p>

<p>A good Jekyll theme does the opposite: it is designed to be used, extended, and maintained — not just previewed. Here is what to look for before you commit.</p>

<h2 id="clean-readable-liquid-templates">Clean, Readable Liquid Templates</h2>

<p>Open the theme’s <code class="language-plaintext highlighter-rouge">_layouts/</code> and <code class="language-plaintext highlighter-rouge">_includes/</code> directories on GitHub and read a few files. Well-written Liquid templates are:</p>

<p><strong>Modular</strong> — the layout is broken into includes (<code class="language-plaintext highlighter-rouge">_includes/header.html</code>, <code class="language-plaintext highlighter-rouge">_includes/footer.html</code>, <code class="language-plaintext highlighter-rouge">_includes/head.html</code>) rather than crammed into one monolithic <code class="language-plaintext highlighter-rouge">default.html</code>. This makes it possible to override a single component without copying the entire layout.</p>

<p><strong>Commented where it matters</strong> — complex Liquid logic (conditional includes, data lookups, paginator handling) should have a brief comment explaining what it does. Templates without any comments are harder to customise correctly.</p>

<p><strong>Free of hard-coded content</strong> — good themes use <code class="language-plaintext highlighter-rouge">_config.yml</code> variables for site name, author, social links, and colour settings. A theme with your branding written directly into templates requires manual find-and-replace to customise.</p>

<p><strong>Consistent naming conventions</strong> — front matter fields should follow a pattern (<code class="language-plaintext highlighter-rouge">title</code>, <code class="language-plaintext highlighter-rouge">description</code>, <code class="language-plaintext highlighter-rouge">image</code>, <code class="language-plaintext highlighter-rouge">author</code>) rather than an idiosyncratic mix that requires reading every template to understand.</p>

<h2 id="well-organised-scss">Well-Organised SCSS</h2>

<p>SCSS structure reveals a lot about how seriously a theme was built. A good Jekyll theme organises styles using:</p>

<p><strong>Variables for everything visual</strong> — colours, font sizes, spacing, border radii, and breakpoints should all be defined as SCSS variables or CSS custom properties at the top of the stylesheet. This means you can retheme the entire site by changing a handful of values.</p>

<p><strong>Logical file structure</strong> — styles split into logical partials: <code class="language-plaintext highlighter-rouge">_variables.scss</code>, <code class="language-plaintext highlighter-rouge">_base.scss</code>, <code class="language-plaintext highlighter-rouge">_typography.scss</code>, <code class="language-plaintext highlighter-rouge">_layout.scss</code>, <code class="language-plaintext highlighter-rouge">_components.scss</code>. A single <code class="language-plaintext highlighter-rouge">style.scss</code> file with 3,000 lines of undifferentiated CSS is a red flag.</p>

<p><strong>No <code class="language-plaintext highlighter-rouge">!important</code> overuse</strong> — <code class="language-plaintext highlighter-rouge">!important</code> is sometimes necessary, but a theme that uses it everywhere is fighting its own specificity and will be painful to customise.</p>

<p><strong>Dark mode handled properly</strong> — if the theme supports dark mode, it should use CSS custom properties swapped via a <code class="language-plaintext highlighter-rouge">[data-theme="dark"]</code> attribute or <code class="language-plaintext highlighter-rouge">prefers-color-scheme</code> media query, not a separate stylesheet loaded with JavaScript.</p>

<h2 id="performance-by-default">Performance by Default</h2>

<p>A static site has no excuse to be slow. Before buying or installing a theme, check what it loads:</p>

<p><strong>Minimal JavaScript</strong> — a blog or portfolio theme should need almost no JavaScript. If a theme loads jQuery, multiple animation libraries, and a carousel script for a site that displays text and images, those are unnecessary dependencies you will carry forever.</p>

<p><strong>Self-hosted or system fonts</strong> — themes that load Google Fonts make an external request on every page load. A well-optimised theme either uses system fonts (<code class="language-plaintext highlighter-rouge">font-family: system-ui, sans-serif</code>) or bundles the fonts locally with <code class="language-plaintext highlighter-rouge">font-display: swap</code>.</p>

<p><strong>Optimised images in templates</strong> — check the image includes. Does the theme use <code class="language-plaintext highlighter-rouge">loading="lazy"</code> on below-the-fold images? Does it specify <code class="language-plaintext highlighter-rouge">width</code> and <code class="language-plaintext highlighter-rouge">height</code> attributes to prevent layout shift?</p>

<p><strong>No render-blocking resources</strong> — CSS should be in the <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code>, JavaScript should be deferred or at the end of <code class="language-plaintext highlighter-rouge">&lt;body&gt;</code>. Check the source of the demo — a <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> tag without <code class="language-plaintext highlighter-rouge">defer</code> or <code class="language-plaintext highlighter-rouge">async</code> in the <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> will block every page from rendering.</p>

<p>Run Lighthouse on the live demo. A good Jekyll theme should score above 90 on Performance with no effort on your part — the theme itself should not be the bottleneck.</p>

<h2 id="seo-ready-out-of-the-box">SEO Ready Out of the Box</h2>

<p>A well-built Jekyll theme handles SEO fundamentals automatically:</p>

<p><strong><code class="language-plaintext highlighter-rouge">jekyll-seo-tag</code> integration</strong> — the theme should include <code class="language-plaintext highlighter-rouge">{% seo %}</code> in <code class="language-plaintext highlighter-rouge">_layouts/default.html</code> or equivalent. This handles title tags, meta descriptions, Open Graph, Twitter cards, and canonical URLs with a single include.</p>

<p><strong>Proper heading hierarchy</strong> — one <code class="language-plaintext highlighter-rouge">&lt;h1&gt;</code> per page (the post or page title), with <code class="language-plaintext highlighter-rouge">&lt;h2&gt;</code> through <code class="language-plaintext highlighter-rouge">&lt;h4&gt;</code> used for content structure. A theme that wraps the site name in an <code class="language-plaintext highlighter-rouge">&lt;h1&gt;</code> on every page is hurting your SEO on every post.</p>

<p><strong>RSS feed</strong> — the theme should include a <code class="language-plaintext highlighter-rouge">&lt;link rel="alternate" type="application/rss+xml"&gt;</code> in the head, pointing to a feed generated by <code class="language-plaintext highlighter-rouge">jekyll-feed</code>.</p>

<p><strong>Structured data support</strong> — better themes include JSON-LD structured data for articles, breadcrumbs, or site links. This is not required, but it is a sign of a theme built with search visibility in mind.</p>

<p><strong>Clean URLs</strong> — the theme should not produce URLs with <code class="language-plaintext highlighter-rouge">.html</code> extensions or unnecessary parameters. Jekyll handles this with <code class="language-plaintext highlighter-rouge">permalink: pretty</code> in <code class="language-plaintext highlighter-rouge">_config.yml</code> — check that the theme’s config sets this or documents how to do it.</p>

<h2 id="accessibility-that-is-not-an-afterthought">Accessibility That Is Not an Afterthought</h2>

<p>Accessibility in a theme is not just about compliance — it is about code quality. Themes that are accessible are also better structured, more maintainable, and more usable for everyone.</p>

<p><strong>Semantic HTML</strong> — content should use <code class="language-plaintext highlighter-rouge">&lt;article&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;nav&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;main&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;aside&gt;</code>, and <code class="language-plaintext highlighter-rouge">&lt;footer&gt;</code> appropriately. A theme built entirely with <code class="language-plaintext highlighter-rouge">&lt;div&gt;</code> elements is not just inaccessible — it is lazy.</p>

<p><strong>Skip navigation link</strong> — a “Skip to content” link as the first focusable element lets keyboard users bypass the navigation on every page. It is one line of HTML and CSS. Themes without it have not thought about keyboard navigation.</p>

<p><strong>ARIA labels on interactive elements</strong> — icon-only buttons (like a dark mode toggle or search icon) need <code class="language-plaintext highlighter-rouge">aria-label</code> attributes to be usable by screen readers.</p>

<p><strong>Focus styles</strong> — pressing Tab through the live demo should show a visible focus ring on every interactive element. Themes that remove focus styles with <code class="language-plaintext highlighter-rouge">outline: none</code> fail basic keyboard accessibility.</p>

<p><strong>Colour contrast</strong> — body text on its background should meet WCAG AA (4.5:1 ratio minimum). Check this with the browser’s accessibility panel or a contrast checker.</p>

<h2 id="active-maintenance-and-a-clear-update-history">Active Maintenance and a Clear Update History</h2>

<p>Even the best theme becomes a liability if it is abandoned. Signs of a well-maintained theme:</p>

<p><strong>A changelog</strong> — a <code class="language-plaintext highlighter-rouge">CHANGELOG.md</code> or release notes on GitHub showing what changed in each version. This tells you the author thinks carefully about changes and communicates them clearly.</p>

<p><strong>Regular releases</strong> — at least a few updates in the past year. Check the releases page on GitHub, not just the commit history.</p>

<p><strong>Responsive issue handling</strong> — open the GitHub issues and look for how the author responds to bug reports. Are bugs acknowledged and fixed? Or are issues sitting unanswered for months?</p>

<p><strong>A versioning scheme</strong> — themes that use semantic versioning (<code class="language-plaintext highlighter-rouge">1.2.0</code>, <code class="language-plaintext highlighter-rouge">1.2.1</code>) signal that the author understands the difference between a breaking change and a patch.</p>

<h2 id="good-documentation">Good Documentation</h2>

<p>Documentation is a direct measure of how much the author cares about the people using their theme.</p>

<p>A well-documented Jekyll theme covers:</p>

<ul>
  <li><strong>Quick start</strong> — how to get the theme running locally in five minutes</li>
  <li><strong>Configuration reference</strong> — every <code class="language-plaintext highlighter-rouge">_config.yml</code> option explained with example values</li>
  <li><strong>Front matter fields</strong> — what each layout accepts and what is required vs optional</li>
  <li><strong>Customisation guide</strong> — how to override layouts, styles, and includes without editing theme source files</li>
  <li><strong>Deployment notes</strong> — any hosting-specific configuration required</li>
  <li><strong>Upgrade guide</strong> — what to do when a new version is released</li>
</ul>

<p>If the only documentation is a README with a screenshot and a “fork this repo” instruction, the author has not thought about the experience of people actually using the theme.</p>

<h2 id="the-quality-checklist">The Quality Checklist</h2>

<p>Use this before buying or installing any Jekyll theme:</p>

<p><strong>Code quality</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Templates are modular (split into <code class="language-plaintext highlighter-rouge">_includes/</code>)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />No hard-coded content — everything comes from <code class="language-plaintext highlighter-rouge">_config.yml</code> or front matter</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />SCSS uses variables for colours, fonts, and spacing</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />No excessive <code class="language-plaintext highlighter-rouge">!important</code> usage</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Dark mode uses CSS custom properties, not a separate stylesheet</li>
</ul>

<p><strong>Performance</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Lighthouse performance score above 90 on the live demo</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />No unnecessary JavaScript libraries</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Fonts are self-hosted or use system fonts</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Images use <code class="language-plaintext highlighter-rouge">loading="lazy"</code> and have <code class="language-plaintext highlighter-rouge">width</code>/<code class="language-plaintext highlighter-rouge">height</code> attributes</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />No render-blocking scripts in <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code></li>
</ul>

<p><strong>SEO</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Includes <code class="language-plaintext highlighter-rouge">jekyll-seo-tag</code></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />One <code class="language-plaintext highlighter-rouge">&lt;h1&gt;</code> per page</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />RSS feed linked in <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Clean URLs (no <code class="language-plaintext highlighter-rouge">.html</code> extensions)</li>
</ul>

<p><strong>Accessibility</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Uses semantic HTML elements (<code class="language-plaintext highlighter-rouge">&lt;article&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;nav&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;main&gt;</code>)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Has a skip navigation link</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Icon buttons have <code class="language-plaintext highlighter-rouge">aria-label</code> attributes</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Focus styles are visible</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Body text passes WCAG AA contrast (4.5:1)</li>
</ul>

<p><strong>Maintenance</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Last commit within 12 months</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Has a changelog or release notes</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Issues are responded to within a reasonable time</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Compatible with Jekyll 4.x</li>
</ul>

<p><strong>Documentation</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Quick start guide works</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Configuration options are documented</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Front matter fields are documented</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Customisation without editing source files is explained</li>
</ul>

<hr />

<p>A theme that scores well on all five areas — code, performance, SEO, accessibility, and maintenance — is genuinely worth using. Most themes will excel in one or two and be mediocre in others. The checklist helps you make that trade-off consciously rather than discovering it after you have built your site.</p>

<p>Ready to find a theme that meets this standard? Browse the <a href="/themes/">JekyllHub theme directory</a> — every theme is hand-reviewed for quality before listing.</p>

<hr />

<h2 id="version-compatibility-and-future-proofing">Version compatibility and future-proofing</h2>

<p>A quality Jekyll theme declares its Ruby and Jekyll version requirements clearly. Look for a <code class="language-plaintext highlighter-rouge">Gemfile</code> or gemspec that specifies a minimum Jekyll version (<code class="language-plaintext highlighter-rouge">jekyll "&gt;= 4.2"</code>). Themes that still list Jekyll 3.x as the target have not been maintained for several years and will accumulate compatibility warnings as Ruby and Jekyll evolve.</p>

<p>Check that the theme uses Sass in a way compatible with Jekyll’s built-in processor, or explicitly requires the <code class="language-plaintext highlighter-rouge">jekyll-sass-converter</code> version it was developed against. Sass changed significantly between versions 1.x and 2.x, and themes that use deprecated <code class="language-plaintext highlighter-rouge">@import</code> patterns without acknowledgement of this fact are a sign of an unmaintained project.</p>

<p>The broader test: does the author show awareness of the Jekyll ecosystem as it exists today — GitHub Actions for deployment, modern Sass conventions, Jekyll 4.x configuration syntax? Themes built and maintained with current knowledge are substantially more valuable than those frozen in 2019.</p>]]></content><author><name>Marcus Webb</name></author><category term="Themes" /><category term="jekyll themes" /><category term="jekyll theme quality" /><category term="best jekyll themes" /><category term="premium jekyll themes" /><category term="jekyll theme checklist" /><summary type="html"><![CDATA[Not all Jekyll themes are created equal. Here is what separates a well-built Jekyll theme from a pretty one — covering code quality, performance, SEO, accessibility, and long-term maintainability.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/what-makes-a-good-jekyll-theme.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/what-makes-a-good-jekyll-theme.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyll Liquid Tags: The Complete Reference Guide</title><link href="https://jekyllhub.com/tutorial/2026/06/26/jekyll-liquid-tags-reference/" rel="alternate" type="text/html" title="Jekyll Liquid Tags: The Complete Reference Guide" /><published>2026-06-26T00:00:00+00:00</published><updated>2026-06-26T00:00:00+00:00</updated><id>https://jekyllhub.com/tutorial/2026/06/26/jekyll-liquid-tags-reference</id><content type="html" xml:base="https://jekyllhub.com/tutorial/2026/06/26/jekyll-liquid-tags-reference/"><![CDATA[<p>Liquid tags are the logic layer of Jekyll templates. Wrapped in <code class="language-plaintext highlighter-rouge">{% %}</code> 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.</p>

<h2 id="assign">assign</h2>

<p>Creates a variable and assigns it a value. The variable is available for the rest of the template (or until overwritten).</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"My Post"</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">is_premium</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">tags_list</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"jekyll,tutorial,liquid"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">split</span><span class="p">:</span><span class="w"> </span><span class="s2">","</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Variables created with <code class="language-plaintext highlighter-rouge">assign</code> are available in the current scope and any includes called from it.</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">author</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">authors</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"name"</span><span class="p">,</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">first</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">author</span><span class="w"> </span><span class="p">%}</span>
  &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">author</span><span class="p">.</span><span class="nv">avatar</span><span class="w"> </span><span class="p">}}</span>" alt="<span class="p">{{</span><span class="w"> </span><span class="nv">author</span><span class="p">.</span><span class="nv">name</span><span class="w"> </span><span class="p">}}</span>"&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="capture">capture</h2>

<p>Builds a string variable from a block of content — useful when the value spans multiple lines or includes Liquid expressions.</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">capture</span><span class="w"> </span><span class="nv">post_url</span><span class="w"> </span><span class="p">%}{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}{%</span><span class="w"> </span><span class="nt">endcapture</span><span class="w"> </span><span class="p">%}</span>
&lt;meta property="og:url" content="<span class="p">{{</span><span class="w"> </span><span class="nv">post_url</span><span class="w"> </span><span class="p">}}</span>"&gt;

</code></pre></div></div>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">capture</span><span class="w"> </span><span class="nv">author_bio</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">}}</span> · <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%B %-d, %Y"</span><span class="w"> </span><span class="p">}}</span> · <span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">reading_time</span><span class="w"> </span><span class="p">}}</span> min read
<span class="p">{%</span><span class="w"> </span><span class="nt">endcapture</span><span class="w"> </span><span class="p">%}</span>
&lt;p class="post-meta"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">author_bio</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">strip</span><span class="w"> </span><span class="p">}}</span>&lt;/p&gt;

</code></pre></div></div>

<p>Unlike <code class="language-plaintext highlighter-rouge">assign</code>, <code class="language-plaintext highlighter-rouge">capture</code> captures everything between its opening and closing tags, including whitespace and newlines. Use <code class="language-plaintext highlighter-rouge">| strip</code> to remove leading/trailing whitespace.</p>

<h2 id="if--elsif--else--endif">if / elsif / else / endif</h2>

<p>Conditional rendering. Outputs content only when the condition is true.</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">price</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">%}</span>
  &lt;span class="badge"&gt;Free&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">elsif</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">price</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">20</span><span class="w"> </span><span class="p">%}</span>
  &lt;span class="badge"&gt;Budget&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
  &lt;span class="badge"&gt;Premium&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Conditions can use:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">==</code> equal to</li>
  <li><code class="language-plaintext highlighter-rouge">!=</code> not equal to</li>
  <li><code class="language-plaintext highlighter-rouge">&gt;</code> <code class="language-plaintext highlighter-rouge">&gt;=</code> <code class="language-plaintext highlighter-rouge">&lt;</code> <code class="language-plaintext highlighter-rouge">&lt;=</code> numeric comparisons</li>
  <li><code class="language-plaintext highlighter-rouge">contains</code> string/array contains</li>
  <li><code class="language-plaintext highlighter-rouge">and</code> both conditions true</li>
  <li><code class="language-plaintext highlighter-rouge">or</code> either condition true</li>
</ul>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">tags</span><span class="w"> </span><span class="ow">contains</span><span class="w"> </span><span class="s2">"jekyll"</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">featured</span><span class="w"> </span><span class="p">%}</span>
  This is a featured Jekyll post.
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">user</span><span class="p">.</span><span class="nv">name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"admin"</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="nv">user</span><span class="p">.</span><span class="nv">role</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"editor"</span><span class="w"> </span><span class="p">%}</span>
  Show edit button
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Truthy/falsy: In Liquid, <code class="language-plaintext highlighter-rouge">nil</code> and <code class="language-plaintext highlighter-rouge">false</code> are falsy. Everything else — including <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">""</code>, and <code class="language-plaintext highlighter-rouge">[]</code> — is truthy. This differs from many other languages.</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">%}</span>      ← false only if image is nil (not set) or false
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">count</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">%}</span>  ← more reliable check for zero

</code></pre></div></div>

<h2 id="unless--endunless">unless / endunless</h2>

<p>The inverse of <code class="language-plaintext highlighter-rouge">if</code> — runs the block when the condition is <strong>false</strong>. Equivalent to <code class="language-plaintext highlighter-rouge">{% if not condition %}</code>.</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">unless</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">hide_sidebar</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>sidebar.html<span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">endunless</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="kr">unless</span><span class="w"> </span><span class="nb">forloop.last</span><span class="w"> </span><span class="p">%}</span>
  &lt;hr&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endunless</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="case--when--else--endcase">case / when / else / endcase</h2>

<p>Multi-branch conditional, cleaner than a long <code class="language-plaintext highlighter-rouge">if/elsif</code> chain when checking a single variable against multiple values.</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">case</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">category</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">when</span><span class="w"> </span><span class="s2">"Tutorial"</span><span class="w"> </span><span class="p">%}</span>
    &lt;span class="badge badge--blue"&gt;Tutorial&lt;/span&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">when</span><span class="w"> </span><span class="s2">"Comparison"</span><span class="w"> </span><span class="p">%}</span>
    &lt;span class="badge badge--purple"&gt;Comparison&lt;/span&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">when</span><span class="w"> </span><span class="s2">"Themes"</span><span class="err">,</span><span class="w"> </span><span class="s2">"Design"</span><span class="w"> </span><span class="p">%}</span>
    &lt;span class="badge badge--green"&gt;Design&lt;/span&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
    &lt;span class="badge"&gt;Article&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endcase</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Multiple values for the same branch: <code class="language-plaintext highlighter-rouge">{% when "Themes", "Design" %}</code> matches either.</p>

<h2 id="for--endfor">for / endfor</h2>

<p>Loops over an array or range. One of the most-used tags in Jekyll templates.</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  &lt;article&gt;
    &lt;h2&gt;&lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;&lt;/h2&gt;
    &lt;p&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">excerpt</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">strip_html</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">truncatewords</span><span class="p">:</span><span class="w"> </span><span class="mi">25</span><span class="w"> </span><span class="p">}}</span>&lt;/p&gt;
  &lt;/article&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="loop-parameters">Loop parameters</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> First 6 posts </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">6</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Skip the first 3 </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">offset</span><span class="o">:</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Reverse order </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">reversed</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Combine: posts 4-9 </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">6</span><span class="w"> </span><span class="na">offset</span><span class="o">:</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="forloop-variables">forloop variables</h3>

<p>Inside a loop, these special variables describe the current iteration:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nb">forloop.first</span><span class="w"> </span><span class="p">%}</span>&lt;ul&gt;<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
  &lt;li class="<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nb">forloop.last</span><span class="w"> </span><span class="p">%}</span>last<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>"&gt;
    <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.index</span><span class="w"> </span><span class="p">}}</span>.  <span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>
  &lt;/li&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nb">forloop.last</span><span class="w"> </span><span class="p">%}</span>&lt;/ul&gt;<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Variable</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">forloop.index</code></td>
      <td>Current iteration, 1-based</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">forloop.index0</code></td>
      <td>Current iteration, 0-based</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">forloop.rindex</code></td>
      <td>Reverse index, 1-based</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">forloop.rindex0</code></td>
      <td>Reverse index, 0-based</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">forloop.first</code></td>
      <td><code class="language-plaintext highlighter-rouge">true</code> on first iteration</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">forloop.last</code></td>
      <td><code class="language-plaintext highlighter-rouge">true</code> on last iteration</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">forloop.length</code></td>
      <td>Total number of items</td>
    </tr>
  </tbody>
</table>

<h3 id="else-in-for-loops">else in for loops</h3>

<p>Runs when the array is empty:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">theme</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.themes</span><span class="w"> </span><span class="p">%}</span>
  &lt;div class="theme-card"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/div&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
  &lt;p&gt;No themes available yet.&lt;/p&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="looping-over-a-number-range">Looping over a number range</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">(1..5)</span><span class="w"> </span><span class="p">%}</span>
  &lt;span&gt;Step <span class="p">{{</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="p">}}</span>&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="nested-loops">Nested loops</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.categories</span><span class="w"> </span><span class="p">%}</span>
  &lt;h2&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">category</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="w"> </span><span class="p">}}</span>&lt;/h2&gt;
  <span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">category[1]</span><span class="w"> </span><span class="p">%}</span>
    &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
  <span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="break-and-continue">break and continue</h2>

<p>Control loop execution:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Stop after finding first featured post </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">featured</span><span class="w"> </span><span class="p">%}</span>
    &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
    <span class="p">{%</span><span class="w"> </span><span class="nt">break</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Skip drafts in a custom loop </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">unless</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">published</span><span class="w"> </span><span class="p">%}{%</span><span class="w"> </span><span class="nt">continue</span><span class="w"> </span><span class="p">%}{%</span><span class="w"> </span><span class="kr">endunless</span><span class="w"> </span><span class="p">%}</span>
  &lt;li&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/li&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="include">include</h2>

<p>Inserts the contents of a file from <code class="language-plaintext highlighter-rouge">_includes/</code>. One of the most-used tags in Jekyll layouts.</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>nav.html<span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>footer.html<span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>components/card.html<span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="include-with-parameters">include with parameters</h3>

<p>Pass variables to an include using key=value pairs:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>components/card.html<span class="err">
</span><span class="w">   </span><span class="na">title</span><span class="o">=</span>theme.title<span class="err">
</span><span class="w">   </span><span class="na">url</span><span class="o">=</span>theme.url<span class="err">
</span><span class="w">   </span><span class="na">price</span><span class="o">=</span>theme.price<span class="err">
</span><span class="w">   </span><span class="na">image</span><span class="o">=</span>theme.card_image<span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Inside the include, access them with <code class="language-plaintext highlighter-rouge">include.keyname</code>:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c">&lt;!-- _includes/components/card.html --&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h3&gt;</span>{{ include.title }}<span class="nt">&lt;/h3&gt;</span>
  <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"{{ include.url }}"</span><span class="nt">&gt;</span>View<span class="nt">&lt;/a&gt;</span>
<span class="nt">&lt;/div&gt;</span>

</code></pre></div></div>

<h3 id="include-with-a-variable-filename">include with a variable filename</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">template</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"components/card.html"</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span><span class="p">{{</span><span class="w"> </span><span class="nv">template</span><span class="w"> </span><span class="p">}}</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Or use <code class="language-plaintext highlighter-rouge">include_relative</code> to include from a path relative to the current file:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">include_relative</span><span class="w"> </span>../shared/notice.html<span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="raw--endraw">raw / endraw</h2>

<p>Prevents Liquid from processing the enclosed content. Essential when writing Liquid code in blog posts.</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
  Use <span class="p">{{</span><span class="w"> </span><span class="nv">variable</span><span class="w"> </span><span class="p">}}</span> syntax to output values.
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">condition</span><span class="w"> </span><span class="p">%}</span>...<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Everything inside <code class="language-plaintext highlighter-rouge">...</code> is output literally, without Liquid processing.</p>

<h2 id="comment--endcomment">comment / endcomment</h2>

<p>Adds a Liquid comment — not rendered in output and not processed:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c">
  TODO: add pagination here
  This section is temporarily disabled
</span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Unlike HTML comments (<code class="language-plaintext highlighter-rouge">&lt;!-- --&gt;</code>), Liquid comments are completely removed from output and cannot be seen by users in the page source.</p>

<h2 id="highlight--endhighlight">highlight / endhighlight</h2>

<p>Syntax-highlights a code block using Rouge:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">highlight</span><span class="w"> </span>ruby<span class="w"> </span><span class="p">%}</span>
def hello
  puts "Hello, Jekyll!"
end
<span class="p">{%</span><span class="w"> </span><span class="nt">endhighlight</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">highlight</span><span class="w"> </span>javascript<span class="w"> </span>linenos<span class="w"> </span><span class="p">%}</span>
const site = "JekyllHub";
console.log(site);
<span class="p">{%</span><span class="w"> </span><span class="nt">endhighlight</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>The optional <code class="language-plaintext highlighter-rouge">linenos</code> adds line numbers. The language identifier must be one Rouge supports (ruby, javascript, python, bash, yaml, html, css, json, etc.).</p>

<h2 id="link-and-post_url">link and post_url</h2>

<p>Generate correct URLs for internal pages and posts, accounting for <code class="language-plaintext highlighter-rouge">baseurl</code>:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Link to a page (fails build if page not found) </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
&lt;a href="<span class="p">{%</span><span class="w"> </span><span class="nt">link</span><span class="w"> </span>_pages/about.md<span class="w"> </span><span class="p">%}</span>"&gt;About&lt;/a&gt;
&lt;a href="<span class="p">{%</span><span class="w"> </span><span class="nt">link</span><span class="w"> </span>_posts/2026-08-03-jekyll-directory-structure.md<span class="w"> </span><span class="p">%}</span>"&gt;Read post&lt;/a&gt;

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Link to a post by filename </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
&lt;a href="<span class="p">{%</span><span class="w"> </span><span class="nt">post_url</span><span class="w"> </span><span class="mi">2026</span><span class="o">-</span><span class="mi">08</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span>jekyll-directory-structure<span class="w"> </span><span class="p">%}</span>"&gt;Read post&lt;/a&gt;

</code></pre></div></div>

<p>Both <code class="language-plaintext highlighter-rouge">link</code> and <code class="language-plaintext highlighter-rouge">post_url</code> cause a build error if the target file does not exist — useful for catching broken internal links. They automatically apply <code class="language-plaintext highlighter-rouge">baseurl</code>.</p>

<h2 id="tablerow">tablerow</h2>

<p>Like <code class="language-plaintext highlighter-rouge">for</code>, but generates an HTML table:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;table&gt;
  <span class="p">{%</span><span class="w"> </span><span class="nt">tablerow</span><span class="w"> </span><span class="nv">plugin</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.data.plugins</span><span class="w"> </span><span class="na">cols</span><span class="o">:</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">%}</span>
    &lt;td&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">plugin</span><span class="p">.</span><span class="nv">name</span><span class="w"> </span><span class="p">}}</span>&lt;/td&gt;
  <span class="p">{%</span><span class="w"> </span><span class="nt">endtablerow</span><span class="w"> </span><span class="p">%}</span>
&lt;/table&gt;

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">cols:</code> sets the number of columns per row. Less commonly used than <code class="language-plaintext highlighter-rouge">for</code>, but useful for tabular data.</p>

<h2 id="jekyll_draft-and-jekyll_version">jekyll_draft and jekyll_version</h2>

<p>Available in Jekyll 4+:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Show content only when serving with --drafts flag </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">jekyll</span><span class="p">.</span><span class="nv">environment</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"development"</span><span class="w"> </span><span class="p">%}</span>
  &lt;div class="draft-banner"&gt;Draft preview&lt;/div&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="whitespace-control">Whitespace control</h2>

<p>Liquid tags add blank lines to output. Strip whitespace with <code class="language-plaintext highlighter-rouge">-</code>:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%-</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">foo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"bar"</span><span class="w"> </span><span class="p">-%}</span>
<span class="p">{%-</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">item</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">list</span><span class="w"> </span><span class="p">-%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">item</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{%-</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">-%}</span>

</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">-</code> inside the tag delimiters strips whitespace (including newlines) before or after that tag. Use this when generating clean HTML or JSON output.</p>

<h2 id="tags-vs-filters-the-difference">Tags vs filters: the difference</h2>

<p>People sometimes confuse tags and filters. The distinction:</p>

<ul>
  <li><strong>Tags</strong> (<code class="language-plaintext highlighter-rouge">{% %}</code>) execute logic — they control flow, assign variables, loop, include files</li>
  <li><strong>Filters</strong> (<code class="language-plaintext highlighter-rouge">|</code>) transform values — they modify the output of <code class="language-plaintext highlighter-rouge">{{ }}</code> expressions</li>
</ul>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">%}</span>   ← tag with filter in the value
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">upcase</span><span class="w"> </span><span class="p">}}</span>                ← output with filter
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>             ← loop tag

</code></pre></div></div>

<p>Knowing both tags and filters — and which is which — gives you the full toolkit for working with any Jekyll theme.</p>

<hr />

<h2 id="why-tags-matter-for-jekyll-theme-development">Why tags matter for Jekyll theme development</h2>

<p>Every Jekyll theme is built on Liquid tags. When you open a layout file like <code class="language-plaintext highlighter-rouge">_layouts/default.html</code> or an include like <code class="language-plaintext highlighter-rouge">_includes/header.html</code>, 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.</p>

<p>The most common tags in production themes are <code class="language-plaintext highlighter-rouge">if</code>, <code class="language-plaintext highlighter-rouge">for</code>, <code class="language-plaintext highlighter-rouge">include</code>, <code class="language-plaintext highlighter-rouge">assign</code>, and <code class="language-plaintext highlighter-rouge">capture</code>. These five account for the majority of the logic in any Jekyll template. The others — <code class="language-plaintext highlighter-rouge">unless</code>, <code class="language-plaintext highlighter-rouge">case</code>, <code class="language-plaintext highlighter-rouge">raw</code>, <code class="language-plaintext highlighter-rouge">comment</code>, <code class="language-plaintext highlighter-rouge">highlight</code>, <code class="language-plaintext highlighter-rouge">link</code>, and <code class="language-plaintext highlighter-rouge">post_url</code> — appear regularly but less frequently.</p>

<p>A key insight for working with tags: they are processed at build time, not at runtime in the browser. Everything inside <code class="language-plaintext highlighter-rouge">{% for %}</code> loops and <code class="language-plaintext highlighter-rouge">{% if %}</code> 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).</p>

<h2 id="debugging-tag-logic">Debugging tag logic</h2>

<p>When a Liquid tag produces unexpected output, a few diagnostic techniques are reliable.</p>

<p>The <code class="language-plaintext highlighter-rouge">{{ variable | inspect }}</code> 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:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">author</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">authors</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"name"</span><span class="p">,</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">first</span><span class="w"> </span><span class="p">%}</span>
&lt;!-- Debug: --&gt;
<span class="p">{{</span><span class="w"> </span><span class="nv">author</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">inspect</span><span class="w"> </span><span class="p">}}</span>
&lt;!-- Expected output: {"name"=&gt;"Marcus Webb", "avatar"=&gt;"/assets/...", ...} --&gt;

</code></pre></div></div>

<p>For loop debugging, confirm the array is not empty before looping:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nb">empty</span><span class="w"> </span><span class="p">%}</span>
  &lt;!-- No themes found --&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
  Found <span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">}}</span> themes
  <span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">theme</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.themes</span><span class="w"> </span><span class="p">%}</span>
    <span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>For <code class="language-plaintext highlighter-rouge">assign</code> 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 — <code class="language-plaintext highlighter-rouge">author</code> and <code class="language-plaintext highlighter-rouge">Author</code> are different variables.</p>

<p>For <code class="language-plaintext highlighter-rouge">include</code> debugging, verify the file path is relative to <code class="language-plaintext highlighter-rouge">_includes/</code>. <code class="language-plaintext highlighter-rouge">{% include nav.html %}</code> looks for <code class="language-plaintext highlighter-rouge">_includes/nav.html</code>. <code class="language-plaintext highlighter-rouge">{% include components/nav.html %}</code> looks for <code class="language-plaintext highlighter-rouge">_includes/components/nav.html</code>. File not found silently outputs nothing in older Jekyll versions; newer versions throw a build error.</p>

<h2 id="combining-tags-for-common-patterns">Combining tags for common patterns</h2>

<p>Real templates combine tags into patterns that appear repeatedly across themes.</p>

<p><strong>Conditional include with data lookup:</strong></p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">author_data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="p">.</span><span class="nv">authors</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"name"</span><span class="p">,</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">first</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">author_data</span><span class="w"> </span><span class="p">%}</span>
    <span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>author-card.html<span class="w"> </span><span class="na">author</span><span class="o">=</span><span class="nv">author_data</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p><strong>Paginated loop with empty state:</strong></p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">posts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"category"</span><span class="p">,</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">category</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">posts</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nb">empty</span><span class="w"> </span><span class="p">%}</span>
  &lt;p&gt;No posts in this category yet.&lt;/p&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">posts</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">12</span><span class="w"> </span><span class="p">%}</span>
    <span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>post-card.html<span class="w"> </span><span class="na">post</span><span class="o">=</span><span class="nv">post</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p><strong>Accumulated string with capture:</strong></p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">capture</span><span class="w"> </span><span class="nv">breadcrumbs</span><span class="w"> </span><span class="p">%}</span>
  &lt;nav aria-label="Breadcrumb"&gt;
    &lt;ol&gt;
      &lt;li&gt;&lt;a href="/"&gt;Home&lt;/a&gt;&lt;/li&gt;
      <span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">page.categories</span><span class="w"> </span><span class="p">%}</span>
        &lt;li&gt;&lt;a href="/category/<span class="p">{{</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">slugify</span><span class="w"> </span><span class="p">}}</span>/"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">category</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;&lt;/li&gt;
      <span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
      &lt;li aria-current="page"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/nav&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endcapture</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">breadcrumbs</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">capture</code> 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.</p>

<p><strong>Loop with separator (no trailing comma):</strong></p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">page.tags</span><span class="w"> </span><span class="p">%}{{</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="p">}}{%</span><span class="w"> </span><span class="kr">unless</span><span class="w"> </span><span class="nb">forloop.last</span><span class="w"> </span><span class="p">%}</span>, <span class="p">{%</span><span class="w"> </span><span class="kr">endunless</span><span class="w"> </span><span class="p">%}{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">unless forloop.last</code> pattern adds a separator between items without a trailing one — cleaner than using <code class="language-plaintext highlighter-rouge">join</code> when you need control over the formatting.</p>

<p>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 <code class="language-plaintext highlighter-rouge">capture</code> instead of <code class="language-plaintext highlighter-rouge">assign</code>, when <code class="language-plaintext highlighter-rouge">unless</code> is cleaner than <code class="language-plaintext highlighter-rouge">if not</code>, and how to combine <code class="language-plaintext highlighter-rouge">for</code> with <code class="language-plaintext highlighter-rouge">where</code> and <code class="language-plaintext highlighter-rouge">sort</code> — 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.</p>

<hr />

<h2 id="the-assign-tag-in-depth">The assign tag in depth</h2>

<p><code class="language-plaintext highlighter-rouge">assign</code> is the most-used tag in real Jekyll templates, and understanding its scoping rules prevents a common class of bugs.</p>

<p>Variables assigned with <code class="language-plaintext highlighter-rouge">{% assign %}</code> exist for the remainder of the current template file and are passed into any <code class="language-plaintext highlighter-rouge">{% include %}</code> calls made after the assignment. However, they do not leak back up from includes into the calling template. An <code class="language-plaintext highlighter-rouge">assign</code> inside <code class="language-plaintext highlighter-rouge">_includes/card.html</code> does not affect variables in the layout file that included it.</p>

<p>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 <code class="language-plaintext highlighter-rouge">site.*</code> variables, which are set before rendering begins and are read-only).</p>

<p>This scoping model is what allows Jekyll to build pages in parallel: each page’s Liquid execution is completely independent of every other page.</p>

<p><strong>Common assign patterns:</strong></p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Computed value used multiple times </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">post_count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">half_count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">post_count</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">divided_by</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Cached filter result </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">sorted_themes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">sort</span><span class="p">:</span><span class="w"> </span><span class="s2">"stars"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">reverse</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Boolean flag </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">show_cta</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">template</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"landing"</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">show_cta</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Data lookup result </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">current_author</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="p">.</span><span class="nv">authors</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"name"</span><span class="p">,</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">first</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="the-for-tag-all-options">The for tag: all options</h2>

<p>The <code class="language-plaintext highlighter-rouge">for</code> tag has several modifiers that are easy to forget:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Basic loop </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Limit: only first N items </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">6</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Offset: skip first N items </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">offset</span><span class="o">:</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Combined: items 4-9 (offset 3, then take 6) </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">6</span><span class="w"> </span><span class="na">offset</span><span class="o">:</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Reversed </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">reversed</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Number range </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">(1..site.posts.size)</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Empty state </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
  No posts found.
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">reversed</code> modifier combined with <code class="language-plaintext highlighter-rouge">limit</code> and <code class="language-plaintext highlighter-rouge">offset</code> gives you the ability to page through content, though for production pagination the <code class="language-plaintext highlighter-rouge">jekyll-paginate-v2</code> plugin is a better approach.</p>

<h2 id="practical-tag-combination-patterns">Practical tag combination patterns</h2>

<p>These patterns solve real problems in Jekyll templates and appear regularly across production themes:</p>

<p><strong>Navigation with active state:</strong></p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">item</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.data.navigation</span><span class="w"> </span><span class="p">%}</span>
  &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">item</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>"
     <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nv">item</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">%}</span>aria-current="page"<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>&gt;
    <span class="p">{{</span><span class="w"> </span><span class="nv">item</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>
  &lt;/a&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p><strong>Related posts by category:</strong></p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">related</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"category"</span><span class="p">,</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">category</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">related_filtered</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">split</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">related</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">unless</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">%}</span>
    <span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">related_filtered</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">related_filtered</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">push</span><span class="p">:</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">endunless</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">related_filtered</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>post-card.html<span class="w"> </span><span class="na">post</span><span class="o">=</span><span class="nv">post</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p><strong>Comma-separated tag list without trailing comma:</strong></p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">page.tags</span><span class="w"> </span><span class="p">%}</span>&lt;a href="/tag/<span class="p">{{</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">slugify</span><span class="w"> </span><span class="p">}}</span>/"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;<span class="p">{%</span><span class="w"> </span><span class="kr">unless</span><span class="w"> </span><span class="nb">forloop.last</span><span class="w"> </span><span class="p">%}</span>, <span class="p">{%</span><span class="w"> </span><span class="kr">endunless</span><span class="w"> </span><span class="p">%}{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p><strong>First post with different layout:</strong></p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">6</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nb">forloop.first</span><span class="w"> </span><span class="p">%}</span>
    <span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>post-card-featured.html<span class="w"> </span><span class="na">post</span><span class="o">=</span><span class="nv">post</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
    <span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>post-card.html<span class="w"> </span><span class="na">post</span><span class="o">=</span><span class="nv">post</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>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.</p>

<h2 id="reading-liquid-with-confidence">Reading Liquid with confidence</h2>

<p>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 <code class="language-plaintext highlighter-rouge">{% for %}</code> loops, check what the <code class="language-plaintext highlighter-rouge">{% if %}</code> conditions are testing, and look for <code class="language-plaintext highlighter-rouge">{% include %}</code> 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.</p>

<p>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.</p>]]></content><author><name>Marcus Webb</name></author><category term="Tutorial" /><summary type="html"><![CDATA[A comprehensive reference for all Jekyll Liquid tags — if, for, assign, capture, include, case, raw, comment, and more with real examples.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/jekyll-liquid-tags-reference.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/jekyll-liquid-tags-reference.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyll Liquid Templating: A Beginner’s Complete Guide</title><link href="https://jekyllhub.com/tutorial/2026/06/25/jekyll-liquid-templating-basics/" rel="alternate" type="text/html" title="Jekyll Liquid Templating: A Beginner’s Complete Guide" /><published>2026-06-25T00:00:00+00:00</published><updated>2026-06-25T00:00:00+00:00</updated><id>https://jekyllhub.com/tutorial/2026/06/25/jekyll-liquid-templating-basics</id><content type="html" xml:base="https://jekyllhub.com/tutorial/2026/06/25/jekyll-liquid-templating-basics/"><![CDATA[<p>Liquid is the template language Jekyll uses to make HTML dynamic. It was created by Shopify and is also used in many other platforms. In Jekyll, Liquid lets you output variables, loop through posts, check conditions, and transform data — all within your HTML files.</p>

<p>If you have ever seen <code class="language-plaintext highlighter-rouge">{{ page.title }}</code> or <code class="language-plaintext highlighter-rouge">{% for post in site.posts %}</code> in a Jekyll theme, that is Liquid. This guide covers everything you need to know to read, write, and customise Liquid templates.</p>

<h2 id="the-three-types-of-liquid-syntax">The three types of Liquid syntax</h2>

<p>Liquid uses three distinct delimiters, each with a different purpose:</p>

<h3 id="1-output-tags--">1. Output tags <code class="language-plaintext highlighter-rouge">{{ }}</code></h3>

<p>Double curly braces output the value of a variable or expression:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">description</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%B %-d, %Y"</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<p>Whatever is inside <code class="language-plaintext highlighter-rouge">{{ }}</code> is evaluated and printed to the page.</p>

<h3 id="2-logic-tags--">2. Logic tags <code class="language-plaintext highlighter-rouge">{% %}</code></h3>

<p>Curly brace with percent signs execute logic — control flow, loops, assignments. They do not output anything themselves:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">featured</span><span class="w"> </span><span class="p">%}</span>
  &lt;span class="badge"&gt;Featured&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  &lt;h2&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/h2&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">author</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="p">.</span><span class="nv">authors</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"name"</span><span class="p">,</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">first</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="3-comment-tags--comment-">3. Comment tags <code class="language-plaintext highlighter-rouge">{% comment %}</code></h3>

<p>Comments are not rendered in output:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c">
  This is a Liquid comment.
  Useful for notes to yourself or temporarily disabling code.
</span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="liquid-objects-in-jekyll">Liquid objects in Jekyll</h2>

<p>Objects are the data sources available in Liquid. Jekyll provides several built-in objects.</p>

<h3 id="site">site</h3>

<p>Site-wide data from <code class="language-plaintext highlighter-rouge">_config.yml</code> and the Jekyll build:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>           → "JekyllHub"
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>             → "https://jekyllhub.com"
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">description</span><span class="w"> </span><span class="p">}}</span>     → your site description
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">}}</span>           → array of all posts
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">pages</span><span class="w"> </span><span class="p">}}</span>           → array of all pages
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="p">}}</span>          → array of theme collection items
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="p">.</span><span class="nv">navigation</span><span class="w"> </span><span class="p">}}</span> → contents of _data/navigation.yml
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">time</span><span class="w"> </span><span class="p">}}</span>            → build time

</code></pre></div></div>

<p>Any key in <code class="language-plaintext highlighter-rouge">_config.yml</code> becomes a <code class="language-plaintext highlighter-rouge">site.*</code> variable:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml</span>
<span class="na">sendy_list_id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">abc123"</span>
</code></pre></div></div>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">sendy_list_id</span><span class="w"> </span><span class="p">}}</span>   → "abc123"

</code></pre></div></div>

<h3 id="page">page</h3>

<p>Data about the current page being rendered:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>           → post or page title
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>             → URL of the current page
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">}}</span>            → date (for posts)
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">content</span><span class="w"> </span><span class="p">}}</span>         → rendered HTML content
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">excerpt</span><span class="w"> </span><span class="p">}}</span>         → post excerpt
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">categories</span><span class="w"> </span><span class="p">}}</span>      → array of categories
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">tags</span><span class="w"> </span><span class="p">}}</span>            → array of tags
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">}}</span>          → value from front matter
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">}}</span>           → any custom front matter variable

</code></pre></div></div>

<p>Every front matter key on the current page is available as <code class="language-plaintext highlighter-rouge">page.keyname</code>.</p>

<h3 id="layout">layout</h3>

<p>Data from the layout file’s front matter:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">layout</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">layout</span><span class="p">.</span><span class="nv">sidebar</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<p>Rarely used, but available if your layout has its own front matter variables.</p>

<h3 id="content">content</h3>

<p>The rendered content of the current page, available only inside layout files:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;!-- _layouts/default.html --&gt;
&lt;main&gt;
  <span class="p">{{</span><span class="w"> </span><span class="nv">content</span><span class="w"> </span><span class="p">}}</span>
&lt;/main&gt;

</code></pre></div></div>

<h3 id="forloop-inside-loops">forloop (inside loops)</h3>

<p>Available inside <code class="language-plaintext highlighter-rouge">{% for %}</code> loops:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.index</span><span class="w"> </span><span class="p">}}</span>      → 1-based position (1, 2, 3...)
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.index0</span><span class="w"> </span><span class="p">}}</span>     → 0-based position (0, 1, 2...)
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.first</span><span class="w"> </span><span class="p">}}</span>      → true on first iteration
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.last</span><span class="w"> </span><span class="p">}}</span>       → true on last iteration
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.length</span><span class="w"> </span><span class="p">}}</span>     → total number of items
  <span class="p">{{</span><span class="w"> </span><span class="nb">forloop.rindex</span><span class="w"> </span><span class="p">}}</span>     → reverse index (counts down)
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="variables-assign-and-capture">Variables: assign and capture</h2>

<p>Create your own variables with <code class="language-plaintext highlighter-rouge">assign</code>:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">greeting</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Hello, world!"</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">greeting</span><span class="w"> </span><span class="p">}}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">featured</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"featured"</span><span class="p">,</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">post_count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">site_url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="p">:</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">baseurl</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Use <code class="language-plaintext highlighter-rouge">capture</code> to build a string from multiple lines:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">capture</span><span class="w"> </span><span class="nv">author_link</span><span class="w"> </span><span class="p">%}</span>
  &lt;a href="/authors/<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">slugify</span><span class="w"> </span><span class="p">}}</span>/"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">author</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endcapture</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{{</span><span class="w"> </span><span class="nv">author_link</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">capture</code> is useful when you need to build a complex string and use it multiple times, or pass it to an include.</p>

<h2 id="filters">Filters</h2>

<p>Filters transform the value on the left using a pipe <code class="language-plaintext highlighter-rouge">|</code>:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">upcase</span><span class="w"> </span><span class="p">}}</span>
→ "JEKYLL LIQUID TEMPLATING BASICS"

<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%B %-d, %Y"</span><span class="w"> </span><span class="p">}}</span>
→ "August 4, 2026"

<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">size</span><span class="w"> </span><span class="p">}}</span>
→ 81

<span class="p">{{</span><span class="w"> </span><span class="s2">"/about/"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>
→ "/about/"  (or "/subdir/about/" if baseurl is set)

<span class="p">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="p">:</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>
→ "https://jekyllhub.com/blog/my-post/"

</code></pre></div></div>

<p>Filters can be chained:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">downcase</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">replace</span><span class="p">:</span><span class="w"> </span><span class="s2">" "</span><span class="p">,</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">truncate</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<p>For a full filter reference, see the <a href="/blog/jekyll-liquid-filters-cheatsheet/">Jekyll Liquid Filters Cheatsheet</a>.</p>

<h2 id="control-flow-if-elsif-else-unless">Control flow: if, elsif, else, unless</h2>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">featured</span><span class="w"> </span><span class="p">%}</span>
  &lt;span class="badge badge--featured"&gt;Featured&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">elsif</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">trending</span><span class="w"> </span><span class="p">%}</span>
  &lt;span class="badge badge--trending"&gt;Trending&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
  &lt;span class="badge"&gt;Standard&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">unless</code> is the opposite of <code class="language-plaintext highlighter-rouge">if</code> — true when the condition is false:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">unless</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">hide_toc</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>toc.html<span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">endunless</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="comparison-operators">Comparison operators</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">price</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">%}</span>Free<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">price</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">%}</span>Paid<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">price</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">%}</span>Not free<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">stars</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="p">%}</span>Popular<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="ow">contains</span><span class="w"> </span><span class="s2">"Jekyll"</span><span class="w"> </span><span class="p">%}</span>...<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="logical-operators">Logical operators</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">featured</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">price</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">%}</span>
  Free and featured!
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">category</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"Tutorial"</span><span class="w"> </span><span class="ow">or</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">category</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"Guide"</span><span class="w"> </span><span class="p">%}</span>
  Educational content
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="checking-if-a-variable-exists">Checking if a variable exists</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">%}</span>
  &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>" alt="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>"&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">tags</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nb">empty</span><span class="w"> </span><span class="p">%}</span>
  &lt;ul&gt;
    <span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">page.tags</span><span class="w"> </span><span class="p">%}</span>
      &lt;li&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">tag</span><span class="w"> </span><span class="p">}}</span>&lt;/li&gt;
    <span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
  &lt;/ul&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="loops-for">Loops: for</h2>

<p>Loop over arrays with <code class="language-plaintext highlighter-rouge">{% for %}</code>:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  &lt;article&gt;
    &lt;h2&gt;&lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;&lt;/h2&gt;
    &lt;time&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s2">"%B %-d, %Y"</span><span class="w"> </span><span class="p">}}</span>&lt;/time&gt;
    &lt;p&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">excerpt</span><span class="w"> </span><span class="p">}}</span>&lt;/p&gt;
  &lt;/article&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="loop-modifiers">Loop modifiers</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Limit to first 6 items </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">6</span><span class="w"> </span><span class="p">%}</span>
  ...
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Skip the first 3 items </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">offset</span><span class="o">:</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">%}</span>
  ...
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> Reverse the order </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">reversed</span><span class="w"> </span><span class="p">%}</span>
  ...
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">comment</span><span class="w"> </span><span class="p">%}</span><span class="c"> First 6, skip the first 3 </span><span class="p">{%</span><span class="w"> </span><span class="nt">endcomment</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="w"> </span><span class="mi">6</span><span class="w"> </span><span class="na">offset</span><span class="o">:</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="p">%}</span>
  ...
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="else-in-for-loops">else in for loops</h3>

<p>The <code class="language-plaintext highlighter-rouge">else</code> clause runs when the array is empty:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">theme</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.themes</span><span class="w"> </span><span class="p">%}</span>
  &lt;div class="card"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">theme</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/div&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
  &lt;p&gt;No themes found.&lt;/p&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="looping-over-a-range-of-numbers">Looping over a range of numbers</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">(1..5)</span><span class="w"> </span><span class="p">%}</span>
  &lt;span&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">i</span><span class="w"> </span><span class="p">}}</span>&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
→ 1 2 3 4 5

</code></pre></div></div>

<h3 id="break-and-continue">break and continue</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nb">forloop.index</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="p">%}</span>
    <span class="p">{%</span><span class="w"> </span><span class="nt">break</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.posts</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">unless</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">featured</span><span class="w"> </span><span class="p">%}</span>
    <span class="p">{%</span><span class="w"> </span><span class="nt">continue</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">endunless</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>  ← only featured posts reach here
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="case--when">case / when</h2>

<p>An alternative to long <code class="language-plaintext highlighter-rouge">if/elsif</code> chains:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">case</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">category</span><span class="w"> </span><span class="p">%}</span>
  <span class="p">{%</span><span class="w"> </span><span class="kr">when</span><span class="w"> </span><span class="s2">"Tutorial"</span><span class="w"> </span><span class="p">%}</span>
    &lt;span class="badge badge--tutorial"&gt;Tutorial&lt;/span&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">when</span><span class="w"> </span><span class="s2">"Comparison"</span><span class="w"> </span><span class="p">%}</span>
    &lt;span class="badge badge--comparison"&gt;Comparison&lt;/span&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">when</span><span class="w"> </span><span class="s2">"Themes"</span><span class="w"> </span><span class="p">%}</span>
    &lt;span class="badge badge--themes"&gt;Themes&lt;/span&gt;
  <span class="p">{%</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">%}</span>
    &lt;span class="badge"&gt;Article&lt;/span&gt;
<span class="p">{%</span><span class="w"> </span><span class="kr">endcase</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h2 id="working-with-arrays">Working with arrays</h2>

<h3 id="filtering-arrays">Filtering arrays</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">free_themes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"price"</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">featured_posts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"featured"</span><span class="p">,</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">tutorial_posts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where</span><span class="p">:</span><span class="w"> </span><span class="s2">"category"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Tutorial"</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">where_exp</code> for more complex filtering:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">recent_posts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where_exp</span><span class="p">:</span><span class="w"> </span><span class="s2">"post"</span><span class="p">,</span><span class="w"> </span><span class="s2">"post.date &gt; '2026-01-01'"</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">popular</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">where_exp</span><span class="p">:</span><span class="w"> </span><span class="s2">"theme"</span><span class="p">,</span><span class="w"> </span><span class="s2">"theme.stars &gt; 1000"</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="sorting-arrays">Sorting arrays</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">themes_by_stars</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">themes</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">sort</span><span class="p">:</span><span class="w"> </span><span class="s2">"stars"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">reverse</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">posts_by_title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">sort</span><span class="p">:</span><span class="w"> </span><span class="s2">"title"</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="grouping-arrays">Grouping arrays</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">posts_by_category</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">group_by</span><span class="p">:</span><span class="w"> </span><span class="s2">"category"</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">group</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">posts_by_category</span><span class="w"> </span><span class="p">%}</span>
  &lt;h2&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">group</span><span class="p">.</span><span class="nv">name</span><span class="w"> </span><span class="p">}}</span>&lt;/h2&gt;
  <span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">group.items</span><span class="w"> </span><span class="p">%}</span>
    &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
  <span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<h3 id="getting-firstlast-items">Getting first/last items</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">latest_post</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">first</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">latest_post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>

<span class="p">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">oldest_post</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">last</span><span class="w"> </span><span class="p">%}</span>
<span class="p">{{</span><span class="w"> </span><span class="nv">oldest_post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>

</code></pre></div></div>

<h2 id="raw-tag">raw tag</h2>

<p>Prevent Liquid from processing a block — essential when writing about Liquid in a Jekyll blog:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
  This <span class="p">{{</span><span class="w"> </span><span class="nv">variable</span><span class="w"> </span><span class="p">}}</span> will not be processed by Liquid.
  <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">%}</span>This tag will not execute.<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Use `` whenever you need to display Liquid code examples.</p>

<h2 id="whitespace-control">Whitespace control</h2>

<p>Liquid tags add blank lines to output. Use <code class="language-plaintext highlighter-rouge">-</code> to strip whitespace:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{%</span><span class="w"> </span><span class="nt">raw</span><span class="w"> </span><span class="p">%}</span>
{%- for post in site.posts -%}
  &lt;li&gt;{{ post.title }}&lt;/li&gt;
{%- endfor -%}

</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">-</code> inside the tag delimiters strips all whitespace (including newlines) before and after the tag.</p>

<h2 id="practical-patterns">Practical patterns</h2>

<h3 id="include-with-a-fallback">Include with a fallback</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;meta name="description" content="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">description</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">default</span><span class="p">:</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">description</span><span class="w"> </span><span class="p">}}</span>"&gt;

</code></pre></div></div>

<h3 id="conditional-class">Conditional class</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;li class="nav-item<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nv">item</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">%}</span> nav-item--active<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>"&gt;

</code></pre></div></div>

<h3 id="truncate-excerpt">Truncate excerpt</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;p&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">excerpt</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">strip_html</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">truncatewords</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="w"> </span><span class="p">}}</span>&lt;/p&gt;

</code></pre></div></div>

<h3 id="absolute-url">Absolute URL</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;meta property="og:url" content="<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">absolute_url</span><span class="w"> </span><span class="p">}}</span>"&gt;

</code></pre></div></div>

<h3 id="build-a-comma-separated-list">Build a comma-separated list</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">tags</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">join</span><span class="p">:</span><span class="w"> </span><span class="s2">", "</span><span class="w"> </span><span class="p">}}</span>
→ "jekyll, tutorial, liquid"

</code></pre></div></div>

<h3 id="check-if-a-string-contains-a-word">Check if a string contains a word</h3>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">content</span><span class="w"> </span><span class="ow">contains</span><span class="w"> </span><span class="s2">"Jekyll"</span><span class="w"> </span><span class="p">%}</span>
  This post mentions Jekyll.
<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Liquid is straightforward once you understand the three delimiter types and the key objects (<code class="language-plaintext highlighter-rouge">site</code>, <code class="language-plaintext highlighter-rouge">page</code>, <code class="language-plaintext highlighter-rouge">content</code>). Most Jekyll template work uses a small subset of the full Liquid language — the patterns above cover the vast majority of what you will encounter in any theme.</p>

<hr />

<h2 id="why-liquid-is-well-suited-to-static-site-generation">Why Liquid is well-suited to static site generation</h2>

<p>Liquid’s design is intentionally restrictive compared to languages like Ruby, PHP, or JavaScript. You cannot execute arbitrary code, make network requests, write to files, or access the file system from a Liquid template. This restriction is a feature, not a limitation.</p>

<p>For static site generation, the restriction ensures that templates are predictable, deterministic, and safe. The same template with the same data produces the same output every time — no hidden state, no side effects, no security vulnerabilities from malicious template injection. This is why Liquid is used not only in Jekyll but also in Shopify, Craft CMS, and several other platforms where template safety matters.</p>

<p>The trade-off is that some operations you might reach for in a full programming language require workarounds in Liquid. Computing a reading time, reformatting a data structure, or performing complex mathematical operations are all possible but sometimes verbose. When a Liquid workaround becomes too complex, that is typically a signal to use a Jekyll plugin (written in Ruby) to do the computation and expose the result as a variable or filter.</p>

<h2 id="liquid-performance-in-large-jekyll-sites">Liquid performance in large Jekyll sites</h2>

<p>Liquid templates are compiled and executed during the Jekyll build. For most sites, build time is dominated by the number of pages being rendered, not the complexity of individual templates. A blog with 50 posts builds fast regardless of template complexity; a site with 2,000 posts builds slowly even with simple templates.</p>

<p>That said, a few Liquid patterns are significantly more expensive than others.</p>

<p><strong>Nested loops with filtering.</strong> A <code class="language-plaintext highlighter-rouge">{% for post in site.posts %}</code> loop that contains an inner <code class="language-plaintext highlighter-rouge">{% for tag in site.tags %}</code> loop creates O(n×m) iterations. For a site with 200 posts and 100 tags, this is 20,000 iterations per page rendered. Restructure your data beforehand with <code class="language-plaintext highlighter-rouge">assign</code> and <code class="language-plaintext highlighter-rouge">where</code> outside the inner loop wherever possible.</p>

<p><strong>Repeated <code class="language-plaintext highlighter-rouge">include</code> calls.</strong> Each <code class="language-plaintext highlighter-rouge">{% include %}</code> call parses and executes the include file. Calling <code class="language-plaintext highlighter-rouge">{% include card.html %}</code> inside a loop of 100 items processes the card file 100 times. This is fine for most sites, but when build times are slow, optimising which includes are called in hot loops is an effective lever.</p>

<p><strong>Unfiltered <code class="language-plaintext highlighter-rouge">site.posts</code> on every page.</strong> If your layout includes a “related posts” section that filters <code class="language-plaintext highlighter-rouge">site.posts</code> on every single rendered page, it runs that filtering operation once per page. With 200 posts, that is 200 executions of the filter. Pre-compute with <code class="language-plaintext highlighter-rouge">assign</code> in the layout or consider a plugin-based solution.</p>

<p>For most Jekyll sites under 500 pages, none of these matter — build times stay under 30 seconds regardless. They become relevant at scale, where small Liquid inefficiencies compound into meaningful build time increases.</p>

<h2 id="liquid-vs-javascript-for-interactivity">Liquid vs JavaScript for interactivity</h2>

<p>Liquid runs at build time and produces static HTML. JavaScript runs in the browser after the page loads. Understanding this distinction prevents a common mistake: trying to use Liquid for things that require runtime behaviour.</p>

<p>Liquid is appropriate for: filtering posts by category on an archive page (the filter runs at build time, producing separate archive pages), rendering different layouts based on front matter, generating navigation from a data file, and transforming variables for display.</p>

<p>JavaScript is appropriate for: responding to user actions (clicks, searches, form input), loading data asynchronously, animating elements, and any operation that depends on user context (browser storage, viewport size, authentication state).</p>

<p>The boundary is the browser. Anything that needs to respond to what the user does requires JavaScript. Anything that depends only on your content and configuration can be Liquid.</p>

<p>The best Jekyll sites use both: Liquid renders fast, SEO-friendly, pre-built HTML, and JavaScript adds interactivity on top without requiring the content to be rendered client-side. Alpine.js is particularly well-suited to this pattern — it lets you add reactivity to Liquid-rendered HTML with minimal overhead.</p>

<h2 id="template-inheritance-and-the-layout-chain">Template inheritance and the layout chain</h2>

<p>Jekyll’s layout system is built on Liquid and implements template inheritance. A post uses <code class="language-plaintext highlighter-rouge">layout: post</code>, <code class="language-plaintext highlighter-rouge">post.html</code> uses <code class="language-plaintext highlighter-rouge">layout: default</code>, and <code class="language-plaintext highlighter-rouge">default.html</code> is the root template. Jekyll renders these from the inside out: the post’s Markdown becomes HTML, which becomes <code class="language-plaintext highlighter-rouge">content</code> in <code class="language-plaintext highlighter-rouge">post.html</code>, which becomes <code class="language-plaintext highlighter-rouge">content</code> in <code class="language-plaintext highlighter-rouge">default.html</code>.</p>

<p>This chain can be as deep as you need. A complex theme might have <code class="language-plaintext highlighter-rouge">layout: home</code> → <code class="language-plaintext highlighter-rouge">layout: default</code>, while blog posts have <code class="language-plaintext highlighter-rouge">layout: post</code> → <code class="language-plaintext highlighter-rouge">layout: default</code>, and documentation pages have <code class="language-plaintext highlighter-rouge">layout: doc</code> → <code class="language-plaintext highlighter-rouge">layout: with-sidebar</code> → <code class="language-plaintext highlighter-rouge">layout: default</code>. Each level adds its wrapper HTML around the <code class="language-plaintext highlighter-rouge">{{ content }}</code> block from the level below.</p>

<p>The <code class="language-plaintext highlighter-rouge">page</code> variable is always the same throughout the chain — it refers to the original document (the post or page being rendered), not any intermediate layout. Only the <code class="language-plaintext highlighter-rouge">layout</code> variable changes — it refers to the front matter of the current layout file, not the page.</p>

<p>Understanding this means you can design layouts that adapt to the page’s front matter at every level of the chain. A <code class="language-plaintext highlighter-rouge">default.html</code> layout can check <code class="language-plaintext highlighter-rouge">{% if page.full_width %}</code> to render a full-width variant, even though the logic lives in the root template rather than in each individual page file.</p>

<h2 id="includes-as-a-component-system">Includes as a component system</h2>

<p>Jekyll includes are Liquid’s answer to component-based design. Each include is a reusable fragment of HTML that can be inserted into any template and can accept parameters.</p>

<p>The include system does not have the full power of modern component frameworks (no state, no event handling, no lifecycle hooks), but it covers the vast majority of what static site templates need: reusable cards, navigation items, form fragments, meta tag blocks, and schema markup.</p>

<p>Effective use of includes follows a few principles. Keep includes focused — an include should do one thing. Avoid deeply nested includes (more than two or three levels) because they become hard to trace when debugging. Accept parameters for all variable content rather than relying on global variables where possible — this makes the include’s contract explicit.</p>

<p>For the portions of your Jekyll site that need true interactivity, Alpine.js works seamlessly alongside Liquid includes. Your Liquid include renders the HTML structure; Alpine attributes (<code class="language-plaintext highlighter-rouge">x-data</code>, <code class="language-plaintext highlighter-rouge">x-show</code>, <code class="language-plaintext highlighter-rouge">x-model</code>) layer the interactive behaviour on top. The two systems are designed for exactly this kind of collaboration — Liquid for structure, Alpine for interactivity — making it possible to build sophisticated, interactive interfaces without a JavaScript rendering framework.</p>

<p>Learning Liquid thoroughly pays dividends whenever you work with a Jekyll theme — reading someone else’s templates, debugging unexpected output, or building your own layouts from scratch. The consistent syntax, the clear data model, and the deliberate limitations all contribute to templates that are readable and maintainable. Once the patterns in this guide are familiar, reading any Jekyll theme’s templates is straightforward, regardless of the theme’s complexity.</p>]]></content><author><name>Marcus Webb</name></author><category term="Tutorial" /><summary type="html"><![CDATA[Learn Jekyll's Liquid template language from scratch — objects, tags, filters, control flow, loops, and practical patterns used in real Jekyll themes.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/jekyll-liquid-templating-basics.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/jekyll-liquid-templating-basics.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyll Directory Structure Explained: What Every File and Folder Does</title><link href="https://jekyllhub.com/tutorial/2026/06/24/jekyll-directory-structure/" rel="alternate" type="text/html" title="Jekyll Directory Structure Explained: What Every File and Folder Does" /><published>2026-06-24T00:00:00+00:00</published><updated>2026-06-24T00:00:00+00:00</updated><id>https://jekyllhub.com/tutorial/2026/06/24/jekyll-directory-structure</id><content type="html" xml:base="https://jekyllhub.com/tutorial/2026/06/24/jekyll-directory-structure/"><![CDATA[<p>When you first open a Jekyll project, the folder structure can look confusing. Underscores everywhere, a <code class="language-plaintext highlighter-rouge">_site</code> folder that appears after building, special filenames with dates. Once you understand what each piece does, it all makes sense — Jekyll’s structure is actually quite logical.</p>

<p>Here is a complete reference for every file and folder in a Jekyll project.</p>

<h2 id="the-full-structure-at-a-glance">The full structure at a glance</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my-jekyll-site/
│
├── _config.yml          ← site configuration
├── Gemfile              ← Ruby dependency list
├── Gemfile.lock         ← locked dependency versions
│
├── _posts/              ← blog post files
├── _drafts/             ← unpublished drafts
├── _pages/              ← static page files (optional convention)
│
├── _layouts/            ← HTML wrapper templates
├── _includes/           ← reusable HTML fragments
├── _sass/               ← Sass/SCSS partials
│
├── _data/               ← structured data (YAML, JSON, CSV)
├── _plugins/            ← custom Ruby plugins
│
├── _collections/        ← custom collection directories
│   ├── _themes/
│   └── _authors/
│
├── assets/              ← static files (CSS, JS, images)
│   ├── css/
│   ├── js/
│   └── images/
│
├── index.html           ← homepage
└── _site/               ← generated output (do not edit)
</code></pre></div></div>

<h2 id="_configyml">_config.yml</h2>

<p>The master configuration file. Controls site-wide settings, plugins, collections, permalink structure, build options, and any custom data you want available in all templates as <code class="language-plaintext highlighter-rouge">site.*</code> variables.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">Jekyll</span><span class="nv"> </span><span class="s">Site"</span>
<span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://example.com"</span>
<span class="na">plugins</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">jekyll-feed</span>
  <span class="pi">-</span> <span class="s">jekyll-seo-tag</span>
</code></pre></div></div>

<p>Jekyll reads this file at startup only — restart the server after changes.</p>

<h2 id="gemfile-and-gemfilelock">Gemfile and Gemfile.lock</h2>

<p><strong><code class="language-plaintext highlighter-rouge">Gemfile</code></strong> lists your Ruby dependencies — Jekyll itself and any plugins:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"jekyll"</span><span class="p">,</span> <span class="s2">"~&gt; 4.3"</span>
<span class="n">gem</span> <span class="s2">"jekyll-feed"</span>
<span class="n">gem</span> <span class="s2">"jekyll-seo-tag"</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">Gemfile.lock</code></strong> is auto-generated by Bundler and records the exact version of every gem installed. Commit this file — it ensures anyone who clones your project gets identical gem versions.</p>

<p>Never edit <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> by hand. Update it with <code class="language-plaintext highlighter-rouge">bundle update</code>.</p>

<h2 id="_posts">_posts/</h2>

<p>All blog posts live here as Markdown (or HTML) files. Jekyll requires a specific naming convention:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>YYYY-MM-DD-title-of-post.md
</code></pre></div></div>

<p>Examples:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_posts/
├── 2026-08-03-jekyll-directory-structure.md
├── 2026-07-29-jekyll-front-matter-guide.md
└── 2026-01-15-my-first-post.md
</code></pre></div></div>

<p>The date in the filename sets the post’s default date. Jekyll uses it for sorting, URL generation, and the <code class="language-plaintext highlighter-rouge">page.date</code> variable. Posts are accessible via <code class="language-plaintext highlighter-rouge">site.posts</code> in templates.</p>

<h2 id="_drafts">_drafts/</h2>

<p>Drafts are posts without a date in the filename. They live in <code class="language-plaintext highlighter-rouge">_drafts/</code> and are excluded from normal builds:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_drafts/
├── my-unfinished-post.md
└── ideas-for-later.md
</code></pre></div></div>

<p>To preview drafts locally: <code class="language-plaintext highlighter-rouge">bundle exec jekyll serve --drafts</code></p>

<p>Drafts are never built in production unless you explicitly pass <code class="language-plaintext highlighter-rouge">--drafts</code> to the build command.</p>

<h2 id="_pages-convention-not-built-in">_pages/ (convention, not built-in)</h2>

<p>Jekyll has no built-in <code class="language-plaintext highlighter-rouge">_pages/</code> directory — but it is a widely used convention for storing static pages separately from posts. Files here are processed exactly like files in the root directory.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_pages/
├── about.md
├── contact.md
├── themes.html
└── faq.md
</code></pre></div></div>

<p>To make Jekyll process files from <code class="language-plaintext highlighter-rouge">_pages/</code>, either list it explicitly in <code class="language-plaintext highlighter-rouge">_config.yml</code> or keep your pages in the root directory. Many themes include <code class="language-plaintext highlighter-rouge">_pages/</code> in their configuration via:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">include</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">_pages</span>
</code></pre></div></div>

<p>Or use a collection:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">collections</span><span class="pi">:</span>
  <span class="na">pages</span><span class="pi">:</span>
    <span class="na">output</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">permalink</span><span class="pi">:</span> <span class="s">/:name/</span>
</code></pre></div></div>

<h2 id="_layouts">_layouts/</h2>

<p>HTML templates that wrap page content. Jekyll replaces <code class="language-plaintext highlighter-rouge">{{ content }}</code> in a layout with the page’s rendered output.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_layouts/
├── default.html    ← base shell (&lt;html&gt;, &lt;head&gt;, nav, footer)
├── page.html       ← inherits default, adds page container
├── post.html       ← inherits default, adds article structure
└── home.html       ← inherits default, adds hero section
</code></pre></div></div>

<p>Layouts can inherit from each other via front matter:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">default   ← this layout wraps inside default.html</span>
<span class="nn">---</span>
</code></pre></div></div>

<h2 id="_includes">_includes/</h2>

<p>Reusable HTML fragments embedded in layouts or content with <code class="language-plaintext highlighter-rouge">{% include filename.html %}</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_includes/
├── head.html              ← &lt;head&gt; contents
├── nav.html               ← navigation bar
├── footer.html            ← footer
├── analytics.html         ← analytics scripts
├── components/
│   ├── card.html          ← theme card component
│   └── badge.html         ← badge component
└── sections/
    ├── home-hero.html     ← homepage hero section
    └── home-newsletter.html
</code></pre></div></div>

<p>Unlike layouts, includes can be used anywhere — in layouts, in other includes, even mid-content in Markdown files.</p>

<h2 id="_sass">_sass/</h2>

<p>Sass/SCSS partial files that Jekyll compiles into CSS. Files starting with <code class="language-plaintext highlighter-rouge">_</code> are partials (not compiled to standalone CSS files):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_sass/
├── _variables.scss    ← colour and spacing tokens
├── _base.scss         ← reset, body, typography
├── _nav.scss          ← navigation styles
├── _cards.scss        ← card component styles
├── _post.scss         ← blog post styles
└── _utilities.scss    ← helper classes
</code></pre></div></div>

<p>These partials are imported by a main SCSS entry file in <code class="language-plaintext highlighter-rouge">assets/css/</code>:</p>

<div class="language-scss highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* assets/css/main.scss */</span>
<span class="nt">---</span>
<span class="nt">---</span>
<span class="o">@</span><span class="nt">import</span> <span class="s2">"variables"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"base"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"nav"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"cards"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"post"</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">"utilities"</span><span class="p">;</span>
</code></pre></div></div>

<p>The empty front matter (<code class="language-plaintext highlighter-rouge">---
---</code>) at the top tells Jekyll to process this file through Sass.</p>

<h2 id="_data">_data/</h2>

<p>Structured data files in YAML, JSON, CSV, or TSV format. Accessible in all templates as <code class="language-plaintext highlighter-rouge">site.data.filename</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_data/
├── navigation.yml     → site.data.navigation
├── authors.yml        → site.data.authors
├── faq.yml            → site.data.faq
├── showcase.yml       → site.data.showcase
└── bundle.yml         → site.data.bundle
</code></pre></div></div>

<p>Example usage:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">item</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.data.navigation</span><span class="w"> </span><span class="p">%}</span>
  &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">item</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;<span class="p">{{</span><span class="w"> </span><span class="nv">item</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">}}</span>&lt;/a&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>

</code></pre></div></div>

<p>Useful for any structured content that does not need individual pages — navigation menus, team members, FAQs, testimonials, pricing tables.</p>

<h2 id="_plugins">_plugins/</h2>

<p>Custom Ruby plugin files that extend Jekyll’s functionality. Files here are loaded automatically at build time:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_plugins/
├── my_generator.rb    ← custom page generator
├── my_filter.rb       ← custom Liquid filter
└── my_hook.rb         ← Jekyll build hook
</code></pre></div></div>

<p>Note: Custom plugins in <code class="language-plaintext highlighter-rouge">_plugins/</code> do not work on GitHub Pages (security restriction). They work on Netlify, Cloudflare Pages, and Vercel where you control the build environment.</p>

<h2 id="custom-collection-directories">Custom collection directories</h2>

<p>Collections defined in <code class="language-plaintext highlighter-rouge">_config.yml</code> get their own <code class="language-plaintext highlighter-rouge">_collectionname/</code> directory:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml</span>
<span class="na">collections</span><span class="pi">:</span>
  <span class="na">themes</span><span class="pi">:</span>
    <span class="na">output</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">permalink</span><span class="pi">:</span> <span class="s">/themes/:name/</span>
  <span class="na">authors</span><span class="pi">:</span>
    <span class="na">output</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">permalink</span><span class="pi">:</span> <span class="s">/authors/:name/</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_themes/
├── minimal-mistakes.md
├── chirpy.md
└── al-folio.md

_authors/
├── marcus-webb.md
└── sarah-chen.md
</code></pre></div></div>

<p>Collection items are available as <code class="language-plaintext highlighter-rouge">site.themes</code> and <code class="language-plaintext highlighter-rouge">site.authors</code> in templates.</p>

<h2 id="assets">assets/</h2>

<p>Static files served directly — CSS, JavaScript, fonts, and images. Unlike <code class="language-plaintext highlighter-rouge">_</code>-prefixed directories, <code class="language-plaintext highlighter-rouge">assets/</code> is copied to <code class="language-plaintext highlighter-rouge">_site/</code> without processing (except for SCSS files with front matter).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>assets/
├── css/
│   └── main.scss       ← compiled to main.css
├── js/
│   ├── main.js
│   └── bookmarks.js
├── images/
│   ├── logo.png
│   ├── social-card.png
│   └── blog/
│       └── post-cover.webp
└── fonts/
    └── inter.woff2
</code></pre></div></div>

<p>Reference assets in templates using <code class="language-plaintext highlighter-rouge">relative_url</code>:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
&lt;link rel="stylesheet" href="<span class="p">{{</span><span class="w"> </span><span class="s1">'/assets/css/main.css'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>"&gt;
&lt;img src="<span class="p">{{</span><span class="w"> </span><span class="s1">'/assets/images/logo.png'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="p">}}</span>" alt="Logo"&gt;

</code></pre></div></div>

<h2 id="root-level-files">Root-level files</h2>

<p><strong><code class="language-plaintext highlighter-rouge">index.html</code> or <code class="language-plaintext highlighter-rouge">index.md</code></strong> — your homepage. Can use any layout.</p>

<p><strong><code class="language-plaintext highlighter-rouge">404.html</code></strong> — custom 404 page. Most hosts serve this automatically for missing pages.</p>

<p><strong><code class="language-plaintext highlighter-rouge">feed.xml</code></strong> or <strong><code class="language-plaintext highlighter-rouge">atom.xml</code></strong> — RSS/Atom feed (usually auto-generated by <code class="language-plaintext highlighter-rouge">jekyll-feed</code>).</p>

<p><strong><code class="language-plaintext highlighter-rouge">sitemap.xml</code></strong> — XML sitemap (auto-generated by <code class="language-plaintext highlighter-rouge">jekyll-sitemap</code>).</p>

<p><strong><code class="language-plaintext highlighter-rouge">robots.txt</code></strong> — instructions for search engine crawlers:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">_redirects</code></strong> — redirect rules for Netlify/Cloudflare Pages.</p>

<p><strong><code class="language-plaintext highlighter-rouge">.gitignore</code></strong> — files to exclude from Git:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_site/
.jekyll-cache/
.sass-cache/
.bundle/
vendor/
node_modules/
</code></pre></div></div>

<h2 id="_site">_site/</h2>

<p>The generated output — never edit files here directly. Jekyll wipes and rebuilds this directory on every build. It mirrors what your visitors see:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_site/
├── index.html
├── about/
│   └── index.html
├── blog/
│   ├── index.html
│   └── jekyll-directory-structure/
│       └── index.html
├── assets/
│   ├── css/
│   │   └── main.css       ← compiled from main.scss
│   └── js/
│       └── main.js
├── feed.xml
└── sitemap.xml
</code></pre></div></div>

<p>Add <code class="language-plaintext highlighter-rouge">_site/</code> to <code class="language-plaintext highlighter-rouge">.gitignore</code> — deploy from your build pipeline, not from a committed <code class="language-plaintext highlighter-rouge">_site/</code>.</p>

<h2 id="jekyll-cache">.jekyll-cache/</h2>

<p>Jekyll’s internal build cache. Speeds up incremental builds by storing processed files. Safe to delete if you see stale content — Jekyll regenerates it. Add to <code class="language-plaintext highlighter-rouge">.gitignore</code>.</p>

<h2 id="files-jekyll-ignores-by-default">Files Jekyll ignores by default</h2>

<p>Jekyll automatically excludes these from the build output:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Gemfile</code> and <code class="language-plaintext highlighter-rouge">Gemfile.lock</code></li>
  <li><code class="language-plaintext highlighter-rouge">node_modules/</code></li>
  <li>Any file or directory starting with <code class="language-plaintext highlighter-rouge">.</code> (dotfiles)</li>
  <li>Any file or directory starting with <code class="language-plaintext highlighter-rouge">_</code> (except those explicitly handled)</li>
  <li>Files listed in <code class="language-plaintext highlighter-rouge">exclude:</code> in <code class="language-plaintext highlighter-rouge">_config.yml</code></li>
</ul>

<h2 id="the-build-flow">The build flow</h2>

<p>When you run <code class="language-plaintext highlighter-rouge">bundle exec jekyll build</code>:</p>

<ol>
  <li>Jekyll reads <code class="language-plaintext highlighter-rouge">_config.yml</code></li>
  <li>Reads all files in <code class="language-plaintext highlighter-rouge">_posts/</code>, <code class="language-plaintext highlighter-rouge">_pages/</code>, <code class="language-plaintext highlighter-rouge">_data/</code>, collections</li>
  <li>Processes files with front matter through Liquid templating</li>
  <li>Applies layouts (wrapping content in layout HTML)</li>
  <li>Compiles Sass/SCSS to CSS</li>
  <li>Copies static assets unchanged</li>
  <li>Writes everything to <code class="language-plaintext highlighter-rouge">_site/</code></li>
</ol>

<p>Understanding this flow makes it clear why <code class="language-plaintext highlighter-rouge">_</code> directories are special (processed by Jekyll) while <code class="language-plaintext highlighter-rouge">assets/</code> is not (copied as-is), and why changes to <code class="language-plaintext highlighter-rouge">_config.yml</code> require a restart.</p>

<h2 id="how-jekylls-build-process-uses-the-directory-structure">How Jekyll’s build process uses the directory structure</h2>

<p>Understanding the directory structure becomes much clearer when you trace how Jekyll uses each directory during a build. Jekyll reads configuration from <code class="language-plaintext highlighter-rouge">_config.yml</code> first, then processes files in a specific order: front matter defaults are applied, collections are processed, posts are sorted and paginated, Liquid templates are rendered, and finally all output is written to <code class="language-plaintext highlighter-rouge">_site/</code>.</p>

<p>The underscore prefix convention — <code class="language-plaintext highlighter-rouge">_layouts/</code>, <code class="language-plaintext highlighter-rouge">_includes/</code>, <code class="language-plaintext highlighter-rouge">_posts/</code>, <code class="language-plaintext highlighter-rouge">_data/</code> — signals to Jekyll that these directories should be processed rather than copied. Jekyll reads their contents and uses them during the build but does not create corresponding directories in <code class="language-plaintext highlighter-rouge">_site/</code>. Everything else (assets, pages, any directory without an underscore prefix) is processed with front matter if present, or copied unchanged if not.</p>

<p>This distinction explains a common source of confusion: if you put a file in <code class="language-plaintext highlighter-rouge">_includes/</code>, it is available to templates via the <code class="language-plaintext highlighter-rouge">{% include %}</code> tag but never appears as a standalone URL. If you put the same file in <code class="language-plaintext highlighter-rouge">assets/</code>, it is copied to the output and accessible at its path, but not available to templates via include. The directory location determines how Jekyll handles the file, not its extension or content.</p>

<h2 id="the-_site-directory-understanding-your-output">The _site directory: understanding your output</h2>

<p>Everything inside <code class="language-plaintext highlighter-rouge">_site/</code> after a build is exactly what gets deployed to your hosting provider. Browsing this directory is the most direct way to verify that Jekyll is producing what you expect. Common checks: does <code class="language-plaintext highlighter-rouge">_site/index.html</code> contain the correct homepage content? Does <code class="language-plaintext highlighter-rouge">_site/blog/</code> contain your post HTML files? Are images in <code class="language-plaintext highlighter-rouge">_site/assets/images/</code>?</p>

<p>The <code class="language-plaintext highlighter-rouge">_site/</code> directory should be in your <code class="language-plaintext highlighter-rouge">.gitignore</code> because it is generated output, not source files. Committing it creates noise in your Git history and causes merge conflicts when multiple people build the site locally. Hosting providers (Netlify, Cloudflare Pages, Vercel) all build the site themselves from your source files — they do not use a pre-built <code class="language-plaintext highlighter-rouge">_site/</code> directory.</p>

<p>If a file is missing from <code class="language-plaintext highlighter-rouge">_site/</code>, the cause is usually one of three things: the file has a YAML front matter parsing error and was skipped by Jekyll; the file is in an underscore-prefixed directory that Jekyll processed but did not output; or the file is excluded in <code class="language-plaintext highlighter-rouge">_config.yml</code> via the <code class="language-plaintext highlighter-rouge">exclude:</code> setting. Running <code class="language-plaintext highlighter-rouge">bundle exec jekyll build --verbose</code> outputs detailed information about each file processed, making it straightforward to identify why a specific file did not appear in the output.</p>

<h2 id="keeping-the-root-directory-clean">Keeping the root directory clean</h2>

<p>As a Jekyll site grows, the root directory accumulates files: <code class="language-plaintext highlighter-rouge">_config.yml</code>, <code class="language-plaintext highlighter-rouge">Gemfile</code>, <code class="language-plaintext highlighter-rouge">Gemfile.lock</code>, <code class="language-plaintext highlighter-rouge">.gitignore</code>, a README, GitHub Actions workflows, deployment configuration for Netlify or Cloudflare, and various dot files from tools like EditorConfig and Prettier. This accumulation is normal, but organising it deliberately keeps the root navigable.</p>

<p>Move GitHub Actions workflows to <code class="language-plaintext highlighter-rouge">.github/workflows/</code> (where they are required to be). Keep deployment configuration files like <code class="language-plaintext highlighter-rouge">netlify.toml</code>, <code class="language-plaintext highlighter-rouge">vercel.json</code>, or <code class="language-plaintext highlighter-rouge">_redirects</code> in the root since most platforms expect them there. Use the <code class="language-plaintext highlighter-rouge">exclude:</code> setting in <code class="language-plaintext highlighter-rouge">_config.yml</code> to prevent non-site files from being copied to <code class="language-plaintext highlighter-rouge">_site/</code> — exclude your <code class="language-plaintext highlighter-rouge">README.md</code>, <code class="language-plaintext highlighter-rouge">package.json</code> (if you have one), and any scripts or tooling files that should not be in the built output.</p>

<p>A clean root directory with clear purpose for each file is a sign of a well-organised Jekyll project. When a new contributor clones the repository, they should be able to understand the project structure within five minutes by reading the top-level files and directory names. That clarity is worth maintaining as a deliberate practice throughout the project’s lifetime.</p>

<h2 id="working-with-jekylls-directory-structure-as-a-team">Working with Jekyll’s directory structure as a team</h2>

<p>Individual developers working alone can be loose about directory organisation without much consequence. Teams need more discipline, because inconsistencies in where files live create confusion and make codebase navigation slower for everyone.</p>

<p>Document your directory conventions in a <code class="language-plaintext highlighter-rouge">CONTRIBUTING.md</code> or a brief architecture note. Where do images for blog posts go? (<code class="language-plaintext highlighter-rouge">assets/images/posts/year/</code> or <code class="language-plaintext highlighter-rouge">assets/images/posts/post-slug/</code>?) Where does page-specific JavaScript live? Where do reusable partials go versus page-specific includes? Answering these questions once and writing them down prevents each contributor from independently inventing their own conventions.</p>

<p>Use Jekyll’s front matter defaults to enforce consistency automatically. Requiring all posts to use the <code class="language-plaintext highlighter-rouge">post</code> layout and all pages to use the <code class="language-plaintext highlighter-rouge">page</code> layout via <code class="language-plaintext highlighter-rouge">_config.yml</code> defaults means contributors do not need to remember to set the layout on every file — it is applied automatically. Similarly, default values for <code class="language-plaintext highlighter-rouge">author</code> and <code class="language-plaintext highlighter-rouge">categories</code> reduce the variation in front matter that creates template edge cases.</p>

<p>Jekyll’s directory structure is intentionally simple. Its conventions — underscore for processed, plain for output — are consistent and learnable in an afternoon. Building on those conventions with clear team practices produces a codebase that is clean, navigable, and maintainable for the full lifetime of the site.</p>]]></content><author><name>Marcus Webb</name></author><category term="Tutorial" /><summary type="html"><![CDATA[A complete walkthrough of Jekyll's directory structure — every folder, file, and naming convention explained with practical examples for beginners and theme developers.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jekyllhub.com/assets/images/blog/jekyll-directory-structure.webp" /><media:content medium="image" url="https://jekyllhub.com/assets/images/blog/jekyll-directory-structure.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>