Project structure
The folder structure that keeps Baseline out of your way. If you've read what Baseline is and how Baseline works, you already have the shape. This page is the practical companion: where files go, 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 but the cost is real.
your-site/
eleventy.config.js # plugin call + config re-export
package.json # "type": "module", Baseline is ESM
.env # ELEVENTY_ENV, BASELINE_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.jsoncarries"type": "module". - Node 22 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.
Settings (src/_data/settings.js)
Settings is site identity: title, tagline, url, languages, head extras. What matters structurally is where the file sits. Putting it at src/_data/settings.js means Eleventy also picks it up as global data, so the same object the plugin reads is available in every template as settings. One definition, two audiences.
The full shape, every key, and what reads each one is in the site-settings reference. Two structural notes that belong here rather than there:
- Head extras live in
settings.head. There is no_data/head.jsto maintain. settings.urlis origin only. Subpath deployments belong inpathPrefix, which Eleventy layers on top; put the path in both and you get it twice.
Environment variables and scripts
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 |
The difference between them is what each one names. ELEVENTY_ENV is the environment your process started in; ELEVENTY_RUN_MODE is what Eleventy is doing right now. Reach for the one that names the signal you mean.
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 quickstart carries the scripts and the .env most projects land on.
Don't reach for custom environment variables for things these two already cover. If you find yourself reading process.env.SOMETHING_CUSTOM to decide whether to render a page, check first whether one of the two above is the real signal.
Multilingual
Multilingual changes the shape of the content tree: each language gets its own folder under src/content/. The folder names are yours, and what ties a folder to a language is the lang key in its directory data file. By convention the language's entry in settings.languages records the same path as contentDir, so anything that needs to find a language's content has one place to ask. Everything else about it, the three things that must be set together and what activation turns on, is on the multilang module reference. The multilingual tutorial walks the setup.
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.
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 else is skipped, deliberately; the assets module has the guard that does it.
Both processors take configuration, esbuild through options.assets.esbuild and PostCSS through a postcss.config.js at the project root. Neither changes the shape of the project. See the assets module for what they accept and Customise the assets pipeline for the recipe.
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.jsandindex.css. Renaming them tomain.jsis fighting. - Directory configuration. Re-export
config, or spread it. The four Eleventy keys (input,output,includes,data) are yours to change; Baseline reads back whatever Eleventy resolves. The two Baseline adds,dir.assetsanddir.public, need to survive any override: droppublicand the on-disk folder falls back fromstatic/topublic/with nothing to tell you. See Config export for the per-key detail. - 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.
_pageContextis 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
page.translationsor the translation filters; don't recompute.
Stay close to the shape of the project and Baseline mostly disappears.
One Eleventy rule about data files is worth knowing before it costs you an afternoon: a .11tydata.js silently wins over a sibling .11tydata.json. Both formats are valid, and if a directory ends up with both, only the JavaScript one is read. The JSON file stays on disk looking authoritative and edits to it do nothing. It usually happens mid-migration, when one is converted to the other and the original is not deleted.
See also
- Quickstart for the install/configure/run checklist.
- Site settings for the full settings shape.
- Plugin entrypoint for the options surface.