Color mode

Page System

Page System は、Plugin が Site の route を直接追加せずに独立したページを提供するための仕組みです。

たとえば Plugin が、

text
/explore
/report
/tags/example

のような独自ページを提供したい場合に使用します。

Page System がない場合、Plugin ごとに HonoX の route を Site Application へ追加する必要があります。

Page System では Plugin は「この URL に、このページを提供する」という情報だけを公開し、実際の routing や document の描画は Site が担当します。

Diagram source
text
flowchart LR
    Plugin["Plugin"]
    Page["Page Type<br/>path + body"]
    Resolver["共通 Route Resolver"]
    Site["Site Application"]
    Browser["Browser"]
 
    Plugin --> Page
    Page --> Resolver
    Resolver --> Site
    Site --> Browser

pageTypes は通常の RiebeckitePlugin が持つ capability の1つです。

Page 専用の別種類の Plugin を作るわけではありません。

どんなときに使うか

Page Type は、Plugin が独立した URL を持つページを提供するときに使用します。

たとえば、

  • Garden Explorer
  • Plugin のレポート画面
  • Plugin が生成する一覧ページ
  • 独自の検索・閲覧ページ

などです。

一方、記事本文の中へ何かを表示するだけなら Page Type は必要ありません。

Diagram source
text
flowchart TD
    Q{"Plugin が何を表示する?"}
 
    Q -->|"独立したURLを持つページ"| Page["pageTypes"]
    Q -->|"記事本文へ埋め込む"| Renderer["renderers"]
 
    Page --> P1["例: /explore"]
    Page --> P2["例: /report"]
 
    Renderer --> R1["Canvas"]
    Renderer --> R2["Bases"]
    Renderer --> R3["Excalidraw"]

Canvas、Bases、Excalidraw のように記事内へ埋め込む機能は renderers を使用します。

誰が何を担当するか

Page System では、Plugin がページ全体を所有するわけではありません。

責務を次のように分離します。

層 責務
Core Page Type の contract、検証、path 列挙、解決、競合検出
Plugin Page Type ID、公開 path、framework 非依存の HTML body
HonoX Integration 共通 route resolver、SSG parameter helper
Site Application route、document frame、metadata、HTML の描画
Theme token と stable hook による見た目

全体では次のような関係になります。

Diagram source
text
flowchart LR
    Plugin["Plugin<br/>Pageを提供"]
    Core["Core<br/>Page contract / resolution"]
    Integration["HonoX Integration<br/>Routing / SSG"]
    Site["Site<br/>Document / Rendering"]
    Theme["Theme<br/>CSS"]
 
    Plugin --> Core
    Core --> Integration
    Integration --> Site
    Theme --> Site

route と document frame は Site Application が所有します。

Plugin はページの内容を提供しますが、

html
<html>
<head>
...
<body>

のような document 全体を生成するわけではありません。

Page System が解決する問題

Page System 導入前は、独立ページを持つ Plugin ごとに Site 側へ専用 route が必要でした。

Diagram source
text
flowchart LR
    Request["Request"]
    Route["Plugin専用Route"]
    Component["Plugin専用Component"]
    Frame["Site Document Frame"]
 
    Request --> Route
    Route --> Component
    Component --> Frame

この方式では Plugin を追加するだけでは動かず、Site Application の route も変更する必要があります。

つまり Plugin と Site の結合が強くなります。

Page System では、すべての Plugin Page が共通 route resolver に参加します。

Diagram source
text
flowchart LR
    Request["Request"]
    Catch["Catch-all Route"]
    Resolver["resolveRiebeckiteRoute"]
    Page["Plugin Page"]
    Content["Content"]
    Redirect["Redirect"]
 
    Request --> Catch
    Catch --> Resolver
 
    Resolver --> Page
    Resolver --> Content
    Resolver --> Redirect

これにより、新しい Plugin Page を追加するたびに HonoX route を追加する必要がありません。

