Eleventy notes theme

Eleventy notes theme is a reusable Eleventy 3.x base theme packaging layouts, CSS, client-side JS, webfonts and an Eleventy plugin that wires them into a consuming site. Sites install it as a dependency, override any template or design token, and add their own templates and styles alongside it.

https://github.com/stephen-cox/eleventy-notes-theme

The problem

Once you have more than one Eleventy site sharing a look and feel, you need somewhere for the shared parts to live. Eleventy has no theme system in the way that WordPress or Drupal do; there is no package type the build knows how to find layouts and assets in, and no defined override order between a theme's templates and a site's own.

The usual answer is a starter kit. You build a site, strip it back, and use it as the starting point for the next one. That works for the first day and then stops working, because the starter is a snapshot rather than a dependency. Fix a bug in the header markup or tighten up the focus styles, and the fix exists in whichever site you were working on. Getting it into the other sites means finding the equivalent files, hand-merging the change around whatever each site has since customised, and hoping nothing was missed. The sites drift apart, and the drift compounds: the longer since the last sync, the harder the next one is, so the syncs get rarer.

How this solves it

The theme is a real package, consumed straight from the repository as a git dependency, with no npm publishing involved:

"dependencies": {
  "@src-dev/eleventy-notes-theme": "github:stephen-cox/eleventy-notes-theme#semver:^0.1"
}

Releases are tagged in the theme repository and each site upgrades deliberately with npm update, so a theme change reaches a site only when the site takes it, and the site's visual regression tests vet the upgrade before it lands. Shared fixes are made once, in one place, and there is no hand-merging.

The plugin does the wiring that Eleventy does not do itself:

const notesTheme = require('@src-dev/eleventy-notes-theme');

module.exports = function (eleventyConfig) {
  eleventyConfig.addPlugin(notesTheme, {
    contentGlob: './src/**/*.md',
  });
};

It registers base.njk and page.njk as virtual templates in the site's includes directory, adds a Nunjucks loader that searches the site's includes directory first and the theme's layouts/ second, and supplies includes and macros (header, footer, breadcrumb, card), filters (formatDate, trim), shortcodes (socialImage, year), the pages and noIndex collections, the configured markdown-it instance, eleventy-navigation, an environment global, and passthrough-copied webfonts.

Overriding

The loader order is what makes the theme usable rather than restrictive. Create a file with the same name in the site's includes directory and it wins over the theme's copy, for both front matter layouts and {% extends %} / {% include %} lookups, so src/_layouts/includes/header.njk replaces the theme header. Site-only layouts live in the site as normal and can extend base.njk into the theme.

CSS follows the same pattern through design tokens. Everything in css/base/variables.css is a custom property, so a site can import the whole theme and redefine the few tokens it cares about, or import the individual files in the canonical order from css/theme.css to interleave its own styles at exact points in the cascade. Client-side JS is plain CommonJS for the site's webpack bundle.

In exchange, sites are expected to provide a small set of globals (site.title, site.alt_title, site.site_url, site.twitter, optionally site.gtm_id), a favicon copied to /_assets/images/favicon.svg, and the assetLink / scriptLink shortcodes from @src-dev/eleventy-template-asset-pipeline.


Links

  • [[2026-W38]]