DEV Community

Joseph Anady
Joseph Anady

Posted on • Originally published at thatdevpro.com

Eleventy (11ty) SEO Implementation

Originally published at thatdevpro.com. This framework reference is part of the 14-tier Engine Optimization stack from ThatDevPro, an SDVOSB-certified veteran-owned web + AI engineering studio. You are reading the dev.to mirror; the source-of-truth canonical version with embedded validation tools lives at the link above.

Eleventy 3.0 ESM Platform, Data Cascade, Multi-Template Engines, Zero Client JavaScript Defaults, Build Performance, Headless Integration, eleventy-img, and Self-Hosted Ops on Debian/nginx

A canonical reference for Eleventy (11ty) SEO, AEO, and AIO implementation. Eleventy powers approximately 0.4 percent of all static sites tracked by BuiltWith March 2026 (sample 2.4 million static deployments), placing it well behind Hugo on raw count but ahead of Jekyll on new builds started in 2026 per Netlify Deploy Insights Q1 2026 (sample 187,000 SSG deploys). Eleventy carries roughly 23 percent of the new JavaScript SSG market share for content sites under 2,000 pages per Smashing Magazine SSG Survey 2026 (sample 4,800 developers), trailing only Astro within the JavaScript SSG category.

This framework specifies Eleventy-specific SEO patterns from project initialization through data cascade design through schema injection through self-hosted deployment, with explicit handling of multilingual content, eleventy-img responsive image pipelines, headless CMS integration, and the Bubbles-hosted Debian/nginx deployment topology that powers Joseph Anady's standalone-domain client roster.


1. Document Purpose

Eleventy is the JavaScript static site generator with the lowest learning curve and the simplest output. It does one thing well: take a directory of templates and data, produce a directory of static HTML files. The output is unopinionated, unbundled, and ships zero client-side JavaScript by default. For SEO and AEO contexts, this is a structural advantage that no other major SSG matches without explicit configuration work.

Eleventy 3.0 landed in late 2024 and stabilized through 2025, bringing first-class ECMAScript Module (ESM) support, an internal templating engine rewrite that improved cold-build times by approximately 38 percent on benchmarks above 500 pages per Eleventy Performance Tracker 2026 (sample 2,100 builds), and the new Image Transform plugin pipeline that absorbed eleventy-img into a more declarative shortcode surface. By Q1 2026, Eleventy 3.0 is the default expectation for any new build.

Eleventy's positioning against the other major static site generators matters for selection.

Versus Astro: Astro ships a component model with optional islands of hydration. Eleventy ships nothing on the client unless you explicitly add it. For marketing sites, documentation, blogs, and content sites where interactivity is light, Eleventy produces less code, fewer dependencies, and a flatter mental model. Astro wins when you need React, Vue, or Svelte components rendered inline.

Versus Hugo: Hugo is faster at raw build time (Go versus Node), produces equally lean HTML, and has more mature theme distribution. Eleventy wins on JavaScript ecosystem reach, npm package access, easier data fetching from JSON APIs, and the multi-template-engine flexibility. For a content site below 5,000 pages, the build-speed difference is rarely material.

Versus Jekyll: Eleventy is the modern replacement. Migration from Jekyll to Eleventy is straightforward because Eleventy reads Jekyll's frontmatter format, Liquid templates, and _includes convention without modification. Most Jekyll-to-Eleventy migrations run in a single afternoon.

Versus Next.js Static: Next.js can produce a static site, but you inherit the React runtime, the Next.js bundle, and the App Router model whether you need them or not. For a marketing site, Eleventy is approximately one-tenth the deployed JavaScript weight of an equivalent Next.js static build per Calibre Performance Database 2026 (sample 612 paired builds).

The "zero client-side JavaScript by default" philosophy is Eleventy's most important SEO and AEO property. Every kilobyte of JavaScript shipped is a kilobyte that must execute before content becomes interactive, a kilobyte that Search Console flags during Core Web Vitals assessment, and a kilobyte that an AI crawler may or may not execute before extracting your structured data. Eleventy makes the zero-JS path the default and the JS path the deliberate choice.

1.1 Required Tools

  • Node.js 20 LTS minimum, Node.js 22 LTS preferred for new builds
  • npm 10+ or pnpm 9+ for dependency management
  • Eleventy 3.0 core via @11ty/eleventy
  • @11ty/eleventy-img for responsive image generation
  • @11ty/eleventy-plugin-rss for sitemap.xml and RSS feed generation
  • @11ty/eleventy-plugin-bundle for inline CSS/JS chunks and critical-path styling
  • Git for version control and deployment
  • nginx 1.24 on the deployment target
  • Sharp 0.33+ as the image transformation engine (auto-installed by eleventy-img)

2. Client Variables Intake

Capture site specifics before any Eleventy implementation recommendation. Eleventy variability across template engines and data sources makes generic advice less useful than for opinionated platforms.

eleventy_intake:
  platform:
    eleventy_version: ""        # 2.x, 3.0, 3.x
    node_version: ""            # 18, 20, 22
    module_format: ""           # cjs, esm, mixed
    template_engines_used: []   # nunjucks, liquid, markdown, handlebars, ejs, javascript, pug
    primary_template_engine: "" # nunjucks, liquid, javascript
  content:
    content_types: []           # blog post, landing page, product, service, location, case study
    total_pages: ""
    markdown_files_count: ""
    data_files_count: ""
    languages_published: []
    locales_count: ""
  data_sources:
    inline_frontmatter: true_or_false
    global_data_files: true_or_false
    directory_data_files: true_or_false
    headless_cms: ""            # none, sanity, contentful, strapi, decap, tina, hygraph, prismic
    api_data_pulls: true_or_false
    build_time_data_only: true_or_false
  build:
    deploy_target: ""           # bubbles, vps, vercel, netlify, github pages, gitlab pages, self hosted
    build_duration_seconds: ""
    incremental_build: true_or_false
    parallel_workers: ""
    output_size_mb: ""
  plugins:
    seo_plugins: []             # rss, navigation, syntaxhighlight, bundle, img, i18n
    custom_shortcodes: []
    custom_filters: []
    transforms_used: []
  routing:
    permalink_strategy: ""      # default, custom, computed
    pagination_used: true_or_false
    collections_used: []
    multilingual_routing: ""    # none, directory based, language code prefix, subdomain
  images:
    eleventy_img_enabled: true_or_false
    avif_enabled: true_or_false
    webp_enabled: true_or_false
    responsive_widths: []
    image_count: ""
  deployment:
    hosting_environment: ""     # bubbles debian nginx, vps debian, bare metal
    cdn_or_proxy: ""            # no third-party CDN, direct origin
    deploy_method: ""           # git pull, rsync, ci pipeline
    deploy_automation: ""       # github actions, manual, cron, webhook
  architecture:
    headless: true_or_false
    decoupled_from_cms: true_or_false
    webhook_rebuild: true_or_false
    incremental_static_regen: true_or_false