Build と Request の流れ

Page System には、大きく2つの処理があります。

Build 時

Build 時には、Plugin が提供する Page Type から静的生成する URL を集めます。

Diagram source
text
flowchart LR
    Plugin["Plugin Page Type"]
    Paths["Public Paths"]
    Params["SSG Parameters"]
    Build["Static Generation"]
 
    Plugin --> Paths
    Paths --> Params
    Params --> Build

たとえば Plugin が、

text
/report

を提供していれば、その path を SSG の生成対象にできます。

Request 時

Request が来ると、共通 resolver がどの種類のページかを判断します。

Diagram source
text
flowchart TD
    Request["Request"]
    Resolver["resolveRiebeckiteRoute"]
 
    Request --> Resolver
 
    Resolver --> Q{"何が一致した?"}
 
    Q -->|"Plugin Page"| Page["Plugin Page"]
    Q -->|"Content"| Content["Content"]
    Q -->|"Redirect"| Redirect["Redirect"]
    Q -->|"なし"| NotFound["Not Found"]

Plugin Page が解決された場合、その body を Site の document frame 内へ描画します。

Diagram source
text
flowchart LR
    Page["Plugin Page<br/>body / metadata"]
    Frame["Site Document Frame"]
    Theme["Theme CSS"]
    Client["Plugin Client Entry"]
    Browser["Final Page"]
 
    Page --> Frame
    Theme --> Frame
    Client --> Frame
    Frame --> Browser

Page Type を作る

Page Type は Plugin の pageTypes に定義します。

ts
import { definePlugin } from "@riebeckite/core";
 
export function reportPlugin() {
  return definePlugin({
    name: "report",
 
    pageTypes: [
      {
        id: "example.report",
        paths: ["/report"],
 
        resolve: ({ pathname, manifest }) =>
          pathname === "/report"
            ? {
                type: "example.report",
                pathname,
                title: "Report",
                body: `<p>${manifest.publicEntries.length} published entries</p>`,
              }
            : null,
      },
    ],
  });
}

この例では、

text
/report

というページを Plugin が提供します。

ページ本文には、到達可能な記事数を表示しています。

Plugin が HonoX component や route file を作る必要はありません。

Manifest と公開境界

Page Type の resolve には PluginPageContext が渡されます。

その中の、

ts
PluginPageContext.manifest

は解決済みの Manifest です。entries には draft と scheduled も含まれるため、Page の出力には使いません。

到達可能な URL を網羅する場合は publicEntries(public と unlisted)、読者へ一覧として見せる場合は discoverableEntries(public のみ)を使います。entry ごとに分岐が必要な場合だけ、解決済みの entry.publishing を読みます。

Diagram source
text
flowchart LR
    All["All Content"]
    Publish["Publish Boundary"]
    Public["publicEntries / discoverableEntries"]
    Page["Plugin Page"]
 
    All --> Publish
    Publish --> Public
    Public --> Page

非公開コンテンツを Plugin Page 側で独自に探索して表示する設計にはしません。

これによって Page System も通常の Site と同じ公開境界を維持できます。

Page Type ID

各 Page Type には ID が必要です。

ts
id: "example.report"

Page Type ID は、すべての Plugin を通して一意である必要があります。

たとえば、

text
riebeckite.garden-explorer
example.report
example.search

のように Plugin や用途が分かる namespace を持たせると衝突を避けやすくなります。

Path

静的なページは paths に公開 path を指定します。

ts
paths: ["/report"]

複数のページを提供することもできます。

ts
paths: [
  "/report",
  "/report/archive",
]

動的ページを静的生成する場合は、SSG path を manifest.publicEntries から導出します。

filesystem を独自に scan して path を作るのではなく、Content System が解決した公開状態を利用してください。

Page Type の競合

複数の Page Type が同じ URL に一致する可能性があります。

その場合は priority を使って解決します。

