---
title: 'Add a layout and a stylesheet'
description: 'Wrap the pages in a layout with baseline-head, then add the CSS and JS the settings file already points at.'
slug: 'add-a-layout'
type: 'article'
date: 2026-08-15T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/tutorial/add-a-layout/'
---

Your pages render, and they arrive with nothing around them: no `<head>`, no navigation, nothing to look at. This page gives them a shell. It is also where two things you set up earlier finally reach a file.

---

## Write the layout

One line in here is going to look wrong. Leave it alone, it is the point of the page. Create `src/_includes/layouts/base.njk`:

{% raw %}

```nunjucks
<!DOCTYPE html>
<html lang="{{ lang or settings.defaultLanguage }}">
	<baseline-head></baseline-head>
	<body>
		<header>
			<a href="/">{{ settings.title }}</a>
			<p>{{ settings.tagline }}</p>
		</header>

		<main id="main">
			<h1>{{ title }}</h1>
			{{ content | safe }}
		</main>

		<footer>
			<p>Proudly powered by Eleventy and Baseline.</p>
		</footer>
	</body>
</html>
```

{% endraw %}

`<baseline-head></baseline-head>` is not a tag that goes inside the head. It *is* the head. Baseline replaces the whole element at build time with a real `<head>`, assembled from your front matter, your settings file and its own defaults. That is why it sits as a sibling of `<body>` rather than wrapping anything, and why you never write a `<title>` yourself. If you catch yourself hand-writing a head tag, something has gone sideways. The [[head | head module]] lists everything it emits.

Header, main, footer. Three siblings, no wrapper, which is as much structure as this site needs. The header reads `settings.title` and `settings.tagline` straight out of the settings file from two pages ago. You did not import it. Living in `src/_data/` is what does that.

The `lang` attribute reads the page's own `lang` and falls back to `settings.defaultLanguage`. On a single-language site those are the same value and the fallback is the one doing the work. It matters later: [[multilingual-baseline-site | the multilingual tutorial]] gives each language folder its own `lang`, and a layout that only ever read `defaultLanguage` would label every Dutch page as English.

`{% raw %}{{ content | safe }}{% endraw %}` is where each page's rendered Markdown lands. `safe` tells Nunjucks it is already HTML and should not be escaped.

---

## Point the pages at it

The layout exists and nothing is using it. One key fixes that, in one file. Create `src/content/content.11tydata.js`:

```js
export default {
	layout: 'layouts/base.njk'
};
```

Directory data reaches down, not across. Sitting at the top of `content/` means it reaches everything below: `pages/`, `posts/`, and the homepage at the root. One line, every page.

That is the difference between the two rules you now have. The layout is the same everywhere, so it is set once at the top. The address rule is not, which is why it lives in `pages/` and `posts/` separately and says something different in each. Anything a page disagrees with, it overrides in its own front matter.

Restart the dev server and look again. The words are the same, but there is a document around them now, and a title in the browser tab you did not write.

---

## Add the stylesheet

Baseline compiles two entry points, one for CSS and one for JS, both named `index`. That sounds restrictive until you notice that `@import` and `import` still reach everything else from there. The [[assets | assets module]] covers what the pipeline does with them.

First a small piece of housekeeping, before the files rather than after. Create `src/assets/assets.11tydata.js`:

```js
export default {
	eleventyExcludeFromCollections: true
};
```

Baseline registers CSS and JS as template formats so it can compile them, which technically makes them pages. Without this file they turn up in `collections.all`, which is the collection most listing templates reach for, and a stylesheet ends up in a list of things somebody might want to read. One line, one folder, problem gone.

Now the stylesheet, `src/assets/css/index.css`:

```css
body {
	font-family: system-ui, sans-serif;
	margin: 0;
	padding: 2rem;
	line-height: 1.5;
	color: #1f2937;
	background: #f8fafc;
}

h1 {
	margin-bottom: 0.5rem;
}
```

And `src/assets/js/index.js`, which the site does not need but the settings file has been promising since page three:

```js
document.addEventListener('DOMContentLoaded', () => {
	console.log('Howdy!');
});
```

Both compile into `dist/assets/`, mirroring the folders they came from, so `src/assets/css/index.css` is served at `/assets/css/index.css`. That is the path already sitting in `settings.head.link`, which is why the stylesheet loads without you adding a `<link>` anywhere. Compiling and loading are two separate jobs. Baseline only does the first.

Reload once more and the page has a face.

---

## Put the about page on the homepage

One thing is missing from the shape: the homepage lists posts but says nothing about the site. Open `src/content/index.md` and add this above the `## Latest posts` heading, so the site introduces itself before it lists anything:

{% raw %}

```md
{% set about = _navigator.nodes["/about/"] %}

<p>{{ about.excerpt }}</p>
<p><a href="{{ about.url }}">Read more</a></p>
```

{% endraw %}

Nothing new went into the about page to make this work. `_navigator` is a global Baseline registers, and `nodes` is every page on the site keyed by its URL. The `excerpt` on each one is that page's opening paragraph, read out of the rendered HTML rather than out of front matter, which is why you are not maintaining the sentence in two places.

That is the pre-pass, which the logs have been mentioning since page three, finally doing something you can see. Baseline builds the whole site once before it builds it for real, so that any page can know what is on any other page. Without it a template could only ever see itself.

{% alertBlock "info" %}

This is also why the excerpt would have been empty an hour ago. Extraction reads the rendered document and looks for `<main>` inside it, so before the layout existed there was nothing to extract from. The layout is what turned your pages into documents.

{% endalertBlock %}

The `"/about/"` written into the template is the ugly part. It works, and it is honest about what it does, but a site with more pages would want to look the page up rather than name it. The [[globals | globals reference]] has the rest of what is on `_navigator`.

The site is finished, in the sense that everything it needs now exists. What is left is knowing what happens when you run it for real.
