Home Blog Jekyll Directory Structure Explained: What Every File and Folder Does
Tutorial

Jekyll Directory Structure Explained: What Every File and Folder Does

A complete walkthrough of Jekyll's directory structure — every folder, file, and naming convention explained with practical examples for beginners and theme developers.

Jekyll Directory Structure Explained: What Every File and Folder Does

When you first open a Jekyll project, the folder structure can look confusing. Underscores everywhere, a _site 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.

Here is a complete reference for every file and folder in a Jekyll project.

The full structure at a glance

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)

_config.yml

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 site.* variables.

title: "My Jekyll Site"
url: "https://example.com"
plugins:
  - jekyll-feed
  - jekyll-seo-tag

Jekyll reads this file at startup only — restart the server after changes.

Gemfile and Gemfile.lock

Gemfile lists your Ruby dependencies — Jekyll itself and any plugins:

source "https://rubygems.org"
gem "jekyll", "~> 4.3"
gem "jekyll-feed"
gem "jekyll-seo-tag"

Gemfile.lock 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.

Never edit Gemfile.lock by hand. Update it with bundle update.

_posts/

All blog posts live here as Markdown (or HTML) files. Jekyll requires a specific naming convention:

YYYY-MM-DD-title-of-post.md

Examples:

_posts/
├── 2026-08-03-jekyll-directory-structure.md
├── 2026-07-29-jekyll-front-matter-guide.md
└── 2026-01-15-my-first-post.md

The date in the filename sets the post’s default date. Jekyll uses it for sorting, URL generation, and the page.date variable. Posts are accessible via site.posts in templates.

_drafts/

Drafts are posts without a date in the filename. They live in _drafts/ and are excluded from normal builds:

_drafts/
├── my-unfinished-post.md
└── ideas-for-later.md

To preview drafts locally: bundle exec jekyll serve --drafts

Drafts are never built in production unless you explicitly pass --drafts to the build command.

_pages/ (convention, not built-in)

Jekyll has no built-in _pages/ 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.

_pages/
├── about.md
├── contact.md
├── themes.html
└── faq.md

To make Jekyll process files from _pages/, either list it explicitly in _config.yml or keep your pages in the root directory. Many themes include _pages/ in their configuration via:

include:
  - _pages

Or use a collection:

collections:
  pages:
    output: true
    permalink: /:name/

_layouts/

HTML templates that wrap page content. Jekyll replaces {{ content }} in a layout with the page’s rendered output.

_layouts/
├── default.html    ← base shell (<html>, <head>, nav, footer)
├── page.html       ← inherits default, adds page container
├── post.html       ← inherits default, adds article structure
└── home.html       ← inherits default, adds hero section

Layouts can inherit from each other via front matter:

---
layout: default   ← this layout wraps inside default.html
---

_includes/

Reusable HTML fragments embedded in layouts or content with {% include filename.html %}.

_includes/
├── head.html              ← <head> 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

Unlike layouts, includes can be used anywhere — in layouts, in other includes, even mid-content in Markdown files.

_sass/

Sass/SCSS partial files that Jekyll compiles into CSS. Files starting with _ are partials (not compiled to standalone CSS files):

_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

These partials are imported by a main SCSS entry file in assets/css/:

/* assets/css/main.scss */
---
---
@import "variables";
@import "base";
@import "nav";
@import "cards";
@import "post";
@import "utilities";

The empty front matter (--- ---) at the top tells Jekyll to process this file through Sass.

_data/

Structured data files in YAML, JSON, CSV, or TSV format. Accessible in all templates as site.data.filename:

_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

Example usage:


{% for item in site.data.navigation %}
  <a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}

Useful for any structured content that does not need individual pages — navigation menus, team members, FAQs, testimonials, pricing tables.

_plugins/

Custom Ruby plugin files that extend Jekyll’s functionality. Files here are loaded automatically at build time:

_plugins/
├── my_generator.rb    ← custom page generator
├── my_filter.rb       ← custom Liquid filter
└── my_hook.rb         ← Jekyll build hook

Note: Custom plugins in _plugins/ do not work on GitHub Pages (security restriction). They work on Netlify, Cloudflare Pages, and Vercel where you control the build environment.

Custom collection directories

Collections defined in _config.yml get their own _collectionname/ directory:

# _config.yml
collections:
  themes:
    output: true
    permalink: /themes/:name/
  authors:
    output: true
    permalink: /authors/:name/
