Home Blog Jekyll Layouts Explained: How to Structure Your Site Templates
Tutorial

Jekyll Layouts Explained: How to Structure Your Site Templates

A complete guide to Jekyll layouts — how they work, layout inheritance, passing data, and building a clean layout hierarchy for any Jekyll site.

Jekyll Layouts Explained: How to Structure Your Site Templates

Layouts are Jekyll’s template system — reusable HTML wrappers that surround your content. Every page on your Jekyll site uses a layout, even if you have not thought about it explicitly. Understanding how layouts work, and how to structure them, is one of the most important skills in Jekyll development.

What is a layout?

A layout is an HTML file in the _layouts/ directory that contains a {{ content }} placeholder. When Jekyll builds a page, it takes the page’s content and injects it wherever {{ content }} appears in the layout.


<!-- _layouts/default.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ page.title }} | {{ site.title }}</title>
  <link rel="stylesheet" href="{{ '/assets/css/main.css' | relative_url }}">
</head>
<body>
  {% include nav.html %}

  <main>
    {{ content }}
  </main>

  {% include footer.html %}
</body>
</html>

When this layout is applied to a post, {{ content }} is replaced with the post’s rendered HTML. The <head>, navigation, and footer appear on every page that uses this layout.

The _layouts directory

All layouts live in _layouts/ at your project root:

_layouts/
├── default.html    # base layout
├── page.html       # for standard pages
├── post.html       # for blog posts
├── theme.html      # for theme collection items
└── home.html       # for the homepage

Jekyll looks for the layout file specified in front matter — layout: post maps to _layouts/post.html.

Applying a layout

Specify a layout in the front matter of any page, post, or collection item:

---
layout: post
title: "My Blog Post"
---

Post content here.

Or use _config.yml defaults to apply layouts automatically to entire directories or types:

# _config.yml
defaults:
  - scope:
      type: posts
    values:
      layout: post
  - scope:
      type: pages
    values:
      layout: page

With this in place, you do not need to specify layout: in every post’s front matter.

Layout inheritance

The most powerful Jekyll layout feature is inheritance — a layout can itself use another layout. This lets you build a hierarchy that avoids repetition.

The base layout

Start with a default.html that contains everything common to all pages: <html>, <head>, nav, footer:


<!-- _layouts/default.html -->
<!DOCTYPE html>
<html lang="{{ page.lang | default: 'en' }}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  {% seo %}
  <link rel="stylesheet" href="{{ '/assets/css/main.css' | relative_url }}">
  {% include analytics.html %}
</head>
<body class="{% if page.dark %}dark{% endif %}">
  {% include nav.html %}

  {{ content }}

  {% include footer.html %}
  <script src="{{ '/assets/js/main.js' | relative_url }}" defer></script>
</body>
</html>

A child layout

A post.html layout adds post-specific structure — the article wrapper, header, and sidebar — and uses default.html as its parent via front matter:


<!-- _layouts/post.html -->
---
layout: default
---

<div class="container post-container">
  <article class="post">
    <header class="post__header">
      <div class="post__meta">
        <span>{{ page.category }}</span>
        <time>{{ page.date | date: "%B %-d, %Y" }}</time>
      </div>
      <h1 class="post__title">{{ page.title }}</h1>
      {% if page.description %}
        <p class="post__description">{{ page.description }}</p>
      {% endif %}
      {% if page.image %}
        <img src="{{ page.image | relative_url }}" alt="{{ page.title }}" class="post__hero">
      {% endif %}
    </header>

    <div class="post__body">
      {{ content }}
    </div>
  </article>

  {% include sidebar.html %}
</div>

When a blog post uses layout: post, Jekyll:

  1. Renders the post’s Markdown content to HTML
  2. Injects it into post.html at {{ content }}
  3. Renders the resulting HTML
  4. Injects that into default.html at {{ content }}

This nesting can go as deep as you need.

A typical layout hierarchy

default.html          ← base: <html>, <head>, nav, footer
├── page.html         ← adds: container, optional sidebar
├── post.html         ← adds: article header, author info, TOC
├── home.html         ← adds: hero section, no standard container
└── theme.html        ← adds: gallery, price sidebar, details

Each child adds only what it needs; the parent handles the shared shell.

Accessing variables in layouts

Layouts have access to three levels of data:

page variables — front matter from the current page:


