---
title: SPA navigation through region swaps
description: "Why Pennington's SPA fetches the canonical HTML page and swaps marked regions — reusing the server render and its rewriters instead of a parallel JSON envelope."
canonical_url: https://usepennington.net/explanation/spa/islands/
sidecar_url: https://usepennington.net/explanation/spa/islands.md
content_hash: sha256:eaccef16eef3887310249309870c7b6a12c3f918837b07e627fc6d38f2da1087
tokens: 9515
uid: explanation.spa.islands
reading_time_minutes: 8
---

Under the Hood
# SPA navigation through region swaps

Why Pennington's SPA fetches the canonical HTML page and swaps marked regions — reusing the server render and its rewriters instead of a parallel JSON envelope.

 
DocSite ships a small SPA navigation engine — in-site clicks fetch the canonical URL, parse the response, swap marked regions, and merge head metadata. The design question this page answers: why fetch the same URL the address bar shows and parse it client-side, instead of round-tripping a small JSON envelope or letting the browser do a full reload?

 
## Context

 
Classic server-rendered sites swap the whole document on every click — simple, full-fidelity, but heavy and visually jarring when the shared chrome redrawing is indistinguishable from the content changing. Full SPAs hydrate the entire app in the browser, make every navigation instant, and pay for it with a multi-megabyte runtime on first load, a separate SEO story, and a rendering path that diverges from whatever the server would have produced. Documentation sites sit awkwardly between those two extremes. Most of each page is static prose that does not benefit from client rendering, a couple of areas — the article body, the top bar with the language switcher — genuinely want to update on each navigation, and a third category of chrome (the sidebar's active link, the page outline) should not re-render at all but does need its state nudged when the page changes around it.

 
Pennington takes a third position. Every page is fully server-rendered on first load. When the visitor clicks an in-site link, the browser fetches the same URL — the canonical HTML page — parses it with `DOMParser`, swaps regions tagged `data-spa-region` from the new document into the current one, and merges head metadata. The server render and its HTML rewriters are reused as-is, so there is no second pipeline to keep in sync.

 
## How it works

 
### One render, many slices

 
The first request to any URL returns complete server-rendered HTML, exactly as it would without SPA support — good for cold loads, good for crawlers, functional when JavaScript is disabled. Once the browser has that page and the `spa-engine.js` script from `Pennington.UI` is active, the client intercepts same-origin link clicks and re-fetches the destination URL. The response is parsed into a `Document` that supplies both the regions that change and the head deltas that follow them.

 
Every server-side rewriter — xref resolution, locale-aware link rewriting, base-URL prefixing, anything else registered as `IHtmlResponseRewriter` — applies to that response by default, because it travelled through the same `ResponseProcessingMiddleware` as a fresh-tab visit. There is no second pipeline to mirror.

 
.b-53295d71{--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-53295d71{--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-53295d71{--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-53295d71{font-family:var(--beck-font);}.b-53295d71 .beck-fx-node{transform-box:fill-box;transform-origin:center;}.b-53295d71 .beck-packet-label{font-family:var(--beck-font-mono);font-size:11px;font-weight:600;}.b-53295d71 .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-53295d71 .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-53295d71 .beck-node{filter:drop-shadow(0 1px 3px rgb(0 0 0/.3)) drop-shadow(0 4px 14px rgb(0 0 0/.4));}}.b-53295d71 .beck-node--external{stroke-dasharray:5 4;}.b-53295d71 .beck-node--subtle{opacity:.72;}.b-53295d71 .beck-node--ghost{fill:transparent;stroke-dasharray:5 4;filter:none;}.b-53295d71 .beck-icon-chip{fill:color-mix(in srgb, var(--beck-accent) 15%, var(--beck-icon-bg));}.b-53295d71 .beck-node--ghost .beck-icon-chip{fill:transparent;}.b-53295d71 .beck-icon{color:var(--beck-accent);}.b-53295d71 .beck-node-title{fill:var(--beck-text);}.b-53295d71 .beck-node-subtitle,.b-53295d71 .beck-ghost-label{fill:var(--beck-text-muted);}.b-53295d71 .beck-status-inline{fill:var(--beck-accent);}.b-53295d71 .beck-status-bg{fill:color-mix(in srgb, var(--beck-accent) 14%, transparent);}.b-53295d71 .beck-status-text{fill:var(--beck-accent);}.b-53295d71 .beck-group{fill:none;stroke:var(--beck-group-border);stroke-width:1.5;stroke-dasharray:6 6;}.b-53295d71 .beck-group-label-bg{fill:var(--beck-surface);}.b-53295d71 .beck-group-label{fill:var(--beck-group-label);}.b-53295d71 .beck-node--start{fill:var(--beck-text-muted);}.b-53295d71 .beck-node--end{fill:none;stroke:var(--beck-text-muted);stroke-width:2;}.b-53295d71 .beck-end-dot{fill:var(--beck-text-muted);}.b-53295d71 .beck-class-head{fill:color-mix(in srgb, var(--beck-accent) 10%, transparent);}.b-53295d71 .beck-class-head-border{stroke:color-mix(in srgb, var(--beck-accent) 28%, var(--beck-node-border));stroke-width:1;}.b-53295d71 .beck-class-divider{stroke:var(--beck-node-border);stroke-width:1;}.b-53295d71 .beck-class-stereo{fill:var(--beck-text-muted);}.b-53295d71 .beck-class-title{fill:var(--beck-text);}.b-53295d71 .beck-class-field{fill:var(--beck-text-muted);}.b-53295d71 .beck-class-method{fill:var(--beck-text);}.b-53295d71 .beck-lifeline{stroke-width:2;stroke-dasharray:6 7;}.b-53295d71 .beck-activation{filter:drop-shadow(0 0 5px color-mix(in srgb, var(--beck-accent) 45%, transparent));}.b-53295d71 .beck-msg-chip{fill:var(--beck-node-bg);stroke:color-mix(in srgb, var(--beck-accent) 40%, transparent);stroke-width:1;}.b-53295d71 .beck-msg-text{fill:color-mix(in srgb, var(--beck-accent) 34%, var(--beck-text));}.b-53295d71 .beck-msg--reply .beck-msg-chip{stroke:none;}.b-53295d71 .beck-msg--reply .beck-msg-text,.b-53295d71 .beck-msg-text--bare{fill:var(--beck-text-muted);}.b-53295d71 .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-53295d71 .beck-band-chip{fill:var(--beck-surface);stroke:color-mix(in srgb, var(--beck-accent) 40%, transparent);stroke-width:1;}.b-53295d71 .beck-band-label{fill:color-mix(in srgb, var(--beck-accent) 70%, var(--beck-text));}.b-53295d71 .beck-edge{fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;}.b-53295d71 .beck-edge-label{fill:var(--beck-text-muted);paint-order:stroke;stroke:var(--beck-surface);stroke-width:3px;stroke-linejoin:round;}.b-53295d71 .beck-title{fill:var(--beck-text);}.b-53295d71 .beck-subtitle{fill:var(--beck-text-muted);}@media (prefers-reduced-motion:no-preference){.b-53295d71 .bp0-53295d71{animation:kp0-53295d71 16.24s linear infinite;}@keyframes kp0-53295d71{0%{offset-distance:0%;opacity:0;}0%{offset-distance:0%;opacity:1;animation-timing-function:linear;}10.5646%{offset-distance:100%;opacity:1;}10.5746%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-53295d71 .bt0-53295d71{animation:kt0-53295d71 16.24s linear infinite;}@keyframes kt0-53295d71{0%{stroke-dashoffset:360.4;}0%{stroke-dashoffset:360.4;animation-timing-function:linear;}10.5646%{stroke-dashoffset:0;}87.6884%{stroke-dashoffset:0;}87.6984%{stroke-dashoffset:360.4;}100%{stroke-dashoffset:360.4;}}.b-53295d71 .bp1-53295d71{animation:kp1-53295d71 16.24s linear infinite;}@keyframes kp1-53295d71{0%{offset-distance:0%;opacity:0;}14.248%{opacity:0;}14.258%{offset-distance:0%;opacity:1;animation-timing-function:linear;}17.9515%{offset-distance:100%;opacity:1;}17.9615%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-53295d71 .bt1-53295d71{animation:kt1-53295d71 16.24s linear infinite;}@keyframes kt1-53295d71{0%{stroke-dashoffset:79.22;}14.258%{stroke-dashoffset:79.22;animation-timing-function:linear;}17.9515%{stroke-dashoffset:0;}87.6884%{stroke-dashoffset:0;}87.6984%{stroke-dashoffset:79.22;}100%{stroke-dashoffset:79.22;}}.b-53295d71 .bp2-53295d71{animation:kp2-53295d71 16.24s linear infinite;}@keyframes kp2-53295d71{0%{offset-distance:0%;opacity:0;}21.635%{opacity:0;}21.645%{offset-distance:0%;opacity:1;animation-timing-function:linear;}34.7599%{offset-distance:100%;opacity:1;}34.7699%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-53295d71 .bt2-53295d71{animation:kt2-53295d71 16.24s linear infinite;}@keyframes kt2-53295d71{0%{stroke-dashoffset:447.4;}21.645%{stroke-dashoffset:447.4;animation-timing-function:linear;}34.7599%{stroke-dashoffset:0;}87.6884%{stroke-dashoffset:0;}87.6984%{stroke-dashoffset:447.4;}100%{stroke-dashoffset:447.4;}}.b-53295d71 .bp3-53295d71{animation:kp3-53295d71 16.24s linear infinite;}@keyframes kp3-53295d71{0%{offset-distance:0%;opacity:0;}38.4434%{opacity:0;}38.4534%{offset-distance:0%;opacity: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%);}47.06%{offset-distance:100%;opacity:1;}47.07%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-53295d71 .bt3-53295d71{animation:kt3-53295d71 16.24s linear infinite;}@keyframes kt3-53295d71{0%{stroke-dashoffset:447.4;}38.4534%{stroke-dashoffset:447.4;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%);}47.06%{stroke-dashoffset:0;}87.6884%{stroke-dashoffset:0;}87.6984%{stroke-dashoffset:447.4;}100%{stroke-dashoffset:447.4;}}.b-53295d71 .bp4-53295d71{animation:kp4-53295d71 16.24s linear infinite;}@keyframes kp4-53295d71{0%{offset-distance:0%;opacity:0;}50.7434%{opacity:0;}50.7534%{offset-distance:0%;opacity:1;animation-timing-function:linear;}54.4469%{offset-distance:100%;opacity:1;}54.4569%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-53295d71 .bt4-53295d71{animation:kt4-53295d71 16.24s linear infinite;}@keyframes kt4-53295d71{0%{stroke-dashoffset:79.22;}50.7534%{stroke-dashoffset:79.22;animation-timing-function:linear;}54.4469%{stroke-dashoffset:0;}87.6884%{stroke-dashoffset:0;}87.6984%{stroke-dashoffset:79.22;}100%{stroke-dashoffset:79.22;}}.b-53295d71 .bp5-53295d71{animation:kp5-53295d71 16.24s linear infinite;}@keyframes kp5-53295d71{0%{offset-distance:0%;opacity:0;}58.1304%{opacity:0;}58.1404%{offset-distance:0%;opacity:1;animation-timing-function:linear;}61.8339%{offset-distance:100%;opacity:1;}61.8439%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-53295d71 .bt5-53295d71{animation:kt5-53295d71 16.24s linear infinite;}@keyframes kt5-53295d71{0%{stroke-dashoffset:79.22;}58.1404%{stroke-dashoffset:79.22;animation-timing-function:linear;}61.8339%{stroke-dashoffset:0;}87.6884%{stroke-dashoffset:0;}87.6984%{stroke-dashoffset:79.22;}100%{stroke-dashoffset:79.22;}}.b-53295d71 .bp6-53295d71{animation:kp6-53295d71 16.24s linear infinite;}@keyframes kp6-53295d71{0%{offset-distance:0%;opacity:0;}65.5174%{opacity:0;}65.5274%{offset-distance:0%;opacity:1;animation-timing-function:linear;}69.2209%{offset-distance:100%;opacity:1;}69.2309%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-53295d71 .bt6-53295d71{animation:kt6-53295d71 16.24s linear infinite;}@keyframes kt6-53295d71{0%{stroke-dashoffset:79.22;}65.5274%{stroke-dashoffset:79.22;animation-timing-function:linear;}69.2209%{stroke-dashoffset:0;}87.6884%{stroke-dashoffset:0;}87.6984%{stroke-dashoffset:79.22;}100%{stroke-dashoffset:79.22;}}.b-53295d71 .bp7-53295d71{animation:kp7-53295d71 16.24s linear infinite;}@keyframes kp7-53295d71{0%{offset-distance:0%;opacity:0;}72.9044%{opacity:0;}72.9144%{offset-distance:0%;opacity:1;animation-timing-function:linear;}76.6079%{offset-distance:100%;opacity:1;}76.6179%{opacity:0;}100%{offset-distance:0%;opacity:0;}}.b-53295d71 .bt7-53295d71{animation:kt7-53295d71 16.24s linear infinite;}@keyframes kt7-53295d71{0%{stroke-dashoffset:79.22;}72.9144%{stroke-dashoffset:79.22;animation-timing-function:linear;}76.6079%{stroke-dashoffset:0;}87.6884%{stroke-dashoffset:0;}87.6984%{stroke-dashoffset:79.22;}100%{stroke-dashoffset:79.22;}}.b-53295d71 .bn1-53295d71 .beck-fx-node{animation:kn1-53295d71 16.24s linear infinite;}@keyframes kn1-53295d71{0%{transform:none;}10.5646%{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%);}11.6726%{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%);}14.258%{transform:none;}17.9515%{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%);}19.0596%{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%);}21.645%{transform:none;}47.06%{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%);}48.168%{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%);}50.7534%{transform:none;}54.4469%{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.555%{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%);}58.1404%{transform:none;}61.8339%{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%);}62.942%{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%);}65.5274%{transform:none;}69.2209%{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.329%{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%);}72.9144%{transform:none;}76.6079%{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%);}77.7159%{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%);}80.3014%{transform:none;}100%{transform:none;}}.b-53295d71 .bn2-53295d71 .beck-fx-node{animation:kn2-53295d71 16.24s linear infinite;}@keyframes kn2-53295d71{0%{transform:none;}34.7599%{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%);}35.8679%{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%);}38.4534%{transform:none;}100%{transform:none;}}.b-53295d71 .brip0-53295d71{animation:krip0-53295d71 16.24s linear infinite;}@keyframes krip0-53295d71{0%{opacity:0;transform:scale(1);}10.5546%{opacity:0;transform:scale(1);}10.5646%{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%);}13.5194%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-53295d71 .brip1-53295d71{animation:krip1-53295d71 16.24s linear infinite;}@keyframes krip1-53295d71{0%{opacity:0;transform:scale(1);}17.9415%{opacity:0;transform:scale(1);}17.9515%{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%);}20.9063%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-53295d71 .brip2-53295d71{animation:krip2-53295d71 16.24s linear infinite;}@keyframes krip2-53295d71{0%{opacity:0;transform:scale(1);}34.7499%{opacity:0;transform:scale(1);}34.7599%{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%);}37.7147%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-53295d71 .brip3-53295d71{animation:krip3-53295d71 16.24s linear infinite;}@keyframes krip3-53295d71{0%{opacity:0;transform:scale(1);}47.05%{opacity:0;transform:scale(1);}47.06%{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.0147%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-53295d71 .brip4-53295d71{animation:krip4-53295d71 16.24s linear infinite;}@keyframes krip4-53295d71{0%{opacity:0;transform:scale(1);}54.4369%{opacity:0;transform:scale(1);}54.4469%{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%);}57.4017%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-53295d71 .brip5-53295d71{animation:krip5-53295d71 16.24s linear infinite;}@keyframes krip5-53295d71{0%{opacity:0;transform:scale(1);}61.8239%{opacity:0;transform:scale(1);}61.8339%{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%);}64.7887%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-53295d71 .brip6-53295d71{animation:krip6-53295d71 16.24s linear infinite;}@keyframes krip6-53295d71{0%{opacity:0;transform:scale(1);}69.2109%{opacity:0;transform:scale(1);}69.2209%{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%);}72.1757%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-53295d71 .brip7-53295d71{animation:krip7-53295d71 16.24s linear infinite;}@keyframes krip7-53295d71{0%{opacity:0;transform:scale(1);}76.5979%{opacity:0;transform:scale(1);}76.6079%{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%);}79.5627%{opacity:0;transform:scale(1.15);}100%{opacity:0;transform:scale(1);}}.b-53295d71 .beck-msg path{opacity:0.15;}.b-53295d71 .beck-msg-chip,.b-53295d71 .beck-msg-text{opacity:0.35;}.b-53295d71 .beck-band{opacity:0.45;}.b-53295d71 .beck-activation{opacity:0.25;}.b-53295d71 .beck-msg[data-msg="msg0"] path{animation:kchl0-53295d71 16.24s linear infinite;}@keyframes kchl0-53295d71{0%{opacity:0.15;}0%{opacity:0.15;}1.539%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.15;}100%{opacity:0.15;}}.b-53295d71 .beck-msg[data-msg="msg0"] .beck-msg-chip,.b-53295d71 .beck-msg[data-msg="msg0"] .beck-msg-text{animation:kcht0-53295d71 16.24s linear infinite;}@keyframes kcht0-53295d71{0%{opacity:0.35;}0%{opacity:0.35;}1.539%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.35;}100%{opacity:0.35;}}.b-53295d71 .beck-msg[data-msg="msg1"] path{animation:kchl1-53295d71 16.24s linear infinite;}@keyframes kchl1-53295d71{0%{opacity:0.15;}14.248%{opacity:0.15;}14.258%{opacity:0.15;}15.797%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.15;}100%{opacity:0.15;}}.b-53295d71 .beck-msg[data-msg="msg1"] .beck-msg-chip,.b-53295d71 .beck-msg[data-msg="msg1"] .beck-msg-text{animation:kcht1-53295d71 16.24s linear infinite;}@keyframes kcht1-53295d71{0%{opacity:0.35;}14.248%{opacity:0.35;}14.258%{opacity:0.35;}15.797%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.35;}100%{opacity:0.35;}}.b-53295d71 .beck-msg[data-msg="msg2"] path{animation:kchl2-53295d71 16.24s linear infinite;}@keyframes kchl2-53295d71{0%{opacity:0.15;}21.635%{opacity:0.15;}21.645%{opacity:0.15;}23.184%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.15;}100%{opacity:0.15;}}.b-53295d71 .beck-msg[data-msg="msg2"] .beck-msg-chip,.b-53295d71 .beck-msg[data-msg="msg2"] .beck-msg-text{animation:kcht2-53295d71 16.24s linear infinite;}@keyframes kcht2-53295d71{0%{opacity:0.35;}21.635%{opacity:0.35;}21.645%{opacity:0.35;}23.184%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.35;}100%{opacity:0.35;}}.b-53295d71 .beck-msg[data-msg="msg3"] path{animation:kchl3-53295d71 16.24s linear infinite;}@keyframes kchl3-53295d71{0%{opacity:0.15;}38.4434%{opacity:0.15;}38.4534%{opacity:0.15;}39.9923%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.15;}100%{opacity:0.15;}}.b-53295d71 .beck-msg[data-msg="msg3"] .beck-msg-chip,.b-53295d71 .beck-msg[data-msg="msg3"] .beck-msg-text{animation:kcht3-53295d71 16.24s linear infinite;}@keyframes kcht3-53295d71{0%{opacity:0.35;}38.4434%{opacity:0.35;}38.4534%{opacity:0.35;}39.9923%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.35;}100%{opacity:0.35;}}.b-53295d71 .beck-msg[data-msg="msg4"] path{animation:kchl4-53295d71 16.24s linear infinite;}@keyframes kchl4-53295d71{0%{opacity:0.15;}50.7434%{opacity:0.15;}50.7534%{opacity:0.15;}52.2924%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.15;}100%{opacity:0.15;}}.b-53295d71 .beck-msg[data-msg="msg4"] .beck-msg-chip,.b-53295d71 .beck-msg[data-msg="msg4"] .beck-msg-text{animation:kcht4-53295d71 16.24s linear infinite;}@keyframes kcht4-53295d71{0%{opacity:0.35;}50.7434%{opacity:0.35;}50.7534%{opacity:0.35;}52.2924%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.35;}100%{opacity:0.35;}}.b-53295d71 .beck-msg[data-msg="msg5"] path{animation:kchl5-53295d71 16.24s linear infinite;}@keyframes kchl5-53295d71{0%{opacity:0.15;}58.1304%{opacity:0.15;}58.1404%{opacity:0.15;}59.6794%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.15;}100%{opacity:0.15;}}.b-53295d71 .beck-msg[data-msg="msg5"] .beck-msg-chip,.b-53295d71 .beck-msg[data-msg="msg5"] .beck-msg-text{animation:kcht5-53295d71 16.24s linear infinite;}@keyframes kcht5-53295d71{0%{opacity:0.35;}58.1304%{opacity:0.35;}58.1404%{opacity:0.35;}59.6794%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.35;}100%{opacity:0.35;}}.b-53295d71 .beck-msg[data-msg="msg6"] path{animation:kchl6-53295d71 16.24s linear infinite;}@keyframes kchl6-53295d71{0%{opacity:0.15;}65.5174%{opacity:0.15;}65.5274%{opacity:0.15;}67.0664%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.15;}100%{opacity:0.15;}}.b-53295d71 .beck-msg[data-msg="msg6"] .beck-msg-chip,.b-53295d71 .beck-msg[data-msg="msg6"] .beck-msg-text{animation:kcht6-53295d71 16.24s linear infinite;}@keyframes kcht6-53295d71{0%{opacity:0.35;}65.5174%{opacity:0.35;}65.5274%{opacity:0.35;}67.0664%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.35;}100%{opacity:0.35;}}.b-53295d71 .beck-msg[data-msg="msg7"] path{animation:kchl7-53295d71 16.24s linear infinite;}@keyframes kchl7-53295d71{0%{opacity:0.15;}72.9044%{opacity:0.15;}72.9144%{opacity:0.15;}74.4534%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.15;}100%{opacity:0.15;}}.b-53295d71 .beck-msg[data-msg="msg7"] .beck-msg-chip,.b-53295d71 .beck-msg[data-msg="msg7"] .beck-msg-text{animation:kcht7-53295d71 16.24s linear infinite;}@keyframes kcht7-53295d71{0%{opacity:0.35;}72.9044%{opacity:0.35;}72.9144%{opacity:0.35;}74.4534%{opacity:1;}83.0715%{opacity:1;}86.765%{opacity:0.35;}100%{opacity:0.35;}}.b-53295d71 .beck-activation[data-bar="0"]{animation:kcha0-53295d71 16.24s linear infinite;}@keyframes kcha0-53295d71{0%{opacity:0.25;}33.8265%{opacity:0.25;}33.8365%{opacity:0.25;}35.6832%{opacity:1;}47.06%{opacity:1;}49.2145%{opacity:0.25;}100%{opacity:0.25;}}}click in-site linkinterceptGET canonical URLHTML responseDOMParserswap data-spa-region regionsdata-head sweepspa:commit firesReaderSPA clientServer
### The `data-spa-region` contract

 
Anywhere in the layout that should update on navigation gets a `data-spa-region="name"` attribute. The DocSite layout marks two regions out of the box:

 
 - `content` — the article body, including breadcrumbs and prev/next links.
 - `outline` — the right-rail page outline, populated client-side from the article headings on every commit.
 
 
