A twist on the app-shell idea from the article: what if the server decided how much to send, based on how the page was requested?
Picture an app shell (a navbar and some state) with one marked region for
the page. Click /about from inside the
running app and the server sends back just a
<template for="page">, the patch that fills the marked
region. Request the same /about directly
(type it in, or hard-refresh) and you get the whole document: the app
shell with that same patch streamed right behind it. Same URL, two
responses, one patch.
How does the server know the difference? The browser tells it. A full
navigation carries Sec-Fetch-Mode: navigate; a
fetch() from your code carries cors or
same-origin. The server branches on that one header.
The frame loads Home as a full document, which is a navigation. Click the nav links for in-app navigation: only the page region streams in, and the load timestamp (stamped when the shell is built, so it only changes on a full render) stays put. Hit ↻ reload in the frame's address bar to force a full request and watch the timestamp change.
// The same URL, /about, served two ways. The patch is identical;
// a direct request just gets the app shell streamed in front of it.
// The patch re-emits its own <?start>/<?end> range. A marker is consumed
// when it's patched, so every update has to leave a fresh one behind for
// the next one to fill.
const patch =
`<template for="page"><?start name="page">${pageContent}<?end></template>`;
// Browsers send "Sec-Fetch-Mode: navigate" on a full page load,
// but "cors" / "same-origin" on a fetch(), so the server can tell them apart.
if (request.headers.get("Sec-Fetch-Mode") === "navigate") {
return shell + patch; // shell streams first, then the patch fills the marker
}
return patch; // in-app request: just the patch
// Intercept in-app links and fetch the next page from the server.
link.addEventListener("click", async (event) => {
event.preventDefault();
const response = await fetch(link.href); // a fetch, not a navigation
// Append the streamed <template for="page"> into <main>. It finds the
// page marker already in the DOM and replaces only that region. The
// navbar, its state, and its DOM identity are all left untouched.
await response.body
.pipeThrough(new TextDecoderStream())
.pipeTo(main.streamAppendHTMLUnsafe());
});
Registering service worker…
Load a page directly and the server sends a complete document: the app
shell, a page marker showing a brief loading state, and a
<template for="page"> that fills the marker with the
page's content. The shell paints first, then the content drops in.
Navigate inside the app and the server sends back just that
<template for="page">, with no shell. It fills the
marker that's already on the page and replaces only that region, so the
navbar, the load timestamp, and your typed text all stay exactly as they
were. The same patch either rides in with the shell (your first visit)
or arrives on its own (every visit after).
One detail that makes the "every visit after" part work: a marker is
consumed when it's patched. Patching the
<?start name="page">…<?end> range replaces the
range itself, not just what's inside it, so a patch that carried only
the page's content would leave nothing behind for the next navigation to
fill. That's why the server wraps every patch in a fresh
<?start name="page">…<?end> range: each update
hands the next one a new marker to aim at.