---
title: 'head'
description: 'Drop a <baseline-head> placeholder in your layout and get the full head: title, description, canonical, hreflang, Open Graph, Twitter, and JSON-LD.'
slug: 'head'
type: 'article'
date: 2026-04-15T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/modules/head/'
---

`INTERNAL_KEY`: '\_head'

---

The renderer is PostHTML (a small HTML AST tool that ships with Eleventy); Baseline owns the seed-building, the dedupe, the sort, and the placeholder replacement.

---

## What it does

The head module composes a page's `<head>` in two stages, then replaces a `<baseline-head>` element you place inside your layout with the result.

During the data cascade, it builds seeds: the per-page values the renderer needs (title, description, robots, canonical, head extras, locale). At transform-time, after Eleventy has rendered HTML, the PostHTML driver reads those seeds, builds hreflang alternates from the translation index when relevant, dedupes, sorts the elements by capo.js weights, and swaps the placeholder for the final `<head>`.

---

### Active when

Always. The head module is always loaded.

---

### Lifecycle

- **Cascade-time.** The page-context registry builds per-page seeds: composed title, description, robots, canonical, generator, the `<head>` extras you configured, and the page's locale. Seeds get cached, keyed by `page.url`.
- **Transform-time.** The PostHTML driver looks up the seeds for the current page, builds hreflang alternates from the translation-index store, emits the element list, dedupes meta and link entries, capo-sorts, and replaces `<baseline-head>` with a real `<head>`.

The two-stage split is necessary because Eleventy's HTML transformer only exposes page metadata, not the data cascade. The cascade-time pass caches what the transform-time pass needs.

---

### Scope

The head module emits the complete `<head>`: charset, viewport, `<title>`, `<meta name="description">`, robots, canonical, optional generator, the user-supplied `head.*` extras (link, script, meta, style), hreflang alternates when multilang is active, and the SEO payload, Open Graph and Twitter Card meta plus the JSON-LD graph.

The SEO payload is configured through `settings.seo` and per-page front matter, not through the head module directly. Baseline resolves it and the driver emits the tags from there. See [[site-settings | Site settings]] for the keys.

---

## The placeholder

`<baseline-head>` is not a tag that goes inside the head. It _is_ the head. Place it where the `<head>` element would go, as a sibling of `<body>`, and the driver replaces the whole element with a real `<head>` containing the composed list:

{% raw %}

```njk
<!doctype html>
<html lang="{{ page.lang or 'en' }}">
	<baseline-head></baseline-head>
	<body>
		{{ content | safe }}
	</body>
</html>
```

{% endraw %}

Do not wrap it in a `<head>` of your own. Baseline emits the opening and closing tags along with everything that belongs between them.

If the placeholder is missing, the driver has nothing to match and the page ships without a `<head>`. Silently: no error, no warning, no partial output. The warning you may be thinking of is a different case, a page with no seeds, covered under How it works below.

---

## How it works

{% stepsBlock "compact" %}

1. **Cascade-time seed build.** The page-context registry composes a seed object per page: `site`, `page`, `entry`, `query`, `meta`, `render`, `head`. See [[page-context | Page context]] for the shape.
2. **Transform-time lookup.** The PostHTML driver runs once per page. It looks up the seeds by `page.url`. If no seeds exist (the page opted out via `_internal: true`, or has a non-HTML output), the driver logs and skips.
3. **Emit.** Standard meta first (charset, viewport, title, description, robots, canonical, optional generator). Then user extras from `seeds.head`. Then hreflang alternates from the translation index. Then the SEO payload (Open Graph, Twitter, JSON-LD), read from the resolved `seo` namespace through its own handle rather than from `seeds.head`.
4. **Dedupe.** Meta and link entries are deduplicated; later entries (page-level) win over earlier ones (site-level).
5. **Sort.** The list is sorted using capo.js element weights so the order in the final `<head>` follows the recommended ordering.
6. **Replace.** The driver matches the `<baseline-head>` element in the PostHTML tree and swaps it for a real `<head>` with the sorted nodes inside.

{% endstepsBlock %}

---

## Defaults

{% stepsBlock "compact" %}

- **Title separator.** `' – '` (en dash with surrounding spaces). Composed title shape: `Page title – Site title`. A `titleTemplate` (global option or per-page front matter) overrides this composition; see Options.
- **Generator.** Off. Set `head.showGenerator: true` to emit `<meta name="generator">` with the Eleventy version.
- **Robots.** Always emitted. An indexable page gets `<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">`; a page carrying `noindex` (its own or `settings.noindex`) gets `<meta name="robots" content="noindex, nofollow">` instead.
- **Canonical.** Sourced from the resolved `seo` namespace: the page's absolute URL with the query string and fragment stripped. Keep the query string with `preserveQueryParams` (site or page), or set an explicit URL with a page-level `canonical`. Omitted on noindex pages, and omitted when `settings.url` is missing: without an absolute base there is nothing to build, so baseline emits no canonical and warns about the missing `settings.url` at startup.