Anything outside a marked region — the top bar, the sidebar, the outer page chrome, the mobile menu's expanded state, scroll position — stays put. The header and sidebar are intentionally outside the region system: the search button keeps its event handlers across navigations, and the sidebar keeps its scroll position while its active-state flags are patched in place from the destination's HTML. Both follow the persistent-chrome pattern covered in the next section.

 
The client picks the regions in the current document, finds elements with the same name in the parsed response, and swaps `innerHTML`. If the set of regions does not match — for example, navigating from a `MainLayout` page (`content` plus `outline`) to a `FullWidthLayout` page (only `content`) — the engine triggers a full page load. Crossing a layout boundary reloads rather than half-updating the page.

 
### Persistent chrome

 
Some chrome has the same shape on every page. The DocSite sidebar is the canonical example — across every doc page within a layout, the table of contents is structurally identical, and the only thing that varies is which link carries `data-current="true"`. Marking that as a swap region works, but it throws away DOM nodes the engine is about to rebuild to the same shape, and along with them: the user's scroll position in a long sidebar, focus on the link they tabbed to, any expand/collapse state a reader interacted with, and any iframe or animation state inside the region.

 
Leaving the element outside `data-spa-region` is the answer. The engine never queries it, never swaps its `innerHTML`, never re-runs scripts inside it. The same DOM nodes survive every navigation — `scrollTop`, focus, and live state are preserved by the browser automatically, because nothing relocates them. The cost is that the active-state attributes no longer change automatically; the server-rendered destination has the right `data-current` flags, but they live in HTML the engine no longer looks at.

 
The `spa:commit` event is the extension point. It fires after each navigation with `detail.doc` — the parsed `Document` of the destination, the same HTML the server would have rendered for that URL. Consumers read the destination's chrome out of `doc`, copy whatever state actually changed onto the live nodes, and let the server-rendered destination stay authoritative. Active-state flags, the active-area pill, anything the server already computes — gets patched in place rather than re-derived on the client. The DocSite uses this to keep the sidebar's `data-current` flags in sync without ever rebuilding the tree.

 
### Head merging

 
The `<head>` of the parsed response is authoritative for everything page-specific. The client sets the title from the destination, then reconciles the rest with one generic sweep — it removes every `data-head` element from the live head and clones every `data-head` element out of the fetched document, so any tag the server marks survives navigation without a per-tag allowlist to maintain. [The head subsystem](https://usepennington.net/explanation/core/head-subsystem.md) owns that attribute and the server-side composition behind it. Stylesheet `<link>` elements are merged by href — any new ones append to the head before the region swap so the browser has the rules ready when the new content paints. A stylesheet tagged `data-spa-reload` re-fetches with a cache buster on every navigation, the opt-in workaround for JIT stylesheets like [MonorailCSS](https://monorailcss.github.io/MonorailCss.Framework/) in dev where the URL stays constant but the contents diverge per page; the attribute is documented in [SPA engine attributes and events](https://usepennington.net/reference/spa/attributes.md).

 
### Synchronous swap, no animation

 
The round trip is small but not instant. View-transition wrappers (`document.startViewTransition` with a short cross-fade) and per-region skeleton placeholders were both on the table during the design and rejected. Both layers introduce more visible motion than they mask: the cross-fade is a flash for the eye to notice, and a skeleton replaces real content with shimmer the moment the network takes longer than a tick. The engine waits instead — old content stays on screen while the fetch runs — and the swap, scroll reset, and head update all execute in one synchronous block so the browser paints the new page as a single frame. Hover-prefetch hides the wait for the cases where it would otherwise be felt.

 
A top-of-viewport progress bar handles the unusual case where the response takes longer than the engine's silent threshold — a cold cache, a slow CDN edge. It only shows after the threshold elapses, so fast navigations never see it.

 
### Why one render path

 
A second rendering path — the JSON-envelope approach an earlier version of this engine used — looked appealing at first: a small payload, a typed metadata header, no head parsing. In practice it carved a permanent fork down the middle of the codebase. Locale rewriting did not apply to JSON responses unless re-implemented. Per-island parameter dictionaries duplicated whatever the page's Razor render already built. Active-state nav, breadcrumbs in the sidebar, language-switcher hrefs — anything outside the swapped region went stale, and consumers patched it back up with one-off client-side JavaScript. Each new HTML rewriter had to be applied twice, or quietly skipped on SPA navigation.

 
The single-path approach trades some payload size for the elimination of all of that. The full HTML response gzips to within a few KB of the JSON envelope it replaced, and the prefetch-on-hover path hides whatever cost remains.

 
## Further reading

 
 - Reference: [SPA engine attributes and events](https://usepennington.net/reference/spa/attributes.md) — the `data-spa-*` attribute contract and the `spa:commit`/`spa:before-navigate` events this page describes.
 - How-to: [Ship a custom client-side widget](https://usepennington.net/how-to/rich-content/client-side-widget.md) — attach your own browser behavior to the server-rendered HTML and re-bind it from `spa:commit` after each navigation.
 - Reference: [Pennington.DocSite.DocSiteOptions](https://usepennington.net/reference/api/doc-site-options.md)
 - External: [Islands Architecture (Jason Miller)](https://jasonformat.com/islands-architecture/) — the term "island" originates here; Pennington's regions are a degenerate case where the server renders every "island" itself.
 
 
[Previous
                
                Cross-reference resolution](https://usepennington.net/explanation/routing/cross-references.md)[Next
                    
                Locale-aware URLs and content fallback](https://usepennington.net/explanation/localization/urls-and-fallback.md)