Jekyll SEO
How to optimize Jekyll sites for search — why its static HTML is crawler-friendly by default, plus the jekyll-seo-tag and jekyll-sitemap plugins, permalinks, collections, robots.txt, and the GitHub Pages plugin whitelist.
1 evidence signal on this page
- Linked source datadependency list
Jekyll outputs flat, static HTML — crawlers get complete content on the first fetch, with no JavaScript rendering delay. So the SEO work isn't fighting the framework; it's configuration. Install jekyll-seo-tag (title, description, canonical, Open Graph, Twitter Card, JSON-LD) and jekyll-sitemap (sitemap.xml), and set url: in _config.yml or both produce broken output. Pick clean permalinks (/:title/, not date-based) for evergreen content. Create robots.txt yourself — Jekyll won't. Watch two GitHub Pages traps: the plugin whitelist (only a fixed set runs without a GitHub Actions build) and baseurl misconfiguration on project sites, which breaks every canonical URL.
TL;DR — Jekyll builds your whole site into plain HTML files ahead of time, so Google sees your content on the first visit — no waiting for JavaScript. That’s a great starting point for SEO. The work is in the setup: install two plugins (
jekyll-seo-tagfor meta tags,jekyll-sitemapfor your sitemap), set your site URL in the config, pick clean URLs, and add arobots.txtyourself.
What Jekyll is
Jekyll is a static site generator — a tool that turns your content (written in
Markdown) and templates into finished HTML files before anyone visits. It’s
written in Ruby, and it’s the engine behind GitHub Pages, so if you’ve ever
pushed a project’s docs or a personal blog to a github.io address, you’ve
probably used Jekyll without thinking about it. Evidence for this claim Jekyll transforms source content and templates into a static website and is supported by GitHub Pages. Scope: Jekyll and GitHub Pages. Confidence: high · Verified: Jekyll documentation GitHub Pages: Jekyll
The important part for SEO: because Jekyll produces plain HTML files, a search engine gets your complete content the moment it fetches the page. There’s no JavaScript that has to run first. That removes one entire failure mode from the table — it doesn’t guarantee indexing or rankings by itself, since that still depends on your content, configuration, and the metadata covered below.
What Jekyll does not do for you
Here’s the catch beginners hit: Jekyll is crawler-friendly, but it isn’t “SEO-friendly” out of the box. A default Jekyll theme often ships with no title tags, meta descriptions, or structured data at all. You have to add those. The good news is two official plugins do almost all of it:
jekyll-seo-tag— adds your title tag, meta description, canonical URL, and social-sharing tags automatically.jekyll-sitemap— builds yoursitemap.xmlautomatically. Evidence for this claim The jekyll-seo-tag and jekyll-sitemap plugins generate SEO tags and sitemap files for Jekyll sites. Scope: Named Jekyll plugins. Confidence: high · Verified: Jekyll SEO Tag Jekyll Sitemap
The five things to get right
- Install
jekyll-seo-tagand put{% seo %}in your layout’s<head>. - Install
jekyll-sitemapso search engines get a list of your pages. - Set
url:in_config.ymlto your real domain — both plugins need it, or your canonical URLs and sitemap point atlocalhostand break. - Use a custom domain, not the free
username.github.io/repoaddress, for any site where ranking matters. - Add a
robots.txtyourself — Jekyll doesn’t create one.
The GitHub Pages surprise
If you host on GitHub Pages, you can’t just install any plugin you want. GitHub
runs Jekyll in a locked-down mode that only allows a short list of plugins.
Thankfully jekyll-seo-tag and jekyll-sitemap are both on that list — so the
essentials work. For anything fancier you’d need a different build setup (covered
in the Advanced tab).
Want the full version — the plugin whitelist, the baseurl bug that breaks
canonical URLs, permalinks, collections, and custom structured data? Switch to the
Advanced tab.
TL;DR — Jekyll emits flat static HTML at build time, so content is in the raw response on the first crawl — no Web Rendering Service, no Wave 2 delay. The SEO work is configuration, not architecture. Install
jekyll-seo-tag(title, description, canonical, OG, Twitter Card, JSON-LD) andjekyll-sitemap(sitemap.xml); both requireurl:in_config.ymlor they produce broken output. Use clean permalinks (/:title/) over date-based ones for evergreen content. Two GitHub Pages traps dominate: the plugin whitelist (GitHub builds with--safe; non-whitelisted plugins need a GitHub Actions build) and a missingbaseurlon project sites, which breaks every generated canonical URL. GitHub Pages also pins specific plugin versions (3.10.0 core, olderjekyll-seo-tag/jekyll-sitemapreleases), not just which plugins run. Collections needoutput: trueor they’re never rendered. Drafts, future-dated posts, andpublished: falsedocuments are excluded from a normal build, and--incrementalis experimental — don’t use it for production deploys. Androbots.txtis not auto-generated — make it yourself.
Why Jekyll’s static output is good for SEO
Jekyll is a static site generator: it runs your Markdown and Liquid templates through a build step and emits finished HTML, once, before any request. That timing is the whole SEO advantage. Evidence for this claim Jekyll processes text and templates into static files during a build. Scope: Jekyll build architecture. Confidence: high · Verified: Jekyll documentation
Google’s pipeline is crawl → render → index, and rendering JavaScript is “a separate step” that sits in a queue — what people loosely call the “two-wave” process. Wave 1 fetches raw HTML and indexes text and links immediately; Wave 2 queues the page for the Web Rendering Service to run JavaScript, “a few seconds to weeks” later depending on crawl budget. With Jekyll, Wave 1 already contains all your content — there’s no JavaScript required to render the body. Wave 1 = Wave 2. No rendering delay, no rendering budget spent. This is the same argument that makes any static site generator the lowest-risk architecture for indexability.
The downstream benefits follow:
- Faster TTFB. Pre-built files served from a CDN (GitHub Pages sits behind Fastly; Netlify/Vercel have their own edges) mean no database queries and no server processing — good for LCP and the rest of Core Web Vitals.
- No required JS payload for content → better FCP and LCP than a hydrating SPA.
- Proper HTTP status codes at the CDN layer, not client-side error handling.
But none of that changes the fundamentals: static sites still need meta tags, sitemaps, canonical URLs, structured data, and good content. Jekyll’s output is crawler-friendly; the metadata is on you.
GitHub Pages and Jekyll SEO
The most common Jekyll deployment is GitHub Pages — push to a branch with Pages enabled and GitHub auto-builds the site. That convenience comes with SEO-relevant constraints.
Project sites vs. user/org sites — the URL decision
username.github.io/repo-name(a project site) lives on a shared subdomain with thousands of other unrelated sites. It works fine for docs, demos, and personal projects where the URL itself doesn’t need to carry your brand.- A custom domain (point a
CNAMEat GitHub Pages) puts the site on a URL you control, with HTTPS auto-provisioned by GitHub. For a site where the domain itself matters — a business site, a blog you’re building an audience around — a custom domain from the start avoids a domain migration later.
If you expect to move to a custom domain eventually, set it up early. Any
domain migration means 301 redirects (jekyll-redirect-from handles this) and a
period where inbound links and any accumulated signals point at the old URL —
avoidable churn if you pick the final domain up front rather than switching
later. This is a migration-planning argument, not a claim that a github.io
subdomain is penalized on its own.
baseurl — the #1 canonical bug on project sites
Project sites live under a subfolder (/repo-name/). If you don’t set
baseurl: /repo-name in _config.yml, every canonical URL jekyll-seo-tag
generates — and your internal links — will be wrong, missing the subfolder. This is
the single most common canonical-URL bug on Jekyll project sites. (User/org sites
served at the domain root don’t need baseurl.)
The plugin whitelist
GitHub Pages runs Jekyll with the --safe flag and only allows a fixed set of
plugins. Evidence for this claim GitHub Pages builds Jekyll in safe mode and supports a documented set of plugins. Scope: GitHub Pages hosted builds. Confidence: high · Verified: GitHub Pages: Jekyll plugins The SEO-relevant whitelisted ones:
jekyll-seo-tag✓jekyll-sitemap✓jekyll-redirect-from✓ (for 301s when you change URLs)jekyll-paginate✓
Not whitelisted (and silently won’t run, or will error):
jekyll-last-modified-at— needed for accurate sitemap<lastmod>from file timestamps- Custom structured-data plugins
- Anything dropped in a
_plugins/folder
It’s not just which plugins — it’s which versions
GitHub Pages doesn’t only restrict which plugins run; it pins the exact version
of each one, and that build environment itself is pinned to an older Jekyll.
As of the dependency list’s last
update, the hosted build runs Jekyll 3.10.0 with jekyll-seo-tag 2.8.0,
jekyll-sitemap 1.4.0, and jekyll-feed 0.17.0 — while jekyllrb.com’s own
docs describe the current upstream release, 4.4.1. A behavior documented in a
plugin’s latest README isn’t guaranteed to exist in the version GitHub Pages
actually runs; check pages.github.com/versions.json for the pinned version
before relying on a specific flag or output. A GitHub Actions build sidesteps
this too — you control the Gemfile, so you get the versions you pin, not
GitHub’s.
The GitHub Actions workaround
The fix for the whitelist isn’t “add more gems” — it’s to stop letting GitHub do
the build. Run Jekyll yourself in CI (actions/jekyll-build-pages, or build
locally and push _site/ to the deploy branch with peaceiris/actions-gh-pages)
and the whitelist no longer applies. Now any plugin runs, and you pick the
Jekyll and plugin versions instead of inheriting GitHub’s pinned set.
The jekyll-seo-tag plugin
This is the official, maintained plugin that covers most of the core metadata
Jekyll doesn’t add on its own. Exactly what it emits depends on which version
you have installed, your _config.yml and front matter, and whether your
layout actually calls {% seo %} — GitHub Pages, for instance, pins a specific
version rather than always shipping the latest release (more on that below).
Check the advanced usage guide
for your version’s exact output, and confirm what actually landed by grepping
your own built HTML rather than assuming it’s complete.
Install:
# Gemfile
gem 'jekyll-seo-tag'# _config.yml
plugins:
- jekyll-seo-tag<!-- _layouts/default.html, before </head> -->
{% seo %}What it generates automatically:
<title>— page title with site name appended (Page Title | Site Name)<meta name="description">— fromdescription:front matter or the site description<link rel="canonical">— built fromsite.url+page.url- Open Graph tags (
og:title,og:description,og:url,og:site_name,og:image) - Twitter Card tags (
twitter:card,twitter:title,twitter:description,twitter:creator,twitter:image) - JSON-LD structured data (
BlogPostingfor posts,WebSitefor the home page) - Pagination meta (next/prev URLs)
Required _config.yml settings — without these the plugin produces broken
output:
title: Your Site Title
description: Your site description
url: "https://yourdomain.com" # CRITICAL — drives canonical URL generation
author:
name: Patrick Stox
twitter: patrickstox
url: https://patrickstox.com # author disambiguation
twitter:
username: patrickstox
card: summary_large_imagePer-page front matter overrides:
---
title: "Jekyll SEO Guide"
description: "How to optimize Jekyll sites for search engines."
image:
path: /assets/jekyll-seo-og.png
width: 1200
height: 630
alt: "Jekyll SEO diagram"
canonical_url: "https://example.com/jekyll-seo/" # override if needed
robots: noindex # per-page noindex
seo:
type: BlogPosting # schema.org type override
date_modified: 2025-01-15 # dateModified override for JSON-LD
---Suppression (when your layout already outputs its own):
{% seo title=false %} <!-- suppress the <title> -->
{% seo canonical=false %} <!-- suppress the canonical link -->One gotcha worth flagging: many minimal themes (including minima) ship without
jekyll-seo-tag wired in. Adding the gem to your Gemfile does nothing unless the
theme’s layout actually calls {% seo %}.
Sitemaps with jekyll-sitemap
Same install pattern (Gemfile + _config.yml). It generates a sitemaps.org-
compliant sitemap.xml at /sitemap.xml on every build.
It requires url: in _config.yml — without it, sitemap entries have no
domain.
Controlling <lastmod>, in priority order:
last_modified_at:in front matter (best — explicit control)- Post creation date (fallback — often wrong for evergreen content updated later)
- Filesystem modification date (needs the non-whitelisted
jekyll-last-modified-at)
Best practice: add last_modified_at: YYYY-MM-DD to every post, and bump it
when you update content — that’s the freshness signal recrawl prioritization leans
on.
Excluding pages:
# Per-page front matter
sitemap: false
# Global pattern (in _config.yml)
defaults:
- scope:
path: "assets/**/*.pdf"
values:
sitemap: falsePermalink configuration
Permalinks are set globally in _config.yml or overridden per page in front matter.
| Style | Pattern | SEO notes |
|---|---|---|
date (default) | /:categories/:year/:month/:day/:title.html | Date-heavy, fragile if the post date changes |
pretty | /:categories/:year/:month/:day/:title/ | Trailing slash, no .html |
none | /:categories/:title.html | No date burial |
| Custom | /:title/ or /:categories/:title/ | Most control — recommended for evergreen content |
Recommended for most sites:
permalink: /:title/
# or
permalink: /:categories/:title/Why avoid date-based URLs for evergreen posts:
- Changing a post’s
date:front matter changes its URL → inbound links break. - Deep hierarchy (
/2019/03/14/post-title/) buries content for no reason. - (For news and journalism, date URLs are fine and expected — this is a per-context call, not a universal rule.)
Collections need their own permalink config:
collections:
case_studies:
output: true
permalink: /case-studies/:name/A warning on changing patterns: once URLs are indexed, switching permalink
styles requires 301 redirects (use jekyll-redirect-from). Skip the redirects and
you break link equity and generate 404s in Search Console.
Collections and SEO
Collections are Jekyll’s custom content types beyond posts and pages — docs sections, portfolio items, team members, case studies, FAQs. Two requirements make or break their SEO:
-
output: truemust be set. Without it, collection documents are never rendered as individual HTML files and therefore can’t be indexed. This is a silent indexing killer — the content exists in your repo but never becomes a crawlable page.collections: docs: output: true # REQUIRED for indexable pages permalink: /docs/:name/ -
Each document needs front matter — even an empty
---block. Without it, Jekyll treats the file as a binary static file: no Liquid processing, no metadata, nojekyll-seo-tagintegration.
Note that collections aren’t included in RSS feeds (only posts are). For large documentation sites, collections are usually the right structure anyway.
Publishing states and incremental builds
Jekyll has several independent switches that control what actually ends up in the built site. Mixing them up produces either pages that silently never get built, or draft/future content that ships to production by accident:
- Drafts (
_drafts/) are excluded from a normal build entirely. They only appear when you runbundle exec jekyll serve --drafts(orbuild --drafts) locally — moving a post into_posts/with a real date is what actually publishes it. - Future-dated posts (a
date:later than the current time) are excluded by default.future: truein_config.yml, or the--futureflag, includes them — handy for local preview, but leaving it on in a production config means scheduled posts go live the moment you build, not on their intended date. published: falsein front matter excludes a document from the build regardless of its date — a separate switch from drafts and future posts, and easy to leave set after testing a page you meant to ship.- Incremental regeneration (
--incremental) is documented by Jekyll as experimental, and it tracks a limited dependency graph — mainly includes and layouts. A page that iteratessite.postsor other collection data (a tag index, an archive, related-posts logic) can go stale under--incrementalwithout Jekyll detecting that the underlying posts changed. Don’t run it as part of a production deploy; use a fullbundle exec jekyll buildfor anything you’re pushing live.
Before trusting any of these, build without the flags you use locally
(--drafts, --future, --incremental) and confirm the output in _site/
matches what you intend to publish — the safest default for a deploy pipeline
is a clean, full, non-incremental build.
Custom head partials with Liquid
When jekyll-seo-tag isn’t enough, you build your own _includes/head.html:
<head>
<meta charset="UTF-8">
<title>
{% if page.title %}{{ page.title }} | {{ site.title }}
{% else %}{{ site.title }}{% endif %}
</title>
<meta name="description" content="
{%- if page.description -%}{{ page.description }}
{%- elsif page.excerpt -%}{{ page.excerpt | strip_html | strip_newlines }}
{%- else -%}{{ site.description }}
{%- endif -%}">
<link rel="canonical" href="{{ page.url | prepend: site.url }}">
{% seo %}
</head>Key Liquid filters for SEO:
| strip_html— removes tags from an auto-excerpt (essential for clean descriptions)| strip_newlines— removes line breaks from excerpts| truncate: 160— an optional template-side character cap, not a Google limit. Prefer an authored, page-specific description and preview the rendered snippet; fit varies by query, device, language, and script.| prepend: site.url— builds absolute URLs for canonical and OG tags| date_to_xmlschema— ISO 8601 dates for JSON-LDdatePublished/dateModified| default: fallback— fallback when a variable is nil
Custom JSON-LD beyond what jekyll-seo-tag emits:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": {{ page.title | jsonify }},
"datePublished": "{{ page.date | date_to_xmlschema }}",
"dateModified": "{{ page.last_modified_at | default: page.date | date_to_xmlschema }}",
"author": {
"@type": "Person",
"name": "{{ page.author.name | default: site.author.name }}",
"url": "{{ page.author.url | default: site.author.url }}"
}
}
</script>robots.txt is not auto-generated
Jekyll does not create robots.txt. You make it yourself in the site root.
Include an empty front-matter block so Liquid processes the file (so {{ site.url }}
resolves):
---
---
User-agent: *
Allow: /
Sitemap: {{ site.url }}/sitemap.xmlWithout this, there’s no sitemap reference in robots.txt — and some crawlers use
that as a discovery mechanism. (robots.txt controls crawling, not indexing —
for the full picture see crawling.)
Common Jekyll SEO myths
“Jekyll handles SEO automatically.” The static output is crawler-friendly, but metadata requires explicit setup. Default themes often ship with no SEO tags at all.
“GitHub Pages is fine for SEO — it’s free.” GitHub Pages itself is fine — the
static output is crawler-friendly regardless of host. The catches are the plugin
whitelist (and the specific versions it pins, not just which plugins) and,
if your brand depends on the URL, deciding on a custom domain before you build
an audience on github.io.
“Static sites don’t need sitemaps.” Google can find pages via links, but a
sitemap speeds discovery and carries <lastmod> signals. jekyll-sitemap makes it
trivial.
“Jekyll is dead.” It’s in mature maintenance mode — v4.4.1 shipped in January 2025. Minimal new features, but maintained and secure, and GitHub Pages will support it indefinitely. For simple blogs and docs, it’s a solid, boring choice.
“I can use any plugin on GitHub Pages.” No — --safe plus a fixed whitelist.
The fix is GitHub Actions, not more gems.
Where this fits
Jekyll is one of the six generators in the static site generators cluster — the GitHub Pages default and the one most developers meet first. For the broader context on how Google renders JavaScript and when you actually need it, see the JavaScript SEO hub. The build-freshness discipline that applies to every SSG — a static site is only as current as its last build — applies to Jekyll too: edits don’t reach search engines until you rebuild and redeploy.
AI summary
A condensed take on the Advanced version:
- Jekyll is a Ruby static site generator and the engine behind GitHub Pages. It emits flat HTML at build time, so content is in the raw response on the first crawl — no JavaScript rendering step, no Wave 2 delay. That’s the core SEO advantage, shared by all static site generators.
- The SEO work is configuration, not architecture. Default themes often have no SEO metadata at all.
jekyll-seo-tag(official plugin) auto-generates title, description, canonical, Open Graph, Twitter Card, and JSON-LD (BlogPosting/WebSite). Add{% seo %}to your layout’s<head>. Overridable per page in front matter.jekyll-sitemapauto-generatessitemap.xml. Both plugins requireurl:in_config.ymlor output is broken (canonical/sitemap point at localhost).- Permalinks: prefer
/:title/or/:categories/:title/over date-based URLs for evergreen content (date URLs break if the post date changes). - Collections need
output: true(or they’re never rendered as pages) and front matter on each document (or no metadata processing). - Publishing states: drafts (
_drafts/), future-dated posts, andpublished: falsedocuments are all excluded from a normal build unless you explicitly opt in (--drafts,--future).--incrementalis experimental and can miss pages that depend onsite.posts— don’t use it for a production deploy. robots.txtis NOT auto-generated — create it manually (with empty front matter so Liquid resolves{{ site.url }}in theSitemap:line).- Two GitHub Pages traps: the plugin whitelist (
--safebuild; onlyjekyll-seo-tag,jekyll-sitemap,jekyll-redirect-from,jekyll-paginateamong SEO plugins — others need a GitHub Actions build), and missingbaseurlon project sites, which breaks every canonical URL. GitHub Pages also pins specific plugin versions (e.g. Jekyll 3.10.0 core), which can trail the latest upstream release. - If a custom domain matters to your brand, set it up early rather than on
username.github.io/repo— moving domains later means 301s and a period where inbound links point at the old URL. GitHub Pages also pins specific plugin versions (not just which plugins), which can trail the latest upstream release. - Build freshness applies as to any SSG: changes only reach search after a rebuild + redeploy.
Official documentation
Primary-source documentation — from Jekyll, its plugins, GitHub Pages, and the search engines.
Jekyll
- Jekyll docs — the documentation hub.
- Permalinks — the built-in styles and custom patterns.
- Front Matter — the YAML block that drives per-page metadata.
- Collections — custom content types and the
output: truerequirement. - Plugins — how Jekyll’s plugin system works.
- Variables — the
site/pagevariables and Liquid filters used in head partials.
Official plugins
- jekyll-seo-tag — the metadata plugin, plus its advanced usage guide.
- jekyll-sitemap — automatic
sitemap.xml. - jekyll-redirect-from — 301 redirects when you change URLs.
GitHub Pages
- About GitHub Pages and Jekyll — how the auto-build works.
- About custom domains and GitHub Pages — the custom-domain setup.
- pages.github.com/versions.json — the live plugin whitelist and locked dependency versions.
- Jekyll releases and endoflife.date/jekyll — version and maintenance status.
Search engines
- JavaScript SEO basics — the crawl → render → index phases (two-wave rendering) that a static build lets you skip for content.
- In-Depth Guide to How Google Search Works — where rendering sits in the pipeline.
- Core Web Vitals (web.dev) — the performance metrics a fast static site benefits from structurally.
Quotes from the source
There are no on-record quotes from Google or Bing reps specifically about Jekyll. Google doesn’t comment on individual static site generators. The SEO case for Jekyll rests on Google’s general rendering and static-HTML guidance, which applies to any static site.
Google — rendering is a separate, queued step (the thing Jekyll’s static output removes from the critical path)
- “During the crawl, Google renders the page and runs any JavaScript it finds using a recent version of Chrome.” — Google Search Central docs. With Jekyll, there’s no JavaScript needed to render the body, so this step costs you nothing. Jump to quote
- “Rendering is important because websites often rely on JavaScript to bring content to the page, and without rendering Google might not see that content.” — i.e., if your content is already in the static HTML (as it is with Jekyll), rendering can’t cost you visibility. Jump to quote
Jekyll SEO checklist
A scannable pass to confirm your Jekyll site is set up to rank:
-
url:is set in_config.ymlto your real production domain (notlocalhost:4000). - For a GitHub Pages project site,
baseurl: /repo-nameis set (skip on user/org root sites). -
jekyll-seo-tagis installed and the theme layout actually calls{% seo %}in the<head>. -
jekyll-sitemapis installed and/sitemap.xmlis generating. - Sitemap is submitted in Google Search Console and Bing Webmaster Tools.
-
title:,description:, andauthor:are set in_config.yml. - Each post/page has an explicit
description:(don’t rely on auto-excerpt). -
last_modified_at:is on posts and bumped when content changes. - Permalinks use
/:title/or/:categories/:title/for evergreen content (not the date default). - A
robots.txtexists (with empty front matter) and references the sitemap. - Every collection that should be indexed has
output: true. - Every collection document has front matter (even an empty
---). - A custom domain is configured (HTTPS auto-provisioned), not the bare
github.iosubdomain. - URL/permalink changes are paired with
jekyll-redirect-from301s. - Images are compressed before commit (Jekyll doesn’t optimize them at build).
- One trailing-slash convention is enforced via the permalink style (no
/page/vs/pageduplicates). - Production builds run without
--draftsor--future, and no page you intend to ship still haspublished: false. - Production deploys use a full
bundle exec jekyll build, not--incremental(experimental, and can misssite.posts-dependent pages).
Jekyll SEO — cheat sheet
The two essential plugins
| Plugin | Does | Whitelisted on GitHub Pages? | Needs |
|---|---|---|---|
jekyll-seo-tag | Title, description, canonical, OG, Twitter Card, JSON-LD | Yes ✓ | url:, {% seo %} in <head> |
jekyll-sitemap | Auto sitemap.xml | Yes ✓ | url: |
jekyll-redirect-from | 301 redirects | Yes ✓ | redirect_from: in front matter |
jekyll-last-modified-at | Filesystem <lastmod> | No ✗ | GitHub Actions build |
Required _config.yml keys (or output breaks)
| Key | Why |
|---|---|
url: | Canonical URLs + sitemap entries; without it → localhost |
baseurl: | Project sites under /repo/ — without it → broken canonicals |
title: / description: | Defaults for jekyll-seo-tag |
author: | Author info in JSON-LD |
Permalink styles
permalink: value | Output | Use for |
|---|---|---|
date (default) | /cat/2019/03/14/title.html | Avoid for evergreen |
pretty | /cat/2019/03/14/title/ | Date-relevant content |
none | /cat/title.html | No date burial |
/:title/ | /title/ | Most evergreen sites |
Fast facts
- Jekyll output is static HTML → no Wave 2 rendering delay; content in the first fetch.
robots.txtis NOT auto-generated — create it (with empty front matter).- Collections need
output: trueand front matter, or they don’t index. - GitHub Pages builds with
--safe+ a plugin whitelist — non-whitelisted plugins need a GitHub Actions build. - Current upstream Jekyll: v4.4.1 (Jan 2025); GitHub Pages runs a locked
v3.10.0 with its own pinned plugin versions (
jekyll-seo-tag2.8.0,jekyll-sitemap1.4.0,jekyll-feed0.17.0) — check pages.github.com/versions.json for the current list. - If a custom domain matters to your brand, set it up before building an
audience on
username.github.io/repo— avoids a later domain migration.
Build and check your Jekyll site locally
Before trusting production, build locally and verify your metadata is actually in the HTML.
macOS / Linux
# Install dependencies and serve locally (defaults to http://localhost:4000)
bundle install
bundle exec jekyll serve
# Build the production site into _site/ (override the dev url)
JEKYLL_ENV=production bundle exec jekyll build
# Confirm jekyll-seo-tag actually emitted a canonical + title into a built page
grep -i 'rel="canonical"' _site/index.html
grep -i "<title>" _site/index.html
# Confirm the sitemap was generated and points at your real domain (not localhost)
grep -i "<loc>" _site/sitemap.xml | headWindows (PowerShell)
# Install dependencies and serve locally
bundle install
bundle exec jekyll serve
# Build the production site (set the env var for this command)
$env:JEKYLL_ENV="production"; bundle exec jekyll build
# Confirm canonical + title made it into the built HTML
Select-String -Path _site\index.html -Pattern 'rel="canonical"'
Select-String -Path _site\index.html -Pattern "<title>"
# Confirm the sitemap was generated with your real domain
Select-String -Path _site\sitemap.xml -Pattern "<loc>" | Select-Object -First 10If <loc> entries or the canonical show localhost:4000, your url: isn’t set (or
JEKYLL_ENV=production wasn’t applied) — fix _config.yml before deploying.
A starter robots.txt for Jekyll
Save this as robots.txt in your site root. The empty front-matter block is what
makes Liquid process the file so {{ site.url }} resolves:
---
---
User-agent: *
Allow: /
Sitemap: {{ site.url }}/sitemap.xml Patrick's relevant free tools
- SEO Incident Simulator — Practice thirty deterministic technical SEO incident investigations — indexability, crawl controls, redirects, sitemaps, markup, caching, DNS, bot verification, rendering, hreflang, and faceted navigation — with clearly labeled fixture evidence and Find → Fix → Verify handoffs.
- Raw vs. Rendered HTML Checker — See what's in your page's initial HTML versus after JavaScript runs — headless-Chrome rendering only when the page actually needs it, a rendering-strategy verdict (SSR / prerendered / CSR / hybrid), ~15 calibrated JavaScript-SEO checks (noindex, canonicals, robots.txt blocking, links, soft 404s), a side-by-side raw-vs-rendered diff, and shareable reports.
Tools for Jekyll SEO
jekyll-seo-tag— the official metadata plugin (title, description, canonical, OG, Twitter Card, JSON-LD). The single biggest SEO win for a Jekyll site.jekyll-sitemap— official automaticsitemap.xml.jekyll-redirect-from— generate 301 redirects in front matter when you change permalinks.- GitHub Actions (
actions/jekyll-build-pages,peaceiris/actions-gh-pages) — build Jekyll yourself to escape the GitHub Pages plugin whitelist and run any plugin. - Google Search Console — verify the site, submit the sitemap, and watch indexing/coverage. Essential for a new Jekyll site.
- Bing Webmaster Tools — submit the sitemap (you can import credentials from GSC).
- View Source / URL Inspection — confirm your title, meta description, and canonical are in the raw HTML (they should be, since Jekyll is static).
- PageSpeed Insights / web.dev — check Core Web Vitals; a static Jekyll site should score well, but unoptimized images are the common drag.
Jekyll SEO mistakes to avoid
Concrete mistakes people actually make setting up Jekyll for search — prevention, not diagnosis.
Leaving url: unset in _config.yml
Both jekyll-seo-tag and jekyll-sitemap build absolute URLs from site.url.
Skip it and your canonical tags and sitemap <loc> entries either point at
localhost:4000 (the dev default) or come out with no domain at all — that’s
canonical URLs and a sitemap that actively mislead crawlers instead of helping
them. Do this instead: set url: to your real production domain in
_config.yml before the first deploy, and grep the built _site/ output for
localhost to confirm it didn’t leak through.
Skipping baseurl on a GitHub Pages project site
username.github.io/repo-name sites live under a /repo-name/ subfolder. If
baseurl: /repo-name isn’t set, every canonical URL jekyll-seo-tag generates —
and your internal links — is missing that subfolder, pointing at a URL that
doesn’t exist. This is the single most common canonical-URL bug on Jekyll project
sites. Do this instead: set baseurl: on project sites (user/org sites at
the domain root don’t need it), and spot-check a built page’s canonical against
its real, live URL.
Assuming a collection is indexable without output: true
Collections (docs sections, case studies, team bios) are Jekyll’s custom content
type, but without output: true in _config.yml, the documents are never
rendered as individual HTML files — the content sits in your repo and simply
never becomes a crawlable page. There’s no error, no warning, just pages that
never exist. Do this instead: set output: true on every collection meant
to be public, and after a build, confirm the expected HTML files actually landed
in _site/.
Using date-based permalinks for evergreen content
The Jekyll default permalink style (/:categories/:year/:month/:day/:title.html)
bakes the publish date into the URL. Change the post’s date: front matter later
— which people do, often just to bump it for a re-publish — and the URL changes
with it, breaking every inbound link and forcing a redirect cleanup you didn’t
plan for. Do this instead: use /:title/ or /:categories/:title/ for
evergreen posts; reserve date-based permalinks for genuinely date-relevant
content like news, where the date belongs in the URL.
Trying to install a non-whitelisted plugin on GitHub Pages and expecting it to run
GitHub builds Jekyll with the --safe flag, which silently skips or errors on
any plugin outside a fixed whitelist — dropping a gem like
jekyll-last-modified-at or a custom structured-data plugin into _plugins/
and pushing it does not make it run on the default GitHub Pages build. Do this
instead: check the plugin against
pages.github.com/versions.json first;
if it’s not listed, build with GitHub Actions (actions/jekyll-build-pages or a
local build pushed via peaceiris/actions-gh-pages) instead of fighting the
whitelist.
Assuming Jekyll ships a robots.txt
Jekyll does not generate robots.txt — there’s no plugin toggle, no default
file, nothing. A site with no robots.txt at all isn’t broken for indexing, but
it also has no Sitemap: reference for crawlers that use robots.txt as a
discovery mechanism. Do this instead: create robots.txt yourself in the
site root with an empty front-matter block (--- ---) so Liquid processes it
and {{ site.url }} resolves in the Sitemap: line.
Test yourself: Jekyll SEO
Five quick questions on optimizing Jekyll sites for search. Pick an answer for each, then check.
Resources worth your time
My writing
- JavaScript SEO: A Definitive Guide — rendering, DOM parity, and why static/prerendered output (like Jekyll’s) is the low-risk end of the spectrum.
- The Beginner’s Guide to Technical SEO — where rendering architecture and sitemaps fit in the bigger picture.
My speaking
- How Search Works (SlideShare) — my walkthrough of crawling, rendering, indexing, and ranking. (My standing disclaimer applies: “This is my understanding of systems… not going to be 100% complete or accurate.”)
From around the industry
- Jekyll docs — the official documentation, including permalinks and collections.
- jekyll-seo-tag — the official metadata plugin and its advanced usage guide.
- jekyll-sitemap — the official sitemap plugin.
- About GitHub Pages and Jekyll — GitHub’s own docs on the build process and constraints.
- pages.github.com/versions.json — the authoritative GitHub Pages plugin whitelist and locked versions.
- Google Search Central — JavaScript SEO basics — the rendering phases a static build lets you skip.
- CloudCannon — jekyll-seo-tag showcase — a practitioner walkthrough of the plugin.
Jekyll SEO
Jekyll SEO is the set of technical and on-page practices for optimizing sites built with Jekyll — the Ruby static site generator that powers GitHub Pages. Jekyll outputs flat HTML with no rendering delay, so the SEO work is configuration: jekyll-seo-tag for metadata, jekyll-sitemap for sitemaps, clean permalinks, and a manual robots.txt.
Related: JavaScript SEO
Jekyll SEO
Jekyll is the Ruby-based static site generator originally created by GitHub co-founder Tom Preston-Werner, and it’s the engine behind GitHub Pages. It turns Markdown/HTML plus Liquid templates into a folder of finished, flat HTML files at build time — so a crawler receives complete, rendered content on the first fetch, with no JavaScript rendering step and no “Wave 2” delay. That static output is the foundation of Jekyll’s SEO advantage; it’s the same argument that makes any static site generator the lowest-risk architecture for indexability.
Because the output is already crawler-friendly, Jekyll SEO is mostly about configuration, not the framework. The core moving parts:
jekyll-seo-tag— the official plugin that auto-generates the title tag, meta description, canonical URL, Open Graph and Twitter Card tags, and JSON-LD structured data from your front matter and_config.yml. Drop{% seo %}in your layout’s<head>.jekyll-sitemap— auto-generates a sitemaps.org-compliantsitemap.xml. Both plugins needurl:set in_config.ymlor their output is broken.- Permalinks — avoid the default date-based URLs for evergreen content; prefer
/:title/or/:categories/:title/. robots.txt— not auto-generated; you create it manually as a static file.
The two things that trip people up are the GitHub Pages plugin whitelist (only a fixed set of plugins runs in GitHub’s --safe build; everything else needs a GitHub Actions workaround) and baseurl misconfiguration on project sites, which silently breaks every canonical URL.
Related: JavaScript SEO
Build-time retrieval analysis plus live signals for this exact article. The automatic chunk report includes a deterministic readiness score and is ready without a model download.
Search Console
sampleGA4 traffic (28d)
sampleCloudflare traffic (7d)
sampledCrUX field data (28d, phone)
sampleGoogle NLP entities
localChangelog
Revision history
Compare the published article with an archived editorial snapshot. Added and removed words are shown only after you open a comparison.
Updated Jul 27, 2026.
Editorial summary and recorded change details.Summary
Clarified that a 160-character Liquid truncation is a template choice, not a Google meta-description limit.
Change details
-
The custom-head example no longer hard-truncates excerpts, and its filter guidance now recommends authored descriptions plus locale-aware rendered previews.
Updated Jul 18, 2026.
Editorial summary and recorded change details.Summary
Distinguished upstream Jekyll/plugin versions from GitHub Pages' pinned build, added a publishing-states section (drafts, future posts, published: false, incremental regeneration), and removed unsupported percentage/authority claims (jekyll-seo-tag's coverage, github.io link-equity) in favor of version-specific and migration-focused framing.
Change details
-
Added a Publishing states and incremental builds section covering _drafts/, future-dated posts, published: false, and Jekyll's experimental --incremental regeneration and its site.posts staleness risk.
-
Noted GitHub Pages' pinned plugin versions (Jekyll 3.10.0, jekyll-seo-tag 2.8.0, jekyll-sitemap 1.4.0, jekyll-feed 0.17.0) versus upstream Jekyll 4.4.1, and pointed to pages.github.com/versions.json as the source of truth.
-
Removed the unsupported 'jekyll-seo-tag covers roughly 90%' figure and the 'github.io carries no brand authority / doesn't transfer link equity' claims, replacing them with version-check and migration-planning guidance.
Full comparison unavailable — no prior snapshot was archived for this revision.