Jekyll _config.yml: The Complete Configuration Guide
Everything you need to know about Jekyll's _config.yml — global settings, build options, plugins, collections, front matter defaults, and real-world examples.
_config.yml is the single most important file in a Jekyll project. It controls everything: your site’s URL, which plugins run, how collections are defined, what files are excluded from builds, and any custom data you want available across every template. Understanding it thoroughly is essential for anyone building or customising a Jekyll site.
File location and format
_config.yml lives at the root of your Jekyll project:
my-jekyll-site/
├── _config.yml ← here
├── _posts/
├── _layouts/
├── _includes/
└── index.html
It uses YAML format — indented key-value pairs. Two spaces per indent level, no tabs.
Important: Jekyll reads _config.yml only at startup. If you change it while running jekyll serve, you must restart the server to see the changes take effect. Content changes in posts and pages hot-reload automatically; config changes do not.
Essential site settings
# Site identity
title: "JekyllHub"
description: "A marketplace for premium and free Jekyll themes."
# URLs — critical to get right
url: "https://jekyllhub.com" # your production domain with scheme
baseurl: "" # subdirectory, if any (e.g. "/blog")
# Contact
email: hello@jekyllhub.com
url vs baseurl
This distinction trips up many Jekyll users.
url is your site’s root domain: https://jekyllhub.com. It is used to construct absolute URLs (for RSS feeds, sitemaps, canonical tags).
baseurl is a subdirectory path if your site does not live at the root of the domain. For a site at https://username.github.io/my-project/, set baseurl: "/my-project". For a site at the root, leave it empty: baseurl: "".
In templates, always use the relative_url or absolute_url filters instead of hardcoding paths — they automatically prepend baseurl:
<a href="{{ '/about/' | relative_url }}">About</a>
<link rel="canonical" href="{{ page.url | absolute_url }}">
Build settings
# Build
source: . # where Jekyll reads files (default: current dir)
destination: _site # where Jekyll writes output (default: _site)
# Markdown
markdown: kramdown
highlighter: rouge # syntax highlighting engine
# Kramdown options
kramdown:
input: GFM # GitHub Flavoured Markdown
hard_wrap: false
syntax_highlighter: rouge
syntax_highlighter_opts:
block:
line_numbers: true
# Liquid
liquid:
error_mode: warn # warn | strict | lax
strict_filters: false
strict_variables: false
Plugins
List all plugins in the plugins key:
plugins:
- jekyll-feed # generates /feed.xml RSS feed
- jekyll-seo-tag # adds meta tags, OG tags, JSON-LD
- jekyll-sitemap # generates /sitemap.xml
- jekyll-paginate-v2 # pagination for posts
- jekyll-redirect-from # add redirects via front matter
- jekyll-archives # generates category and tag archive pages
Plugins must also be in your Gemfile:
group :jekyll_plugins do
gem "jekyll-feed"
gem "jekyll-seo-tag"
gem "jekyll-sitemap"
gem "jekyll-paginate-v2"
end
GitHub Pages note: GitHub Pages only supports a specific list of whitelisted plugins. If deploying to GitHub Pages, check the whitelist. For full plugin support, use Cloudflare Pages, Netlify, or Vercel with a build step.
Collections
Collections let you create custom content types beyond posts and pages:
collections:
themes:
output: true # generate individual pages for each item
permalink: /themes/:name/ # URL pattern
authors:
output: true
permalink: /authors/:name/
showcase:
output: false # data only, no individual pages
With this configuration, files in _themes/ generate pages at /themes/minimal-mistakes/, etc. Files in _showcase/ are available as site.showcase but do not generate pages.
Front matter defaults
Avoid repeating the same front matter on every post with defaults:
defaults:
# All posts get layout: post and author: Marcus Webb
- scope:
path: ""
type: posts
values:
layout: post
author: Marcus Webb
toc: true
featured: false
comments: true
# All pages get layout: page
- scope:
path: ""
type: pages
values:
layout: page
# Theme collection items get layout: theme
- scope:
path: ""
type: themes
values:
layout: theme
# Files in a specific directory
- scope:
path: "guides"
values:
layout: guide
sidebar: true
# A specific file
- scope:
path: "index.html"
values:
layout: home
Specificity rules: more specific scopes override less specific ones. A file-level front matter value always wins over any default.
Excluding and including files
By default, Jekyll excludes dotfiles, Gemfile, Gemfile.lock, node_modules, and a few others. Customise with:
# Exclude from build output
exclude:
- .sass-cache/
- .jekyll-cache/
- Gemfile
- Gemfile.lock
- node_modules/
- vendor/
- "*.sh"
- README.md
- package.json
- package-lock.json
- tools/
- CHANGELOG.md
# Include files that would otherwise be excluded
include:
- _redirects # Netlify/Cloudflare redirects file
- _headers # Cloudflare/Netlify headers file
- .htaccess # Apache config (dotfile, excluded by default)
Pagination
Using jekyll-paginate-v2:
pagination:
enabled: true
per_page: 12
permalink: "/page/:num/"
title: ":title - Page :num"
sort_field: "date"
sort_reverse: true
# Enable autopages for categories and tags
autopages:
enabled: true
categories:
enabled: true
permalink: "/category/:cat/"
layouts:
- "category.html"
tags:
enabled: false
SEO and analytics settings
Using jekyll-seo-tag:
# Used by jekyll-seo-tag
title: "JekyllHub"
tagline: "Find Your Perfect Jekyll Theme"
description: "Browse free and premium Jekyll themes for blogs, portfolios, and business sites."
url: "https://jekyllhub.com"
logo: /assets/images/logo.png
author:
name: Marcus Webb
email: marcus@jekyllhub.com
twitter: marcuswebb
twitter:
username: jekyllhub
card: summary_large_image
social:
name: JekyllHub
links:
- https://twitter.com/jekyllhub
- https://github.com/jekyllhub
# Analytics (custom keys — not built-in Jekyll)
google_analytics: "G-XXXXXXXXXX"
plausible_domain: "jekyllhub.com"
# Newsletter (custom)
sendy_url: "https://sendpress.org/s/subscribe"
sendy_list_id: "YOUR_LIST_ID"
Jekyll Feed settings
feed:
posts_limit: 20
excerpt_only: false
collections:
- posts
Sass/SCSS settings
sass:
sass_dir: _sass # where .scss partials live
style: compressed # compressed | expanded | nested | compact
load_paths:
- _sass
- node_modules # if using npm packages
Custom data available in all templates
Any key in _config.yml is available as site.keyname throughout all templates. Use this to store site-wide settings:
# Custom site settings
nav_links:
- title: Browse Themes
url: /themes/
- title: Blog
url: /blog/
- title: Showcase
url: /showcase/
social_links:
github: https://github.com/jekyllhub
twitter: https://twitter.com/jekyllhub
support_email: support@jekyllhub.com
theme_submission_url: /submit/
Access in templates:
{% for link in site.nav_links %}
<a href="{{ link.url }}">{{ link.title }}</a>
{% endfor %}
<a href="{{ site.social_links.github }}">GitHub</a>
Environment-specific configuration
Use multiple config files for different environments:
# _config.yml (base, committed to repo)
title: "JekyllHub"
url: "https://jekyllhub.com"
google_analytics: ""
# _config.development.yml (local overrides, not committed)
url: "http://localhost:4000"
google_analytics: ""
Run Jekyll with multiple configs — later files override earlier ones:
# Development
bundle exec jekyll serve --config _config.yml,_config.development.yml
# Production
JEKYLL_ENV=production bundle exec jekyll build
Serving options
Settings for jekyll serve:
# Local server
port: 4000
host: "127.0.0.1"
livereload: true # auto-refresh browser on changes
open_url: true # open browser automatically
# Show drafts during development
show_drafts: false # set to true or use --drafts flag
future: false # show posts with future dates
unpublished: false # show unpublished posts
Timezone
timezone: "Europe/London" # IANA timezone name
This affects how page.date is interpreted and how dates are formatted. Set it to your local timezone to avoid date-off-by-one issues.
Permalink structure
# For posts
permalink: /:categories/:year/:month/:day/:title/
# Common permalink styles:
permalink: pretty # /year/month/day/title/
permalink: date # /year/month/day/title.html
permalink: ordinal # /year/ordinal/title.html
permalink: weekdate # /year/week/short_day/title/
permalink: none # /title.html
A common setup for blogs:
permalink: /blog/:title/
This gives clean, category-free URLs like /blog/jekyll-front-matter-guide/.
Full example _config.yml
# ─── Site Identity ───────────────────────────────────────────────────────
title: "JekyllHub"
tagline: "Find Your Perfect Jekyll Theme"
description: "A marketplace for premium and free Jekyll themes — for blogs, portfolios, documentation, and business sites."
url: "https://jekyllhub.com"
baseurl: ""
email: hello@jekyllhub.com
author: "Marcus Webb"
logo: /assets/images/logo.png
# ─── Build ───────────────────────────────────────────────────────────────
timezone: "Europe/London"
markdown: kramdown
highlighter: rouge
permalink: /blog/:title/
kramdown:
input: GFM
syntax_highlighter: rouge
# ─── Plugins ─────────────────────────────────────────────────────────────
plugins:
- jekyll-feed
- jekyll-seo-tag
- jekyll-sitemap
- jekyll-paginate-v2
- jekyll-redirect-from
# ─── Collections ─────────────────────────────────────────────────────────
collections:
themes:
output: true
permalink: /themes/:name/
authors:
output: true
permalink: /authors/:name/
# ─── Defaults ────────────────────────────────────────────────────────────
defaults:
- scope:
path: ""
type: posts
values:
layout: post
toc: true
featured: false
- scope:
path: ""
type: pages
values:
layout: page
- scope:
path: ""
type: themes
values:
layout: theme
- scope:
path: ""
type: authors
values:
layout: author
# ─── Pagination ──────────────────────────────────────────────────────────
pagination:
enabled: true
per_page: 12
permalink: "/page/:num/"
sort_field: date
sort_reverse: true
# ─── Analytics ───────────────────────────────────────────────────────────
google_analytics: "G-XXXXXXXXXX"
# ─── Newsletter ──────────────────────────────────────────────────────────
sendy_url: "https://sendpress.org/s/subscribe"
sendy_list_id: ""
sendy_waitlist_id: ""
# ─── Exclude ─────────────────────────────────────────────────────────────
exclude:
- Gemfile
- Gemfile.lock
- node_modules/
- vendor/
- tools/
- package.json
- README.md
include:
- _redirects
- _headers
The _config.yml file is the control panel of your Jekyll site. Mastering it means being able to change almost any aspect of how Jekyll builds and serves your content without touching a single template file.
Validating your _config.yml
YAML is sensitive to syntax errors — a misplaced tab or incorrect indentation produces cryptic Jekyll errors. Two tools help:
yamllint — a command-line YAML linter:
pip install yamllint --break-system-packages
yamllint _config.yml
Online YAML validators — paste your config into yaml.org/start.html or any online YAML parser to check for syntax errors before running Jekyll.
The most common YAML mistakes in _config.yml:
Mixing tabs and spaces — YAML only allows spaces. If your editor inserts tabs when you press Tab, configure it to use spaces instead.
Forgetting quotes around values with special characters — a value containing :, #, {, }, or [ must be quoted:
description: "A marketplace for Jekyll themes: free & premium" # quotes required
tagline: Building with Jekyll # no special chars, quotes optional
Incorrect list syntax — list items need a dash and a space:
# Wrong
plugins:
-jekyll-feed
-jekyll-seo-tag
# Correct
plugins:
- jekyll-feed
- jekyll-seo-tag
Reading config values in JavaScript
Your _config.yml values are available in Liquid templates but not directly in JavaScript. To make config values available to client-side scripts, expose them via data attributes or a generated JSON file.
Data attribute approach — add to your layout’s <body> tag:
<body
data-baseurl="{{ site.baseurl }}"
data-title="{{ site.title | escape }}"
data-search-enabled="{{ site.data.settings.features.search }}">
In JavaScript:
const baseurl = document.body.dataset.baseurl;
const searchEnabled = document.body.dataset.searchEnabled === 'true';
Generated JSON approach — create site-config.json at your project root:
---
layout: null
---
{
"baseurl": {{ site.baseurl | jsonify }},
"title": {{ site.title | jsonify }},
"postsPerPage": {{ site.pagination.per_page | jsonify }}
}
Fetch this in JavaScript and use the values as needed. This approach works well for build-time config that complex JavaScript modules need to reference.
_config.yml for team sites
On sites with multiple contributors, _config.yml often becomes a source of merge conflicts. A few organisational practices reduce this:
Use _data/ files for frequently-changing values — navigation links, team member lists, feature flags. These files are separate from _config.yml and easier to update without touching the main config.
Keep _config.yml for build settings only — URL, plugins, collections, permalink structure. Things that rarely change. Custom display data lives in _data/.
Comment every non-obvious setting — future contributors (and future you) should understand why a particular setting exists:
# permalink uses :title only (not date) to keep URLs short and stable.
# Changing this would break all existing inbound links — do not change without setting up redirects.
permalink: /blog/:title/
Use a _config.defaults.yml for team documentation — a commented, example configuration file that explains every available setting. New contributors read this file to understand what can be configured and how.
The _config.yml file is the single most important piece of configuration in a Jekyll project. A well-organised, well-commented config file is a sign of a mature, maintainable site — and makes onboarding new contributors significantly easier.
Securing your _config.yml
_config.yml is committed to your Git repository and typically publicly visible on GitHub. This makes it the wrong place for sensitive values. The distinction is important:
Safe to commit: site URL, title, description, plugin names, permalink structure, navigation data, analytics property IDs (these are public by design — they are in your HTML anyway), any value that appears in your built site’s source.
Never commit: API keys with write access, webhook secrets, payment processor secrets, database credentials, SMTP passwords, admin tokens.
For values that need to be available during the build but should not be in your repository, use environment variables and access them in a Jekyll plugin or in your build script. Your CI/CD platform (GitHub Actions, Netlify, Cloudflare Pages) has a secrets/environment-variable system for exactly this purpose.
The test: if a value appears anywhere in _site/ after a build, it is effectively public regardless of whether it is in _config.yml or an environment variable. Keep truly secret values server-side and never embed them in static HTML.
_config.yml performance impact
A few config settings have meaningful build-time performance impact:
limit_posts — during development, set this to a small number (20-30) to skip building all posts. Your local server starts in seconds instead of minutes:
# In _config.development.yml — not in main _config.yml
limit_posts: 20
incremental: true — enables Jekyll’s incremental build feature. Only pages that have changed (or that include/extend changed files) are rebuilt. Can speed up builds significantly for large sites, but occasionally misses changes to layouts and includes. Use during active writing sessions, not for production builds:
incremental: true
profile: true — outputs a build profiling table showing which files take the most time to render. Useful for diagnosing slow builds:
bundle exec jekyll build --profile
strict_front_matter: true — makes Jekyll error on YAML front matter parsing failures rather than silently ignoring them. Recommended for production builds to catch typos in post front matter:
strict_front_matter: true
Understanding these options turns _config.yml from a configuration file you set up once and forget into an active tool for managing your development workflow and build quality. Every setting in the file is documented on jekyllrb.com/docs/configuration/ — worth reading end-to-end when you set up a new project.
Evolving your config as your site grows
A new Jekyll site might need thirty lines in _config.yml. A mature site with pagination, archives, SEO metadata, analytics, and multiple collections might need two hundred. That growth is natural and manageable — the key is keeping the file organised as it expands.
Group related settings with comments that explain intent, not just what the setting is. # Pagination is a useful section header. # Show 12 posts per page — increase if posts are short, decrease for image-heavy posts is a comment that saves future-you from having to re-derive the reasoning. Good config comments are a form of documentation, and the config file is often the first place a new collaborator looks when trying to understand how a Jekyll site is structured.
Use _config.yml as a single source of truth wherever possible. If your site name appears in the config, reference it in templates with site.title rather than hardcoding it in a layout. If your author name appears in the config, use site.author.name in your bio sections. Every piece of data in the config that you can reference rather than repeat is one less place to update when that data changes. Over time, this habit keeps your codebase clean and maintainable — a sign of a well-architected Jekyll site.
Validating your config
YAML is unforgiving of formatting errors — a misplaced tab or inconsistent indentation will cause Jekyll to fail silently or throw a confusing error. Before pushing a config change to production, validate your YAML using an online YAML validator or the ruby -e "require 'yaml'; YAML.load_file('_config.yml')" command in your terminal. This runs in under a second and saves the frustration of a failed production build caused by a subtle YAML syntax error.
When adding complex nested settings — like jekyll-paginate-v2 options or collections configurations with multiple keys — write them with consistent two-space indentation throughout. Jekyll’s YAML parser is strict about consistency, and mixing tabs and spaces is the single most common cause of _config.yml parse errors. Many editors can be configured to automatically convert tabs to spaces in YAML files, eliminating this class of error entirely.
Keep a local development config at _config_dev.yml for settings that differ between development and production — a development url of http://localhost:4000, disabled analytics, and verbose logging. Load it alongside your main config with bundle exec jekyll serve --config _config.yml,_config_dev.yml. The development config values override the main config for the same keys, so you never accidentally commit a localhost URL to your production config.