← back to index

01. basic marker placeholder

A single declarative <?marker> processing instruction is replaced when a matching <template for> arrives later in the same response. The connection stays open between the shell and the late fragment. No JavaScript runs on the page.

The marker, initially showing a skeleton

What was streamed in

<template for="bio">
  <h2>Paul Kinlan</h2>
  <p>Chrome DevRel...</p>
</template>

Hint: the server holds the connection open for ~1.8s then flushes the template. The shell renders immediately.

learn more

source

Everything the server runs for this example. Each file links to GitHub so you can copy or fork it.

handler.ts/app/src/examples/01-basic-marker/handler.ts
import { sleep, streamingResponse } from "../../lib/streaming.ts";
import { loadShell, readSibling, tryStatic } from "../../lib/files.ts";

const SHELL = loadShell(import.meta.url);
const LATE = readSibling(import.meta.url, "late.html");

export default function handle(_req: Request, path: string): Response | Promise<Response> {
  if (path === "/" || path === "/index.html") {
    return streamingResponse(async (write) => {
      await write(SHELL);
      await sleep(1800);
      await write(LATE);
    });
  }
  return tryStatic(import.meta.url, path) ?? new Response("Not found", { status: 404 });
}

open on GitHub →

index.html/app/src/examples/01-basic-marker/index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>01 · Basic marker</title>
  <link rel="stylesheet" href="/public/styles.css">
</head>
<body>
<main>
  <p class="crumbs"><a href="/">&larr; back to index</a></p>
  <h1>01. basic marker placeholder</h1>
  <p class="lede">A single declarative <code>&lt;?marker&gt;</code> processing instruction is replaced when a matching <code>&lt;template for&gt;</code> arrives later in the same response. The connection stays open between the shell and the late fragment. No JavaScript runs on the page.</p>

  <h3>The marker, initially showing a skeleton</h3>
  <section class="demo-area" id="bio">
    <?start name="bio">
      <div class="skeleton lg"></div>
      <div class="skeleton"></div>
      <div class="skeleton sm"></div>
    <?end>
  </section>

  <h3>What was streamed in</h3>
  <pre><code>&lt;template for="bio"&gt;
  &lt;h2&gt;Paul Kinlan&lt;/h2&gt;
  &lt;p&gt;Chrome DevRel...&lt;/p&gt;
&lt;/template&gt;</code></pre>

  <p style="margin-top:2rem;color:var(--muted);font-size:.85rem;">Hint: the server holds the connection open for ~1.8s then flushes the template. The shell renders immediately.</p>

  <section class="refs">
    <h3>learn more</h3>
    <ul>
      <li><a href="https://developer.chrome.com/blog/declarative-partial-updates?hl=en" target="_blank" rel="noopener">Chrome blog: Declarative Partial Updates</a> <span class="note">— Barry Pollard's introduction with worked examples.</span></li>
      <li><a href="https://github.com/WICG/declarative-partial-updates/blob/main/patching-explainer.md" target="_blank" rel="noopener">WICG explainer: patching</a> <span class="note">— Why <code>&lt;?start&gt;/&lt;?end&gt;</code> and <code>&lt;template for&gt;</code> exist and how they compose.</span></li>
      <li><a href="https://github.com/GoogleChromeLabs/template-for-polyfill" target="_blank" rel="noopener">template-for polyfill</a> <span class="note">— Works in browsers that don't yet support it.</span></li>
      <li><a href="https://github.com/WICG/declarative-partial-updates" target="_blank" rel="noopener">WICG/declarative-partial-updates</a> <span class="note">— Specification repo, where issues and discussion live.</span></li>
    </ul>
  </section>

  <!-- source-viewer goes here -->

  <footer class="byline">made by <a href="https://paul.kinlan.me/" target="_blank" rel="noopener">Paul Kinlan</a></footer>
</main>

open on GitHub →

late.html/app/src/examples/01-basic-marker/late.html
<template for="bio">
  <h2 style="margin:0 0 .4rem;">Paul Kinlan</h2>
  <p style="margin:0;color:var(--muted);">Chrome DevRel. Likes the web, agents, and pubs in Liverpool.</p>
</template>
</body>
</html>

open on GitHub →

lib/streaming.ts/app/src/lib/streaming.ts
export class StreamAborted extends Error {
  constructor() {
    super("stream aborted by client");
    this.name = "StreamAborted";
  }
}

export type ChunkSource = (write: (chunk: string) => Promise<void>) => Promise<void>;

const encoder = new TextEncoder();

export function streamingResponse(
  source: ChunkSource,
  contentType = "text/html; charset=utf-8",
): Response {
  let aborted = false;
  const body = new ReadableStream<Uint8Array>({
    async start(controller) {
      const write = async (chunk: string) => {
        if (aborted) throw new StreamAborted();
        try {
          controller.enqueue(encoder.encode(chunk));
        } catch {
          aborted = true;
          throw new StreamAborted();
        }
      };
      try {
        await source(write);
      } catch (err) {
        if (!(err instanceof StreamAborted)) console.error("stream source error", err);
      } finally {
        if (!aborted) {
          try {
            controller.close();
          } catch {
            // already closed
          }
        }
      }
    },
    cancel() {
      aborted = true;
    },
  });
  return new Response(body, {
    headers: {
      "content-type": contentType,
      "cache-control": "no-store",
      "x-content-type-options": "nosniff",
    },
  });
}

export function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

open on GitHub →

lib/files.ts/app/src/lib/files.ts
// File helpers for examples: read sibling files, render the page shell with the
// source viewer injected, and serve sibling static assets (CSS/JS/HTML) on demand.

import { sourceBlock } from "./source.ts";

const MIME: Record<string, string> = {
  css: "text/css; charset=utf-8",
  js: "application/javascript; charset=utf-8",
  html: "text/html; charset=utf-8",
  svg: "image/svg+xml",
  json: "application/json; charset=utf-8",
};

function dirFor(handlerUrl: string): string {
  return new URL(".", handlerUrl).pathname;
}

export function readSibling(handlerUrl: string, name: string): string {
  return Deno.readTextFileSync(dirFor(handlerUrl) + name);
}

// Reads a sibling HTML file. The marker `<!-- source-viewer goes here -->` is replaced
// with the source-viewer block. Extra named placeholders ({{name}}) can be supplied via `vars`.
const SOURCE_MARKER = "<!-- source-viewer goes here -->";

export function loadShell(
  handlerUrl: string,
  name = "index.html",
  vars: Record<string, string> = {},
): string {
  let raw = readSibling(handlerUrl, name);
  raw = raw.replace(SOURCE_MARKER, sourceBlock(handlerUrl));
  for (const [k, v] of Object.entries(vars)) {
    raw = raw.replaceAll(`{{${k}}}`, v);
  }
  return raw;
}

// Try to serve a sibling static file from the example folder. Returns null when the
// file is missing, the path escapes the folder, or the path is empty.
export function tryStatic(handlerUrl: string, requestPath: string): Response | null {
  const safe = requestPath.replace(/^\/+/, "");
  if (!safe || safe.includes("..")) return null;
  // Never let the browser fetch handler.ts or README.md through this fallback.
  if (safe === "handler.ts" || safe === "README.md") return null;
  try {
    const content = Deno.readFileSync(dirFor(handlerUrl) + safe);
    const ext = safe.split(".").pop() ?? "";
    return new Response(content, {
      headers: {
        "content-type": MIME[ext] ?? "application/octet-stream",
        "cache-control": "no-store",
      },
    });
  } catch {
    return null;
  }
}

open on GitHub →