---
title: 'Content organisation'
description: 'Two layers behind every page: structural metadata (section, type, layout, permalink) lives in a sibling `*.11tydata.js`; content metadata (title, description, date) lives in front matter. A pattern, not a Baseline rule.'
slug: 'content-organisation'
type: 'article'
date: 2026-05-02T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/concept/content-organisation/'
---

Every page on a Baseline site has two layers behind it.

One is structural: where the page lives, what layout wraps it, whether it shows up in nav. The other is the content's own metadata: title, description, date. They want different homes.

The pattern is to keep structure in a sibling `*.11tydata.js` file and content metadata in the page's front matter. This isn't a Baseline rule. It's a way of using Eleventy's data cascade that scales as your sections grow.

```
STRUCTURE (*.11tydata.js)  -> section, type, layout, permalink
CONTENT   (front matter)    -> title, slug, description, date
```

Structure is what every page in a section shares. Content is what makes one page different from the next. Mix them and the section gets harder to reason about every time you add a page.

---

## The structural side

Drop a sibling data file next to the section's pages. Eleventy applies its values as defaults to every page in the directory, and per-page front matter can still override anything specific.

`layout` is read by Eleventy. `draft` is read by Baseline's drafts preprocessor. `section` and `type` are read by Baseline into page context, so the content graph and your templates see them through the same surface.

`section` is a hierarchical path written as an array, even when there's only one level. One-level is `['blog']`. Nested is `['blog', '2026']`. Nest as deep as your site needs. The shape mirrors Jekyll's `categories` – same job, same array form.

```js
// src/content/blog/blog.11tydata.js
export default {
	section: ['blog'], // hierarchical path; nest as ['blog', '2026', 'q1']
	type: 'post', // free-form classifier for collections and templates

	draft: false, // overridden by individual pages when needed
	layout: 'layouts/post.njk' // cascades to every page in the folder

	// permalink (covered below)
};
```

Keep the ones you actually use; empty conventions rot.

---

## The content side

Front matter stays small and per-page:

```yaml
---
title: 'A short title'
slug: 'a-short-title'
description: 'One-sentence description of this specific page.'
date: 2026-05-01
---
```

Four fields, every page. Title and description for the head module and link previews. Slug for the URL and for wikilinks to find the page. Date for ordering and for the sitemap's `lastmod`.

Per-page exceptions go here too. If a single post needs a different layout, declare `layout` in its front matter; the cascade picks the more specific value.

---

## The two layers meet at the permalink

The cleanest place to see the split is the permalink function. It reads `slug` from front matter and the structural keys from the data file, and composes the URL from both.

A page in `src/content/blog/` with `slug: 'first-post'` and `section: ['blog']` resolves to `/blog/first-post/`.

Extend `section` to `['blog', '2026']` and every page in the folder gets `/blog/2026/<slug>/` without each post having to spell out its own path. The array is the path – nest as deep as your site needs.

```js
// src/content/blog/blog.11tydata.js
export default {
	// permalink (covered below)
	permalink: function (data) {
		const { slug, page, section } = data;

		// Don't try to render the data file itself as a page.
		if (page.inputPath.includes('11tydata.js')) {
			return false;
		}

		// Front-matter slug wins; otherwise fall back to the file's own slug.
		const slugified = slug ? this.slugify(slug) : page.fileSlug;

		// Compose ///.../<slug>/.
		const parts = (section ?? []).map((part) => this.slugify(part));
		parts.push(slugified);

		// Leading slash anchors to the site root; trailing slash is the project convention.
		return '/' + parts.join('/') + '/';
	}
};
```

---

## Adding a language prefix

For a multilingual site, the permalink needs one more step: prefix the URL with the language code, except for the default language which sits at the site root.

The signal comes from two places already in the cascade. `data.lang` is set per language tree (typically in `en.11tydata.js`, `nl.11tydata.js`, and so on). `data.settings.defaultLanguage` lives in `_data/settings.js` and names the language that owns the root.

Drop the data file one level up, at the top of your content tree, so its values cascade to every section underneath:

```js
// src/content/content.11tydata.js
export default {
	permalink: function (data) {
		// Don't try to render the data file itself as a page.
		if (data.page.inputPath.includes('11tydata.js')) return false;

		// Front-matter slug wins; otherwise fall back to the file's own slug.
		const slug = data.slug ? this.slugify(data.slug) : data.page.fileSlug;

		// Prefix non-default languages; default language and single-language sites sit at root.
		const isDefaultLang = !data.lang || data.lang === data.settings?.defaultLanguage;
		const prefix = isDefaultLang ? '' : `/${data.lang}`;

		// Compose ///.../<slug>/, with optional language prefix.
		const sections = (data.section ?? []).map((s) => this.slugify(s));
		return `${prefix}/${[...sections, slug].join('/')}/`;
	}
};
```

A page with `lang: 'en'` on a site whose `defaultLanguage` is `'en'` resolves to `/blog/first-post/`. The same page with `lang: 'nl'` resolves to `/nl/blog/first-post/`. A single-language site leaves `lang` unset and never sees a prefix.

The optional chaining on `data.settings?.defaultLanguage` keeps the check safe when `settings` or `defaultLanguage` isn't defined, which is the single-language case.

---

## Worth knowing

- **`section` is always an array.** Even a one-level section is written as `['blog']`. The shape teaches the path-like nature of the field and lets you nest deeper without restructuring later.
- **Front matter overrides the data file.** Eleventy resolves to the more specific value, so a single page can opt out of any default by declaring it in front matter.
- **The data file isn't a page.** The `inputPath.includes('11tydata.js')` check returns `false` from the permalink so Eleventy doesn't try to render it.
- **Paths use a leading slash.** Without it, the permalink resolves relative to the input subdirectory rather than the site root.

---

## Opting out

A page can exist and render without showing up in indexes. Two keys cover the common cases – one Baseline, one Eleventy-native – and either works from front matter or the data file:

```yaml
---
baselineExcludeFromGraph: true        # keep this page out of the content graph
eleventyExcludeFromCollections: true  # keep this page out of all Eleventy collections
---
```

`baselineExcludeFromGraph` is read by the content graph's prepass; the page still renders, but nothing the graph feeds (navigator listings, sitemap, related-page surfaces) will see it.

`eleventyExcludeFromCollections` is Eleventy's built-in opt-out from `collections.all` and any tagged collection – reach for it when a page needs to render but shouldn't be picked up by any iteration over collections.

---

## See also

- [[project-structure]] for the project-shape conventions this pattern slots into.
- [[page-context|Page context]] reference for what Baseline reads off each page at cascade time.
