Eleventy SEO

Eleventy (11ty) ships zero client-side JavaScript by default, so your content is in the raw HTML on the first fetch. The catch — it generates no title tags, meta descriptions, canonicals, or sitemaps. Here's how to configure SEO on an Eleventy site, built around the data cascade.

First published: Jun 26, 2026 · Last updated: Jul 17, 2026 · Advanced
demand #4 in Static Site Generators#41 in Platform SEO#271 in Technical SEO#378 on the site
1 evidence signal on this page

Eleventy outputs plain HTML with zero client-side JavaScript by default, which is the safest possible starting point for indexability — content is in the first byte, nothing to render. The trade-off: Eleventy generates no SEO markup at all. Every title, meta description, canonical, sitemap, and bit of structured data is something you implement yourself. The data cascade (_data/site.js → directory data → front matter → eleventyComputed) is the architecture that makes that scalable: declare defaults once, derive canonicals automatically. Plugins like eleventy-plugin-seo and @11ty/eleventy-img add convenience, not capability. Pick any template language — it doesn't affect SEO.

TL;DR — Eleventy emits plain HTML at build time with zero client-side JS by default, so content is in the response before the first crawler request — the lowest-risk indexability setup there is. But Eleventy ships no SEO markup: title, description, canonical, robots, OG, sitemap, and JSON-LD are all yours to build. The data cascade (_data/site.js → directory data files → front matter → eleventyComputed) is the architecture that makes this scale — declare defaults once, derive canonicals in eleventyComputed. Plugins (eleventy-plugin-seo, @11ty/eleventy-img, @quasibit/eleventy-plugin-sitemap) add convenience, not capability. robots.txt is a passthrough file. Template language is an SEO non-issue — pick by team familiarity. v3 is ESM-first and makes the Image Transform plugin the default path to CWV-clean images. Watch three edge cases: eleventyComputed dependency ordering can fail silently, collection membership doesn’t guarantee a URL was emitted, and a local build is not proof of what production actually serves — verify the deployed artifact directly.

Why Eleventy is a strong SEO baseline

Eleventy builds every route to static HTML at build time and, unlike Next.js, Nuxt, Gatsby, or Astro, ships no JavaScript to the browser unless you add it. Evidence for this claim Eleventy compiles templates to static output and adds no client-side framework runtime by default. Scope: Eleventy core behavior. Confidence: high · Verified: Eleventy documentation There’s no client-side rendering for a crawler to wait on, no rendering budget impact, and no hydration bundle taxing Core Web Vitals. Real production Eleventy sites routinely report Lighthouse scores of 99–100 across the board and homepages that ship 0 KB of JavaScript.

It’s also fast to build, which matters more for SEO than it looks: faster builds mean tighter deploy cycles, so content fixes reach search engines sooner. On a 4,000-Markdown-file benchmark Eleventy builds in ~1.93s versus ~22.9s for Astro, ~29s for Gatsby, and ~70.6s for Next.js, with a fraction of the node_modules footprint (34 MB vs Gatsby’s 583 MB).

But — and this is the whole point of the page — none of that buys you SEO markup. Eleventy generates clean HTML and stops there. Every <title>, meta description, canonical, sitemap entry, and JSON-LD block is something you implement. The good news is that Eleventy’s data model makes implementing them once-and-everywhere genuinely pleasant.

The data cascade: your SEO architecture foundation

The single most important concept for scalable Eleventy SEO is the data cascade — the order in which Eleventy resolves a value, lowest priority to highest: Evidence for this claim Eleventy's data cascade resolves data from multiple sources according to a documented priority order. Scope: Eleventy data cascade. Confidence: high · Verified: Eleventy: Data cascade

  1. Eleventy-supplied global data
  2. Global data files in _data/ (e.g. _data/site.js with title, description, url, author)
  3. Directory data files (e.g. blog/blog.11tydata.js — give every blog post og:type: article)
  4. Template front matter (per-page overrides)
  5. eleventyComputed (highest — derive values from other data)

