EnglishNederlandsFrançais Toggle theme

Eleventy Baseline

Start building your site, skip the recurring setup work.
Table of Contents

Project structure

A short list of conventions that keep Baseline out of your way. None of these are arbitrary. Each one is somewhere the plugin assumes you'll do a particular thing, and gets quieter and more useful when you do.

If you've read the overview and the concepts page, you already have the shape. This page is the practical companion: where files go, how the call looks, which environment variables matter, and the few seams Baseline expects to own.


The shape of a Baseline project

Baseline expects a small, predictable layout. You can change it. The cost is real.

your-site/
  eleventy.config.js     # plugin call + config re-export
  package.json           # "type": "module" — Baseline is ESM
  .env                   # ELEVENTY_ENV, URL
  src/
    _data/
      settings.js        # site identity (read by head, sitemap, multilang)
    _includes/
      layouts/
        base.njk         # contains <baseline-head>
    assets/
      css/index.css      # the one CSS entry point; @import the rest
      js/index.js        # the one JS entry point; import the rest
    content/             # your pages
    static/              # passthrough-copied to the site root
  dist/                  # build output

Three things are non-negotiable, or close to it:

  • ESM. package.json carries "type": "module".
  • Node 20.15.0 or newer. Older versions work for a while, then suddenly don't.
  • src/ for input, dist/ for output. Baseline ships a directory configuration that says so.

Everything else has a default. The closer you stay, the less you have to think about.


The plugin call

The plugin call has a specific shape. Use this form, even on small projects.

import baseline, { config as baselineConfig } from '@apleasantview/eleventy-plugin-baseline';
import settings from './src/_data/settings.js';

export default function (eleventyConfig) {
	eleventyConfig.addPlugin(
		baseline(settings, {
			sitemap: true,
			navigator: true
		})
	);
}

export const config = baselineConfig;

Two arguments: site identity, then runtime behaviour. The config re-export at the bottom is the unusual part. Eleventy reads its directory configuration before any plugin runs, so Baseline ships that block as a separate export and asks you to forward it. Skip the re-export and the virtual directories Baseline registers (assets, public) won't be there, and a few modules will get quietly confused. The config-export reference has the full version.


Settings (src/_data/settings.js)

Settings is site identity. The head module reads it, so do canonical resolution, sitemap URLs, and the multilingual machinery. By convention it lives at src/_data/settings.js so the same object is also available in templates as settings.

export default {
	title: '', // <title> composition, og:site_name, navigator
	tagline: '', // home page suffix, default meta description
	url: process.env.URL, // absolute, with protocol; anchors canonical and sitemap URLs
	noindex: false, // site-wide robots: noindex switch
	defaultLanguage: 'en', // BCP47 code; required for multilingual mode
	languages: {}, // map of lang to {}; required for multilingual mode
	head: {
		// Site-wide head extras. Each entry's keys become attributes on
		// the rendered tag. Page-level extras belong in front matter.
		link: [{ rel: 'stylesheet', href: '/assets/css/index.css' }],
		script: [{ src: '/assets/js/index.js', defer: true }],
		meta: [],
		style: []
	}
};

A few things worth knowing upfront:

  • url should be absolute, with protocol. Baseline warns once at startup if it is missing and falls back to relative URLs everywhere.
  • Head extras live in settings.head. There is no _data/head.js to maintain.
  • Every key is optional. Wrong types are logged but don't throw.

Settings is identity. Module behaviour goes in options. The full shape is in the site-settings reference.


Options (defaults you usually keep)

The shipped defaults are deliberate. Most projects don't change them.

Option Default What it does
verbose false Turns on info-level logs. Warnings and errors emit either way.
multilingual false Activates the multilang module (with the prerequisites below).
sitemap true Generates /sitemap.xml, or per-language sitemaps in multilingual mode.
navigator dev only Debug surface. Filters and globals; the virtual page is dev-only.
assets.esbuild {} Forwarded to esbuild on every entry-point build.
head.titleSeparator ' - ' Sits between page title and site title in <title>.
head.showGenerator false Adds <meta name="generator"> if you want Baseline credited.

The assets and head modules are always on. You can tune them; you cannot switch them off. The user-facing key is multilingual even though the module is named multilang. That is on purpose.


The head placeholder

<baseline-head> is the one element you write yourself. Drop it where the <head> element would normally go. Baseline replaces it wholesale.