_themes/
├── minimal-mistakes.md
├── chirpy.md
└── al-folio.md

_authors/
├── marcus-webb.md
└── sarah-chen.md

Collection items are available as site.themes and site.authors in templates.

assets/

Static files served directly — CSS, JavaScript, fonts, and images. Unlike _-prefixed directories, assets/ is copied to _site/ without processing (except for SCSS files with front matter).

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

Reference assets in templates using relative_url:


<link rel="stylesheet" href="{{ '/assets/css/main.css' | relative_url }}">
<img src="{{ '/assets/images/logo.png' | relative_url }}" alt="Logo">

Root-level files

index.html or index.md — your homepage. Can use any layout.

404.html — custom 404 page. Most hosts serve this automatically for missing pages.

feed.xml or atom.xml — RSS/Atom feed (usually auto-generated by jekyll-feed).

sitemap.xml — XML sitemap (auto-generated by jekyll-sitemap).

robots.txt — instructions for search engine crawlers:

User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml

_redirects — redirect rules for Netlify/Cloudflare Pages.

.gitignore — files to exclude from Git:

_site/
.jekyll-cache/
.sass-cache/
.bundle/
vendor/
node_modules/

_site/

The generated output — never edit files here directly. Jekyll wipes and rebuilds this directory on every build. It mirrors what your visitors see:

_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

Add _site/ to .gitignore — deploy from your build pipeline, not from a committed _site/.

.jekyll-cache/

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 .gitignore.

Files Jekyll ignores by default

Jekyll automatically excludes these from the build output:

  • Gemfile and Gemfile.lock
  • node_modules/
  • Any file or directory starting with . (dotfiles)
  • Any file or directory starting with _ (except those explicitly handled)
  • Files listed in exclude: in _config.yml

The build flow

When you run bundle exec jekyll build:

  1. Jekyll reads _config.yml
  2. Reads all files in _posts/, _pages/, _data/, collections
  3. Processes files with front matter through Liquid templating
  4. Applies layouts (wrapping content in layout HTML)
  5. Compiles Sass/SCSS to CSS
  6. Copies static assets unchanged
  7. Writes everything to _site/

Understanding this flow makes it clear why _ directories are special (processed by Jekyll) while assets/ is not (copied as-is), and why changes to _config.yml require a restart.

How Jekyll’s build process uses the directory structure

Understanding the directory structure becomes much clearer when you trace how Jekyll uses each directory during a build. Jekyll reads configuration from _config.yml 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 _site/.

The underscore prefix convention — _layouts/, _includes/, _posts/, _data/ — 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 _site/. Everything else (assets, pages, any directory without an underscore prefix) is processed with front matter if present, or copied unchanged if not.

This distinction explains a common source of confusion: if you put a file in _includes/, it is available to templates via the {% include %} tag but never appears as a standalone URL. If you put the same file in assets/, 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.

The _site directory: understanding your output

Everything inside _site/ 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 _site/index.html contain the correct homepage content? Does _site/blog/ contain your post HTML files? Are images in _site/assets/images/?

The _site/ directory should be in your .gitignore 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 _site/ directory.

If a file is missing from _site/, 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 _config.yml via the exclude: setting. Running bundle exec jekyll build --verbose outputs detailed information about each file processed, making it straightforward to identify why a specific file did not appear in the output.

Keeping the root directory clean

As a Jekyll site grows, the root directory accumulates files: _config.yml, Gemfile, Gemfile.lock, .gitignore, 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.

Move GitHub Actions workflows to .github/workflows/ (where they are required to be). Keep deployment configuration files like netlify.toml, vercel.json, or _redirects in the root since most platforms expect them there. Use the exclude: setting in _config.yml to prevent non-site files from being copied to _site/ — exclude your README.md, package.json (if you have one), and any scripts or tooling files that should not be in the built output.

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.

Working with Jekyll’s directory structure as a team

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.

Document your directory conventions in a CONTRIBUTING.md or a brief architecture note. Where do images for blog posts go? (assets/images/posts/year/ or assets/images/posts/post-slug/?) 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.

Use Jekyll’s front matter defaults to enforce consistency automatically. Requiring all posts to use the post layout and all pages to use the page layout via _config.yml defaults means contributors do not need to remember to set the layout on every file — it is applied automatically. Similarly, default values for author and categories reduce the variation in front matter that creates template edge cases.

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.

Share LinkedIn