{{ page.title }}
{{ page.description }}
{{ page.author }}
{{ page.date | date: "%B %-d, %Y" }}

site variables — data from _config.yml and the site as a whole:


{{ site.title }}
{{ site.description }}
{{ site.url }}
{{ site.posts }}        — all posts
{{ site.themes }}       — all items in the themes collection

layout variables — front matter from the layout file itself:


{{ layout.title }}
{{ layout.sidebar }}

Conditional content in layouts

Use Liquid conditionals to show or hide layout sections based on page front matter:


<!-- _layouts/post.html -->
---
layout: default
---

<article>
  {{ content }}

  {% if page.toc %}
    {% include toc.html %}
  {% endif %}

  {% if page.show_author != false %}
    {% include author-bio.html %}
  {% endif %}

  {% if page.related_posts != false %}
    {% include related-posts.html %}
  {% endif %}
</article>

Now individual posts can opt out of sections without modifying the layout:

---
layout: post
title: "My Post"
toc: false              # hide table of contents
related_posts: false    # hide related posts
---

Passing data from pages to layouts

Any front matter variable in the page is accessible in the layout. Use this to customise layout behaviour per page:

---
layout: page
title: "Homepage"
hero_image: /assets/images/hero.webp
hero_title: "Find Your Perfect Jekyll Theme"
hero_cta: "Browse Themes"
hero_cta_url: /themes/
body_class: "homepage"
---

<!-- _layouts/page.html -->
---
layout: default
---

{% if page.hero_image %}
<section class="hero" style="background-image: url('{{ page.hero_image | relative_url }}')">
  <h1>{{ page.hero_title | default: page.title }}</h1>
  {% if page.hero_cta %}
    <a href="{{ page.hero_cta_url }}" class="btn">{{ page.hero_cta }}</a>
  {% endif %}
</section>
{% endif %}

<main class="{{ page.body_class }}">
  {{ content }}
</main>

Layout-level front matter

Layouts can have their own front matter, accessible via {{ layout.* }} in includes:

<!-- _layouts/post.html -->
---
layout: default
sidebar: true
show_comments: true
---

This is useful for setting defaults that applies to all pages using a layout, which individual pages can override.

The none layout

To render a file with no layout at all — just raw content — set:

---
layout: none
---

Or use an empty string:

---
layout: ""
---

Useful for: JSON data files, XML sitemaps, text files, or any output that should not be wrapped in HTML.

Example: Building a complete layout system

Here is a complete, practical layout hierarchy for a Jekyll blog:

_layouts/default.html — the shell:


---
# no layout — this is the base
---
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  {% seo %}
  <link rel="stylesheet" href="{{ '/assets/css/main.css' | relative_url }}">
</head>
<body>
  {% include nav.html %}
  {{ content }}
  {% include footer.html %}
  <script src="{{ '/assets/js/main.js' | relative_url }}" defer></script>
</body>
</html>

_layouts/page.html — for standard pages:


---
layout: default
---
<main class="container page-container">
  <h1>{{ page.title }}</h1>
  {{ content }}
</main>

_layouts/post.html — for blog posts:


---
layout: default
---
<main class="container">
  <article class="post">
    <h1>{{ page.title }}</h1>
    <time>{{ page.date | date: "%B %-d, %Y" }}</time>
    {{ content }}
  </article>
  {% include related-posts.html %}
</main>

_layouts/home.html — for the homepage only:


---
layout: default
---
{% include home-hero.html %}
{% include featured-themes.html %}
{{ content }}
{% include home-newsletter.html %}

This structure is clear, maintainable, and easy to extend. Adding a new page type means adding one layout file — not editing a monolithic template.

Tips for clean layouts

Keep layouts thin. Layouts should structure content — not contain it. Move repeating blocks to _includes/.

Use meaningful layout names. post, page, theme, author are clear. layout1, template_v2 are not.

Default to default. Most child layouts should inherit from default.html, not from each other. Deep chains (default → page → section → content) become hard to follow.

Test with no layout. If a layout-related bug appears, temporarily set layout: none on the affected page to isolate whether the issue is in the content or the layout.

Understanding Jekyll’s layout system is what separates a collection of Markdown files from a real, maintainable website. Once the hierarchy is in place, adding new page types or changing the global structure becomes a matter of editing one file.

Advanced layout techniques

