---
title: 'Project structure'
description: 'How to stay in step with Baseline so the plugin stays out of your way: project shape, the plugin call, settings, options, environment variables, and the small set of seams Baseline expects to own.'
slug: 'project-structure'
type: 'article'
date: 2026-05-02T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/concept/project-structure/'
---

The folder structure that keeps Baseline out of your way. If you've read the [[overview | overview]] and the [[concept-overview | concept overview]], 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 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, 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.

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

export default async function (eleventyConfig) {
	await 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 | 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`.

```js
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: canonicals are then omitted, and other absolute URLs (sitemap, social images) fall back to relative.
- 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 | site-settings reference]].

---

## Options (defaults you usually keep)

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

{% tableBlock true %}

| Option                | Default  | What it does                                                                                                                                         |
| --------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verbose`             | `true`   | Build narrative: banner, version line, `info`-level logs. Warnings and errors emit either way. Set to `false` for a silent build.                    |
| `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 | Gates the runtime-introspection virtual page (`/navigator-core.html`). The `_navigator` read surface and debug filters/globals are always available. |
| `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.                                                                                        |

{% endtableBlock %}

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.

---

### Eleventy environment variables and scripts

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

{% tableBlock true %}

| 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                          |

{% endtableBlock %}

`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 and `.env` most projects land on:

```json
{
	"scripts": {
		"dev": "rimraf dist/ && npx @11ty/eleventy --serve",
		"build": "rimraf dist/ && cross-env ELEVENTY_ENV=production npx @11ty/eleventy"
	}
}
```

```text
# .env
ELEVENTY_ENV=development
URL=http://localhost:8080/
```

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 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-baseline-site | multilingual tutorial]] walks through the full 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 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.

{% alertBlock "warning" %}

**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.

{% endalertBlock %}

---

### Tuning the processors

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

```js
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-assets-pipeline | Customise the assets pipeline]] how-to has the full recipe.

---

## 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.

{% raw %}

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

{% endraw %}

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](https://rviscomi.github.io/capo.js/) and deduped on the way out.

---

## 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

- [[quickstart|Quickstart]] for the install/configure/run checklist.
- [[site-settings]] for the full settings shape.
- [[plugin-entrypoint]] for the options surface.
