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

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:

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).

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.

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.