Skip to content

Page fragments

The page:fragments hook contributes scripts or raw HTML to the head or body of a rendered public page. Use it for browser code that structured metadata cannot express, such as an analytics loader with a <noscript> fallback.

Fragment output runs as first-party page code. EmDash invokes this hook only for trusted, in-process plugins; sandboxed plugins never contribute fragments. If the page needs meta tags, canonical or alternate links, or JSON-LD, use the sandbox-compatible page:metadata hook instead.

page:fragments requires hooks.page-fragments:register in the native runtime definition. The following hook adds an external script and an HTML fallback only when a configured analytics ID exists:

src/index.ts
return definePlugin({
id: "plugin-analytics",
version: "0.1.0",
capabilities: ["hooks.page-fragments:register"],
hooks: {
"page:fragments": async (event, ctx) => {
const analyticsId = await ctx.kv.get<string>("settings:analyticsId");
if (!analyticsId || event.page.path.startsWith("/_emdash/")) return null;
const encodedId = encodeURIComponent(analyticsId);
return [
{
kind: "external-script",
placement: "head",
src: `https://analytics.example.com/client.js?id=${encodedId}`,
async: true,
key: "analytics-client",
},
{
kind: "html",
placement: "body:end",
html: "<noscript>Analytics requires JavaScript.</noscript>",
key: "analytics-fallback",
},
];
},
},
});

Raw HTML contributions are inserted verbatim. Keep the fragment static when possible; otherwise escape every value that can come from settings, content, request data, or an external service.

For native plugins, declare the capability in definePlugin(). The descriptor does not need a second copy because createPlugin() supplies the runtime capability list.

If the capability is missing, EmDash logs a warning and does not register the hook. Hook errors are logged and do not prevent the page from rendering.

The host theme chooses which placements it supports. It must pass the same PublicPageContext to the corresponding EmDash components:

  1. Build the page context once in the layout.

    src/layouts/Base.astro
    ---
    import { createPublicPageContext } from "emdash/page";
    import {
    EmDashBodyEnd,
    EmDashBodyStart,
    EmDashHead,
    } from "emdash/ui";
    interface Props {
    title: string;
    description?: string;
    content?: { collection: string; id: string; slug?: string | null };
    }
    const { title, description, content } = Astro.props;
    const page = createPublicPageContext({
    Astro,
    kind: content ? "content" : "custom",
    pageType: content ? "article" : "website",
    title,
    description,
    content,
    });
    ---
  2. Render each supported placement in the matching part of the document.

    src/layouts/Base.astro
    <html lang="en">
    <head>
    <title>{title}</title>
    <EmDashHead page={page} />
    </head>
    <body>
    <EmDashBodyStart page={page} />
    <slot />
    <EmDashBodyEnd page={page} />
    </body>
    </html>

EmDashHead renders head fragments as well as EmDash and plugin metadata. EmDashBodyStart renders body:start fragments immediately after the opening <body>, and EmDashBodyEnd renders body:end fragments after the page content. A theme that omits a component does not render fragments for that placement. Document any required insertion point in the plugin README.

A hook can return one contribution, an array, or null. The supported contribution shapes are:

kindRequired fieldsOptional fieldsOutput behavior
external-scriptplacement, srcasync, defer, attributes, keyRenders a <script src="…"> element.
inline-scriptplacement, codeattributes, keyRenders code inside a <script> element.
htmlplacement, htmlkeyInserts html without sanitizing it.

placement accepts head, body:start, or body:end.

EmDash HTML-escapes attribute names and values and removes attributes whose names begin with on. For inline scripts, it escapes </ so a value cannot close the script element. These renderer checks do not make untrusted HTML or JavaScript safe; the plugin must still encode interpolated data for the language and context where it is inserted.

The following hook safely places a JSON value in an inline script:

src/index.ts
"page:fragments": async (event) => {
if (event.page.kind !== "content" || !event.page.content) return null;
return {
kind: "inline-script",
placement: "body:start",
code: `window.currentContent = ${JSON.stringify({
collection: event.page.content.collection,
id: event.page.content.id,
})};`,
key: "current-content",
};
},

Within one placement, contributions with the same key keep the first value. External scripts without a key are also deduplicated by src. Use a stable key when two hooks could describe the same logical fragment.

The hook receives { page }. The page object comes from the host layout, with optional SEO-panel values overlaid for a content entry that was loaded during the request.

FieldType and meaning
urlAbsolute page URL.
pathURL pathname.
localeActive locale, or null.
kindcontent for an EmDash entry or custom for another page.
pageTypeTheme-defined type, commonly article or website.
title, pageTitleFull document title and optional page-only title.
description, canonical, imagePage metadata values, each nullable.
contentOptional { collection, id, slug } reference for the rendered entry.
seoOptional Open Graph title, description, image, and robots overrides.
articleMetaOptional published time, modified time, and author.
siteName, siteUrlOptional public site identity and origin.
breadcrumbsOptional root-first { name, url } items. An empty array explicitly means no breadcrumbs.

Use kind, pageType, path, locale, or content to limit where a fragment appears. Do not fetch an entry again when the page context already carries the identity needed for the decision.

Use page:metadata for the following output:

  • <meta name="…"> and <meta property="…"> tags
  • canonical, alternate, author, license, nlweb, and site.standard.document links
  • JSON-LD graphs

page:metadata validates its structured contributions, deduplicates them with the page’s base metadata, and works in both native and sandboxed plugins. Use page:fragments when the browser must receive executable code or markup that the structured hook cannot represent.