13 min read

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-headings and jekyll-relative-links).
  • A GitHub Actions workflow that runs bundle exec jekyll build with your own Gemfile.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 with jekyll serve --drafts, so they never leak to production by accident.
  • Future dates are skipped. The configuration defaults set future: false. A post whose front matter date is later than the build time is left out, and nothing rebuilds the site when that time arrives. Set timezone in _config.yml and 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-LD publisher property is only present when logo is set, and its absence shows up as an error in Google's Rich Results Test.
  • Set a default image with 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 plugins in _config.yml, or no sitemap is built.
  • lastmod comes from last_modified_at in front matter if you set it, otherwise from the post date. The jekyll-last-modified-at plugin reads file times but is not available on the classic Pages build, so on that build set last_modified_at by 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

AreaSetting or fileWhat good looks like
BuildPages source, GemfileLocal build uses the same Jekyll and plugins as production
Hosturl, baseurlProduction origin; baseurl set only for project sites
Post files_posts/YYYY-MM-DD-slug.mdEvery post date-prefixed; drafts in _drafts/
Datestimezone, futureDates with an offset, none in the future at build time
URLspermalinkShort, :title-based, decided early
Head tags{% seo %}One title, canonical, Open Graph, JSON-LD; logo set
Descriptionsfront matterUnique description per post
Social imagefront matter defaults1200x630 PNG or JPG default image
Sitemapjekyll-sitemap in pluginsAll posts listed; thin pages sitemap: false
Robots and feedrobots.txt, jekyll-feedCrawling allowed, sitemap listed, feed linked
Renamed postsredirect_fromOld URLs redirect to the new one
Verificationcurl after deploy200, 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.

Put your blog on autopilot

Blogger Dog researches, writes and commits SEO/AEO posts like this one straight into your GitHub or GitLab repo on a schedule. Your first 3 posts are free, then plans start at $19/month.

More from the archive