{% endstepsBlock %}

---

### Settings

The head module reads two parts of the `settings` argument. Full shape on [[site-settings | Site settings]].

{% tableBlock true %}

| Key             | Type     | Used for                                                                                                                           |
| --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `settings.url`  | `string` | Building absolute canonicals.                                                                                                      |
| `settings.head` | `object` | Site-wide head extras: `link[]`, `script[]`, `meta[]`, `style[]`. Each entry is an object of attributes (plus optional `content`). |

{% endtableBlock %}

---

### Options

{% tableBlock true %}

| Option                | Type      | Default | Meaning                                                               |
| --------------------- | --------- | ------- | --------------------------------------------------------------------- |
| `head.titleSeparator` | `string`  | `' – '` | String placed between the page title and the site title in `<title>`. |
| `head.titleTemplate`  | `string`  | (none)  | Template for the whole `<title>`, with tokens `%s` (page title), `%siteTitle%`, `%tagline%`. Overrides the separator composition. |
| `head.showGenerator`  | `boolean` | `false` | Emit `<meta name="generator">` with Eleventy's version.               |

{% endtableBlock %}

When `titleTemplate` is set, it shapes the entire `<title>` and the separator is ignored. With nothing set, Baseline reproduces the `Page title – Site title` composition above.

### Per-page front matter

Set `head:` on a page to add or override entries. The shape mirrors `settings.head`:

```yaml
---
title: 'About'
description: 'A short page-level description.'
head:
  link:
    - { rel: 'preload', as: 'font', href: '/assets/fonts/inter.woff2', crossorigin: '' }
  script:
    - { src: '/assets/js/about-only.js', defer: true }
  meta:
    - { name: 'theme-color', content: '#0c1a2c' }
  style:
    - { content: 'body { background: #fafafa; }' }
---
```

Set `titleTemplate` on a page to shape its `<title>` directly, using the same `%s` / `%siteTitle%` / `%tagline%` tokens as the global option. Set it to `null` for a bare title with no site suffix.

Page entries merge with site entries. Dedupe is applied per element type, last-wins on conflict (page entries land after site entries, so they win):

- `<meta>` dedupes on whichever of `charset`, `name`, `property`, `http-equiv` is present (in that order).
- `<link>` dedupes on the `rel + hreflang + href` triple. Different `rel` or different `hreflang` for the same `href` are kept as separate entries.
- `<script>` and `<style>` are not deduplicated. If you list the same script twice, you get it twice.

An entry with nothing to key on is dropped rather than emitted: a `<meta>` carrying none of `charset`, `name`, `property` or `http-equiv`, or a `<link>` missing either `rel` or `href`. If a tag you added never appears, that is the first thing to check.

---

## Hreflang

When the multilang module is active and the page has a `translationKey`, the driver builds an `<link rel="alternate" hreflang="...">` for every translation listed in the translation index. Nothing to configure on the head side. If the page has no `translationKey`, no alternates are emitted.

---

## Tips

- Keep `<baseline-head>` where the `<head>` belongs, as a sibling of `<body>`. The driver replaces the placeholder wherever it sits, so a misplaced one produces a `<head>` in the wrong part of the document.
- For absolute canonicals and other absolute URLs, set `settings.url`. Without it, baseline emits no canonical at all, so set it before relying on canonical tags.
- A page can opt out of head injection by setting `_internal: true` in its data. The page-context registry skips it, the driver finds no seeds, and the placeholder (if any) is left untouched.
- Want a different renderer down the line? The driver is a single file in `_baseline/modules/head/drivers/`. The seam is internal today and not a user option, but the architecture is set up for swap.

---

## Peer deps

PostHTML, bundled with Eleventy. capo.js, bundled with Baseline.

---

## See also

- [[site-settings | Site settings]] for the canonical `settings.head` shape.
- [[page-context | Page context]] for the seed object the driver reads.
- [[seo-graph | SEO graph]] for the resolved `_seoGraph` the driver emits as canonical, OG/Twitter, and JSON-LD.
- [[multilang | multilang module]] for the translation relationships that power hreflang.
- [[internals | Internals]] for the driver seam.
- [[head-and-noindex | Tutorial: head and noindex]]
