Table of Contents
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 bypage.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 today
This pass covers what the spec calls bucket 1: charset, viewport, <title>, <meta name="description">, robots, canonical, optional generator, the user-supplied head.* extras (link, script, meta, style), and hreflang alternates when multilang is active.
Open Graph, Twitter cards, and JSON-LD are staged in code but not wired through to output yet. They are not part of this pass; do not configure them through the head module today. If you need them now, render them yourself through head.meta[] and head.link[] on the page or in settings.head.
The placeholder
Place a single <baseline-head> element inside your <head> tag. The driver replaces it with the composed element list:
<!doctype html>
<html lang="en">
<head>
<baseline-head></baseline-head>
</head>
<body>
</body>
</html>
If the placeholder is missing, the driver does nothing and logs a warning. No error, no partial output.
How it works
- Cascade-time seed build. The page-context registry composes a seed object per page:
site,page,entry,query,meta,render,head. See Page context for the shape. - 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. - 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. - Dedupe. Meta and link entries are deduplicated; later entries (page-level) win over earlier ones (site-level).
- Sort. The list is sorted using capo.js element weights so the order in the final
<head>follows the recommended ordering. - Replace. The driver matches the
<baseline-head>element in the PostHTML tree and swaps it for a real<head>with the sorted nodes inside.
Defaults
- Title separator.
' – '(en dash with surrounding spaces). Composed title shape:Page title – Site title. - Generator. Off. Set
head.showGenerator: trueto emit<meta name="generator">with the Eleventy version. - Robots. Omitted unless the page (or
settings.noindex) sets it tonoindex. When set, emits<meta name="robots" content="noindex, nofollow">. - Canonical. Built from
page.urlplussettings.urland Eleventy'spathPrefix. Ifsettings.urlis missing, baseline warns at startup and canonicals fall back to relative paths.
Settings
The head module reads two parts of the settings argument. Full shape on Site settings.
| 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). |
Options
| Option | Type | Default | Meaning |
|---|---|---|---|
head.titleSeparator |
string |
' – ' |
String placed between the page title and the site title in <title>. |
head.showGenerator |
boolean |
false |
Emit <meta name="generator"> with Eleventy's version. |
Per-page front matter
Set head: on a page to add or override entries. The shape mirrors settings.head:
---
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; }' }
---
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 ofcharset,name,property,http-equivis present (in that order).<link>dedupes on therel + hreflang + hreftriple. Differentrelor differenthreflangfor the samehrefare 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 any future absolute URLs, set
settings.url. Relative canonicals are valid but limit some downstream tooling. - A page can opt out of head injection by setting
_internal: truein 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 for the canonical
settings.headshape. - Page context for the seed object the driver reads.
- multilang module for the translation map that powers hreflang.
- Internals for the driver seam.
- Tutorial: head and noindex