Jekyll Blog SEO Checklist for GitHub Pages
A practical Jekyll blog SEO checklist for GitHub Pages: url and baseurl, permalinks, jekyll-seo-tag, jekyll-sitemap, post filenames and future dates, redirects, robots.txt, and a post-deploy curl check.
Blog on Jekyll? See how Blogger Dog publishes into Jekyll.
Why a Jekyll blog on GitHub Pages can still be invisible
Jekyll is one of the easiest ways to put a blog on the web. Push Markdown to a repo, GitHub Pages builds it, and you get fast static HTML on a free host. Nothing runs in the browser, so search engines see the whole page on the first request.
The trouble is that Jekyll fails quietly. A post named my-post.md instead of 2026-11-03-my-post.md is never built, and nothing tells you. A post dated a few hours ahead of the build machine's clock is skipped. A plugin listed in your Gemfile but not in _config.yml does nothing on GitHub Pages. A missing url setting leaves the sitemap and canonical tags pointing at the wrong host. None of these break the build, so the site looks fine to you and wrong to Google.
This checklist covers the setup we would want on any Jekyll blog that is meant to bring in search traffic, with GitHub Pages as the default host. Settings are checked against the Jekyll docs (v4.4.1 at the time of writing) and the GitHub Pages dependency versions, which currently pin Jekyll 3.10.0, jekyll-seo-tag 2.8.0, jekyll-sitemap 1.4.0 and jekyll-redirect-from 0.16.0 for the built-in Pages build. If you run another stack, the Hugo blog SEO checklist, the Astro blog SEO checklist, the Next.js blog SEO checklist, the Gatsby blog SEO checklist and the Docusaurus blog SEO checklist cover the same ground for those tools. If the blog is hosted on GitLab Pages, the GitLab Pages blog SEO checklist covers the hosting side: the Pages job, custom domains and redirects.
1. Know which Jekyll actually builds your site
There are two ways a GitHub Pages site gets built, and they behave differently:
- The classic Pages build ("Deploy from a branch"). GitHub builds the site with the versions on the dependency versions list, which means Jekyll 3.10, not 4.x. The GitHub Pages and Jekyll docs say Pages "cannot build sites using unsupported plugins", and some plugins are always on (including
jekyll-optional-front-matter,jekyll-titles-from-headingsandjekyll-relative-links). - A GitHub Actions workflow that runs
bundle exec jekyll buildwith your ownGemfile.lock. You get Jekyll 4, any plugin you like, and the same build locally and in CI.
Either is fine for SEO. What matters is that you test locally with the same versions production uses. If you are on the classic build, put gem "github-pages", group: :jekyll_plugins in your Gemfile so bundle exec jekyll serve matches GitHub. If you need Jekyll 4 or an unsupported plugin, switch the Pages source to GitHub Actions rather than fighting the allowlist.
2. Set url and baseurl correctly
Jekyll builds every absolute URL from two settings: url (protocol and host) and baseurl (the path the site lives under). The jekyll-sitemap README is explicit that it needs url to be "the base hostname & protocol for your site", and jekyll-seo-tag uses it for canonical and Open Graph URLs.
# _config.yml
title: Example Engineering Blog
description: Notes on building and running Example.
url: "https://example.com" # no trailing slash
baseurl: "" # "" for a custom domain or user.github.io
permalink: /blog/:title/
timezone: America/Chicago
plugins:
- jekyll-seo-tag
- jekyll-sitemap
- jekyll-feed
- jekyll-redirect-from
The common GitHub Pages trap is the project site. A repo named docs-site under acme is served at https://acme.github.io/docs-site/, so url is https://acme.github.io and baseurl is /docs-site. Leave baseurl empty there and every stylesheet, image and internal link points one level too high. Once a custom domain is set up, baseurl goes back to "".
Inside templates, never hard-code paths. Use the relative_url and absolute_url filters so both settings are respected:
<a href="{{ '/blog/' | relative_url }}">Blog</a>
<meta property="og:image" content="{{ '/assets/og-default.png' | absolute_url }}">
3. Name posts the way Jekyll requires
The Jekyll posts docs are strict: posts live in _posts/ and the file name must be YEAR-MONTH-DAY-title.MARKUP, for example 2026-11-03-jekyll-seo-checklist.md. A file in _posts/ without the date prefix is not a post, and Jekyll skips it without an error. The build passes, the post is missing, and nobody notices until someone looks for it.
Two related rules catch people:
- Drafts belong in
_drafts/, without a date in the file name. They only build withjekyll serve --drafts, so they never leak to production by accident. - Future dates are skipped. The configuration defaults set
future: false. A post whose front matterdateis later than the build time is left out, and nothing rebuilds the site when that time arrives. Settimezonein_config.ymland give dates an explicit offset (date: 2026-11-03 09:00:00 -0600) so "today" means the same thing on your laptop and on the build server.
If a post is missing after a push, check the name and the date first. Run bundle exec jekyll build --verbose and look for the file; if it is not in the output, Jekyll never considered it a post.
4. Pick a clean permalink pattern before you have traffic
Jekyll's default post permalink style is date, which expands to /:categories/:year/:month/:day/:title:output_ext, so a post gets a URL like /2026/11/03/my-post.html, with an extra folder for each category it has. The permalink docs list the built-in styles (date, pretty, ordinal, weekdate, none) and the placeholders you can combine yourself.
For a blog that should rank on evergreen topics, a short pattern is usually better:
permalink: /blog/:title/
:title comes from the file name after the date (and can be overridden with slug in the front matter), so _posts/2026-11-03-jekyll-seo-checklist.md is published at /blog/jekyll-seo-checklist/. Dates and categories in the URL make updated posts look stale and make every recategorisation a broken link. Whatever you choose, choose it once: changing the pattern later means a redirect for every post (step 8).
Keep one URL style everywhere. With a trailing-slash pattern, every internal link should also end in a slash, and step 10 checks that the host does not serve the other variant as a second 200.
5. Add jekyll-seo-tag to the head
Jekyll prints no <title> logic, meta description or canonical tag of its own; your theme's head include decides all of it. The simplest fix is jekyll-seo-tag, which is on the GitHub Pages allowlist. Its installation guide is three steps: add the gem, add it to plugins in _config.yml, and put the tag right before </head>:
<!-- _includes/head.html -->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% seo %}
{% feed_meta %}
</head>
From there it outputs the page title with the site title appended, a meta description, a canonical URL, next/prev links on paginated pages, Open Graph and Twitter card tags, and JSON-LD for the site and each post. It reads title, description, url, author, logo, social and twitter from _config.yml; per post it reads title, description, image and author, and honours canonical_url if a post is a republished copy of something that lives elsewhere.
Three things to check after you add it:
- No duplicate tags. Many themes already print a
<title>or Open Graph tags. View source on a post; if you see two titles, remove the theme's or use{% seo title=false %}. - Set
logo. The plugin's docs note that the JSON-LDpublisherproperty is only present whenlogois set, and its absence shows up as an error in Google's Rich Results Test. - Set a default
imagewith front matter defaults, so posts without their own image still get a social preview. Use a 1200x630 PNG or JPG; most social platforms do not render SVG.
defaults:
- scope:
path: ""
type: posts
values:
layout: post
image: /assets/og-default.png
That same defaults block is also the right place for layout: post, so posts that arrive without a layout key (generated or imported ones, for example) still get your post template.
6. Give every post a real description
jekyll-seo-tag falls back to the site description when a post has none, which means every post without one shares the same meta description. Google's guidance on snippets and meta descriptions asks for descriptions that are unique to each page and summarise it accurately.
Put it in front matter and keep it around 150 characters:
---
title: "Cut Your CI Build Time in Half"
date: 2026-11-03 09:00:00 -0600
description: "How we split a 14-minute GitHub Actions build into cached jobs and got it under 7 minutes, with the workflow file."
tags: [ci, github actions]
image: /assets/posts/ci-build-time.png
---
While you are in front matter, do not repeat the title as a # Heading at the top of the body. Most Jekyll post layouts already print page.title as the H1. One H1 per page keeps the outline clean.
7. Generate a sitemap, a feed and robots.txt
Jekyll has no built-in sitemap. Add jekyll-sitemap and it writes /sitemap.xml from url plus every page and post. Two details from its README matter on GitHub Pages:
- The GitHub Pages gem ignores plugins that are only in the Gemfile. The plugin must also be listed under
pluginsin_config.yml, or no sitemap is built. lastmodcomes fromlast_modified_atin front matter if you set it, otherwise from the post date. Thejekyll-last-modified-atplugin reads file times but is not available on the classic Pages build, so on that build setlast_modified_atby hand when you substantially update a post.
Keep thin pages out with sitemap: false in their front matter (tag archives, thank-you pages, search pages), or exclude a whole path through front matter defaults.
jekyll-sitemap also writes a robots.txt if you do not have one. If you write your own, make sure it allows crawling and points at the sitemap:
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
Add jekyll-feed for an Atom feed at /feed.xml and keep {% feed_meta %} in the head (step 5) so readers and tools can discover it. If you want AI assistants to find a clean index of your posts as well, our guide to llms.txt for developer blogs shows how to add that file.
8. Redirect renamed and moved posts
GitHub Pages has no server-side redirect configuration: no _redirects file, no .htaccess. The standard answer is jekyll-redirect-from, which is on the allowlist. List old URLs in the post's front matter:
redirect_from:
- /jekyll/update/2024/05/02/old-post-name.html
- /blog/old-post-name/
Its README explains that it writes an HTML file at each old path with a meta refresh to the new URL; it does not generate server rules. Google's redirects guide says it treats an instant meta refresh as a permanent redirect, so this is good enough on GitHub Pages. If you later move to a host with real 301s (Netlify, Cloudflare Pages, Vercel), switch to those.
Do this every time you change a permalink pattern or rename a post's slug. An old URL that returns 404 throws away whatever links and rankings it had.
9. Keep posts linked, paginated and in plain HTML
Search engines find posts by following links. Make sure every post can be reached through plain <a href> links: a blog index, paginated archive pages (jekyll-paginate is always enabled on GitHub Pages; set paginate: 10 in _config.yml and it paginates the posts on index.html), and two or three in-body links to related posts. A JavaScript "load more" button or a client-side search box is not a crawl path.
If you serve a custom domain (the GitHub Pages custom domain guide covers DNS, verification and redirects step by step), set it once in the Pages settings (which writes a CNAME file) and enable "Enforce HTTPS", so http:// and https:// do not both serve the site. Pick www or the apex and let GitHub redirect the other, so search engines only see one host. Google's guide to consolidating duplicate URLs covers why one canonical host matters.
10. Check the live page, not _site/
_site/ only tells you what Jekyll wrote on your machine. GitHub Pages decides what is actually served, after its own build with its own versions. After each deploy, check one real post:
URL="https://example.com/blog/jekyll-seo-checklist/"
curl -s -o /dev/null -w "%{http_code}\n" "$URL"
curl -s "$URL" | grep -oE '<link rel="canonical" href="[^"]+' | head -1
curl -s "$URL" | grep -c '<h1'
curl -s https://example.com/sitemap.xml | grep -c "$URL"
curl -sI "${URL%/}" | grep -iE '^(HTTP|location)'
You want a 200, a canonical equal to the URL, exactly one H1, the URL present in the sitemap, and the slashless variant redirecting to the slash version rather than returning a second 200. If the post returns 404, go back to step 3: the file name or the date is the usual cause. This is the same idea behind live URL verification for GitHub blog publishing: a post only counts once its public page proves it.
The checklist in one place
| Area | Setting or file | What good looks like |
|---|---|---|
| Build | Pages source, Gemfile | Local build uses the same Jekyll and plugins as production |
| Host | url, baseurl | Production origin; baseurl set only for project sites |
| Post files | _posts/YYYY-MM-DD-slug.md | Every post date-prefixed; drafts in _drafts/ |
| Dates | timezone, future | Dates with an offset, none in the future at build time |
| URLs | permalink | Short, :title-based, decided early |
| Head tags | {% seo %} | One title, canonical, Open Graph, JSON-LD; logo set |
| Descriptions | front matter | Unique description per post |
| Social image | front matter defaults | 1200x630 PNG or JPG default image |
| Sitemap | jekyll-sitemap in plugins | All posts listed; thin pages sitemap: false |
| Robots and feed | robots.txt, jekyll-feed | Crawling allowed, sitemap listed, feed linked |
| Renamed posts | redirect_from | Old URLs redirect to the new one |
| Verification | curl after deploy | 200, matching canonical, one H1, in the sitemap |
FAQ
Is Jekyll good for SEO?
Yes. It produces static HTML with no client-side rendering, and GitHub Pages serves it quickly over HTTPS. The gaps are all configuration: url and baseurl, a missing SEO tag in the theme, no sitemap plugin, and posts that silently fail to build.
Does GitHub Pages generate a sitemap for Jekyll?
Not unless you enable it. Add jekyll-sitemap to the plugins list in _config.yml (listing it only in the Gemfile is not enough on GitHub Pages) and set url.
Why is my Jekyll post not showing up?
Usually one of three things: the file in _posts/ is missing the YYYY-MM-DD- prefix, its date is later than the build time (Jekyll skips future posts by default), or it is still in _drafts/. Build locally with --verbose to see which files Jekyll treats as posts.
Can I use Jekyll 4 on GitHub Pages?
Yes, by building with a GitHub Actions workflow instead of the classic "Deploy from a branch" build. The classic build uses the pinned versions on GitHub's dependency list, currently Jekyll 3.10.
Where should Jekyll blog posts live?
In _posts/ at the site root, named YYYY-MM-DD-slug.md. Our Jekyll integration page explains how Blogger Dog detects a Jekyll site and writes posts there.
Keep the posts coming
A well-configured Jekyll blog with no new posts is still a quiet blog. Blogger Dog researches and writes cited SEO and AEO posts, commits them into your _posts folder with the date-prefixed file name Jekyll requires and title, date, description, tags and slug front matter, and only counts a post once its public URL is live on GitHub Pages or your own host. It works the same way for Next.js, Astro, Hugo, Gatsby and Docusaurus, on GitHub or GitLab.
You get 3 posts free, then plans start at $19/month. See pricing and plans.