8 Jekyll Sitemap Checks for GitHub Pages Sites

Diagnose missing or incorrect Jekyll sitemaps on GitHub Pages. Check plugins, project paths, exclusions, dates and deployed XML with a URL audit script.

Editorial policy

If your Jekyll sitemap is missing or contains the wrong URLs on GitHub Pages, check three things in order: the builder that generates it, the content selected for inclusion, and the XML actually served at the public address. A successful local preview proves none of those production results by itself.

These eight checks take you from plugin activation to a complete URL-set comparison. They focus on sitemap implementation; use our broader Jekyll SEO checklist for page metadata, structured data and internal links.

The examples follow jekyll-sitemap 1.4.0. When checked on November 9, 2026, the GitHub Pages dependency table listed that version alongside Jekyll 3.10.0. A custom GitHub Actions build can use different dependencies, so inspect its workflow and lockfile before applying version-specific behavior. This guide is based on official documentation and plugin source, rather than a hosted-site benchmark.

1. Enable the plugin in the builder that publishes the site

Start with the repository's Pages publishing configuration and the last successful deployment. Determine whether GitHub builds the Jekyll source through its supported environment or a custom Actions workflow builds and uploads the output.

For the supported Pages environment, merge this entry into the existing _config.yml plugin list:

YAML
plugins:
  - jekyll-sitemap

Keep any other required plugins. The plugin's installation instructions specifically require configuration activation when using github-pages; having a gem available does not establish that the publishing build loads it.

For a separately managed Bundler build, also declare gem "jekyll-sitemap" in its Gemfile and install the dependencies. Preserve the project's existing Jekyll or github-pages dependency strategy rather than adding a second competing setup.

Do not disable safe mode as a blanket fix. GitHub's supported build enables permitted plugins; arbitrary code in _plugins is a different case. GitHub's Jekyll documentation explains that distinction. A custom Actions workflow gives you control over its build environment, but you must verify which configuration, working directory and output directory it actually uses.

If the local file exists but the deployment artifact lacks it, investigate this build difference before changing search settings.

2. Find the file that owns sitemap.xml

An enabled plugin can deliberately leave an existing sitemap alone. In jekyll-sitemap 1.4.0's generator, generation is skipped when a page or static file already has the URL /sitemap.xml.

Look for a source sitemap.xml, a page with a matching permalink, or another generator producing that destination. Inspect themes and copied starter files as well as files you wrote yourself.

Choose one owner:

Intended owner What to verify
jekyll-sitemap No existing page or static file claims its sitemap destination
A custom Liquid template It deliberately supplies the complete inventory, correct absolute URLs and valid XML
Another build step Its output is included in the artifact that Pages actually publishes

Keep a custom template when it serves a real requirement. Back it up and understand its behavior before replacing it. Mixing instructions for a custom template with the plugin's front matter can silently produce different membership rules.

The plugin also runs at lowest generator priority. If another plugin creates pages, verify that those pages exist when the sitemap is assembled. The plugin README discusses ordering; merely placing two plugins in the same repository does not prove their output is included.

Finally, inspect the emitted file itself. A stale hand-written sitemap can be valid XML and return HTTP 200 while omitting every recently added page.

3. Match the origin and project baseurl

The plugin builds each location using Jekyll's absolute_url filter. Treat url as the origin and baseurl as the public path prefix, following Jekyll's configuration reference.

Actual public location url baseurl Expected sitemap address
Account site at https://acme.github.io/ https://acme.github.io "" https://acme.github.io/sitemap.xml
Project at https://acme.github.io/docs/ https://acme.github.io /docs https://acme.github.io/docs/sitemap.xml
Site mounted at custom-domain root https://docs.example.com "" https://docs.example.com/sitemap.xml

Use the actual mount point. A repository name does not always remain the public prefix after custom-domain configuration.

For the project example, the relevant configuration is:

YAML
url: "https://acme.github.io"
baseurl: "/docs"

An intended page at /guide/ should become https://acme.github.io/docs/guide/, with the prefix exactly once. Putting /docs into both settings risks duplicating it.

Inspect the publishing command for additional configuration files and command-line overrides. Later configuration files can override earlier values. A local jekyll serve preview uses development addressing; inspect a production build made with the publishing configuration instead. Setting JEKYLL_ENV=production alone does not correct an incorrect origin.

Check several locations in the XML, including a nested page and the home URL. Fix incorrect addressing at its configuration source rather than applying a search-and-replace to the deployed XML.

4. Check all three sources of sitemap entries