In practice:

  • Declare your SEO defaults once in _data/site.js.
  • Override at the directory level for a content type (all posts get the article OG type, a noindex section gets noindex, etc.).
  • Override per page in front matter where a page is special.
  • Use eleventyComputed for derived values — the canonical being the canonical example:
// _data/eleventyComputed.js  (or eleventyComputed in a layout data file)
module.exports = {
  canonical: (data) => `${data.site.url}${data.page.url}`,
};

Now every page has a correct self-referencing canonical with zero per-page work. That’s the leverage the cascade gives you.

One edge case worth testing rather than assuming: eleventyComputed values can depend on other computed values (a title that feeds into a computed description, say), and Eleventy has to resolve that dependency graph before it can render anything. Straightforward chains work fine, but a computed value that accidentally depends on itself — directly or through another computed field — is a circular reference, and ordering issues here fail silently as a missing or wrong value rather than a loud build error. If you lean on multiple layered eleventyComputed fields, check the rendered output for the exact values you expect, not just that the build succeeded. Evidence for this claim Computed Data values can derive from other computed values, so dependency ordering and circular references need testing against the rendered output. Scope: Eleventy eleventyComputed resolution. Confidence: high · Verified: Eleventy: Computed Data

Meta tags in the base layout

Standard architecture is a base layout that owns the <head>, with content layouts extending it:

_includes/
  layouts/
    base.njk      ← <head> with all SEO meta tags
    post.njk      ← extends base, adds BlogPosting schema
_data/
  site.js         ← sitewide SEO defaults

The meta tags you need to implement yourself, at minimum:

  • <title> — unique per page
  • <meta name="description"> — unique per page
  • <link rel="canonical"> — Eleventy has no built-in canonical; add it explicitly
  • <meta name="robots">noindex for draft/preview pages (and thin paginated pages, if any)
  • Open Graph — og:title, og:description, og:image, og:url, og:type
  • Twitter/X Cards — twitter:card, twitter:title, twitter:description, twitter:image

A minimal Nunjucks <head> pulling from the cascade:

<title>{{ title }} | {{ site.title }}</title>
<meta name="description" content="{{ description or site.description }}">
<link rel="canonical" href="{{ canonical }}">
<meta property="og:title" content="{{ title }}">
<meta property="og:image" content="{{ ogImage or site.defaultImage }}">
<meta name="twitter:card" content="summary_large_image">

SEO plugins, compared

You don’t need a plugin — a manual base layout plus the data cascade gives you full control with no dependencies, and many practitioners prefer exactly that. But two plugins are worth knowing:

  • eleventy-plugin-seo (artstorm) — the most mature and widely used. A single {% seo %} shortcode (Liquid) / {% seo "" %} (Nunjucks) emits title, description, canonical, robots, Open Graph, Twitter Card, and author from a config block (site title, description, URL, author, Twitter handle, default image). It also handles robot directives on paginated pages, a title divider, and a minimalistic mode. Install: npm install eleventy-plugin-seo.
  • eleventy-plugin-metagen (tannerdolby) — more granular, with named shortcode parameters; generates charset, viewport, title, author, description, generator, OG, Twitter Card, canonical, and CSS/JS tags.

The rule of thumb: plugins add convenience, not capability. Anything they do, you can do in the base layout. Reach for one if you’d rather not maintain the boilerplate.

robots.txt

Eleventy does not auto-generate robots.txt. Two approaches:

1 — Static passthrough (simplest). Drop a robots.txt in your source and copy it verbatim:

eleventyConfig.addPassthroughCopy("src/robots.txt");

2 — Template-generated (for environment-specific output — block everything on staging, allow on production):

---
permalink: /robots.txt
eleventyExcludeFromCollections: true
---
User-agent: *
{% if environment == "production" %}
Allow: /
Sitemap: {{ site.url }}/sitemap.xml
{% else %}
Disallow: /
{% endif %}