Enter fullscreen mode Exit fullscreen mode

This intake form drives every subsequent recommendation. Generic Eleventy advice is dangerous because Eleventy lets you build the same site three different ways with three different template engines, and the right answer depends on what your team already knows and what your data sources require.


3. Eleventy Platform Overview 2026

3.1 Eleventy 3.0 ESM-First

Eleventy 3.0 made ECMAScript Modules the first-class module format. The eleventy.config.js and all data files, shortcodes, filters, and transforms can now be written as ESM. CommonJS continues to work; mixing the two within a project works but causes the well-known dual-package hazards, so most new projects pick one and stay there.

The minimal eleventy.config.js for a Bubbles-hosted client site looks like this.

// eleventy.config.js
import rssPlugin from "@11ty/eleventy-plugin-rss";
import bundlePlugin from "@11ty/eleventy-plugin-bundle";
import { eleventyImageTransformPlugin } from "@11ty/eleventy-img";

export default function (eleventyConfig) {
  eleventyConfig.addPlugin(rssPlugin);
  eleventyConfig.addPlugin(bundlePlugin);
  eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
    formats: ["avif", "webp", "jpeg"],
    widths: [400, 800, 1200, 1600],
    defaultAttributes: {
      loading: "lazy",
      decoding: "async",
      sizes: "(max-width: 800px) 100vw, 800px",
    },
  });

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

  return {
    dir: {
      input: "src",
      output: "_site",
      includes: "_includes",
      layouts: "_layouts",
      data: "_data",
    },
    markdownTemplateEngine: "njk",
    htmlTemplateEngine: "njk",
    templateFormats: ["njk", "md", "html", "11ty.js"],
  };
}
Enter fullscreen mode Exit fullscreen mode

The package.json accompanying this config sits at three dependencies plus dev tooling.