Do not audit only posts. The 1.4.0 sitemap template and generator draw from three sources:

Source Eligible content in this version Common surprise
Collections, including posts Documents in collections whose output is enabled, unless sitemap: false A source document may exist without having a public output page
HTML pages site.html_pages, excluding /404.html and entries marked false Generated archive pages can appear too
Static files Extensions .htm, .html, .xhtml, .pdf, excluding marked-false files and static files named 404.html A copied PDF can enter the sitemap without post front matter

Images, CSS and every other asset are not automatically included by this static-file extension list.

For a collection of public guides, the collection output setting belongs in the site configuration:

YAML
collections:
  guides:
    output: true
    permalink: /guides/:name/

Confirm that a guide actually produces its intended file and URL. Also inspect publication dates, draft status and the production build's inclusion settings when expected posts are absent. Do not enable all future or unpublished content merely to make an inventory comparison pass.

Prepare an independent list of intended canonical URLs from the content you want published: posts, ordinary pages, output collections and useful public documents. Include meaningful category or navigation pages when they deserve indexing; a page type is not automatically low quality.

This list becomes the expectation for check 7. Counting sitemap entries alone cannot reveal one missing guide balanced by one unwanted PDF.

5. Apply exclusions to the right content type

For a processed page or post, use the plugin's boolean flag in front matter:

YAML
---
title: "Campaign confirmation"
sitemap: false
---

For static files, use configuration defaults instead of inserting YAML into the asset. For example, if a directory contains downloadable historical copies that you intentionally omit from this sitemap:

YAML
defaults:
  - scope:
      path: "assets/old-editions"
    values:
      sitemap: false

Merge this rule with existing defaults. Check its scope carefully: the example excludes that directory's eligible static files, not every PDF. Jekyll documents front matter defaults for static files; the plugin supports using them for sitemap exclusion.

The files remain public and downloadable. Sitemap exclusion is neither access protection nor an instruction to remove a URL from Google's index. Likewise, Jekyll's build-level exclude setting changes what is emitted, which is a different operation.

Check canonical duplicates explicitly. The 1.4.0 sitemap template uses the page or document URL; it does not consult a canonical_url override used by an SEO template. If a duplicate points elsewhere canonically, decide which URL belongs in the sitemap and inspect the result. Our broader Jekyll guide explains the separate canonical and noindex controls.

Pass condition: intended exclusions are absent, useful documents remain present, and the public pages retain their intended access and indexing policies.

6. Trace lastmod to its real source

Different entry types do not share one modification-date rule. In the unmodified 1.4.0 template:

Entry type Source of lastmod
Collection document or post last_modified_at, otherwise date; omitted if neither exists
HTML page last_modified_at when supplied
Included static file Filesystem modified_time

For a substantially revised processed page, a deliberately maintained value might look like:

YAML
last_modified_at: 2026-11-08T14:30:00+00:00

Use the real significant-change time. Do not copy that illustrative date or update it on every deployment.

For PDFs and other static entries, inspect what the build environment does to filesystem timestamps. A recently copied or checked-out file can have a recent filesystem time without a meaningful content revision. Compare the emitted value with your actual content history rather than assuming it represents an editorial update.

Adding last_modified_at through static-file defaults does not change this version's static-entry template: it reads modified_time. If useful documents need another date source, a deliberately maintained custom sitemap may be more appropriate. Recheck its complete inventory when changing ownership.

Google's sitemap guidance calls for reliable modification dates reflecting significant page changes. Different dates are not inherently better than identical accurate dates. Google also ignores priority and changefreq, so adding those fields is not a repair for incorrect locations or missing pages.

7. Compare the complete URL set before deployment

Save a reviewed inventory as expected-urls.txt, one intended absolute URL per line. For a hypothetical three-page project, it could contain:

Plain text
https://acme.github.io/docs/
https://acme.github.io/docs/about/
https://acme.github.io/docs/guides/getting-started/

Replace the example with your complete intended inventory. Do not derive it by copying the sitemap being checked; that would reproduce its omissions.

Save the following as audit-sitemap.py. It uses Python 3's standard library and reads files locally:

Python
import argparse
from collections import Counter
from pathlib import Path
from urllib.parse import urlsplit
import xml.etree.ElementTree as ET

parser = argparse.ArgumentParser()
parser.add_argument("sitemap")
parser.add_argument("public_base", help="e.g. https://acme.github.io/docs/")
parser.add_argument("expected", help="one intended absolute URL per line")
args = parser.parse_args()
base = urlsplit(args.public_base)
if base.scheme not in {"http", "https"} or not base.netloc or base.query or base.fragment:
    parser.error("public_base must be an HTTP(S) site base without query or fragment")