Remember the universal rule: Disallow blocks crawling, not indexing — and a noindex tag on a crawl-blocked URL is never seen.

Sitemap generation

Also not built in. The most-control option is a manual template — create sitemap.xml.njk with permalink: /sitemap.xml and eleventyExcludeFromCollections: true, loop over collections.all, skip excluded pages, and emit <loc> / <lastmod> entries:

---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for page in collections.all %}
  {% if not page.data.excludeFromSitemap %}
  <url>
    <loc>{{ site.url }}{{ page.url }}</loc>
    <lastmod>{{ page.date | dateToISO }}</lastmod>
  </url>
  {% endif %}
{% endfor %}
</urlset>

Or use @quasibit/eleventy-plugin-sitemap (configure with hostname; it also supports multilingual alternate links). Either way, mark utility pages with eleventyExcludeFromCollections: true so they don’t leak in.

One thing worth being precise about: looping over collections.all walks collection membership, not guaranteed output. A template can belong to a collection and still not emit a URL — most commonly because its permalink is set to false, or because a data-only file was never meant to render a page in the first place. Membership in collections.all is not proof an item belongs in the sitemap; check the rendered <loc> entries against the pages you actually expect to be crawlable. Evidence for this claim Collection membership does not by itself guarantee that an item emits a URL or belongs in the sitemap. Scope: Eleventy collections and permalink interaction. Confidence: high · Verified: Eleventy: Collections

Image optimization for Core Web Vitals

A static site’s HTML is fast, but unoptimized images will still tank LCP and CLS. The fix is @11ty/eleventy-img, and in v3 the recommended path is the Image Transform plugin, which post-processes every <img> tag in your built HTML automatically — no per-image shortcode needed:

const { eleventyImageTransformPlugin } = require("@11ty/eleventy-img");

eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
  formats: ["avif", "webp", "jpeg"],
  defaultAttributes: {
    loading: "lazy",
    decoding: "async",
  },
});

What that buys you for SEO:

  • Auto-stamped width and height → prevents layout shift (CLS)
  • srcset with multiple widths → responsive images
  • <picture> with AVIF + WebP + JPEG fallback
  • No upscaling

For above-the-fold hero images, override the lazy default so they load eagerly:

<img src="hero.jpg" loading="eager" fetchpriority="high" eleventy:widths="800,1200">

Eleventy’s default is clean URLs with a trailing slash: about.njk_site/about/index.html/about/. Override in front matter:

permalink: "/blog/{{ title | slugify }}/"

Useful flags: permalink: false processes a template for collections but skips writing it to disk (handy for data-only templates), and dynamicPermalink: false treats the permalink string literally. The one SEO thing to stay disciplined about: trailing-slash consistency — mixing /page and /page/ is a duplicate-content own-goal.

Worth stating plainly because it trips people up: permalink: false suppresses output entirely — the source file compiles and can still feed collections and data, but no HTML page is written and no URL exists to crawl. A source file sitting in your content directory is not proof that a corresponding page shipped; if a page is missing from search, check the built _site output for the file before assuming it’s an indexing problem rather than a permalink one. Evidence for this claim A permalink value of false suppresses page output entirely, so a source file's existence is not proof of a crawlable page. Scope: Eleventy permalink output control. Confidence: high · Verified: Eleventy: Permalinks

Pagination SEO

Eleventy’s pagination splits a collection across output files. The first page is /blog/, then /blog/1/, /blog/2/, and so on (customizable via permalink). Each page in the set needs a unique URL and pagination.alias to expose per-page data. The SEO decisions:

  • Canonicals. Two schools: point every page at page 1 (consolidates signals but buries legitimate deep-page content), or self-referencing canonicals where each page’s canonical is its own URL — Google’s preferred approach in current documentation. I default to self-referencing unless the deeper pages truly have nothing worth indexing.
  • rel="prev" / rel="next". Google confirmed back in 2019 it no longer uses these as ranking signals. Some still add them to help crawlers understand series structure; if you do, wire them from pagination.href.previous / pagination.href.next.
  • noindex on thin pages. If later pages have insufficient content, add noindex (via the noindex field if you’re using eleventy-plugin-seo, or manually in the head).

