---
title: The head subsystem
description: "Why everything that writes to the document head — title, canonical, JSON-LD, OpenGraph, alternates, Standard Site links — funnels through one typed model, one rewriter that finalizes it, and a shared `data-head` attribute."
canonical_url: https://usepennington.net/explanation/core/head-subsystem/
sidecar_url: https://usepennington.net/explanation/core/head-subsystem.md
content_hash: sha256:3c1bd492004a46e919bab53894ff1b94020e1b952b7344a33c1d0553530294e1
tokens: 8355
uid: explanation.core.head-subsystem
reading_time_minutes: 8
---

Under the Hood
# The head subsystem

Why everything that writes to the document head — title, canonical, JSON-LD, OpenGraph, alternates, Standard Site links — funnels through one typed model, one rewriter that finalizes it, and a shared `data-head` attribute.

 
A surprising number of concerns want to write into the document `<head>`: the title and description, the canonical link, schema.org JSON-LD, OpenGraph and Twitter card meta, RSS and llms.txt alternates, `hreflang` locale alternates, the dev-host meta that live reload reads, and the Standard Site verification links. Why does Pennington route all of them through a single `IHeadContributor` extension point instead of letting each one emit its own markup where it already lives?

 
## Context

 
The naive approach needs no subsystem at all: let each concern emit its own head markup wherever it already lives. Some tags are literal markup in a template's `App.razor` head; the canonical link and JSON-LD ride in head-scoped `IHtmlResponseRewriter`s; per-page meta lives in Razor `<HeadContent>` blocks. Each writer is reasonable on its own.

 
They stop being reasonable together. All of them target the same small region of the document, and nothing coordinates them — so tags duplicate, their relative order is left to hand-picked integers that share no scale, and the client, which reconciles the head in JavaScript on every [SPA navigation](https://usepennington.net/explanation/spa/islands.md) rather than reloading, needs some way to know which tags to carry across a swap. The closing section returns to these failures once the mechanism that fixes each is in view; the rest of this page builds that mechanism.

 
## How it works

 
The subsystem has four parts: a typed data model, the `IHeadContributor` extension point, a single rewriter that finalizes the head, and a `data-head` attribute that ties the server and client halves together.

 
.b-ad96f682{--beck-surface:var(--color-base-50, #ffffff);--beck-node-bg:var(--color-base-50, #ffffff);--beck-node-border:var(--color-base-200, #e2e8f0);--beck-node-shadow:0 1px 3px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.06);--beck-text:var(--color-base-800, #1e293b);--beck-text-muted:var(--color-base-500, #64748b);--beck-text-faint:var(--color-base-400, #94a3b8);--beck-primary:var(--color-primary-600, #175ddc);--beck-success:var(--color-emerald-500, #10b981);--beck-warn:var(--color-amber-500, #f59e0b);--beck-danger:var(--color-red-500, #ef4444);--beck-info:var(--color-violet-500, #8b5cf6);--beck-neutral:var(--color-base-400, #94a3b8);--beck-group-border:color-mix(in srgb, var(--beck-neutral) 45%, transparent);--beck-group-label:var(--beck-text-muted);--beck-edge:var(--color-base-300, #cbd5e1);--beck-packet:var(--beck-primary);--beck-icon-bg:var(--color-base-100, #f1f5f9);--beck-accent:var(--beck-primary);--beck-font:'Inter', system-ui, -apple-system, sans-serif;--beck-font-mono:'IBM Plex Mono', ui-monospace, monospace;}[data-theme='dark'] .b-ad96f682{--beck-surface:var(--color-base-950, #0d1117);--beck-node-bg:var(--color-base-900, #161b22);--beck-node-border:var(--color-base-700, #30363d);--beck-node-shadow:0 1px 3px rgb(0 0 0 / 0.3), 0 4px 14px rgb(0 0 0 / 0.4);--beck-text:var(--color-base-50, #f0f6fc);--beck-text-muted:var(--color-base-400, #8b949e);--beck-text-faint:var(--color-base-500, #6e7681);--beck-edge:var(--color-base-700, #30363d);--beck-icon-bg:var(--color-base-800, #21262d);--beck-font:'Inter', system-ui, -apple-system, sans-serif;--beck-font-mono:'IBM Plex Mono', ui-monospace, monospace;}@media (prefers-color-scheme: dark){:root:not([data-theme='light']) .b-ad96f682{--beck-surface:var(--color-base-950, #0d1117);--beck-node-bg:var(--color-base-900, #161b22);--beck-node-border:var(--color-base-700, #30363d);--beck-node-shadow:0 1px 3px rgb(0 0 0 / 0.3), 0 4px 14px rgb(0 0 0 / 0.4);--beck-text:var(--color-base-50, #f0f6fc);--beck-text-muted:var(--color-base-400, #8b949e);--beck-text-faint:var(--color-base-500, #6e7681);--beck-edge:var(--color-base-700, #30363d);--beck-icon-bg:var(--color-base-800, #21262d);--beck-font:'Inter', system-ui, -apple-system, sans-serif;--beck-font-mono:'IBM Plex Mono', ui-monospace, monospace;}}.b-ad96f682{font-family:var(--beck-font);}.b-ad96f682 .beck-fx-node{transform-box:fill-box;transform-origin:center;}.b-ad96f682 .beck-packet-label{font-family:var(--beck-font-mono);font-size:11px;font-weight:600;}.b-ad96f682 .beck-node{fill:var(--beck-node-bg);stroke:color-mix(in srgb, var(--beck-accent) 32%, var(--beck-node-border));stroke-width:1.5;filter:drop-shadow(0 1px 3px rgb(0 0 0/.05)) drop-shadow(0 4px 12px rgb(0 0 0/.06));}[data-theme='dark'] .b-ad96f682 .beck-node{filter:drop-shadow(0 1px 3px rgb(0 0 0/.3)) drop-shadow(0 4px 14px rgb(0 0 0/.4));}@media (prefers-color-scheme: dark){:root:not([data-theme='light']) .b-ad96f682 .beck-node{filter:drop-shadow(0 1px 3px rgb(0 0 0/.3)) drop-shadow(0 4px 14px rgb(0 0 0/.4));}}.b-ad96f682 .beck-node--external{stroke-dasharray:5 4;}.b-ad96f682 .beck-node--subtle{opacity:.72;}.b-ad96f682 .beck-node--ghost{fill:transparent;stroke-dasharray:5 4;filter:none;}.b-ad96f682 .beck-icon-chip{fill:color-mix(in srgb, var(--beck-accent) 15%, var(--beck-icon-bg));}.b-ad96f682 .beck-node--ghost .beck-icon-chip{fill:transparent;}.b-ad96f682 .beck-icon{color:var(--beck-accent);}.b-ad96f682 .beck-node-title{fill:var(--beck-text);}.b-ad96f682 .beck-node-subtitle,.b-ad96f682 .beck-ghost-label{fill:var(--beck-text-muted);}.b-ad96f682 .beck-status-inline{fill:var(--beck-accent);}.b-ad96f682 .beck-status-bg{fill:color-mix(in srgb, var(--beck-accent) 14%, transparent);}.b-ad96f682 .beck-status-text{fill:var(--beck-accent);}.b-ad96f682 .beck-group{fill:none;stroke:var(--beck-group-border);stroke-width:1.5;stroke-dasharray:6 6;}.b-ad96f682 .beck-group-label-bg{fill:var(--beck-surface);}.b-ad96f682 .beck-group-label{fill:var(--beck-group-label);}.b-ad96f682 .beck-node--start{fill:var(--beck-text-muted);}.b-ad96f682 .beck-node--end{fill:none;stroke:var(--beck-text-muted);stroke-width:2;}.b-ad96f682 .beck-end-dot{fill:var(--beck-text-muted);}.b-ad96f682 .beck-class-head{fill:color-mix(in srgb, var(--beck-accent) 10%, transparent);}.b-ad96f682 .beck-class-head-border{stroke:color-mix(in srgb, var(--beck-accent) 28%, var(--beck-node-border));stroke-width:1;}.b-ad96f682 .beck-class-divider{stroke:var(--beck-node-border);stroke-width:1;}.b-ad96f682 .beck-class-stereo{fill:var(--beck-text-muted);}.b-ad96f682 .beck-class-title{fill:var(--beck-text);}.b-ad96f682 .beck-class-field{fill:var(--beck-text-muted);}.b-ad96f682 .beck-class-method{fill:var(--beck-text);}.b-ad96f682 .beck-lifeline{stroke-width:2;stroke-dasharray:6 7;}.b-ad96f682 .beck-activation{filter:drop-shadow(0 0 5px color-mix(in srgb, var(--beck-accent) 45%, transparent));}.b-ad96f682 .beck-msg-chip{fill:var(--beck-node-bg);stroke:color-mix(in srgb, var(--beck-accent) 40%, transparent);stroke-width:1;}.b-ad96f682 .beck-msg-text{fill:color-mix(in srgb, var(--beck-accent) 34%, var(--beck-text));}.b-ad96f682 .beck-msg--reply .beck-msg-chip{stroke:none;}.b-ad96f682 .beck-msg--reply .beck-msg-text,.b-ad96f682 .beck-msg-text--bare{fill:var(--beck-text-muted);}.b-ad96f682 .beck-band-box{fill:color-mix(in srgb, var(--beck-accent) 5%, transparent);stroke:color-mix(in srgb, var(--beck-accent) 30%, transparent);stroke-width:1.5;stroke-dasharray:6 6;}.b-ad96f682 .beck-band-chip{fill:var(--beck-surface);stroke:color-mix(in srgb, var(--beck-accent) 40%, transparent);stroke-width:1;}.b-ad96f682 .beck-band-label{fill:color-mix(in srgb, var(--beck-accent) 70%, var(--beck-text));}.b-ad96f682 .beck-edge{fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;}.b-ad96f682 .beck-edge-label{fill:var(--beck-text-muted);paint-order:stroke;stroke:var(--beck-surface);stroke-width:3px;stroke-linejoin:round;}.b-ad96f682 .beck-title{fill:var(--beck-text);}.b-ad96f682 .beck-subtitle{fill:var(--beck-text-muted);}@media (prefers-reduced-motion:no-preference){.b-ad96f682 .bp0-ad96f682{animation:kp0-ad96f682 36.17s linear infinite;}@keyframes kp0-ad96f682{0%{offset-distance:0%;opacity:0;}18.1483%{opacity:0;}18.1583%{offset-distance:0%;opacity:1;animation-timing-function:linear;}21.3801%{offset-distance:100%;opacity:1;}21.3901%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-ad96f682 .bt0-ad96f682{animation:kt0-ad96f682 36.17s linear infinite;}@keyframes kt0-ad96f682{0%{stroke-dashoffset:244.69;}18.1583%{stroke-dashoffset:244.69;animation-timing-function:linear;}21.3801%{stroke-dashoffset:0;}96.6819%{stroke-dashoffset:0;}96.6919%{stroke-dashoffset:244.69;}100%{stroke-dashoffset:244.69;}}.b-ad96f682 .bp1-ad96f682{animation:kp1-ad96f682 36.17s linear infinite;}@keyframes kp1-ad96f682{0%{offset-distance:0%;opacity:0;}23.0291%{opacity:0;}23.0391%{offset-distance:0%;opacity:1;animation-timing-function:linear;}26.2609%{offset-distance:100%;opacity:1;}26.2709%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-ad96f682 .bt1-ad96f682{animation:kt1-ad96f682 36.17s linear infinite;}@keyframes kt1-ad96f682{0%{stroke-dashoffset:244.69;}23.0391%{stroke-dashoffset:244.69;animation-timing-function:linear;}26.2609%{stroke-dashoffset:0;}96.6819%{stroke-dashoffset:0;}96.6919%{stroke-dashoffset:244.69;}100%{stroke-dashoffset:244.69;}}.b-ad96f682 .bp2-ad96f682{animation:kp2-ad96f682 36.17s linear infinite;}@keyframes kp2-ad96f682{0%{offset-distance:0%;opacity:0;}43.1406%{opacity:0;}43.1506%{offset-distance:0%;opacity:1;animation-timing-function:linear;}49.3992%{offset-distance:100%;opacity:1;}49.4092%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-ad96f682 .bt2-ad96f682{animation:kt2-ad96f682 36.17s linear infinite;}@keyframes kt2-ad96f682{0%{stroke-dashoffset:474.57;}43.1506%{stroke-dashoffset:474.57;animation-timing-function:linear;}49.3992%{stroke-dashoffset:0;}96.6819%{stroke-dashoffset:0;}96.6919%{stroke-dashoffset:474.57;}100%{stroke-dashoffset:474.57;}}.b-ad96f682 .bp3-ad96f682{animation:kp3-ad96f682 36.17s linear infinite;}@keyframes kp3-ad96f682{0%{offset-distance:0%;opacity:0;}51.0482%{opacity:0;}51.0582%{offset-distance:0%;opacity:1;animation-timing-function:linear;}55.1605%{offset-distance:100%;opacity:1;}55.1705%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-ad96f682 .bt3-ad96f682{animation:kt3-ad96f682 36.17s linear infinite;}@keyframes kt3-ad96f682{0%{stroke-dashoffset:311.56;}51.0582%{stroke-dashoffset:311.56;animation-timing-function:linear;}55.1605%{stroke-dashoffset:0;}96.6819%{stroke-dashoffset:0;}96.6919%{stroke-dashoffset:311.56;}100%{stroke-dashoffset:311.56;}}.b-ad96f682 .bp4-ad96f682{animation:kp4-ad96f682 36.17s linear infinite;}@keyframes kp4-ad96f682{0%{offset-distance:0%;opacity:0;}68.1365%{opacity:0;}68.1465%{offset-distance:0%;opacity:1;animation-timing-function:linear;}69.8055%{offset-distance:100%;opacity:1;}69.8155%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-ad96f682 .bt4-ad96f682{animation:kt4-ad96f682 36.17s linear infinite;}@keyframes kt4-ad96f682{0%{stroke-dashoffset:96;}68.1465%{stroke-dashoffset:96;animation-timing-function:linear;}69.8055%{stroke-dashoffset:0;}96.6819%{stroke-dashoffset:0;}96.6919%{stroke-dashoffset:96;}100%{stroke-dashoffset:96;}}.b-ad96f682 .bp5-ad96f682{animation:kp5-ad96f682 36.17s linear infinite;}@keyframes kp5-ad96f682{0%{offset-distance:0%;opacity:0;}90.5888%{opacity:0;}90.5988%{offset-distance:0%;opacity:1;animation-timing-function:linear;}92.2578%{offset-distance:100%;opacity:1;}92.2678%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-ad96f682 .bt5-ad96f682{animation:kt5-ad96f682 36.17s linear infinite;}@keyframes kt5-ad96f682{0%{stroke-dashoffset:96;}90.5988%{stroke-dashoffset:96;animation-timing-function:linear;}92.2578%{stroke-dashoffset:0;}96.6819%{stroke-dashoffset:0;}96.6919%{stroke-dashoffset:96;}100%{stroke-dashoffset:96;}}.b-ad96f682 .bn3-ad96f682 .beck-fx-node{animation:kn3-ad96f682 36.17s linear infinite;}@keyframes kn3-ad96f682{0%{transform:none;}21.3801%{transform:none;animation-timing-function:linear(0 0%, 0.2557 3.33%, 0.4834 6.67%, 0.6845 10%, 0.8603 13.33%, 1.0121 16.67%, 1.1414 20%, 1.2495 23.33%, 1.3377 26.67%, 1.4074 30%, 1.46 33.33%, 1.4967 36.67%, 1.5191 40%, 1.5284 43.33%, 1.5259 46.67%, 1.4913 53.33%, 1.4261 60%, 1.3411 66.67%, 1.2472 73.33%, 1.1554 80%, 1.0763 86.67%, 1.0209 93.33%, 1 100%);}21.8778%{transform:translateY(-2px) scale(1.04);animation-timing-function:linear(0 0%, 0.1286 1.67%, 0.2749 3.33%, 0.5785 6.67%, 0.7194 8.33%, 0.8455 10%, 0.9534 11.67%, 1.0415 13.33%, 1.1093 15%, 1.1575 16.67%, 1.1878 18.33%, 1.2023 20%, 1.2035 21.67%, 1.1941 23.33%, 1.154 26.67%, 1.0496 33.33%, 1.0082 36.67%, 0.9807 40%, 0.9668 43.33%, 0.964 46.67%, 0.9773 53.33%, 0.9952 60%, 1.0049 66.67%, 1.0061 73.33%, 1.0032 80%, 1.0003 86.67%, 0.999 93.33%, 1 100%);}23.0391%{transform:none;}26.2609%{transform:none;animation-timing-function:linear(0 0%, 0.2557 3.33%, 0.4834 6.67%, 0.6845 10%, 0.8603 13.33%, 1.0121 16.67%, 1.1414 20%, 1.2495 23.33%, 1.3377 26.67%, 1.4074 30%, 1.46 33.33%, 1.4967 36.67%, 1.5191 40%, 1.5284 43.33%, 1.5259 46.67%, 1.4913 53.33%, 1.4261 60%, 1.3411 66.67%, 1.2472 73.33%, 1.1554 80%, 1.0763 86.67%, 1.0209 93.33%, 1 100%);}26.7586%{transform:translateY(-2px) scale(1.04);animation-timing-function:linear(0 0%, 0.1286 1.67%, 0.2749 3.33%, 0.5785 6.67%, 0.7194 8.33%, 0.8455 10%, 0.9534 11.67%, 1.0415 13.33%, 1.1093 15%, 1.1575 16.67%, 1.1878 18.33%, 1.2023 20%, 1.2035 21.67%, 1.1941 23.33%, 1.154 26.67%, 1.0496 33.33%, 1.0082 36.67%, 0.9807 40%, 0.9668 43.33%, 0.964 46.67%, 0.9773 53.33%, 0.9952 60%, 1.0049 66.67%, 1.0061 73.33%, 1.0032 80%, 1.0003 86.67%, 0.999 93.33%, 1 100%);}27.92%{transform:none;}100%{transform:none;}}.b-ad96f682 .bn4-ad96f682 .beck-fx-node{animation:kn4-ad96f682 36.17s linear infinite;}@keyframes kn4-ad96f682{0%{transform:none;}49.3992%{transform:none;animation-timing-function:linear(0 0%, 0.2557 3.33%, 0.4834 6.67%, 0.6845 10%, 0.8603 13.33%, 1.0121 16.67%, 1.1414 20%, 1.2495 23.33%, 1.3377 26.67%, 1.4074 30%, 1.46 33.33%, 1.4967 36.67%, 1.5191 40%, 1.5284 43.33%, 1.5259 46.67%, 1.4913 53.33%, 1.4261 60%, 1.3411 66.67%, 1.2472 73.33%, 1.1554 80%, 1.0763 86.67%, 1.0209 93.33%, 1 100%);}49.8969%{transform:translateY(-2px) scale(1.04);animation-timing-function:linear(0 0%, 0.1286 1.67%, 0.2749 3.33%, 0.5785 6.67%, 0.7194 8.33%, 0.8455 10%, 0.9534 11.67%, 1.0415 13.33%, 1.1093 15%, 1.1575 16.67%, 1.1878 18.33%, 1.2023 20%, 1.2035 21.67%, 1.1941 23.33%, 1.154 26.67%, 1.0496 33.33%, 1.0082 36.67%, 0.9807 40%, 0.9668 43.33%, 0.964 46.67%, 0.9773 53.33%, 0.9952 60%, 1.0049 66.67%, 1.0061 73.33%, 1.0032 80%, 1.0003 86.67%, 0.999 93.33%, 1 100%);}51.0582%{transform:none;}55.1605%{transform:none;animation-timing-function:linear(0 0%, 0.2557 3.33%, 0.4834 6.67%, 0.6845 10%, 0.8603 13.33%, 1.0121 16.67%, 1.1414 20%, 1.2495 23.33%, 1.3377 26.67%, 1.4074 30%, 1.46 33.33%, 1.4967 36.67%, 1.5191 40%, 1.5284 43.33%, 1.5259 46.67%, 1.4913 53.33%, 1.4261 60%, 1.3411 66.67%, 1.2472 73.33%, 1.1554 80%, 1.0763 86.67%, 1.0209 93.33%, 1 100%);}55.6582%{transform:translateY(-2px) scale(1.04);animation-timing-function:linear(0 0%, 0.1286 1.67%, 0.2749 3.33%, 0.5785 6.67%, 0.7194 8.33%, 0.8455 10%, 0.9534 11.67%, 1.0415 13.33%, 1.1093 15%, 1.1575 16.67%, 1.1878 18.33%, 1.2023 20%, 1.2035 21.67%, 1.1941 23.33%, 1.154 26.67%, 1.0496 33.33%, 1.0082 36.67%, 0.9807 40%, 0.9668 43.33%, 0.964 46.67%, 0.9773 53.33%, 0.9952 60%, 1.0049 66.67%, 1.0061 73.33%, 1.0032 80%, 1.0003 86.67%, 0.999 93.33%, 1 100%);}56.8195%{transform:none;}100%{transform:none;}}.b-ad96f682 .bn5-ad96f682 .beck-fx-node{animation:kn5-ad96f682 36.17s linear infinite;}@keyframes kn5-ad96f682{0%{transform:none;}69.8055%{transform:none;animation-timing-function:linear(0 0%, 0.2557 3.33%, 0.4834 6.67%, 0.6845 10%, 0.8603 13.33%, 1.0121 16.67%, 1.1414 20%, 1.2495 23.33%, 1.3377 26.67%, 1.4074 30%, 1.46 33.33%, 1.4967 36.67%, 1.5191 40%, 1.5284 43.33%, 1.5259 46.67%, 1.4913 53.33%, 1.4261 60%, 1.3411 66.67%, 1.2472 73.33%, 1.1554 80%, 1.0763 86.67%, 1.0209 93.33%, 1 100%);}70.3033%{transform:translateY(-2px) scale(1.04);animation-timing-function:linear(0 0%, 0.1286 1.67%, 0.2749 3.33%, 0.5785 6.67%, 0.7194 8.33%, 0.8455 10%, 0.9534 11.67%, 1.0415 13.33%, 1.1093 15%, 1.1575 16.67%, 1.1878 18.33%, 1.2023 20%, 1.2035 21.67%, 1.1941 23.33%, 1.154 26.67%, 1.0496 33.33%, 1.0082 36.67%, 0.9807 40%, 0.9668 43.33%, 0.964 46.67%, 0.9773 53.33%, 0.9952 60%, 1.0049 66.67%, 1.0061 73.33%, 1.0032 80%, 1.0003 86.67%, 0.999 93.33%, 1 100%);}71.4646%{transform:none;}100%{transform:none;}}.b-ad96f682 .bn6-ad96f682 .beck-fx-node{animation:kn6-ad96f682 36.17s linear infinite;}@keyframes kn6-ad96f682{0%{transform:none;}92.2578%{transform:none;animation-timing-function:linear(0 0%, 0.2557 3.33%, 0.4834 6.67%, 0.6845 10%, 0.8603 13.33%, 1.0121 16.67%, 1.1414 20%, 1.2495 23.33%, 1.3377 26.67%, 1.4074 30%, 1.46 33.33%, 1.4967 36.67%, 1.5191 40%, 1.5284 43.33%, 1.5259 46.67%, 1.4913 53.33%, 1.4261 60%, 1.3411 66.67%, 1.2472 73.33%, 1.1554 80%, 1.0763 86.67%, 1.0209 93.33%, 1 100%);}92.7555%{transform:translateY(-2px) scale(1.04);animation-timing-function:linear(0 0%, 0.1286 1.67%, 0.2749 3.33%, 0.5785 6.67%, 0.7194 8.33%, 0.8455 10%, 0.9534 11.67%, 1.0415 13.33%, 1.1093 15%, 1.1575 16.67%, 1.1878 18.33%, 1.2023 20%, 1.2035 21.67%, 1.1941 23.33%, 1.154 26.67%, 1.0496 33.33%, 1.0082 36.67%, 0.9807 40%, 0.9668 43.33%, 0.964 46.67%, 0.9773 53.33%, 0.9952 60%, 1.0049 66.67%, 1.0061 73.33%, 1.0032 80%, 1.0003 86.67%, 0.999 93.33%, 1 100%);}93.9169%{transform:none;}100%{transform:none;}}.b-ad96f682 .brip0-ad96f682{animation:krip0-ad96f682 36.17s linear infinite;}@keyframes krip0-ad96f682{0%{opacity:0;transform:scale(1);}21.3701%{opacity:0;transform:scale(1);}21.3801%{opacity:0.6;transform:scale(1);animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}22.7073%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-ad96f682 .brip1-ad96f682{animation:krip1-ad96f682 36.17s linear infinite;}@keyframes krip1-ad96f682{0%{opacity:0;transform:scale(1);}26.2509%{opacity:0;transform:scale(1);}26.2609%{opacity:0.6;transform:scale(1);animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}27.5882%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-ad96f682 .brip2-ad96f682{animation:krip2-ad96f682 36.17s linear infinite;}@keyframes krip2-ad96f682{0%{opacity:0;transform:scale(1);}49.3892%{opacity:0;transform:scale(1);}49.3992%{opacity:0.6;transform:scale(1);animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}50.7264%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-ad96f682 .brip3-ad96f682{animation:krip3-ad96f682 36.17s linear infinite;}@keyframes krip3-ad96f682{0%{opacity:0;transform:scale(1);}55.1505%{opacity:0;transform:scale(1);}55.1605%{opacity:0.6;transform:scale(1);animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}56.4877%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-ad96f682 .brip4-ad96f682{animation:krip4-ad96f682 36.17s linear infinite;}@keyframes krip4-ad96f682{0%{opacity:0;transform:scale(1);}69.7955%{opacity:0;transform:scale(1);}69.8055%{opacity:0.6;transform:scale(1);animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}71.1328%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-ad96f682 .brip5-ad96f682{animation:krip5-ad96f682 36.17s linear infinite;}@keyframes krip5-ad96f682{0%{opacity:0;transform:scale(1);}92.2478%{opacity:0;transform:scale(1);}92.2578%{opacity:0.6;transform:scale(1);animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}93.5851%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-ad96f682 .bbeat0-ad96f682{animation:kbe0-ad96f682 36.17s linear infinite;}@keyframes kbe0-ad96f682{0%{opacity:0;}0.3218%{opacity:0;}0.3318%{opacity:0;animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}1.1613%{opacity:1;}27.92%{opacity:1;animation-timing-function:linear(0 0%, 0.0044 6.67%, 0.0178 13.33%, 0.04 20%, 0.0711 26.67%, 0.1111 33.33%, 0.16 40%, 0.2178 46.67%, 0.2844 53.33%, 0.36 60%, 0.4444 66.67%, 0.5378 73.33%, 0.64 80%, 0.7511 86.67%, 0.8711 93.33%, 1 100%);}28.2518%{opacity:0;}100%{opacity:0;}}.b-ad96f682 .bbeat1-ad96f682{animation:kbe1-ad96f682 36.17s linear infinite;}@keyframes kbe1-ad96f682{0%{opacity:0;}28.2418%{opacity:0;}28.2518%{opacity:0;animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}29.0813%{opacity:1;}56.8195%{opacity:1;animation-timing-function:linear(0 0%, 0.0044 6.67%, 0.0178 13.33%, 0.04 20%, 0.0711 26.67%, 0.1111 33.33%, 0.16 40%, 0.2178 46.67%, 0.2844 53.33%, 0.36 60%, 0.4444 66.67%, 0.5378 73.33%, 0.64 80%, 0.7511 86.67%, 0.8711 93.33%, 1 100%);}57.1513%{opacity:0;}100%{opacity:0;}}.b-ad96f682 .bbeat2-ad96f682{animation:kbe2-ad96f682 36.17s linear infinite;}@keyframes kbe2-ad96f682{0%{opacity:0;}57.1413%{opacity:0;}57.1513%{opacity:0;animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}57.9809%{opacity:1;}71.4646%{opacity:1;animation-timing-function:linear(0 0%, 0.0044 6.67%, 0.0178 13.33%, 0.04 20%, 0.0711 26.67%, 0.1111 33.33%, 0.16 40%, 0.2178 46.67%, 0.2844 53.33%, 0.36 60%, 0.4444 66.67%, 0.5378 73.33%, 0.64 80%, 0.7511 86.67%, 0.8711 93.33%, 1 100%);}71.7964%{opacity:0;}100%{opacity:0;}}.b-ad96f682 .bbeat3-ad96f682{animation:kbe3-ad96f682 36.17s linear infinite;}@keyframes kbe3-ad96f682{0%{opacity:0;}71.7864%{opacity:0;}71.7964%{opacity:0;animation-timing-function:linear(0 0%, 0.187 6.67%, 0.349 13.33%, 0.488 20%, 0.6056 26.67%, 0.7037 33.33%, 0.784 40%, 0.8483 46.67%, 0.8984 53.33%, 0.936 60%, 0.963 66.67%, 0.981 73.33%, 0.992 80%, 0.9976 86.67%, 0.9997 93.33%, 1 100%);}72.6259%{opacity:1;}96.6819%{opacity:1;animation-timing-function:linear(0 0%, 0.0044 6.67%, 0.0178 13.33%, 0.04 20%, 0.0711 26.67%, 0.1111 33.33%, 0.16 40%, 0.2178 46.67%, 0.2844 53.33%, 0.36 60%, 0.4444 66.67%, 0.5378 73.33%, 0.64 80%, 0.7511 86.67%, 0.8711 93.33%, 1 100%);}97.1819%{opacity:0;}100%{opacity:0;}}}runs firstnormalizedreconcileddata-headPage-band contributorsOrder 40 — per-page OpenGraph, titleSite-band contributorsOrder 60 — canonical, alternates, defaultsPage-authored markup<HeadContent> · HeadOutletHeadBuilderkeyed dedup — first add winsHeadCompositionHtmlRewriterin the shared AngleSharp passDocument headevery element stamped data-headSPA navigation scriptLowest Order runs first — a page tag beats a site default at the same keyRendered page tags are pulled into the same model and win their keysOne finalization point, between the locale and base-URL rewritersOn soft navigation the client removes and re-clones every [data-head] element — a rule, not a list
### The typed model

 
Everything that can land in the head is a typed `HeadTag` — a union of `TitleTag`, `MetaNameTag`, `MetaPropertyTag`, `LinkTag`, `ScriptTag`, and a `RawTag` case for markup the engine does not model. Each tag that should appear at most once carries a stable `HeadTagKey`: `title`, `meta:prop:og:image`, `link:rel:canonical`, and so on. Repeatable tags — `hreflang` alternates, JSON-LD blocks, preloads, the Standard Site links — carry no key and always append.

 
The key is what makes dedup work. A `HeadBuilder` keeps a keyed map where the first add at a key wins and later same-key adds are dropped, alongside a keyless list for repeatables.

 
```csharp:symbol,signatures
public sealed class HeadBuilder
{
    private readonly Dictionary<string, HeadTag> _keyed = new(StringComparer.Ordinal);
    private readonly List<string> _keyOrder = [];
    private readonly List<HeadTag> _keyless = [];
  
    /// <summary>Adds a tag under an explicit dedup key; the first add at a key wins.</summary>
    public HeadBuilder Add(HeadTagKey key, HeadTag tag)
    { … }
  
    /// <summary>Adds a repeatable tag (hreflang, JSON-LD, preload) with no deduplication.</summary>
    public HeadBuilder AddRepeatable(HeadTag tag)
    { … }
  
    /// <summary>Sets the document title (deduplicated to one).</summary>
    public HeadBuilder Title(string text) …;
  
    /// <summary>Sets a named meta tag, deduplicated on its <paramref name="name"/>.</summary>
    public HeadBuilder Meta(string name, string content)
        …;
  
    /// <summary>Sets an OpenGraph/property meta tag, deduplicated on its <paramref name="property"/>.</summary>
    public HeadBuilder Property(string property, string content)
        …;
  
    /// <summary>Sets a singleton link (e.g. canonical), deduplicated on its <paramref name="rel"/>.</summary>
    public HeadBuilder Link(string rel, string href)
        …;
  
    /// <summary>The composed entries: keyed tags first (first-seen order), then keyless tags (append order).</summary>
    public IReadOnlyList<HeadEntry> Build()
    { … }
}
```

 
### The contributor extension point

 
A contributor is an ordered, gated unit — the same form as the `IHtmlResponseRewriter` it often replaces.

 
```csharp:symbol
public interface IHeadContributor
{
    /// <summary>
    /// Ascending priority (use the <see cref="HeadOrder"/> bands). Contributors run lowest-first,
    /// and on a <see cref="HeadTagKey"/> collision the lowest order wins.
    /// </summary>
    int Order { get; }
  
    /// <summary>Cheap gate. Return <c>false</c> to skip <see cref="ContributeAsync"/> entirely.</summary>
    bool ShouldContribute(HeadContext context);
  
    /// <summary>Pushes tags into the builder for this request.</summary>
    Task ContributeAsync(HeadContext context, HeadBuilder head);
}
```

 
`Order` does double duty. Contributors run lowest-first, and on a key collision the lowest order wins — so a page-level tag beats a site-level default for the same key without either side knowing about the other. To keep those numbers from drifting back into ad-hoc collisions, `Order` is chosen from named bands rather than raw integers:

 
```csharp:symbol,bodyonly
/// <summary>Page-authored or page-computed tags: title, description, per-page OpenGraph. Wins ties against site defaults.</summary>
public const int Page = 40;
  
/// <summary>Site-wide defaults: canonical, <c>og:site_name</c>, RSS/llms alternates, hreflang.</summary>
public const int Site = 60;
  
/// <summary>Discovery payloads: JSON-LD structured data and Standard Site verification links.</summary>
public const int Discovery = 80;
```

 
A page-OpenGraph contributor at `Page` (40) and a site-default contributor at `Site` (60) can both try to set `og:image`; the page wins because it ran first, and the site default steps aside through the same dedup that would have collapsed two identical tags. The bands encode the precedence relationship that the old hand-picked integers only implied.

 
### The composition rewriter

 
A single rewriter, `HeadCompositionHtmlRewriter`, is the only place head tags are finalized. It runs inside the shared AngleSharp pass described in [the response-processing explanation](https://usepennington.net/explanation/core/response-processing.md) — so composing the entire head costs no extra parse or serialize, just another mutation of the already-parsed document.

 
Its order matters here for a subtle reason. It sits between locale rewriting and base-URL prefixing in the [shared rewriter chain](https://usepennington.net/explanation/core/response-processing.md), so any root-relative `href` a contributor emits — an asset, an alternate link — gets sub-path prefixed by the base-URL rewriter exactly as literal head markup would. A contributor never has to know the deployment base URL; it emits `/rss.xml` and the downstream rewriter handles the rest. That slot is the rewriter's own `Order`:

 
```csharp:symbol
public int Order => 25;
```

 
The rewriter does two things in sequence. First it composes: it runs every registered contributor whose `ShouldContribute` returns true, lowest order first, into one `HeadBuilder`, then reconciles the built tags into the document head. Tags whose keys a page already authored are left untouched — contributors fill gaps, they do not overwrite page intent. Second, it normalizes what the page authored itself.

 
### The data-head attribute

 
Every finalized head element — whether a contributor emitted it or a page authored it — carries a `data-head` attribute. That one attribute drives both halves of the system. Server-side, it marks which elements the reconciler owns. Client-side, it collapses the old allowlist into one generic sweep: on a soft navigation, remove every `[data-head]` element and clone every `[data-head]` element from the fetched document. Every future head tag survives navigation automatically, because surviving is now a property of the attribute, not of a list someone has to remember to update.

 
This is also why page authoring keeps working. A page that writes `<PageTitle>` or a `<HeadContent>` block still renders through Blazor's `HeadOutlet`; the rewriter's normalization step pulls those rendered tags into the same model, stamps them, and dedups them against contributor output — with page authorship winning on a key collision. Markup the engine does not recognize passes through as a `RawTag`, untouched. So `<HeadContent>` does not compete with the contributors; it is one more input feeding the same model.

 
### The deliberate exception

 
Two things stay as literal markup on purpose: the theme-bootstrap inline `<script>` and the stylesheet link. Both are about avoiding a flash. The theme script must run before first paint to apply dark mode, so it cannot wait for a rewriter that runs after the document is built; the stylesheet stays put because the SPA's stylesheet sync deliberately never removes an existing sheet (removing and re-adding it flashes the unstyled page). The subsystem owns meta and discovery tags — the things whose ordering and dedup were the actual problem — and leaves the two pre-paint assets where they have to be.

 
## Why one subsystem rather than leaving writers where they lived

 
The alternative — every concern emits its own head markup at the site it already occupies — is what the codebase had, and it failed in three specific ways the consolidation fixes. Dedup had no home, so two writers targeting `og:image` produced two tags. Ordering lived in scattered integers and template line numbers with no shared scale, so precedence between a page tag and a site default was implicit and fragile. And client reconciliation depended on a hand-maintained list, so the cost of adding a head tag included a second, easy-to-forget edit in JavaScript.

 
The typed model gives dedup a key to work on, the single rewriter gives ordering one scale and one finalization point, and the `data-head` attribute gives the client a rule instead of a list. The price is indirection: a tag that used to be three lines of Razor is now a small class registered in DI. For a one-off tag on a single page that price is real, which is why `<HeadContent>` still works and is still the right tool for genuinely page-local markup. For anything cross-cutting — anything that needs to dedup, order against other writers, or survive navigation — a contributor is worth the indirection.

 
## Further reading

 
 - How-to: [Add tags to the document head](https://usepennington.net/how-to/response-pipeline/head-contributor.md) — write and register an `IHeadContributor`.
 - Related explanation: [The response-processing pipeline](https://usepennington.net/explanation/core/response-processing.md) — the shared AngleSharp pass the rewriter runs inside.
 - Related explanation: [SPA navigation through region swaps](https://usepennington.net/explanation/spa/islands.md) — why the head needs client-side reconciliation at all.
 
 
[Previous
                
                The response-processing pipeline](https://usepennington.net/explanation/core/response-processing.md)[Next
                    
                MonorailCSS integration](https://usepennington.net/explanation/rendering/monorail-css.md)