Diagram source
text
flowchart TD
    Path["/report"]
    A["Page Type A<br/>priority: 10"]
    B["Page Type B<br/>priority: 20"]
 
    Path --> A
    Path --> B
 
    A --> Compare{"priority"}
    B --> Compare
 
    Compare -->|"B が高い"| Result["Page Type B"]

最も高い priority を持つ Page Type が選ばれます。

ただし、同じ最大 priority の Page Type が複数一致した場合は、どちらかを暗黙的に選びません。

Diagram source
text
flowchart TD
    Match["複数Page Typeが一致"]
    Priority{"最大priorityは一意?"}
 
    Match --> Priority
 
    Priority -->|Yes| Resolve["そのPage Typeを使用"]
    Priority -->|No| Error["Conflict Error"]

競合を明示的なエラーにすることで、Plugin の登録順などによって結果が変わることを防ぎます。

HonoX Site へ接続する

Plugin Page を利用する HonoX Site には、共通の catch-all route が必要です。

create-riebeckite などで生成された Site には、必要な構成があらかじめ含まれています。

概念的には次のように接続します。

tsx
import {
  contentRouteSsgParams,
  resolveRiebeckiteContentRequest,
  riebeckiteSsgParams,
} from "@riebeckite/honox/server";
import { PageBody } from "@riebeckite/honox/ui";
import { createRoute } from "honox/factory";
 
export default createRoute(
  contentRouteSsgParams("/:slug{.+}", () => riebeckiteSsgParams(content)),
  async (c) => {
    const resolved = await resolveRiebeckiteContentRequest(c, content);
 
    if (resolved.kind === "response") return resolved.response;
 
    if (resolved.kind === "page") {
      return c.render(<PageBody html={resolved.page.body} />);
    }
 
    return c.render(/* Site 固有の article composition */);
  },
);

通常の Site 利用者が Plugin ごとにこの route を追加する必要はありません。

共通 route が Plugin Page をまとめて解決します。root / も同じ仕組みを共有する resolveRiebeckiteHomeRequest(c, content) が解決します。

Page が返せるもの

Plugin Page は HTML body に加えて、必要に応じてページの metadata を提供できます。

たとえば、

text
body
title
description
headTags
language

などです。

ただし、Plugin がそれらをどの位置へ描画するかまで決定するわけではありません。

Diagram source
text
flowchart LR
    Plugin["Plugin"]
 
    Plugin --> Body["body"]
    Plugin --> Title["title"]
    Plugin --> Description["description"]
    Plugin --> Head["headTags"]
    Plugin --> Language["language"]
 
    Body --> Site["Site Application"]
    Title --> Site
    Description --> Site
    Head --> Site
    Language --> Site
 
    Site --> Document["Final Document"]

Site Application がこれらを受け取り、既存の document frame に反映します。

このため、Plugin Page でも Site 全体の navigation、header、footer、Theme などをそのまま利用できます。

HTML の安全性

Page Type の body は HTML として描画されます。

そのため Plugin は、自分自身が安全に生成できる HTML だけを返す必要があります。

たとえば request parameter をそのまま HTML に埋め込むような処理は避けます。

ts
// Avoid
body: `<p>${untrustedRequestValue}</p>`

信頼できない入力を扱う場合は、適切に escape / sanitize してから使用します。

最終的に HTML をどのように描画するかは Site Application の boundary で決定します。

Page Type が Site の HTML safety policy を回避する仕組みにはしません。

Page Type と Theme

Theme は Page Type ID を知る必要がありません。

たとえば、

text
example.report

専用の Theme logic を作るのではなく、

  • semantic token
  • stable CSS hook
  • data-* attribute
  • CSS cascade

を利用して見た目を変更します。

Diagram source
text
flowchart LR
    Page["Plugin Page"]
    Site["Site Document"]
    Hooks["Stable Hooks / Tokens"]
    Theme["Theme CSS"]
 
    Page --> Site
    Site --> Hooks
    Theme --> Hooks