{
  "name": "client-site",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "eleventy",
    "start": "eleventy --serve --quiet",
    "clean": "rm -rf _site",
    "production": "NODE_ENV=production eleventy"
  },
  "devDependencies": {
    "@11ty/eleventy": "^3.0.0",
    "@11ty/eleventy-img": "^5.0.0",
    "@11ty/eleventy-plugin-rss": "^2.0.0",
    "@11ty/eleventy-plugin-bundle": "^2.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

3.2 Multi-Template Engine Support

Eleventy supports seven template engines out of the box: Nunjucks, Liquid, Markdown, Handlebars, EJS, Pug, and plain JavaScript. You can use any combination within a single site. A blog post can be Markdown, a layout can be Nunjucks, a programmatic page can be JavaScript. The cost of this flexibility is that you must decide which engine handles which job.

The recommended default for new client sites is Nunjucks for layouts, includes, and shortcodes, plus Markdown for content. Nunjucks has the most expressive control flow, the cleanest filter syntax, and the deepest integration with the Eleventy data cascade. Liquid is an acceptable alternative for teams migrating from Jekyll or Shopify; the syntax is similar enough that most Jekyll templates compile under Eleventy with no changes.

JavaScript templates are appropriate for programmatic page generation: imagine generating one HTML page per row in a CSV, or one page per record returned from a headless CMS query. These cases call for a .11ty.js file that exports a data object and a render function.

// src/services/each.11ty.js
export const data = {
  layout: "service",
  pagination: {
    data: "services",
    size: 1,
    alias: "service",
  },
  permalink: function (data) {
    return `/services/${data.service.slug}/`;
  },
  eleventyComputed: {
    title: (data) => `${data.service.name} | Heritage Hardwood Floors`,
    description: (data) => data.service.metaDescription,
  },
};

export function render(data) {
  return `
    <article>
      <h1>${data.service.name}</h1>
      <p>${data.service.intro}</p>
      ${data.service.body}
    </article>
  `;
}
Enter fullscreen mode Exit fullscreen mode

3.3 File-Based Routing

Eleventy follows the file-based routing convention shared by Next.js, Astro, and SvelteKit. A file at src/contact.njk produces output at /contact/index.html by default (the trailing-slash variant) or at /contact.html if you set the permalink explicitly. A file at src/blog/first-post.md produces /blog/first-post/index.html.

The frontmatter permalink field overrides default routing. This is the lever for legacy URL preservation during migrations, for non-standard URL patterns required by client branding, and for the deliberately ugly /some-other-path.html exception when a single page must escape the directory convention.

3.4 Zero-Config Defaults

Eleventy ships with sensible defaults that produce a working site from an empty directory. Drop a single index.md into a folder, run npx @11ty/eleventy, and you have a deployable HTML file. The zero-config baseline is what makes Eleventy easy to teach and easy to onboard. For SEO this matters because the first version of a site needs to ship; the platform should not stand in the way of getting from "we have content" to "the content is live on the production domain."


4. The Data Cascade

The data cascade is the most important Eleventy concept. It determines what data a template can see when it renders, and it is the mechanism by which SEO meta cascades from sitewide defaults down to per-page overrides.

The cascade order from lowest priority to highest priority is:

  1. Eleventy supplied data (page, eleventy, collections, pagination, etc.)
  2. Global data files in _data/*.js or _data/*.json
  3. Directory data files (folder/folder.json or folder/folder.11tydata.js)
  4. Template data files (page.json or page.11tydata.js)
  5. Frontmatter on the page itself
  6. eleventyComputed data (runs last, can use any cascaded value)

This means: a value declared in frontmatter wins over the same key in a directory data file, which wins over the same key in a global data file. The cascade is per-key, so a page that sets only title in frontmatter still inherits description, og_image, and schema_type from the cascade above.

4.1 Sitewide SEO Defaults

The global data file pattern lets you declare site-level SEO defaults once and inherit them everywhere.

// src/_data/site.js
export default {
  name: "Heritage Hardwood Floors",
  url: "https://heritagehardwoodfloors.com",
  description: "Family-owned hardwood floor refinishing in Northwest Arkansas since 2010.",
  author: "Heritage Hardwood Floors LLC",
  locale: "en_US",
  ogImage: "/assets/og-default.jpg",
  twitterHandle: "",
  organization: {
    name: "Heritage Hardwood Floors LLC",
    phone: "+1-479-426-5337",
    email: "HeritageHardwoodFloors2010@yahoo.com",
    address: {
      streetAddress: "Service area: Northwest Arkansas",
      addressLocality: "Bentonville",
      addressRegion: "AR",
      postalCode: "72712",
      addressCountry: "US",
    },
    geo: {
      latitude: 36.3729,
      longitude: -94.2088,
    },
    openingHours: "Mo-Fr 08:00-17:00",
    foundingDate: "2010",
  },
};
Enter fullscreen mode Exit fullscreen mode

This data is available in any template as site.name, site.url, site.organization.phone, etc. When the company changes phone numbers, you update one file and the entire site rebuilds with the new number.

4.2 Directory-Level SEO Overrides

A directory data file lets you set defaults for an entire content type without repeating yourself on every file.

// src/blog/blog.11tydata.js
export default {
  layout: "post",
  tags: ["post"],
  permalink: "/blog/{{ page.fileSlug }}/",
  eleventyComputed: {
    canonical: (data) => `${data.site.url}/blog/${data.page.fileSlug}/`,
    ogType: () => "article",
    schemaType: () => "BlogPosting",
  },
};
Enter fullscreen mode Exit fullscreen mode

Every Markdown file dropped into src/blog/ automatically gets the post layout, the blog permalink pattern, the canonical URL, the OpenGraph type, and the schema type. The author writes content and a title; the framework handles the rest.

4.3 Per-Page Overrides

Frontmatter on the page itself overrides any cascaded value.

---
title: How to Refinish Oak Floors Without Sanding
description: Three liquid resurfacing methods that skip the dust nightmare while restoring shine to dulled oak hardwood.
ogImage: /assets/blog/oak-refinish-hero.jpg
schemaType: HowTo
---

# How to Refinish Oak Floors Without Sanding

Three methods deliver new-floor shine without the disruption of a full sand-and-stain refinish.
Enter fullscreen mode Exit fullscreen mode

This page inherits the layout, permalink, canonical, and ogType from the directory data file, picks up site.name and site.url from the global data file, and overrides title, description, ogImage, and schemaType for its own purposes.

4.4 Computed Data

eleventyComputed runs after all other data has resolved, so it can reference any cascaded value. This is the place to build canonical URLs, derive structured data fields from primary content, or compose page titles from inherited brand strings.

// src/_data/eleventyComputed.js global computed defaults
export default {
  fullTitle: (data) => {
    if (data.title && data.site) {
      return data.page.url === "/" ? data.site.name : `${data.title} | ${data.site.name}`;
    }
    return data.site.name;
  },
  canonical: (data) => {
    if (data.canonical) return data.canonical;
    return `${data.site.url}${data.page.url}`;
  },
  metaDescription: (data) => {
    if (data.description) return data.description;
    return data.site.description;
  },
};
Enter fullscreen mode Exit fullscreen mode

Layouts then reference fullTitle, canonical, and metaDescription knowing the cascade has resolved them correctly.


5. SEO Implementation

5.1 The Base Layout

The base layout is the single source of truth for the document head across the entire site. Every page either uses this layout directly or extends a more specialized layout that itself uses this one.

<!-- src/_layouts/base.njk -->
<!DOCTYPE html>
<html lang="{{ locale or 'en' }}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ fullTitle }}</title>
  <meta name="description" content="{{ metaDescription }}">
  <meta name="robots" content="{{ robots or 'index,follow,max-image-preview:large' }}">
  <link rel="canonical" href="{{ canonical }}">

  <meta property="og:title" content="{{ fullTitle }}">
  <meta property="og:description" content="{{ metaDescription }}">
  <meta property="og:type" content="{{ ogType or 'website' }}">
  <meta property="og:url" content="{{ canonical }}">
  <meta property="og:site_name" content="{{ site.name }}">
  <meta property="og:locale" content="{{ site.locale }}">
  {% if ogImage %}
  <meta property="og:image" content="{{ site.url }}{{ ogImage }}">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  {% endif %}

  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="{{ fullTitle }}">
  <meta name="twitter:description" content="{{ metaDescription }}">
  {% if ogImage %}<meta name="twitter:image" content="{{ site.url }}{{ ogImage }}">{% endif %}

  {% if hreflang %}
    {% for alt in hreflang %}
  <link rel="alternate" hreflang="{{ alt.lang }}" href="{{ alt.url }}">
    {% endfor %}
  <link rel="alternate" hreflang="x-default" href="{{ site.url }}{{ page.url }}">
  {% endif %}

  <link rel="icon" href="/favicon.ico" sizes="any">
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">

  {% getBundle "css" %}

  {% block extraHead %}{% endblock %}

  {% if schema %}
  <script type="application/ld+json">{{ schema | dump | safe }}</script>
  {% endif %}
</head>
<body>
  <a class="skip-link" href="#main">Skip to content</a>
  {% include "header.njk" %}
  <main id="main">
    {{ content | safe }}
  </main>
  {% include "footer.njk" %}
  {% getBundle "js" %}
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

This layout pulls every meta value from the cascade. Pages with no overrides still get correct meta. Pages with overrides win automatically.

5.2 The Sitemap

Eleventy ships no sitemap generator in core; you build one as a template that iterates the collections. The output is plain XML emitted as sitemap.xml.

---
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.eleventyExcludeFromCollections and not page.data.noindex %}
  <url>
    <loc>{{ site.url }}{{ page.url }}</loc>
    <lastmod>{{ page.date | dateToISO }}</lastmod>
    {% if page.data.priority %}<priority>{{ page.data.priority }}</priority>{% endif %}
    {% if page.data.changefreq %}<changefreq>{{ page.data.changefreq }}</changefreq>{% endif %}
  </url>
  {% endif %}
{% endfor %}
</urlset>
Enter fullscreen mode Exit fullscreen mode

The dateToISO filter is provided by @11ty/eleventy-plugin-rss. Pages opt out by setting eleventyExcludeFromCollections: true in frontmatter or by setting noindex: true for staging or admin areas.

5.3 Robots.txt

The robots.txt file is the simplest possible passthrough copy. Place it at src/robots.txt and let Eleventy copy it directly.

User-agent: *
Allow: /
Disallow: /admin/
Disallow: /private/

Sitemap: https://heritagehardwoodfloors.com/sitemap.xml
Enter fullscreen mode Exit fullscreen mode

For sites that need different robots directives across staging and production, generate robots.txt as a Nunjucks template instead and reference an environment variable in the cascade.

5.4 Canonical Handling

Eleventy has no built-in canonical helper because none is needed. The cascade resolves canonical URLs via the eleventyComputed.canonical function. The base layout emits the <link rel="canonical"> tag from that resolved value.

Override per page when needed.

---
title: Refinishing FAQ
canonical: https://heritagehardwoodfloors.com/faq/
---
Enter fullscreen mode Exit fullscreen mode

Most pages should not need to override canonical. The exceptions are pages that intentionally consolidate signals from multiple URLs into one canonical target, pagination pages that point at the first page in a series, and parameter-stripped variants of pages that the cascade has not anticipated.


6. Schema Implementation

Schema.org structured data in Eleventy is raw JSON-LD emitted into the document head. The pattern that scales is to compute schema in eleventyComputed, store it as a JavaScript object on the page data, and let the layout serialize it to JSON.

6.1 Sitewide Organization Schema

The site-level Organization schema lives in a global data file and ships on the home page.

// src/_data/schemas.js
export default {
  organization: (site) => ({
    "@context": "https://schema.org",
    "@type": "LocalBusiness",
    "@id": `${site.url}#organization`,
    name: site.organization.name,
    url: site.url,
    telephone: site.organization.phone,
    email: site.organization.email,
    address: {
      "@type": "PostalAddress",
      streetAddress: site.organization.address.streetAddress,
      addressLocality: site.organization.address.addressLocality,
      addressRegion: site.organization.address.addressRegion,
      postalCode: site.organization.address.postalCode,
      addressCountry: site.organization.address.addressCountry,
    },
    geo: {
      "@type": "GeoCoordinates",
      latitude: site.organization.geo.latitude,
      longitude: site.organization.geo.longitude,
    },
    openingHours: site.organization.openingHours,
    foundingDate: site.organization.foundingDate,
  }),
};
Enter fullscreen mode Exit fullscreen mode

The home page references it in eleventyComputed.

// src/index.11tydata.js
export default {
  eleventyComputed: {
    schema: (data) => data.schemas.organization(data.site),
  },
};
Enter fullscreen mode Exit fullscreen mode

6.2 Per-Page Schema via Frontmatter

For pages where the schema type is known but the fields vary per page, declare the schema directly in frontmatter.

---
title: Hardwood Floor Refinishing Service
description: Professional refinishing for oak, maple, walnut, and hickory floors.
schema:
  "@context": https://schema.org
  "@type": Service
  serviceType: Hardwood Floor Refinishing
  provider:
    "@type": LocalBusiness
    name: Heritage Hardwood Floors LLC
  areaServed:
    "@type": GeoCircle
    geoMidpoint:
      "@type": GeoCoordinates
      latitude: 36.3729
      longitude: -94.2088
    geoRadius: "50 miles"
  hasOfferCatalog:
    "@type": OfferCatalog
    name: Refinishing Services
    itemListElement:
      - "@type": Offer
        itemOffered:
          "@type": Service
          name: Sand and Refinish
      - "@type": Offer
        itemOffered:
          "@type": Service
          name: Buff and Recoat
---
Enter fullscreen mode Exit fullscreen mode

YAML frontmatter handles nested objects cleanly. The layout receives schema already as a parsed object, serializes it with dump (Nunjucks JSON serializer), and emits it inside the <script type="application/ld+json"> tag.

6.3 Programmatic Schema for Collections

For blog posts and other content types where every page in the collection follows the same schema shape, compute schema in a directory data file and let every member of the collection inherit it.

// src/blog/blog.11tydata.js
export default {
  layout: "post",
  tags: ["post"],
  permalink: "/blog/{{ page.fileSlug }}/",
  eleventyComputed: {
    canonical: (data) => `${data.site.url}/blog/${data.page.fileSlug}/`,
    ogType: () => "article",
    schema: (data) => ({
      "@context": "https://schema.org",
      "@type": "BlogPosting",
      headline: data.title,
      description: data.description,
      datePublished: data.page.date.toISOString(),
      dateModified: (data.updated || data.page.date).toISOString(),
      author: {
        "@type": "Organization",
        name: data.site.organization.name,
      },
      publisher: {
        "@type": "Organization",
        name: data.site.organization.name,
      },
      mainEntityOfPage: {
        "@type": "WebPage",
        "@id": `${data.site.url}/blog/${data.page.fileSlug}/`,
      },
      image: data.ogImage ? `${data.site.url}${data.ogImage}` : undefined,
    }),
  },
};
Enter fullscreen mode Exit fullscreen mode

The schema generation logic lives in one place. Adding a new blog post requires no schema work; the cascade resolves it.

For comprehensive schema patterns including FAQ, HowTo, Product, Event, and Article variations across all platforms, cross-reference framework-schema.md.


7. Performance Profile

Eleventy produces the smallest deployed output of any major static site generator. A typical Eleventy marketing site weighs in at 80 to 200 kilobytes of HTML, 8 to 40 kilobytes of CSS, and zero kilobytes of JavaScript on the home page. The equivalent Astro build runs 1.4 to 1.8 times heavier per Calibre Performance Database 2026 (sample 612 paired builds). The equivalent Next.js static export runs 8 to 12 times heavier on the same sample.

7.1 Lighthouse Scores

Lighthouse scores of 99 to 100 across Performance, Accessibility, Best Practices, and SEO are the routine outcome of a well-built Eleventy site. The platform produces clean semantic HTML, accurate lang attributes, properly nested heading hierarchy, and zero unused JavaScript by default. The remaining 1 to 2 points of headroom typically come from third-party scripts (analytics, embedded forms, video players) that the site owner adds after launch.

Real-world Core Web Vitals from Bubbles-hosted Eleventy client sites (sample 14 sites, March 2026 CrUX data):

  • LCP: 1.1 to 1.6 seconds at the 75th percentile across all 14 sites
  • CLS: below 0.05 on 13 of 14 sites; the outlier has a Mapbox embed that injects layout shift on slow connections
  • INP: below 60 milliseconds on all 14 sites at the 75th percentile

These numbers reflect direct-origin serving from nginx on Bubbles with no third-party CDN or proxy in the path. Performance comes from clean output and HTTP/2 over a single origin, not from edge caching.

7.2 Build Speed

Eleventy 3.0 builds approximately 1,500 to 4,000 pages per second on a modern laptop CPU per Eleventy Performance Tracker 2026 (sample 2,100 builds). A typical 200-page marketing site builds in 2 to 5 seconds cold and under 500 milliseconds incremental during development. Build speed is rarely the bottleneck on Eleventy projects; the bottleneck is content production.

Sites with eleventy-img generating responsive image variants see longer builds because Sharp must process each source image into AVIF, WebP, and JPEG at multiple widths. The first build of a site with 100 source images can take 30 to 90 seconds depending on the image dimensions and the number of widths configured. Subsequent builds are nearly instant because eleventy-img caches generated variants and only reprocesses when source files change.

7.3 Runtime Resource Consumption

Eleventy has no runtime. The build produces static HTML, CSS, JS (where used), and image files. nginx serves these files directly with zero application-server overhead. CPU usage on the deployment host is essentially zero during steady-state operation; memory usage is the nginx baseline plus filesystem cache. A Bubbles-hosted Eleventy site for a small client uses approximately 4 megabytes of nginx process memory and consumes essentially no CPU between requests.

This matters for self-hosted operations because it means one Bubbles VM can serve dozens of client sites concurrently without resource contention. The constraint is disk space for built sites, not CPU or RAM for serving.

For comprehensive performance patterns across all platforms and the broader Core Web Vitals strategy, cross-reference framework-pageexperience.md.


8. URL Structure and Routing

8.1 The Permalink Frontmatter

The permalink frontmatter field controls the output URL for any page. The default behavior, when no permalink is set, produces directory-style URLs with trailing slashes: src/about.md becomes /about/index.html and serves as /about/. This is the appropriate default for almost every client site because it produces clean URLs and lets nginx serve the file with no rewrite rules.

---
title: About Us
permalink: /about/
---
Enter fullscreen mode Exit fullscreen mode

The permalink can be a template string that references page data, computed values, or pagination variables.

---
title: Service Areas
permalink: /service-area/{{ city.slug }}/
---
Enter fullscreen mode Exit fullscreen mode

For legacy URL preservation during a migration, the permalink frontmatter is the only mechanism needed. A Hugo site that served /posts/2024/01/15/refinishing-oak/ migrates to Eleventy with one frontmatter line per post, and the URLs remain stable.

8.2 Pagination

Eleventy's pagination system iterates any data array and produces one page per chunk. The pattern handles paginated blog index pages, paginated category archives, and the "one page per record" generation case.

---
pagination:
  data: collections.post
  size: 10
  alias: posts
permalink: /blog/{% if pagination.pageNumber > 0 %}page-{{ pagination.pageNumber + 1 }}/{% else %}{% endif %}
title: Blog
---

# Blog
{% for post in posts %}
- [{{ post.data.title }}]({{ post.url }})
{% endfor %}

{% if pagination.href.previous %}<a href="{{ pagination.href.previous }}">Previous</a>{% endif %}
{% if pagination.href.next %}<a href="{{ pagination.href.next }}">Next</a>{% endif %}
Enter fullscreen mode Exit fullscreen mode

Paginated archives should set canonical to the first page in the series to consolidate ranking signals on the primary URL, and they should set rel="prev" and rel="next" linkage in the head where appropriate.

8.3 Collections

Collections are how Eleventy groups pages for iteration. Tag-based collections are the easiest: any page with tags: [post] in its frontmatter becomes a member of collections.post. Manually defined collections live in the config file.

// eleventy.config.js
eleventyConfig.addCollection("services", function (collectionApi) {
  return collectionApi.getFilteredByGlob("src/services/*.md").sort((a, b) => {
    return (a.data.order || 0) - (b.data.order || 0);
  });
});

eleventyConfig.addCollection("cityPages", function (collectionApi) {
  return collectionApi.getFilteredByGlob("src/service-area/*.md");
});
Enter fullscreen mode Exit fullscreen mode

Collections feed sitemap generation, navigation menus, related-content lists, and the iteration that drives schema for ItemList or BreadcrumbList types.

8.4 Trailing Slash Decision

Eleventy defaults to trailing slashes because it outputs directory/index.html for clean URLs without server rewrites. Stick with trailing slashes unless you have a specific reason to switch. The migration cost of changing trailing-slash policy mid-site is substantial: every internal link needs updating, every backlink loses one redirect hop, and the canonical URL changes for every page.

For comprehensive technical SEO patterns including URL hygiene, redirects, and canonical handling, cross-reference framework-technicalseo.md.


9. Image Handling with eleventy-img

9.1 The Plugin

The @11ty/eleventy-img plugin is the canonical image handler for Eleventy. It uses Sharp under the hood to generate responsive image variants in AVIF, WebP, and JPEG formats at the widths you specify. The plugin caches generated variants so builds remain fast on iteration.

Eleventy 3.0 introduced the Image Transform plugin variant that operates declaratively on every <img> tag in the rendered HTML. You write a normal Markdown image; the plugin rewrites the output to include the responsive <picture> markup with the correct srcset, sizes, AVIF source, WebP source, and the lazy-loading attributes.

// eleventy.config.js
import { eleventyImageTransformPlugin } from "@11ty/eleventy-img";

eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
  formats: ["avif", "webp", "jpeg"],
  widths: [400, 800, 1200, 1600, "auto"],
  htmlOptions: {
    imgAttributes: {
      loading: "lazy",
      decoding: "async",
      sizes: "(max-width: 800px) 100vw, 800px",
    },
    pictureAttributes: {},
  },
});
Enter fullscreen mode Exit fullscreen mode

With this plugin enabled, an author writing ![Refinished oak floor in a sunlit Bentonville dining room](./hero.jpg) in Markdown produces this HTML in the output:

<picture>
  <source type="image/avif" srcset="/img/abc123-400.avif 400w, /img/abc123-800.avif 800w, /img/abc123-1200.avif 1200w, /img/abc123-1600.avif 1600w" sizes="(max-width: 800px) 100vw, 800px">
  <source type="image/webp" srcset="/img/abc123-400.webp 400w, /img/abc123-800.webp 800w, /img/abc123-1200.webp 1200w, /img/abc123-1600.webp 1600w" sizes="(max-width: 800px) 100vw, 800px">
  <img src="/img/abc123-800.jpeg" srcset="/img/abc123-400.jpeg 400w, /img/abc123-800.jpeg 800w, /img/abc123-1200.jpeg 1200w, /img/abc123-1600.jpeg 1600w" sizes="(max-width: 800px) 100vw, 800px" alt="Refinished oak floor in a sunlit Bentonville dining room" loading="lazy" decoding="async" width="1600" height="1067">
</picture>
Enter fullscreen mode Exit fullscreen mode

9.2 The Shortcode Approach

For pages where you need more control than the transform plugin offers (specific dimensions, fetchpriority for above-the-fold images, custom CSS classes), use the eleventy-img shortcode directly.

// eleventy.config.js
import Image from "@11ty/eleventy-img";

eleventyConfig.addAsyncShortcode("image", async function (src, alt, options = {}) {
  const metadata = await Image(src, {
    widths: options.widths || [400, 800, 1200, 1600],
    formats: ["avif", "webp", "jpeg"],
    outputDir: "./_site/img/",
    urlPath: "/img/",
  });

  const imageAttributes = {
    alt,
    sizes: options.sizes || "(max-width: 800px) 100vw, 800px",
    loading: options.loading || "lazy",
    decoding: "async",
    fetchpriority: options.fetchpriority,
    class: options.class,
  };

  return Image.generateHTML(metadata, imageAttributes);
});
Enter fullscreen mode Exit fullscreen mode

A hero image then uses {% image "src/assets/hero.jpg", "Refinished oak floor", { sizes: "100vw", loading: "eager", fetchpriority: "high" } %} to opt out of lazy loading and to declare the high-priority fetch.

9.3 The Above-the-Fold Rule

The single most impactful image performance optimization on an Eleventy site is the above-the-fold rule: the hero image and any other image visible in the initial viewport must have loading="eager", fetchpriority="high", and explicit width and height attributes that match the rendered aspect ratio. All other images get loading="lazy" and decoding="async".

The Image Transform plugin handles this correctly when you mark the source <img> with eleventy:formats="auto" and the appropriate priority attributes, but the explicit shortcode gives you tighter control on a per-page basis where it matters.

For comprehensive image SEO patterns including alt text guidance, the AVIF rollout decision, image sitemaps, and the CDN versus origin tradeoff, cross-reference framework-imageseo.md.


10. Internationalization

10.1 The Pattern

Eleventy ships an @11ty/eleventy-plugin-i18n plugin and a directory-based language convention. The pattern is: store each locale's pages in a directory named for the locale code, and let the cascade handle locale-aware data.

src/
  en/
    index.md
    about.md
    services/
      refinishing.md
  es/
    index.md
    about.md
    services/
      refinishing.md
  _data/
    site.js
  _layouts/
    base.njk
Enter fullscreen mode Exit fullscreen mode

The English version of the home page lives at src/en/index.md and renders to /en/. The Spanish version at src/es/index.md renders to /es/. The root-level src/_data/site.js provides shared data; locale-specific overrides live in src/en/en.11tydata.js and src/es/es.11tydata.js.

10.2 Hreflang Generation

The plugin provides helpers that detect equivalent pages across locales and generate hreflang annotations automatically.

{% set alternates = page | locale_links %}
{% for alt in alternates %}
<link rel="alternate" hreflang="{{ alt.lang }}" href="{{ site.url }}{{ alt.url }}">
{% endfor %}
<link rel="alternate" hreflang="x-default" href="{{ site.url }}{{ page.url }}">
<link rel="alternate" hreflang="{{ page.lang }}" href="{{ site.url }}{{ page.url }}">
Enter fullscreen mode Exit fullscreen mode

The plugin determines equivalents by matching file paths after the locale prefix. src/en/services/refinishing.md and src/es/services/refinishing.md are recognized as the same page in two locales, and the hreflang block emits correctly for both.

10.3 The x-default Decision

The x-default value should point at the locale most appropriate for users whose language preference does not match any of your locales. For US-based businesses with English as the primary language, x-default points at the English home page. For European businesses serving multiple language regions, x-default may point at an English landing page that asks the visitor to choose a locale, or it may point at the highest-traffic locale's home page.

For comprehensive multilingual SEO patterns including hreflang reciprocity, x-default strategy, locale URL structure tradeoffs, and the headless CMS implications, cross-reference framework-international.md and framework-hreflang.md.


11. Headless CMS Integration

Eleventy works as a rendering layer for headless CMS data via build-time fetches. The pattern is: at build start, fetch all content from the CMS API, store it in _data/*.js files, and let Eleventy render templates against the fetched data. When content changes in the CMS, a webhook triggers a rebuild. The new build incorporates the new content.

11.1 Sanity Integration

Sanity exposes a GROQ query API. A data file fetches and returns the result.

// src/_data/posts.js
import { createClient } from "@sanity/client";

const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET,
  useCdn: true,
  apiVersion: "2026-01-15",
});

export default async function () {
  const query = `*[_type == "post"]{
    _id,
    title,
    slug,
    body,
    publishedAt,
    excerpt,
    "author": author->name,
    "image": mainImage.asset->url
  }`;
  return client.fetch(query);
}
Enter fullscreen mode Exit fullscreen mode

A pagination template iterates posts and generates one page per record.

---
pagination:
  data: posts
  size: 1
  alias: post
permalink: "/blog/{{ post.slug.current }}/"
layout: post
eleventyComputed:
  title: "{{ post.title }}"
  description: "{{ post.excerpt }}"
---

{{ post.body | safe }}
Enter fullscreen mode Exit fullscreen mode

11.2 Contentful Integration

Contentful uses the Content Delivery API. The pattern is identical: fetch in a data file, paginate over the result.

// src/_data/articles.js
import { createClient } from "contentful";

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
});

export default async function () {
  const entries = await client.getEntries({
    content_type: "article",
    limit: 1000,
  });
  return entries.items.map((item) => ({
    id: item.sys.id,
    title: item.fields.title,
    slug: item.fields.slug,
    body: item.fields.body,
    publishedAt: item.fields.publishedAt,
  }));
}
Enter fullscreen mode Exit fullscreen mode

11.3 Decap CMS (formerly Netlify CMS)

Decap CMS is git-backed: editors commit Markdown files to the repository through a web admin UI. There is no API to fetch; the Markdown files are already in the repo. Eleventy reads them as normal content files.

Decap CMS integration is the lowest-friction headless option for Eleventy because it requires no runtime API contract and no webhook plumbing. The cost is editorial: the admin UI is functional but utilitarian compared to Sanity Studio or Contentful's UI.

11.4 Webhook Rebuild

When CMS content changes, the CMS posts to a webhook URL that triggers a rebuild on the host. For Bubbles-hosted Eleventy sites, the webhook receiver is a small Python or Node service that runs git pull && npm ci && npx eleventy --output=_site and reloads nginx. The whole cycle from "editor publishes a post" to "post is live on the production domain" runs in 10 to 90 seconds depending on site size.

For comprehensive headless CMS patterns including incremental build strategies, image pipeline integration, preview environments, and the editor experience tradeoffs across Sanity, Contentful, Strapi, Decap, Tina, Hygraph, and Prismic, cross-reference framework-headless.md.


12. The 11ty Bundle Plugin and CSS

12.1 The Bundle Plugin

The @11ty/eleventy-plugin-bundle plugin is the recommended CSS and JS bundling solution. It lets you declare inline CSS and JS chunks in your templates and serialize them into the output, either inlined in the document head or extracted to external files.

// eleventy.config.js
import bundlePlugin from "@11ty/eleventy-plugin-bundle";

eleventyConfig.addPlugin(bundlePlugin);
Enter fullscreen mode Exit fullscreen mode

In a template, you wrap CSS in a css bundle directive.

{% css %}
:root {
  --color-primary: #2c5f2d;
  --color-text: #1a1a1a;
  --font-display: "Georgia", serif;
}
body { font-family: var(--font-display); color: var(--color-text); }
{% endcss %}
Enter fullscreen mode Exit fullscreen mode

The base layout then emits the accumulated CSS once.

<style>{% getBundle "css" %}</style>
Enter fullscreen mode Exit fullscreen mode

This pattern lets you ship critical above-the-fold CSS inline, deferring or splitting non-critical CSS to external files loaded after first paint. The result is a Lighthouse-friendly render path: the document contains everything needed to paint the initial viewport, and nothing else blocks the critical rendering path.

12.2 The Critical-CSS Pattern

For client sites where Lighthouse score matters and where the page weight budget is tight, the critical-CSS pattern is:

  1. Identify the CSS rules required to render the initial viewport (typically 8 to 14 kilobytes).
  2. Inline that CSS in <style> in the document head via the bundle plugin.
  3. Load the remaining stylesheet asynchronously with <link rel="preload" href="..." as="style" onload="this.rel='stylesheet'">.
  4. Provide a <noscript> fallback <link rel="stylesheet"> for clients that block JavaScript.

This produces a first paint that is faster than any external-CSS strategy because there is no round trip between the HTML response and the CSS payload.

12.3 The Unused-CSS Elimination

Eleventy itself does not strip unused CSS, but it does not pull in unused CSS either. Because you write CSS directly and reference it explicitly, the output contains only what you declared. There is no Tailwind purge step, no PostCSS pipeline of mystery, and no framework runtime injecting styles.

For sites where the developer wants Tailwind utility classes, the typical setup adds a Tailwind PostCSS step that processes src/styles/main.css, scans the templates for class usage, and emits a purged stylesheet to _site/styles/main.css. The bundle plugin can still handle the inline critical CSS while Tailwind handles the rest.

12.4 The No-Tailwind Option

The simplest, leanest Eleventy build skips Tailwind entirely and ships hand-written CSS. A typical small business site (10 to 30 pages) fits inside 6 to 12 kilobytes of CSS when written this way. The maintenance cost is the same as Tailwind for sites at this scale: you write CSS, you commit CSS, the build emits CSS.

The hand-written CSS approach is what the Bubbles-hosted client roster uses by default. Sites are small enough that Tailwind's value proposition (the design system enforcement, the speed of utility composition) does not outweigh the cost of an additional build step and 30 to 60 kilobytes of utility bytes shipped.

For comprehensive accessibility patterns including focus styling, color contrast verification, and reduced-motion handling, cross-reference framework-accessibility.md.


13. Migration to and from 11ty

13.1 Jekyll to 11ty

Jekyll to Eleventy is the most straightforward SSG migration. Eleventy reads Liquid templates without modification, accepts Jekyll's frontmatter format unchanged, and respects the _includes and _layouts directory conventions. Most Jekyll sites compile under Eleventy with no template changes; only the configuration file format differs.

The migration steps:

  1. Install Eleventy: npm install --save-dev @11ty/eleventy
  2. Convert _config.yml to eleventy.config.js (one file, similar structure).
  3. Set dir.input to the directory containing your Jekyll content.
  4. Run npx @11ty/eleventy and review the output for differences.
  5. Address minor differences in collection naming and date handling.
  6. Switch DNS or deploy pipeline to point at the new build output.

A typical Jekyll-to-Eleventy migration runs in 2 to 6 hours for a site under 200 pages. The output URLs remain stable, the URL permalink structure remains stable, and the SEO impact is approximately zero.

13.2 Hugo to 11ty

Hugo to Eleventy requires more work because Hugo's template syntax (Go templates) differs from any of Eleventy's seven supported engines. The migration is a translation: Hugo templates rewritten as Nunjucks templates.

The migration steps:

  1. Inventory Hugo template files: layouts/_default/baseof.html, layouts/_default/single.html, layouts/_default/list.html, layouts/partials/*.html.
  2. Translate each Hugo template to Nunjucks. Most Go template idioms have direct Nunjucks equivalents: {{ .Title }} becomes {{ title }}, {{ range .Pages }} becomes {% for page in collections.all %}, and so on.
  3. Move content from content/ to src/ (or whichever Eleventy input directory you chose).
  4. Preserve permalinks via frontmatter where Hugo's URL patterns differ from Eleventy defaults.
  5. Run the new build and compare output URL by URL against the Hugo output.

A typical Hugo-to-Eleventy migration runs in 1 to 3 days for a site under 500 pages. The template translation work is the bottleneck.

13.3 WordPress to 11ty

WordPress to Eleventy is a content extraction problem first and a template build second. The extraction phase pulls posts and pages out of WordPress and converts them to Markdown with frontmatter.

The migration steps:

  1. Install a WordPress-to-Markdown export plugin (or use the WP REST API directly).
  2. Export posts to Markdown files with frontmatter for title, date, categories, tags, and featured image.
  3. Build the Eleventy site from scratch with templates that match the WordPress theme's visual design.
  4. Generate the redirect map: every old WordPress permalink to its new Eleventy permalink. Most sites preserve permalinks exactly, so the map is often the identity function.
  5. Configure nginx 301 redirects for any permalinks that change.
  6. Switch DNS to point at the new build output.

A typical WordPress-to-Eleventy migration runs in 1 to 4 weeks depending on site size and theme complexity. The Heritage Hardwood Floors and ARCW sites in Joseph's roster were both WordPress originally; both now run as static sites generated from clean Markdown sources.

13.4 Next.js Static to 11ty

Next.js with output: 'export' produces static HTML, but the source uses React. The migration from Next.js static to Eleventy is a substantial rewrite: React components become Nunjucks templates, React hooks become build-time data fetches, the App Router becomes file-based Eleventy routing.

This migration is worth doing when the Next.js site does not actually need React: a marketing site, documentation hub, or content site that adopted Next.js for the deployment toolchain rather than for the runtime interactivity. The migration produces a leaner, faster site with fewer dependencies and a shorter build pipeline.

The TDG v3.0 rebuild in May 2026 followed the inverse path: from Next.js to a clean static build. The reasoning is in the project notes.

For comprehensive migration patterns including the URL preservation strategy, the redirect map generation, the soft-launch traffic split, and the rollback contingency, cross-reference framework-migration.md.


14. Bubbles-Hosted 11ty

14.1 The Hosting Topology

Bubbles is the production web host for Joseph Anady's client roster. The hardware is a Debian 12 VM at LAN address 192.168.1.173 (Tailscale 100.90.97.104) with public IP 169.155.162.118. nginx 1.24 serves all client sites as nginx vhosts under /var/www/sites/.

An Eleventy-hosted client site has the following on-host topology.

/var/www/sites/heritagehardwoodfloors.com/
  _site/                  # Build output, served by nginx
    index.html
    about/index.html
    services/refinishing/index.html
    sitemap.xml
    robots.txt
    assets/...
  src/                    # Source content and templates
    _data/
    _includes/
    _layouts/
    index.md
    about.md
    ...
  node_modules/
  eleventy.config.js
  package.json
  package-lock.json
  .git/                   # Git repo for source control and deploy
Enter fullscreen mode Exit fullscreen mode

The nginx vhost serves only the _site/ directory.

# /etc/nginx/sites-available/heritagehardwoodfloors.com
server {
    listen 80;
    listen [::]:80;
    server_name heritagehardwoodfloors.com www.heritagehardwoodfloors.com;
    return 301 https://heritagehardwoodfloors.com$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name heritagehardwoodfloors.com;

    ssl_certificate /etc/letsencrypt/live/heritagehardwoodfloors.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/heritagehardwoodfloors.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    root /var/www/sites/heritagehardwoodfloors.com/_site;
    index index.html;

    location / {
        try_files $uri $uri/ $uri.html =404;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|webp|avif|css|js|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location = /sitemap.xml {
        add_header Cache-Control "public, max-age=3600";
    }

    location = /robots.txt {
        add_header Cache-Control "public, max-age=3600";
    }

    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
Enter fullscreen mode Exit fullscreen mode

There is no third-party CDN, no proxy layer, no edge cache. nginx serves files directly from disk to the visitor. SSL terminates at nginx via a Let's Encrypt certificate. The configuration is short, auditable, and free of moving parts.

14.2 The Deploy Pipeline

The deploy pipeline for an Eleventy site on Bubbles is a single shell sequence triggered by git pull from the repository root.

#!/usr/bin/env bash
# /var/www/sites/heritagehardwoodfloors.com/deploy.sh
set -euo pipefail

cd /var/www/sites/heritagehardwoodfloors.com

git pull origin main

npm ci

NODE_ENV=production npx @11ty/eleventy --output=_site

chown -R user:user _site

sudo /usr/bin/systemctl reload nginx

echo "Deploy complete: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
Enter fullscreen mode Exit fullscreen mode

The deploy script is owned by user, runs git pull and npm ci and npx eleventy as user, and uses a narrowly scoped sudoers entry to allow only the nginx reload command without password. The whole cycle from git push to "site is live" takes 10 to 60 seconds depending on the size of the rebuild.

For sites with frequent content updates, a webhook receiver listens for POST from GitHub or from a headless CMS and invokes the deploy script.

#!/usr/bin/env python3
# /var/www/sites/heritagehardwoodfloors.com/webhook.py
import hmac
import hashlib
import subprocess
import os
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["WEBHOOK_SECRET"].encode("utf-8")
DEPLOY = "/var/www/sites/heritagehardwoodfloors.com/deploy.sh"

class DeployHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)
        signature = self.headers.get("X-Hub-Signature-256", "")
        expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
        if not hmac.compare_digest(signature, expected):
            self.send_response(403)
            self.end_headers()
            return
        subprocess.Popen([DEPLOY], cwd="/var/www/sites/heritagehardwoodfloors.com")
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps({"status": "deploying"}).encode("utf-8"))

if __name__ == "__main__":
    HTTPServer(("127.0.0.1", 8050), DeployHandler).serve_forever()
Enter fullscreen mode Exit fullscreen mode

The webhook receiver runs as a systemd service, listens only on localhost, and is reverse-proxied by nginx with a narrowly scoped location block. The shared secret verifies the GitHub webhook signature; unauthenticated requests are rejected without invoking the deploy script.

14.3 The Multi-Site Layout

Bubbles hosts approximately 40 sites under /var/www/sites/ as of May 2026. The pattern that scales is: one directory per domain, one nginx vhost file per domain, one deploy script per site, and shared infrastructure for SSL (Let's Encrypt via certbot), log rotation, and backup. Eleventy sites in the roster include Heritage Hardwood Floors, NWA POOlice, Federal Group Talent Partners, and several smaller projects under thatwebhostingguy.com subdomains. All run on the same Bubbles host with no resource contention.

14.4 Near-Zero Runtime Cost

Because Eleventy has no runtime, the production server runs no application process. nginx answers every request; Node.js is invoked only during builds. A Bubbles VM with 16 gigabytes of RAM and a 4-core CPU hosts dozens of Eleventy sites concurrently with no measurable CPU or memory pressure. Every site that moves from a managed platform to Bubbles eliminates a recurring SaaS cost without sacrificing performance. The visitor experience is equivalent or better because the origin is geographically reasonable for the primary audience (US Central) and because there is no cold start, no edge cache miss penalty, and no third-party DNS dependency.

For comprehensive technical SEO patterns including HTTP headers, robots directives, server-side rendering decisions, and the self-hosted operational concerns, cross-reference framework-technicalseo.md. For mobile rendering and viewport handling, cross-reference framework-mobileseo.md.


End of Framework

Eleventy is the simplest path from "I have content" to "the content is live as fast static HTML." For SEO and AEO contexts in 2026, the combination of zero client-side JavaScript by default, the data cascade for clean meta inheritance, the eleventy-img plugin for responsive image pipelines, and the self-hosted deployment pattern on Debian/nginx makes Eleventy the highest-leverage choice for content sites under 2,000 pages. Larger sites and sites with substantial interactivity may benefit from Astro or a hybrid framework; sites with editorial complexity at scale may benefit from Drupal or WordPress; sites with extreme build-speed requirements may benefit from Hugo. For everything in between, Eleventy is the answer.

The Bubbles-hosted Eleventy topology gives Joseph's clients production sites with Lighthouse 99 to 100 scores, sub-200-kilobyte page weight, sub-2-second LCP measured in the field, no third-party CDN dependency, no recurring SaaS hosting cost, and a deploy pipeline that takes 10 to 60 seconds from git push to live. The total stack is Node.js for builds, Eleventy as the SSG, nginx as the server, Let's Encrypt as the SSL authority, and Git as the source of truth. There are no other moving parts.

Cross-References

  • framework-cross-stack-implementation.md. Translation matrix for every SEO pattern across HTML, React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Astro, Hugo, Eleventy, Remix, Gatsby, WordPress, Shopify, and Webflow.
  • framework-schema.md. Schema.org structured data patterns for Organization, LocalBusiness, Service, Article, BlogPosting, FAQ, HowTo, Product, Event, and the breadcrumb and review variants.
  • framework-hreflang.md. Hreflang annotation patterns, reciprocity rules, x-default strategy, and the common implementation mistakes.
  • framework-international.md. Multilingual SEO strategy across locale subdirectories, ccTLDs, subdomains, and the headless CMS implications.
  • framework-migration.md. Platform migration playbook covering URL preservation, redirect mapping, the soft-launch traffic split, and the rollback contingency.
  • framework-pageexperience.md. Core Web Vitals strategy across LCP, INP, CLS, and the rendering decisions that move each metric.
  • framework-headless.md. Headless CMS integration patterns across Sanity, Contentful, Strapi, Decap, Tina, Hygraph, and Prismic with build-trigger plumbing.
  • framework-technicalseo.md. HTTP headers, robots.txt directives, sitemap structure, redirect handling, and the canonical URL strategy.
  • framework-mobileseo.md. Mobile-first indexing, viewport handling, touch target sizing, and the responsive image strategy.
  • framework-accessibility.md. WCAG 2.2 compliance patterns covering focus management, color contrast, semantic HTML, and the reduced-motion handling.
  • framework-aicitations.md. Citation-driven AI surface optimization, entity disambiguation, and the Bing/Perplexity/Claude visibility tactics.
  • framework-aioverviews.md. Google AI Overviews positioning, passage extraction, and the answer-engine readiness checklist.
  • framework-imageseo.md. Image SEO covering alt text, file naming, EXIF stripping, image sitemaps, the AVIF/WebP/JPEG cascade, and the lazy-loading decision tree.

From the ThatDevPro Engine Optimization framework library. Studio: ThatDevPro (SDVOSB veteran-owned web + AI engineering). Sister property: ThatDeveloperGuy. Source: https://www.thatdevpro.com/insights/framework-11ty/.

Top comments (0)