Structured data / JSON-LD

Manual is the most flexible: define a schema object in eleventyComputed or a directory data file and serialize it in the layout. The cleanest types for a content site are Organization / WebSite sitewide in the base layout, BlogPosting / Article per post, BreadcrumbList if you have breadcrumbs, and FAQPage on FAQ sections. @quasibit/eleventy-plugin-schema offers a shortcode for BlogPosting and WebPage if you’d rather not hand-roll it (it expects ISO 8601 dates, so register a date filter).

Eleventy v2 vs v3 — what changed for SEO

v3 (current stable) is ESM-first: a config file that uses require() now needs the .cjs extension, or you set "type": "module" in package.json and use import. CommonJS is still fully supported — just be deliberate about it. The two SEO-relevant wins:

  • ~38% faster cold builds on sites over 500 pages → faster deploys, fresher index.
  • The Image Transform plugin is the default path — declarative post-processing of every <img>, which makes shipping responsive, CLS-safe images the path of least resistance instead of a per-image chore.

Verify the production artifact, not just the local build

Everything above is about getting the source right — the data cascade, the layout, the plugins. None of it proves what actually ships. A local _site build (or eleventy --serve) doesn’t tell you what production serves: the host and path your deploy platform actually uses, redirects it injects, headers it sets, or whether a CDN/proxy rewrites anything on the way out. Confirm the installed Eleventy version matches what you’re testing against, then check the deployed site directly, not just the build output:

  • Raw HTML — view-source the live URL, not just _site/index.html; confirm the title, meta description, and canonical are there and correct.
  • Absolute canonical host — a canonical built from a staging or localhost site.url will silently ship wrong in production if that value wasn’t overridden per environment.
  • Sitemap XML — fetch the live /sitemap.xml and confirm it resolves, uses the production host, and lists the URLs you expect (see the sitemap section above on collection membership not guaranteeing an entry).
  • Suppressed/utility pages — spot-check that permalink: false and eleventyExcludeFromCollections: true pages are actually absent from the live site, not just from your local build.
  • Assets — CSS, JS, and images referenced in the built HTML resolve on the production host (a relative-vs-absolute path mismatch is easy to miss locally).
  • Status codes and redirects — request the live URLs directly and confirm 200s where you expect 200s; a deploy platform’s redirect rules can silently turn a canonical page into a 301 chain that never shows up in a local build.

Third-party plugin installation is not proof of correct output either — it changes what gets generated, not what a specific deploy environment does with it. Treat this as a release-checklist item, not a one-time setup. Evidence for this claim Local build output does not by itself prove production hostnames, redirects, headers, or canonical behavior; the deployed artifact needs its own verification pass. Scope: Eleventy production deployment verification. Confidence: medium · Verified: Patrick Stox: Eleventy SEO

Common Eleventy SEO mistakes

  • Assuming Eleventy “handles SEO” — it handles HTML; the SEO markup is all yours.
  • Forgetting the canonical entirely (there’s no built-in one).
  • Shipping no sitemap because you assumed it was automatic.
  • Letting unoptimized images undo the speed advantage.
  • Inconsistent trailing slashes creating duplicate URLs.
  • Using Disallow in robots.txt to try to deindex a page (use noindex instead).
  • Leaving draft/preview pages indexable because no noindex was set on the section.

For where Eleventy sits among the other generators, see the Static Site Generators hub; for the rendering theory behind why static output is low-risk, the parent JavaScript SEO hub.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin an expert quote first.