Page fragments
The page:fragments hook lets a plugin contribute raw HTML, scripts, or stylesheets to public pages. It’s the right tool for analytics tags, third-party widgets, custom CSS, and anything else that needs to ship JavaScript or markup directly into the visitor’s browser.
It’s restricted to native plugins because its output runs as first-party code in the browser, outside any sandbox boundary. If you only need to contribute structured metadata — meta tags, OpenGraph, JSON-LD, allowlisted <link> rels — use page:metadata instead, which is available to both sandboxed and native plugins. See Hooks: page:metadata.
Capability
Section titled “Capability”page:fragments requires the hooks.page-fragments:register capability:
return definePlugin({ id: "analytics-gtm", version: "1.0.0", capabilities: ["hooks.page-fragments:register"], // ...});The capability must also appear on the descriptor.
Where fragments render
Section titled “Where fragments render”Templates opt into receiving fragments by including the relevant components from emdash/ui:
<EmDashHead />— renders fragments withplacement: "head"plus allpage:metadatacontributions.<EmDashBodyStart />— renders fragments withplacement: "body:start".<EmDashBodyEnd />— renders fragments withplacement: "body:end".
Templates that omit one of these components silently ignore fragments targeting that placement — your plugin doesn’t break, the fragments just don’t appear. Document your placement requirement in the plugin’s README.
Contribution kinds
Section titled “Contribution kinds”A hook can return one of three contribution kinds:
type PageFragmentContribution = | { kind: "external-script"; placement: PagePlacement; src: string; async?: boolean; defer?: boolean; attributes?: Record<string, string>; key?: string; } | { kind: "inline-script"; placement: PagePlacement; code: string; attributes?: Record<string, string>; key?: string; } | { kind: "html"; placement: PagePlacement; html: string; key?: string; };PagePlacement is "head" | "body:start" | "body:end".
Examples
Section titled “Examples”External script
Section titled “External script”The following hook injects a third-party tag manager into <head>:
"page:fragments": async (event, ctx) => { const containerId = await ctx.kv.get<string>("settings:gtmContainerId"); if (!containerId) return null;
return { kind: "external-script", placement: "head", src: `https://www.googletagmanager.com/gtm.js?id=${containerId}`, async: true, };},Inline script
Section titled “Inline script”The following hook runs a small piece of JavaScript at the top of <body> on content pages:
"page:fragments": async (event, ctx) => { if (event.page.kind !== "content") return null; return { kind: "inline-script", placement: "body:start", code: `window.contentId = ${JSON.stringify(event.page.content?.id)};`, };},HTML fragment
Section titled “HTML fragment”The following hook appends a noscript fallback at the end of <body>:
"page:fragments": async (event, ctx) => { const containerId = await ctx.kv.get<string>("settings:gtmContainerId"); if (!containerId) return null;
return { kind: "html", placement: "body:end", html: `<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=${containerId}" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>`, };},Multiple fragments
Section titled “Multiple fragments”A hook can return an array to contribute multiple fragments at once. The following hook adds both a script and a noscript fallback:
"page:fragments": async (event, ctx) => { const id = await ctx.kv.get<string>("settings:gtmContainerId"); if (!id) return null;
return [ { kind: "external-script", placement: "head", src: `https://www.googletagmanager.com/gtm.js?id=${id}`, async: true, }, { kind: "html", placement: "body:end", html: `<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=${id}" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>`, }, ];},Page event
Section titled “Page event”The page:fragments hook receives the same event shape as page:metadata:
{ page: { url: string; path: string; locale: string | null; kind: "content" | "custom"; pageType: string; title: string | null; pageTitle?: string | null; description: string | null; canonical: string | null; image: string | null; content?: { collection: string; id: string; slug: string | null }; }}Use event.page.kind and event.page.pageType to decide whether to contribute on a given page — for example, skipping analytics on admin previews or only injecting JSON-LD on blog posts.
When to use page:metadata instead
Section titled “When to use page:metadata instead”If what you actually need is:
- A meta description, robots directive, or Twitter card →
page:metadatawithkind: "meta". - An OpenGraph property →
page:metadatawithkind: "property". - A canonical or alternate
<link>→page:metadatawithkind: "link". - A JSON-LD graph →
page:metadatawithkind: "jsonld".
page:metadata works in sandboxed plugins, gets validation and deduplication for free, and avoids the trust burden of shipping raw HTML to visitors. Reach for page:fragments only when you need to ship JavaScript or HTML.