Updated 2026-10-26
Jekyll SEO: jekyll-seo-tag, meta descriptions, og:image and the sitemap
Jekyll writes static HTML, and two official plugins cover most of what search engines and link previewers read: jekyll-seo-tag for the head and jekyll-sitemap for the sitemap. Both are in the GitHub Pages plugin set, so they work on the classic Pages build too. What they can't do is write a description for a page that doesn't have one. This guide goes through the setup, the fallbacks and the URL settings that break it (jekyll-seo-tag usage).
1. Install jekyll-seo-tag
# Gemfile
group :jekyll_plugins do
gem "jekyll-seo-tag"
gem "jekyll-sitemap"
end
# _config.yml
plugins:
- jekyll-seo-tag
- jekyll-sitemap
Then put {% seo %} in your layout's <head>, and remove any <title> or description tag the theme already writes, or you'll ship two. Many themes (Minima among them) already include {% seo %}; check _layouts and _includes/head.html first. If you want to keep the theme's own title tag, use {% seo title=false %}.
From that one tag you get: <title>, the meta description, a canonical link, og:title, og:description, og:url, og:site_name, og:locale, og:image when there's an image, og:type (article with a published time for dated pages, otherwise website), an X card, and a JSON-LD block whose type is BlogPosting for posts, WebSite for the home page and WebPage otherwise.
2. url and baseurl
The canonical, og:url, og:image and every sitemap entry are absolute URLs built from url and baseurl. Without url, jekyll-seo-tag leaves out the canonical and og:url entirely.
# _config.yml
title: Example Docs
description: Reference and guides for the Example CLI.
url: "https://docs.example.com" # no trailing slash
baseurl: "" # or "/repo": leading slash, no trailing slash
- Trailing slashes.
urltakes none, andbaseurlstarts with a slash and doesn't end with one. Getting either wrong gives you//in every URL or a missing path segment. jekyll servereplacesurlwithhttp://localhost:4000. That's fine locally, but a deploy built withserve, or a copy of_sitefrom a local preview, ships localhost canonicals. Build for production withJEKYLL_ENV=production bundle exec jekyll build.- GitHub Pages fills
urlandbaseurlin for you on its own builds. Set them explicitly when you use a custom domain or build in Actions.
3. Titles and descriptions per page
---
title: Configure the CLI
description: Every option in the config file, with defaults and an example for monorepos.
image: /assets/og/configure.png
---
jekyll-seo-tag builds the <title> as page title | site title. A page without a title, usually the home page, gets site title | tagline, using tagline from _config.yml or, without one, description.
Where the description comes from
The plugin takes, in order, the page's description, its excerpt, then the site's description, and cuts it at 100 words (from lib/jekyll-seo-tag/drop.rb). The difference between posts and pages matters here:
- Posts have an excerpt by default: the first paragraph. That's often a reasonable description, but posts that open with an image, a note or a one-line greeting get that as their description, and 100 words is far longer than the 150 or so characters Google shows.
- Pages have no excerpt, so every page without
descriptiongets the site description. On a docs site built with Jekyll, that's most pages.
To find the shared ones after a build:
grep -rhoE --include='*.html' '<meta name="description" content="[^"]*"' _site | sort | uniq -c | sort -rn | head
Duplicate titles and descriptions on a docs site goes through fixing them.
4. Preview images
Set image in front matter, as above, or as a hash with path, width, height and alt. A path starting with / is made absolute with url and baseurl. Give every page a default with front matter defaults:
# _config.yml
defaults:
- scope:
path: ""
values:
image: /assets/og/default.png
With an image, the X card becomes summary_large_image; without one, it's a small summary card. Use a PNG or JPG around 1200×630; previewers don't render SVG. If cards still come out without a picture, og:image not showing covers the other causes.
5. Permalinks and canonicals
By default about.md becomes /about.html and posts get /:categories/:year/:month/:day/:title.html (permalinks). With permalink: pretty, pages become /about/ and posts end in a slash. Either is fine for search; what matters is that the canonical jekyll-seo-tag writes is the URL your host serves without a redirect. Pick one style before launch and keep it, and use canonical_url in front matter for a page that's a copy of one published elsewhere.
6. The sitemap
jekyll-sitemap writes sitemap.xml with every page, post and collection document, and robots.txt pointing at it if you don't have one (jekyll-sitemap).
sitemap: falsein front matter leaves a page out. Use it on pages you marknoindexand on thin pages such as tag archives.404.htmlis left out already.lastmodcomes fromlast_modified_atin front matter, falling back todatefor posts. Thejekyll-last-modified-atplugin fillslast_modified_atfrom git so you don't maintain it by hand.
Submit https://docs.example.com/sitemap.xml in Search Console.
7. Check the build
JEKYLL_ENV=production bundle exec jekyll build
grep -rL --include='*.html' 'property="og:image"' _site
grep -rho --include='*.html' 'rel="canonical" href="[^"]*"' _site | grep -c localhost
The last count should be 0. Then check a few deployed pages with the free SEO checker, which shows the Google result and the LinkedIn, Slack and X cards, or up to 50 sitemap pages with the site checker.
Automating it with Headlark
jekyll-seo-tag puts the right tags in place; it can't write their content. Headlark's build step runs after jekyll build on the HTML in _site and writes the title, description and preview text from each page's content. It's a Node.js CLI, so the build needs Node 20 or newer next to Ruby. Add it once with npm i -D headlark (that creates a package.json and a lockfile next to your Gemfile), then:
JEKYLL_ENV=production bundle exec jekyll build
npx headlark _site --site-url https://docs.example.com
Use url plus baseurl as the site URL: --site-url https://example.github.io/repo/ for a project site. Set HEADLARK_KEY in the build environment.
On GitHub Pages, the classic "Deploy from a branch" build can't run extra commands. Switch Settings → Pages → Source to GitHub Actions, start from GitHub's Jekyll workflow, and add three steps before the upload: actions/setup-node with Node 20 or newer, npm ci, and the npx headlark _site command with HEADLARK_KEY from a repository secret. GitHub Pages (Actions) has a full workflow to copy the steps from. Netlify users can use the build plugin instead.
- Each page gets a title, a description, Open Graph and X tags written from its own content, and alt text for images that have none, replacing the excerpt and site-description fallbacks.
- An existing
og:image, from front matter or yourdefaults, is kept. Headlark doesn't create images. - Your canonical and robots tags are kept, and so is jekyll-seo-tag's JSON-LD block, as written. Its
descriptionfield still holds the plugin's fallback text, so setdescriptionin front matter on the pages where structured data matters to you.
Try it
The free plan covers one site and 25 pages. Add your site, copy its secret key, and your next build ships with SEO.