このページで分かること

  • page:fragments フックの役割(公開ページの headbody にスクリプト・HTMLを入れる)と、信頼されたネイティブ型プラグインだけが使えること
  • 必要な権限(hooks.page-fragments:register)と、レイアウト側で用意する挿入位置(EmDashHeadEmDashBodyStartEmDashBodyEnd
  • 追加できる内容の種類と安全に書く方法、フックが受け取るページの情報、page:metadata フックで足りる場合
難易度
上級
読む時間
3分
このページの目次

page:fragments フックは、描画された公開ページの head または body に、スクリプトやHTMLそのものを追加します。<noscript> による代替表示付きのアクセス解析の読み込みスクリプトのように、構造化されたメタデータでは表現できないブラウザー用のコードに使います。

フラグメントの出力は、サイト自身のページのコードとして実行されます。EmDashがこのフックを呼び出すのは、信頼された、同じプロセスで動くプラグインだけです。サンドボックス型プラグインがフラグメントを追加することはありません。ページに必要なのがmetaタグ、canonicalやalternateのリンク、JSON-LDであれば、代わりにサンドボックス型でも使えるpage:metadata フックを使います。

権限とフックの宣言

page:fragments には、ネイティブ型の実行時の定義に hooks.page-fragments:register が必要です。次のフックは、アクセス解析のIDが設定されている場合にだけ、外部スクリプトとHTMLの代替表示を追加します。

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

HTMLそのものの追加は、そのまま挿入されます。できるだけフラグメントを固定の内容にします。そうでない場合は、設定、コンテンツ、リクエストのデータ、外部サービスから来る可能性のある値をすべてエスケープします。

ネイティブ型プラグインでは、権限を definePlugin() で宣言します。createPlugin() が実行時の権限の一覧を提供するため、ディスクリプターに同じものを書く必要はありません。

権限がない場合、EmDashは警告をログに出力し、フックを登録しません。フックのエラーはログに出力され、ページの描画を妨げません。

本サイトの補足 やさしい解説

公式の対応表では、WordPressの header.phpfooter.php は、Astroのレイアウトにあたります。page:fragments フックが返したスクリプトやHTMLが公開ページに挿入されます。挿入される場所は、レイアウトに置いた EmDashHeadEmDashBodyStartEmDashBodyEnd です。サイトのコードとして実行されるため、使えるのは信頼されたネイティブ型プラグインだけです。metaタグやJSON-LDだけで足りる場合は、サンドボックス型でも使える page:metadata フックを使います。

レイアウトへの挿入位置の追加

どの配置に対応するかは、ホストのテーマが決めます。テーマは、対応するEmDashのコンポーネントに同じ PublicPageContext を渡す必要があります。

  1. レイアウトの中で、ページのコンテキストを1回だけ作ります。

    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. 対応する配置ごとに、ドキュメントの該当する部分で描画します。

    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 は、head のフラグメントと、EmDashとプラグインのメタデータを描画します。EmDashBodyStartbody:start のフラグメントを <body> の開始タグの直後に描画し、EmDashBodyEndbody:end のフラグメントをページのコンテンツのあとに描画します。コンポーネントを置いていないテーマでは、その配置のフラグメントは描画されません。必要な挿入位置は、プラグインのREADMEに書いておきます。

追加する内容のリファレンス

フックは、1つの追加内容、その配列、または null を返せます。対応している追加内容の形は次のとおりです。

kind 必須のフィールド 省略可能なフィールド 出力の動作
external-script placementsrc asyncdeferattributeskey <script src="…"> 要素を描画する。
inline-script placementcode attributeskey code<script> 要素の中に描画する。
html placementhtml key html を無害化せずに挿入する。

placement には headbody:startbody:end を指定できます。

EmDashは、属性の名前と値をHTMLエスケープし、名前が on で始まる属性を取り除きます。インラインスクリプトでは、値がscript要素を閉じられないように </ をエスケープします。描画時のこれらの確認によって、信頼できないHTMLやJavaScriptが安全になるわけではありません。埋め込むデータは、挿入先の言語と文脈に合わせて、プラグインがエンコードする必要があります。

次のフックは、JSONの値を安全にインラインスクリプトに入れます。

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

1つの配置の中で同じ key を持つ追加内容は、最初の値が残ります。キーのない外部スクリプトも、src によって重複が取り除かれます。2つのフックが論理的に同じフラグメントを記述する可能性がある場合は、変わらないキーを使います。

ページのコンテキストのリファレンス

フックは { page } を受け取ります。pageオブジェクトはホストのレイアウトから渡されます。リクエスト中に読み込まれたコンテンツのエントリーの場合は、SEOパネルで設定された値があれば、それで上書きされます。

フィールド 型と意味
url ページの絶対URL。
path URLのパス名。
locale 有効なロケール、または null
kind EmDashのエントリーなら content、それ以外のページなら custom
pageType テーマが定義する種類。一般的には article または website
titlepageTitle ドキュメントの完全なタイトルと、省略可能なページだけのタイトル。
descriptioncanonicalimage ページのメタデータの値。それぞれ null の場合がある。
content 描画されたエントリーを指す、省略可能な { collection, id, slug }
seo 省略可能な、Open Graphのタイトル、説明、画像、robotsの上書き。
articleMeta 省略可能な、公開日時、更新日時、作者。
siteNamesiteUrl 省略可能な、公開サイトの名前とオリジン。
breadcrumbs 省略可能な、ルートから順に並んだ { name, url } の項目。空の配列は、パンくずがないことを明示的に表す。

kindpageTypepathlocalecontent を使って、フラグメントを表示する場所を絞り込みます。判断に必要な識別情報がページのコンテキストにすでにある場合は、エントリーをもう一度取得しないでください。

構造化されたメタデータの優先

次の出力には page:metadata を使います。

  • <meta name="…"><meta property="…"> のタグ
  • canonical、alternate、author、license、nlwebsite.standard.document のリンク
  • JSON-LDのグラフ

page:metadata は、構造化された追加内容を検証し、ページの基本のメタデータとの重複を取り除き、ネイティブ型とサンドボックス型の両方のプラグインで動作します。構造化されたフックでは表せない、実行可能なコードやマークアップをブラウザーに渡す必要がある場合に、page:fragments を使います。