页面片段

本页内容

page:fragments 钩子会向已渲染的公开页面的 head 或 body 提供脚本或原始 HTML。用它处理结构化元数据无法表达的浏览器代码,例如带 <noscript> 回退的分析加载器。

片段输出会作为页面的第一方代码执行。EmDash 仅对受信任的进程内插件调用此钩子;沙箱插件从不贡献片段。若页面需要 meta 标签、canonical 或 alternate 链接、或 JSON-LD,请改用兼容沙箱的 page:metadata 钩子。

声明能力与钩子

page:fragments 需要原生运行时定义中的 hooks.page-fragments:register。下面的钩子仅在存在已配置的分析 ID 时添加外部脚本和 HTML 回退:

return definePlugin({
	id: "plugin-analytics",
	version: "0.1.0",
	capabilities: ["hooks.page-fragments:register"],
	hooks: {
		"page:fragments": async (event, ctx) => {
			const analyticsId = await ctx.settings.get<string>("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",
				},
			];
		},
	},
});

原始 HTML 贡献会按原文插入。尽可能保持片段为静态;否则请转义任何可能来自设置、内容、请求数据或外部服务的值。

对于原生插件,在 definePlugin() 中声明能力。描述符不需要第二份副本,因为 createPlugin() 会提供运行时能力列表。

若缺少能力,EmDash 会记录警告且不注册钩子。钩子错误会被记录,不会阻止页面渲染。

添加布局插入点

宿主主题选择支持哪些 placement。它必须将同一 PublicPageContext 传给对应的 EmDash 组件:

  1. 在布局中构建一次页面上下文。

    ---
    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. 在文档的相应位置渲染每个受支持的 placement。

    <html lang="en">
        <head>
            <title>{title}</title>
            <EmDashHead page={page} />
        </head>
        <body>
            <EmDashBodyStart page={page} />
            <slot />
            <EmDashBodyEnd page={page} />
        </body>
    </html>

EmDashHead 渲染 head 片段以及 EmDash 与插件元数据。EmDashBodyStart 在开始的 <body> 之后立即渲染 body:start 片段,EmDashBodyEnd 在页面内容之后渲染 body:end 片段。省略某个组件的主题不会渲染该 placement 的片段。请在插件 README 中记录任何必需的插入点。

贡献参考

钩子可以返回一个贡献、一个数组或 null。支持的贡献形式如下:

kind必填字段可选字段输出行为
external-scriptplacement, srcasync, defer, attributes, key渲染 <script src="…"> 元素。
inline-scriptplacement, codeattributes, key在 <script> 元素内渲染 code。
htmlplacement, htmlkey插入 html 且不进行消毒。

placement 接受 head、body:start 或 body:end。

EmDash 会对属性名和值做 HTML 转义,并移除名称以 on 开头的属性。对于内联脚本,会转义 </,使值无法关闭 script 元素。这些渲染器检查不会让不受信任的 HTML 或 JavaScript 变得安全;插件仍须按插入的语言与上下文对插值数据进行编码。

下面的钩子将 JSON 值安全地放入内联脚本:

"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",
	};
},

在同一 placement 内,具有相同 key 的贡献保留第一个值。无 key 的外部脚本也会按 src 去重。当两个钩子可能描述同一逻辑片段时,请使用稳定的 key。

页面上下文参考

钩子接收 { page }。page 对象来自宿主布局,对于请求期间加载的内容条目,可叠加 SEO 面板的可选值。

字段类型与含义
url绝对页面 URL。
pathURL 的 pathname。
locale活动语言区域,或 null。
kindEmDash 条目为 content,其他页面为 custom。
pageType主题定义的类型,通常为 article 或 website。
title, pageTitle完整文档标题与可选的仅页面标题。
description, canonical, image页面元数据值,均可为 null。
content已渲染条目的可选 { collection, id, slug } 引用。
seoOpen Graph 标题、描述、图片与 robots 的可选覆盖。
articleMeta可选的发布时间、修改时间与作者。
siteName, siteUrl可选的公开站点身份与 origin。
breadcrumbs可选的 root-first { name, url } 项。空数组表示明确没有面包屑。

使用 kind、pageType、path、locale 或 content 限制片段出现的位置。当页面上下文已携带决策所需的身份时,不要再次获取条目。

尽可能优先使用结构化元数据

对以下输出使用 page:metadata:

  • <meta name="…"> 与 <meta property="…"> 标签
  • canonical、alternate、author、license、nlweb 与 site.standard.document 链接
  • JSON-LD 图

page:metadata 会验证其结构化贡献,与页面基础元数据去重,并同时适用于原生与沙箱插件。当浏览器需要接收结构化钩子无法表示的可执行代码或标记时,使用 page:fragments。