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.cssandsrc/assets/js/index.js. package.jsonwith"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.jsfully overrides Baseline's fallback. Keep the entry file atsrc/assets/css/index.cssand 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 inindex.cssand is ignored in anything imported from it. The plugin implements PostCSS'sOnce(root)hook, so it runs on the entry file and never sees the partialspostcss-importinlines 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.cssfile 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 }}
- Verify.
- Dev:
npm start - Build:
npm run build - Check
dist/assets/js/**/index.jsfor bundled output and size. - For inlined scripts, view the page source and confirm the bundled code is present.
- Dev:
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
inlineESbuildwith preset options and use that filter instead. - Do not unwrap the tag to add an attribute.
striptagsremoves 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.attributesexists 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.jsfile ships to the browser verbatim and fails there. Pass the value in from the template instead, as adata-attribute or a small inline<script>the bundle reads.