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.
// 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:
---
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.
// 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 /<section[0]>/<section[1]>/.../<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:
// 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 /<section[0]>/<section[1]>/.../<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
sectionis 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 returnsfalsefrom 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:
---
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 reference for what Baseline reads off each page at cascade time.