<!doctype html>
<html lang="{{ page.locale.lang or settings.defaultLanguage }}">
<baseline-head></baseline-head>      {# replaces the entire <head> element #}
<body>
	{% block content %}{% endblock %}
</body>
</html>

The placeholder is the <head>, not a tag inside it. Baseline emits the open and close, and everything that belongs in there. So don't hand-write:

  • <title>
  • <meta charset>
  • <meta viewport>
  • canonical link

Page-specific extras come from front matter. Site-wide extras come from settings.head. The output is sorted via capo and deduped on the way out.


Assets

Two entry points. That's it.

src/assets/css/index.css
src/assets/js/index.js

Reach the rest through @import and import from there. Subfolders with their own index.js or index.css are picked up as separate bundles too, which is handy when one page wants its own JS. Anything not named index is skipped silently by the compile guard. This is intentional, not an oversight.

Compiled output lands under dist/assets/, mirroring the input layout. So src/assets/css/index.css becomes /assets/css/index.css at the served URL.

Loading them is on you.

Baseline compiles, but it does not inject <link> or <script> tags for the bundles. Reference each one in settings.head.link or settings.head.script (see the example above) and the head module will emit the tag on every page. Skip this and the bundles compile but never load.

Tuning the processors

esbuild defaults to minify: true, target: 'es2020'. Override through options.assets.esbuild; values are merged on top of the defaults.

baseline(settings, {
	assets: {
		esbuild: {
			minify: process.env.ELEVENTY_ENV === 'production',
			target: 'es2017'
		}
	}
});

PostCSS has no plugin-level options. Configure it through your own config file: postcss-load-config picks up postcss.config.{js,cjs,mjs}, .postcssrc, or a postcss block in package.json. Without one, the bundled fallback runs (postcss-import, postcss-preset-env, and cssnano in production).

The resolved PostCSS config is cached for the lifetime of the process. Restart Eleventy to pick up changes to postcss.config.js. The Customise the assets pipeline how-to has the full recipe.


Static files

Two names for the same place, on purpose.

  • On disk: src/static/.
  • In templates and config: public. Available as _baseline.paths.public.

The folder name follows the convention you already have. The virtual key matches what most static-site generators call this directory. Drop favicons, robots.txt, downloadable assets, anything else that should land at a known URL without processing.


Eleventy environment variables

Two environment variables steer build behaviour. One is set for you; one is yours to set.

Variable Who sets it What it flips
ELEVENTY_RUN_MODE Eleventy Drafts (dropped on build), image shortcode transformOnRequest
ELEVENTY_ENV You Navigator template, PostCSS minification

ELEVENTY_RUN_MODE is filled in automatically: eleventy --serve makes it serve; eleventy makes it build. Don't override it. If you find yourself wanting to, that is a signal something else is wrong.

ELEVENTY_ENV is yours. Set it in .env for development, and on the production command via cross-env. The shape most projects land on:

{
	"scripts": {
		"dev": "rimraf dist/ && npx @11ty/eleventy --serve",
		"build": "rimraf dist/ && cross-env ELEVENTY_ENV=production npx @11ty/eleventy"
	}
}
# .env
ELEVENTY_ENV=development
URL=http://localhost:8080/

Multilingual activation

The multilang module activates only when all three of these are present:

  • multilingual: true in options
  • defaultLanguage set to a real BCP47 code in settings
  • languages is a non-empty map in settings

Miss one and the module quietly bails with an info log. Half-configured multilingual is worse than single-language; the silence is on purpose. The multilingual tutorial walks through the full setup.


Things Baseline expects to own

These are the seams where fighting the plugin costs the most. If you find yourself working around one, the docs are usually the cheaper place to look first.

  • The <head> element. It is <baseline-head>. Don't hand-write the head.
  • Asset entry-point names. index.js and index.css. Renaming them to main.js is fighting.
  • Directory configuration. Re-export config. Don't override dir.input, dir.output, dir.includes, dir.data.
  • Sitemap generation. It is on by default and reads page front matter for per-page control (sitemap: { ignore, changefreq, priority }). Disable the module rather than registering your own at the same URL.
  • The page context. _pageContext is populated by Baseline at cascade time and consumed by the head module at transform time. Read it; don't try to mutate it.
  • The translation map. Same. Read through the i18n* filters; don't recompute.

Stay close to the shape of the project and Baseline mostly disappears.

See also