prefix = base.path.rstrip("/") + "/"
data = Path(args.sitemap).read_bytes()
root = ET.fromstring(data.decode("utf-8-sig"))
ns = "{http://www.sitemaps.org/schemas/sitemap/0.9}"
if root.tag != ns + "urlset":
    raise SystemExit("Expected one uncompressed sitemap urlset, not an index")
entries = root.findall(ns + "url")
urls = [(entry.findtext(ns + "loc") or "").strip() for entry in entries]
expected = {
    line.strip()
    for line in Path(args.expected).read_text(encoding="utf-8-sig").splitlines()
    if line.strip()
}
if not expected:
    raise SystemExit("Expected URL list is empty")
counts = Counter(urls)
problems = []
if len(entries) > 50000 or len(data) > 52428800:
    problems.append("Single-sitemap URL or uncompressed-size limit exceeded")
if not entries:
    problems.append("Sitemap is empty")
for url in urls:
    parsed = urlsplit(url)
    same_origin = (parsed.scheme, parsed.netloc) == (base.scheme, base.netloc)
    if not url or not same_origin or not parsed.path.startswith(prefix):
        problems.append("Outside expected origin/path or empty loc: " + repr(url))
    if parsed.query or parsed.fragment:
        problems.append("Review query/fragment URL: " + url)
for url, count in counts.items():
    if count > 1:
        problems.append(f"Duplicate ({count}): {url}")
actual = set(urls)
problems += ["Missing: " + url for url in sorted(expected - actual)]
problems += ["Unexpected: " + url for url in sorted(actual - expected)]
print(f"{len(entries)} entries; {len(actual)} unique; {len(expected)} expected")
for problem in problems:
    print(problem)
raise SystemExit(1 if problems else 0)

Run it against the output directory from the publishing build:

Shell
python audit-sitemap.py _site/sitemap.xml https://acme.github.io/docs/ expected-urls.txt

If your workflow uses another destination, replace _site. A zero exit status means the parsed URL set matches the supplied inventory and passes these scope, duplicate and size checks. Any reported difference needs investigation.

The script targets one uncompressed XML urlset, not a sitemap index. It parses XML rather than counting lines, because the plugin can minify its output. It checks the standard single-file limits of 50,000 URLs and 50 MB uncompressed.

This is an inventory gate, not full XML Schema validation or a live canonical/noindex audit. It deliberately flags query strings and fragments for review under a clean-URL policy. It also requires the chosen base path, including a trailing slash for the base URL itself; if your canonical home address differs, adjust that policy consciously. A passing result cannot detect a mistake shared by the XML and your expected list.

8. Verify the public XML and the host's discovery file

After deployment, fetch the actual sitemap address from check 3. Follow and record redirects, inspect the final status and content type, and parse the response as XML. A branded HTML error page with status 200 is not a working sitemap.

Compare its complete URL set with both the expected inventory and the deployed build artifact. Repeat the local script against a downloaded copy if useful. If the artifact is correct but the public file is stale or different, check the selected publishing source, deployment revision and uploaded directory before editing content again.

For representative included URLs, verify that the public response serves the intended content at its final canonical address. Inspect redirects, canonical tags and any robots meta or X-Robots-Tag policy. This catches an XML inventory that points to redirects, errors or pages intentionally excluded from indexing.

Then check discovery at the correct host level. Google's robots.txt location rules require that file at the root of the host it governs. For a project site:

Purpose Address
Project sitemap https://acme.github.io/docs/sitemap.xml
Host-level robots policy https://acme.github.io/robots.txt
File inside the project https://acme.github.io/docs/robots.txt — not the host's robots policy

A project repository generally cannot supply the account site's root file just by generating its own robots.txt. Coordinate with the owner of the root site if it needs an absolute Sitemap: declaration for the project. Do not replace unrelated crawl rules.

The plugin generates a basic robots file only when no page or static file already owns /robots.txt in that Jekyll build. If you maintain your own file, verify its intended sitemap declaration yourself. On a custom-domain root site, the emitted root file can be in the correct location; verify the public response rather than assuming it.

Keep the deployment revision, expected URL list, audit output and fetched XML together. Our XML sitemap guide covers the broader discovery workflow. Successful generation, public delivery, crawler discovery and indexing are separate outcomes: a correct sitemap helps communicate URLs, but does not guarantee that Google will index them.

Related posts

Keep reading practical SEO guides from the Screpy blog.

View all posts