Once you are comfortable with the basic layout hierarchy, several advanced techniques become useful for complex Jekyll sites.

Layout-specific front matter defaults. Rather than setting layout: post on every post in its front matter, define defaults in _config.yml:

defaults:
  - scope:
      path: "_posts"
      type: "posts"
    values:
      layout: "post"
      author: "Site Owner"

Every post now uses the post layout automatically. If a post needs a different layout, it can still override this default in its own front matter. This pattern eliminates repetition and ensures that new posts automatically get the correct layout even if you forget to specify it.

Layout-level data. Layouts can have their own front matter, including variables that templates can read. A sidebar: true value set in a layout’s front matter can be read by an ancestor layout to conditionally render a sidebar — clean separation of concerns without duplicating the conditional logic in multiple templates.

Conditional layout logic. The layout variable is accessible in templates, so you can write conditionals based on which layout is in use. This pattern is occasionally useful for global elements like breadcrumbs that need to behave differently on post pages versus regular pages, without requiring separate layout files for each case.

Layout performance considerations

Layouts directly affect site performance in ways that are easy to overlook. Every {% include %} call in a layout adds processing time to each page that uses that layout. On a small site this is negligible; on a site with a thousand posts, an include that performs a complex Liquid {% for %} loop inside the layout can meaningfully increase build times.

The principle to follow: do expensive operations (sorting, filtering, grouping site.posts) as few times as possible, and as close to the output as possible. A related posts include that loops through all posts and filters by category on every page render is expensive. Pre-computing related posts during build with a plugin, or limiting the loop with a limit filter, is cheaper. Jekyll’s incremental build (--incremental flag) mitigates this during development, but production builds always run in full.

Cache includes where possible. If an include renders the same output regardless of page context — a footer, a navigation bar, an SVG icon set — it is a good candidate for moving into the layout itself rather than a separate include file, reducing the overhead of file reading and parsing on each build.

Layouts and content structure as a team

The best Jekyll sites treat layout structure as an intentional design decision made early and changed rarely. Layout hierarchies that grow organically tend to accumulate complexity — a default.html that has been extended five times, each time adding new sections, until no single person understands the full inheritance chain.

Audit your layout structure periodically. Open each layout file and read its front matter to trace the inheritance chain. If you find layouts that inherit from other non-default layouts more than two levels deep, consider flattening the hierarchy by copying content from the intermediate layouts. The readability cost of slightly duplicated layout code is usually lower than the cognitive cost of a deeply nested inheritance chain.

Document your layout conventions in a comment at the top of each layout file — what it is for, what variables it expects, and what it passes down to children. This two-minute investment per file saves significant time for anyone (including future you) who needs to add a new page type to the site.

Jekyll’s layout system is one of the most elegant parts of the tool. It solves the template inheritance problem with minimal abstraction — just a layout key in front matter and a {{ content }} variable in each template. Master it, and the structure of even complex multi-section Jekyll sites becomes clear and maintainable.

Debugging layout inheritance issues

Layout inheritance bugs are among the most confusing to diagnose in Jekyll because the error often appears in a file far from where the problem originates. A missing variable error in a template may originate from a layout three levels up in the inheritance chain that was supposed to pass a variable down.

The first debugging step is tracing the inheritance chain. For the page that is failing, open its front matter and note the layout value. Open that layout file, note its layout value in front matter. Continue until you reach a layout with no layout key (your root layout, typically default.html). This chain is the full template stack for the page — the error could originate anywhere in it.

Jekyll’s --verbose flag and --trace flag provide additional build output. --trace prints the full Ruby stack trace for any error, which often identifies the exact template file and line number where the error occurs. Combine with bundle exec jekyll build --verbose --trace 2>&1 | less to capture and page through the full output.

Liquid’s {{ variable | inspect }} filter is invaluable for layout debugging — it prints the raw value of any variable, including nil, which outputs as an empty string by default. If you suspect a variable is nil when it should have a value, add {{ page.my_variable | inspect }} to the template and run a build. The output will show either the variable’s actual value or the empty string, confirming the diagnosis.

Variable scope in layouts requires care. Variables assigned in a layout with {% assign %} are NOT available in the child template’s content — the content is rendered before the layout is applied, so variables created inside the layout cannot flow back into the page content. Only front matter variables (page.*) and site variables (site.*) are available throughout the entire template stack.

Share LinkedIn