---
title: 'head'
description: 'The head module composes the page''s <head> at transform-time: builds seeds during the cascade, replaces a <baseline-head> placeholder with a sorted, deduped element list.'
slug: 'head'
type: 'article'
date: 2026-04-15T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/module/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 map 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-map 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

Place a single `<baseline-head>` element inside your `<head>` tag. The driver replaces it with the composed element list:

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

If the placeholder is missing, the driver does nothing and logs a warning. No error, no partial output.

---

## 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 map. 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.** Omitted unless the page (or `settings.noindex`) sets it to `noindex`. When set, emits `<meta name="robots" content="noindex, nofollow">`.
- **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.

---

## 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 map. Nothing to configure on the head side. If the page has no `translationKey`, no alternates are emitted.

---

## Tips

- Keep `<baseline-head>` inside `<head>`, not `<body>`. The driver replaces the placeholder wherever it is, but the result is meaningful only inside `<head>`.
- 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 map that powers hreflang.
- [[internals | Internals]] for the driver seam.
- [[head-and-noindex | Tutorial: head and noindex]]