これにより、新しい Page Type が追加されても Theme と Plugin を直接結合せずに済みます。

Page System の境界

Page System では、Plugin はページを提供するが、Application を所有しないことが最も重要です。

Diagram source
text
flowchart LR
    Plugin["Plugin<br/>何を表示する?"]
    Core["Core<br/>どう表現・解決する?"]
    Integration["Integration<br/>どうRouteへ接続する?"]
    Site["Site<br/>どうDocumentとして描画する?"]
    Theme["Theme<br/>どう見せる?"]
 
    Plugin --> Core
    Core --> Integration
    Integration --> Site
    Theme --> Site

責務は次のとおりです。

text
Plugin
  → Page の内容と公開 path を提供する
 
Core
  → Page Type の共通 contract と解決規則を提供する
 
HonoX Integration
  → Page Type を routing / SSG へ接続する
 
Site Application
  → route、document frame、metadata、HTML safety を所有する
 
Theme
  → Site の見た目を変更する

という関係になります。

この境界によって Plugin は HonoX や特定 Site の構造に依存せず、インストールするだけで独立ページを提供できます。

Page Type の全フィールドと実行時検証については Plugin API、Site との接続については HonoX Integration を参照してください。

History

1 changesCollapseExpand
1 + # Page System
2 +
3 + Page System は、Plugin が **Site の route を直接追加せずに独立したページを提供する**ための仕組みです。
4 +
5 + たとえば Plugin が、
6 +
7 + ```text id="9twdpj"
8 + /explore
9 + /report
10 + /tags/example
11 + ```
12 +
13 + のような独自ページを提供したい場合に使用します。
14 +
15 + Page System がない場合、Plugin ごとに HonoX の route を Site Application へ追加する必要があります。
16 +
17 + Page System では Plugin は「この URL に、このページを提供する」という情報だけを公開し、実際の routing や document の描画は Site が担当します。
18 +
19 + ```mermaid id="l1m0kn"
20 + flowchart LR
21 + Plugin["Plugin"]
22 + Page["Page Type<br/>path + body"]
23 + Resolver["共通 Route Resolver"]
24 + Site["Site Application"]
25 + Browser["Browser"]
26 +
27 + Plugin --> Page
28 + Page --> Resolver
29 + Resolver --> Site
30 + Site --> Browser
31 + ```
32 +
33 + `pageTypes` は通常の `RiebeckitePlugin` が持つ capability の1つです。
34 +
35 + Page 専用の別種類の Plugin を作るわけではありません。
36 +
37 + # どんなときに使うか
38 +
39 + Page Type は、Plugin が**独立した URL を持つページ**を提供するときに使用します。
40 +
41 + たとえば、
42 +
43 + - Garden Explorer
44 + - Plugin のレポート画面
45 + - Plugin が生成する一覧ページ
46 + - 独自の検索・閲覧ページ
47 +
48 + などです。
49 +
50 + 一方、記事本文の中へ何かを表示するだけなら Page Type は必要ありません。
51 +
52 + ```mermaid id="a1h3fe"
53 + flowchart TD
54 + Q{"Plugin が何を表示する?"}
55 +
56 + Q -->|"独立したURLを持つページ"| Page["pageTypes"]
57 + Q -->|"記事本文へ埋め込む"| Renderer["renderers"]
58 +
59 + Page --> P1["例: /explore"]
60 + Page --> P2["例: /report"]
61 +
62 + Renderer --> R1["Canvas"]
63 + Renderer --> R2["Bases"]
64 + Renderer --> R3["Excalidraw"]
65 + ```
66 +
67 + Canvas、Bases、Excalidraw のように記事内へ埋め込む機能は `renderers` を使用します。
68 +
69 + # 誰が何を担当するか
70 +
71 + Page System では、Plugin がページ全体を所有するわけではありません。
72 +
73 + 責務を次のように分離します。
74 +
75 + | 層 | 責務 |
76 + | --- | --- |
77 + | Core | Page Type の contract、検証、path 列挙、解決、競合検出 |
78 + | Plugin | Page Type ID、公開 path、framework 非依存の HTML body |
79 + | HonoX Integration | 共通 route resolver、SSG parameter helper |
80 + | Site Application | route、document frame、metadata、HTML の描画 |
81 + | Theme | token と stable hook による見た目 |
82 +
83 + 全体では次のような関係になります。
84 +
85 + ```mermaid id="7jv9lo"
86 + flowchart LR
87 + Plugin["Plugin<br/>Pageを提供"]
88 + Core["Core<br/>Page contract / resolution"]
89 + Integration["HonoX Integration<br/>Routing / SSG"]
90 + Site["Site<br/>Document / Rendering"]
91 + Theme["Theme<br/>CSS"]
92 +
93 + Plugin --> Core
94 + Core --> Integration
95 + Integration --> Site
96 + Theme --> Site
97 + ```
98 +
99 + **route と document frame は Site Application が所有します**。
100 +
101 + Plugin はページの内容を提供しますが、
102 +
103 + ```html id="snxwcb"
104 + <html>
105 + <head>
106 + ...
107 + <body>
108 + ```
109 +
110 + のような document 全体を生成するわけではありません。
111 +
112 + # Page System が解決する問題
113 +
114 + Page System 導入前は、独立ページを持つ Plugin ごとに Site 側へ専用 route が必要でした。
115 +
116 + ```mermaid id="5bcyu3"
117 + flowchart LR
118 + Request["Request"]
119 + Route["Plugin専用Route"]
120 + Component["Plugin専用Component"]
121 + Frame["Site Document Frame"]
122 +
123 + Request --> Route
124 + Route --> Component
125 + Component --> Frame
126 + ```
127 +
128 + この方式では Plugin を追加するだけでは動かず、Site Application の route も変更する必要があります。
129 +
130 + つまり Plugin と Site の結合が強くなります。
131 +
132 + Page System では、すべての Plugin Page が共通 route resolver に参加します。
133 +
134 + ```mermaid id="ic50kb"
135 + flowchart LR
136 + Request["Request"]
137 + Catch["Catch-all Route"]
138 + Resolver["resolveRiebeckiteRoute"]
139 + Page["Plugin Page"]
140 + Content["Content"]
141 + Redirect["Redirect"]
142 +
143 + Request --> Catch
144 + Catch --> Resolver
145 +
146 + Resolver --> Page
147 + Resolver --> Content
148 + Resolver --> Redirect
149 + ```
150 +
151 + これにより、新しい Plugin Page を追加するたびに HonoX route を追加する必要がありません。
152 +
153 + # Build と Request の流れ
154 +
155 + Page System には、大きく2つの処理があります。
156 +
157 + ## Build 時
158 +
159 + Build 時には、Plugin が提供する Page Type から静的生成する URL を集めます。
160 +
161 + ```mermaid id="4b2b3j"
162 + flowchart LR
163 + Plugin["Plugin Page Type"]
164 + Paths["Public Paths"]
165 + Params["SSG Parameters"]
166 + Build["Static Generation"]
167 +
168 + Plugin --> Paths
169 + Paths --> Params
170 + Params --> Build
171 + ```
172 +
173 + たとえば Plugin が、
174 +
175 + ```text id="9ifm52"
176 + /report
177 + ```
178 +
179 + を提供していれば、その path を SSG の生成対象にできます。
180 +
181 + ## Request 時
182 +
183 + Request が来ると、共通 resolver がどの種類のページかを判断します。
184 +
185 + ```mermaid id="r8vl0u"
186 + flowchart TD
187 + Request["Request"]
188 + Resolver["resolveRiebeckiteRoute"]
189 +
190 + Request --> Resolver
191 +
192 + Resolver --> Q{"何が一致した?"}
193 +
194 + Q -->|"Plugin Page"| Page["Plugin Page"]
195 + Q -->|"Content"| Content["Content"]
196 + Q -->|"Redirect"| Redirect["Redirect"]
197 + Q -->|"なし"| NotFound["Not Found"]
198 + ```
199 +
200 + Plugin Page が解決された場合、その body を Site の document frame 内へ描画します。
201 +
202 + ```mermaid id="brt8r0"
203 + flowchart LR
204 + Page["Plugin Page<br/>body / metadata"]
205 + Frame["Site Document Frame"]
206 + Theme["Theme CSS"]
207 + Client["Plugin Client Entry"]
208 + Browser["Final Page"]
209 +
210 + Page --> Frame
211 + Theme --> Frame
212 + Client --> Frame
213 + Frame --> Browser
214 + ```
215 +
216 + # Page Type を作る
217 +
218 + Page Type は Plugin の `pageTypes` に定義します。
219 +
220 + ```ts id="1ql4hf"
221 + import { definePlugin } from "@riebeckite/core";
222 +
223 + export function reportPlugin() {
224 + return definePlugin({
225 + name: "report",
226 +
227 + pageTypes: [
228 + {
229 + id: "example.report",
230 + paths: ["/report"],
231 +
232 + resolve: ({ pathname, manifest }) =>
233 + pathname === "/report"
234 + ? {
235 + type: "example.report",
236 + pathname,
237 + title: "Report",
238 + body: `<p>${manifest.publicEntries.length} published entries</p>`,
239 + }
240 + : null,
241 + },
242 + ],
243 + });
244 + }
245 + ```
246 +
247 + この例では、
248 +
249 + ```text id="1wlj0z"
250 + /report
251 + ```
252 +
253 + というページを Plugin が提供します。
254 +
255 + ページ本文には、到達可能な記事数を表示しています。
256 +
257 + Plugin が HonoX component や route file を作る必要はありません。
258 +
259 + # Manifest と公開境界
260 +
261 + Page Type の `resolve` には `PluginPageContext` が渡されます。
262 +
263 + その中の、
264 +
265 + ```ts id="dw2bby"
266 + PluginPageContext.manifest
267 + ```
268 +
269 + は解決済みの Manifest です。`entries` には `draft` と `scheduled` も含まれるため、Page の出力には使いません。
270 +
271 + 到達可能な URL を網羅する場合は `publicEntries`(`public` と `unlisted`)、読者へ一覧として見せる場合は `discoverableEntries`(`public` のみ)を使います。entry ごとに分岐が必要な場合だけ、解決済みの `entry.publishing` を読みます。
272 +
273 + ```mermaid id="s4t3xz"
274 + flowchart LR
275 + All["All Content"]
276 + Publish["Publish Boundary"]
277 + Public["publicEntries / discoverableEntries"]
278 + Page["Plugin Page"]
279 +
280 + All --> Publish
281 + Publish --> Public
282 + Public --> Page
283 + ```
284 +
285 + 非公開コンテンツを Plugin Page 側で独自に探索して表示する設計にはしません。
286 +
287 + これによって Page System も通常の Site と同じ公開境界を維持できます。
288 +
289 + # Page Type ID
290 +
291 + 各 Page Type には ID が必要です。
292 +
293 + ```ts id="0evifc"
294 + id: "example.report"
295 + ```
296 +
297 + Page Type ID は、すべての Plugin を通して一意である必要があります。
298 +
299 + たとえば、
300 +
301 + ```text id="tvplf6"
302 + riebeckite.garden-explorer
303 + example.report
304 + example.search
305 + ```
306 +
307 + のように Plugin や用途が分かる namespace を持たせると衝突を避けやすくなります。
308 +
309 + # Path
310 +
311 + 静的なページは `paths` に公開 path を指定します。
312 +
313 + ```ts id="7rvyz2"
314 + paths: ["/report"]
315 + ```
316 +
317 + 複数のページを提供することもできます。
318 +
319 + ```ts id="wea35u"
320 + paths: [
321 + "/report",
322 + "/report/archive",
323 + ]
324 + ```
325 +
326 + 動的ページを静的生成する場合は、SSG path を **`manifest.publicEntries` から導出**します。
327 +
328 + filesystem を独自に scan して path を作るのではなく、Content System が解決した公開状態を利用してください。
329 +
330 + # Page Type の競合
331 +
332 + 複数の Page Type が同じ URL に一致する可能性があります。
333 +
334 + その場合は `priority` を使って解決します。
335 +
336 + ```mermaid id="q0v69g"
337 + flowchart TD
338 + Path["/report"]
339 + A["Page Type A<br/>priority: 10"]
340 + B["Page Type B<br/>priority: 20"]
341 +
342 + Path --> A
343 + Path --> B
344 +
345 + A --> Compare{"priority"}
346 + B --> Compare
347 +
348 + Compare -->|"B が高い"| Result["Page Type B"]
349 + ```
350 +
351 + 最も高い `priority` を持つ Page Type が選ばれます。
352 +
353 + ただし、同じ最大 priority の Page Type が複数一致した場合は、どちらかを暗黙的に選びません。
354 +
355 + ```mermaid id="19y0sc"
356 + flowchart TD
357 + Match["複数Page Typeが一致"]
358 + Priority{"最大priorityは一意?"}
359 +
360 + Match --> Priority
361 +
362 + Priority -->|Yes| Resolve["そのPage Typeを使用"]
363 + Priority -->|No| Error["Conflict Error"]
364 + ```
365 +
366 + 競合を明示的なエラーにすることで、Plugin の登録順などによって結果が変わることを防ぎます。
367 +
368 + # HonoX Site へ接続する
369 +
370 + Plugin Page を利用する HonoX Site には、共通の catch-all route が必要です。
371 +
372 + `create-riebeckite` などで生成された Site には、必要な構成があらかじめ含まれています。
373 +
374 + 概念的には次のように接続します。
375 +
376 + ```tsx id="xxj9rm"
377 + import {
378 + contentRouteSsgParams,
379 + resolveRiebeckiteContentRequest,
380 + riebeckiteSsgParams,
381 + } from "@riebeckite/honox/server";
382 + import { PageBody } from "@riebeckite/honox/ui";
383 + import { createRoute } from "honox/factory";
384 +
385 + export default createRoute(
386 + contentRouteSsgParams("/:slug{.+}", () => riebeckiteSsgParams(content)),
387 + async (c) => {
388 + const resolved = await resolveRiebeckiteContentRequest(c, content);
389 +
390 + if (resolved.kind === "response") return resolved.response;
391 +
392 + if (resolved.kind === "page") {
393 + return c.render(<PageBody html={resolved.page.body} />);
394 + }
395 +
396 + return c.render(/* Site 固有の article composition */);
397 + },
398 + );
399 + ```
400 +
401 + 通常の Site 利用者が Plugin ごとにこの route を追加する必要はありません。
402 +
403 + 共通 route が Plugin Page をまとめて解決します。root `/` も同じ仕組みを共有する `resolveRiebeckiteHomeRequest(c, content)` が解決します。
404 +
405 + # Page が返せるもの
406 +
407 + Plugin Page は HTML body に加えて、必要に応じてページの metadata を提供できます。
408 +
409 + たとえば、
410 +
411 + ```text id="m7yad8"
412 + body
413 + title
414 + description
415 + headTags
416 + language
417 + ```
418 +
419 + などです。
420 +
421 + ただし、Plugin がそれらを**どの位置へ描画するかまで決定するわけではありません**。
422 +
423 + ```mermaid id="h81lcz"
424 + flowchart LR
425 + Plugin["Plugin"]
426 +
427 + Plugin --> Body["body"]
428 + Plugin --> Title["title"]
429 + Plugin --> Description["description"]
430 + Plugin --> Head["headTags"]
431 + Plugin --> Language["language"]
432 +
433 + Body --> Site["Site Application"]
434 + Title --> Site
435 + Description --> Site
436 + Head --> Site
437 + Language --> Site
438 +
439 + Site --> Document["Final Document"]
440 + ```
441 +
442 + Site Application がこれらを受け取り、既存の document frame に反映します。
443 +
444 + このため、Plugin Page でも Site 全体の navigation、header、footer、Theme などをそのまま利用できます。
445 +
446 + # HTML の安全性
447 +
448 + Page Type の `body` は HTML として描画されます。
449 +
450 + そのため Plugin は、**自分自身が安全に生成できる HTML だけを返す**必要があります。
451 +
452 + たとえば request parameter をそのまま HTML に埋め込むような処理は避けます。
453 +
454 + ```ts id="8m9qrl"
455 + // Avoid
456 + body: `<p>${untrustedRequestValue}</p>`
457 + ```
458 +
459 + 信頼できない入力を扱う場合は、適切に escape / sanitize してから使用します。
460 +
461 + 最終的に HTML をどのように描画するかは Site Application の boundary で決定します。
462 +
463 + Page Type が Site の HTML safety policy を回避する仕組みにはしません。
464 +
465 + # Page Type と Theme
466 +
467 + Theme は Page Type ID を知る必要がありません。
468 +
469 + たとえば、
470 +
471 + ```text id="z67p3o"
472 + example.report
473 + ```
474 +
475 + 専用の Theme logic を作るのではなく、
476 +
477 + - semantic token
478 + - stable CSS hook
479 + - `data-*` attribute
480 + - CSS cascade
481 +
482 + を利用して見た目を変更します。
483 +
484 + ```mermaid id="r00e0g"
485 + flowchart LR
486 + Page["Plugin Page"]
487 + Site["Site Document"]
488 + Hooks["Stable Hooks / Tokens"]
489 + Theme["Theme CSS"]
490 +
491 + Page --> Site
492 + Site --> Hooks
493 + Theme --> Hooks
494 + ```
495 +
496 + これにより、新しい Page Type が追加されても Theme と Plugin を直接結合せずに済みます。
497 +
498 + # Page System の境界
499 +
500 + Page System では、**Plugin はページを提供するが、Application を所有しない**ことが最も重要です。
501 +
502 + ```mermaid id="o60md4"
503 + flowchart LR
504 + Plugin["Plugin<br/>何を表示する?"]
505 + Core["Core<br/>どう表現・解決する?"]
506 + Integration["Integration<br/>どうRouteへ接続する?"]
507 + Site["Site<br/>どうDocumentとして描画する?"]
508 + Theme["Theme<br/>どう見せる?"]
509 +
510 + Plugin --> Core
511 + Core --> Integration
512 + Integration --> Site
513 + Theme --> Site
514 + ```
515 +
516 + 責務は次のとおりです。
517 +
518 + ```text id="9w3k80"
519 + Plugin
520 + → Page の内容と公開 path を提供する
521 +
522 + Core
523 + → Page Type の共通 contract と解決規則を提供する
524 +
525 + HonoX Integration
526 + → Page Type を routing / SSG へ接続する
527 +
528 + Site Application
529 + → route、document frame、metadata、HTML safety を所有する
530 +
531 + Theme
532 + → Site の見た目を変更する
533 + ```
534 +
535 + という関係になります。
536 +
537 + この境界によって Plugin は HonoX や特定 Site の構造に依存せず、インストールするだけで独立ページを提供できます。
538 +
539 + Page Type の全フィールドと実行時検証については [Plugin API](../reference/plugin-api.ja.md#page-type)、Site との接続については [HonoX Integration](./honox-integration.ja.md) を参照してください。
540 +