Table of Contents

Customise the assets pipeline

When the fallback configs in the assets module are not enough, drop your own in. PostCSS for CSS, esbuild for JS. The module picks them up.


Prerequisites

  • Baseline registered in eleventy.config.js.
  • Default entries in place: src/assets/css/index.css and src/assets/js/index.js.
  • package.json with "type": "module" and scripts (start, build).

PostCSS config

A postcss.config.js at the project root replaces Baseline's fallback wholesale. Bring the plugins you want, gate cssnano on production, and check the output.

Install PostCSS and the plugins you plan to use

npm install postcss postcss-import postcss-preset-env cssnano postcss-mixins

Once you ship your own postcss.config.js, the fallback plugins are gone. Whatever you list here has to be installed explicitly.


Add postcss.config.js at the project root

import postcssImport from 'postcss-import';
import postcssMixins from 'postcss-mixins';
import postcssPresetEnv from 'postcss-preset-env';
import cssnano from 'cssnano';

const isProd = process.env.ELEVENTY_ENV === 'production';

const plugins = [postcssImport(), postcssMixins(), postcssPresetEnv({ stage: 1 })];

if (isProd) {
	plugins.push(cssnano({ preset: 'default' }));
}

/** @type {import('postcss-load-config').Config} */
export default {
	map: isProd ? false : { inline: true },
	plugins
};

Exercise the config in your CSS

src/assets/css/base.css: whatever you already have in index.css, moved across unchanged. The assets pipeline guide has a starting point if you need one.

src/assets/css/mixins.css:

@define-mixin linksMixin {
	color: #0f172a;
	text-decoration: underline;
	text-decoration-color: #fc35b9;
	text-decoration-thickness: 2px;
	text-underline-offset: 3px;
	font-weight: 600;
}

src/assets/css/links.css:

a {
	@mixin linksMixin;
}

a:hover {
	color: #fc35b9;
}

src/assets/css/index.css:

@import './base.css';
@import './mixins.css';
@import './links.css';

Run and inspect

  • Dev: npm start
  • Build: npm run build
  • Check dist/assets/css/index.css:
    • Dev: expanded CSS with mapped imports; no minification.
    • Prod: minified (cssnano), imports flattened, custom media resolved.

Notes

  • Your postcss.config.js fully overrides Baseline's fallback. Keep the entry file at src/assets/css/index.css and add partials via imports.
  • Reach for what you need (purge, RTL, lightningcss, others). Env guards are the cleanest way to keep dev output readable and production output optimised.
  • Glob imports only work in the entry file. If you use postcss-import-ext-glob, @import-glob './components/*.css' resolves in index.css and is ignored in anything imported from it. The plugin implements PostCSS's Once(root) hook, so it runs on the entry file and never sees the partials postcss-import inlines afterwards. Nothing errors: the line is left as written and the files it named are absent from the output.
  • CSS files are not templates. Baseline does not render asset sources, so {{ ... }} or {% ... %} in a .css file reaches the browser exactly as typed. If a value has to come from data, set a custom property in the head instead.

Esbuild targets

The JS pipeline takes more nudging than the CSS one. You may want extra entry points, different targets, or a one-off inline bundle, and esbuild is the lever for all three.


Install and pin esbuild explicitly

Baseline depends on esbuild already, but esbuild's own docs recommend pinning the version you build against:

npm install --save-exact --save-dev esbuild

Add more entry points

An entry point is the file Baseline hands to esbuild as the start of a bundle. Each index.js under src/assets/js/**/ is one entry point and produces one bundle in dist/assets/js/:

mkdir -p src/assets/js/home src/assets/js/about
echo "console.log('home bundle');" > src/assets/js/home/index.js
echo "console.log('about bundle');" > src/assets/js/about/index.js

Reference them from a page's front matter:

---
title: 'Hello Baseline'
description: 'A minimal Eleventy page powered by eleventy-plugin-baseline.'
permalink: '/'
layout: 'layouts/base.njk'
head:
  script:
    - src: '/assets/js/home/index.js'
      defer: true
---

Inline a small script

For tiny helpers, inline the bundled output with Baseline's inlineESbuild filter. _baseline.paths.assets is the resolved input path to your assets directory; concatenating the relative path keeps the example portable:

{% set jsPath = _baseline.paths.assets ~ "js/about/index.js" %}
{{ jsPath | inlineESbuild | safe }}

This bundles and minifies the file with esbuild's es2020 target, then injects a <script>...</script> tag.


Set esbuild options for the compiled bundles

Pass assets.esbuild options when you register Baseline. They apply to every index.js Baseline compiles, and only to those: the inlineESbuild filter goes straight to the processor and keeps its own defaults.

The object reaches esbuild whole, so define, loader, banner, external and the rest all work. minify, target and bundle come with a Baseline default you can overrule; entryPoints and write are Baseline's and are applied after yours.

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

/** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
export default async function (eleventyConfig) {
	await eleventyConfig.addPlugin(
		baseline(settings, {
			assets: {
				esbuild: {
					minify: process.env.ELEVENTY_ENV === 'production',
					target: 'es2017'
				}
			}
		})
	);
}

export const config = baselineConfig;

Override target or minification for one inline script

For one-offs, pass options directly to the filter. They merge over the processor's own defaults (minify: true, target: 'es2020', bundle: true), which is what the filter starts from whatever you set on assets.esbuild:

{% set jsPath = _baseline.paths.assets ~ "js/about/index.js" %}
{{ jsPath | inlineESbuild({ minify: false, target: "es2018" }) | safe }}

Attributes for the tag go in the same object, under attributes. They are taken out before the rest reaches esbuild:

{{ jsPath | inlineESbuild({ attributes: { type: "module", defer: true } }) | safe }}

inlinePostCSS takes the same key and nothing else:

{{ cssPath | inlinePostCSS({ attributes: { media: "print" } }) | safe }}
  1. Verify.
    • Dev: npm start
    • Build: npm run build
    • Check dist/assets/js/**/index.js for bundled output and size.
    • For inlined scripts, view the page source and confirm the bundled code is present.

Notes

  • Inline only for small helpers; keep larger bundles as external files so they cache.
  • A global override is yours to maintain. Revisit it whenever your browser support changes.
  • If you reach for the same inline overrides repeatedly, write your own filter that calls inlineESbuild with preset options and use that filter instead.
  • Do not unwrap the tag to add an attribute. striptags removes anything tag-shaped and minified JS is dense with < followed by a letter, so it cuts expressions mid-statement and leaves a green build with a broken page. attributes exists so that never has to be the move.
  • JS files are not templates either. Neither the inline filter nor the entry-point compile renders them, so {{ site.title }} in a .js file ships to the browser verbatim and fails there. Pass the value in from the template instead, as a data- attribute or a small inline <script> the bundle reads.

See also