Color mode

Content パイプラインと拡張ポイント

このページは プラグイン作成の詳細 の一部で、Content を扱う拡張ポイント(Markdown / HTML Pipeline、Content Graph、Renderer、Page Type など)を扱います。

10. Markdown / HTML Pipeline

Markdown や HTML の意味を変換する場合は remark / rehype を利用します。

単純な Plugin なら、

ts
definePlugin({
  name: "example",
 
  remarkPlugins: [
    remarkExample,
  ],
 
  rehypePlugins: [
    rehypeExample,
  ],
});

と宣言できます。

Pipeline 自体を構成する必要がある場合は、

ts
definePlugin({
  name: "example",
 
  extendMarkdownPipeline(pipeline) {
    pipeline.use(remarkExample);
  },
 
  extendHtmlPipeline(pipeline) {
    pipeline.use(rehypeExample);
  },
});

を使用します。

Markdown / HTML の意味変換を Application Component に持ち込まず、Plugin の Pipeline 処理として実装するのが基本です。

11. Content Graph

Content Graph を拡張する場合は extendContentGraph を使用します。

Diagram source
text
flowchart LR
    Manifest["Manifest"]
    Graph["Content Graph"]
    Plugin["extendContentGraph"]
    Result["Extended Graph"]
 
    Manifest --> Graph
    Graph --> Plugin
    Plugin --> Result

Backlinks や Graph 系機能を実装するために、Plugin が Filesystem を再走査しないでください。

すでに解決された Manifest / Content Graph を利用します。

12. Public Location

Content の公開 URL を変更する Plugin では resolveContentLocations を使用します。

最初に Core が Default Location を解決します。

text
index
  → /
 
その他
  → /{slug}

その後、解決済み Plugin Order に従って Location Hook が実行されます。

Diagram source
text
flowchart LR
    Content["Content"]
    Default["Default Resolver"]
    PluginA["Plugin A"]
    PluginB["Plugin B"]
    Location["ContentPublicLocation"]
 
    Content --> Default
    Default --> PluginA
    PluginA --> PluginB
    PluginB --> Location

結果は ContentPublicLocation として、

  • Manifest
  • Content Graph
  • Markdown Pipeline

などから利用されます。

URL Strategy は Plugin が所有します。

Core は特定 Plugin の URL 規則を知りません。

Consumer は最終的な、

ts
entry.permalink

を利用します。

Location を解決できない場合は、slug へ暗黙的に fallback せず明示的な Error とします。

13. Renderers

renderers は記事本文内の特殊な Content Target を HTML へ変換します。

たとえば、

text
Canvas
Bases
Excalidraw
Attachment
Media

などです。

Renderer Context には、

text
kind
path
raw
label
url
embed

と通常の PluginContext が含まれます。

Diagram source
text
flowchart TD
    Target["Content Target"]
    Renderer{"このRendererが処理する?"}
 
    Target --> Renderer
    Renderer -->|Yes| HTML["HTML"]
    Renderer -->|No| Next["次のRenderer"]

処理対象でなければ null を返します。

これによって複数 Renderer が同じ Pipeline に参加できます。

14. Page Types

独立した URL を持つ画面を Plugin が提供する場合は pageTypes を使用します。

たとえば、

text
/explore
/report
/tags/example

などです。

Page Type は、

  • 一意な id
  • SSG 用 paths
  • 必要に応じた priority
  • resolve

を持ちます。

Resolver は Framework 非依存の HTML Body または null を返します。

必要なら、

text
title
description
headTags

も返せます。

ただし Document Frame は Site Application が所有します。

Diagram source
text
flowchart LR
    Plugin["Plugin Page Type"]
    Core["Core Resolver"]
    Integration["HonoX Integration"]
    Site["Site Document Frame"]
 
    Plugin --> Core
    Core --> Integration
    Integration --> Site

Taxonomy の一覧や Explorer のような独立画面には Page Type を使用します。

Canvas、Bases、Excalidraw のような記事本文への埋め込みには Renderer を使用します。

Plugin が HonoX Route File や Document Frame を所有しないことが重要です。

Page Type の詳しい仕組みは Page System を参照してください。

15. Assets

Plugin 固有の Stylesheet は Plugin Package 内に置き、assets から公開します。

ts
assets: [
  {
    pluginName: "example",
    kind: "style",
    moduleSpecifier:
      "@riebeckite/plugin-example/style.css",
  },
]

Integration がこの Module Specifier を解決して Browser へ届けます。

Diagram source
text
flowchart LR
    CSS["Plugin style.css"]
    Asset["assets"]
    Integration["Integration"]
    Browser["Browser"]
 
    CSS --> Asset
    Asset --> Integration
    Integration --> Browser

Plugin 固有 CSS を apps/web にコピーしたり、Browser から /node_modules を直接参照させたりしないでください。

16. CSS Hooks

再利用可能な UI を Plugin が描画する場合は、最外要素に Stable Root Hook を付けます。

Plugin / Feature Hook は、

text
rr-<feature>

とします。

たとえば、

text
rr-search
rr-callout
rr-query
rr-code

です。

内部要素では BEM を利用できます。

text
rr-search
rr-search__input
rr-search__result
rr-search--loading

Namespace の役割は次のとおりです。

Namespace 用途
rb-* Framework の構造 Hook
--rb-* Framework の Semantic Token
rr-* Plugin / Feature Hook
--rr-* Plugin 固有 Token

Plugin の出力を rb-* Namespace に置かないでください。

既存 Class がある場合は削除せず、rr-* Hook を追加します。

また、

text
rr-feature__*
rr-feature--*

は原則として内部実装です。

Theme から利用してよい子孫 Class だけを Public Hook として文書化してください。

17. Client Entries

Browser 上で初期化処理が必要な場合だけ clientEntries を使用します。

ts
clientEntries: [
  {
    pluginName: "example",
    moduleSpecifier:
      "@riebeckite/plugin-example/client",
    exportName: "initExample",
 
    publicConfig: {
      selector: ".example",
    },
  },
]
Diagram source
text
flowchart LR
    Plugin["Plugin"]
    Client["Client Entry"]
    Build["Integration"]
    Browser["Browser"]
 
    Plugin --> Client
    Client --> Build
    Build --> Browser

SSR / Build-time だけで完結する Plugin に Client JavaScript を追加しないでください。

publicConfig は Browser へ渡されるため、

  • Token
  • Credential
  • Secret
  • Private Service URL

などを含めてはいけません。

Plugin の options が自動的に Client へ渡されることもありません。

18. Endpoints / SEO

HTTP Endpoint を提供する場合は endpoints Contract を使用します。

Diagram source
text
flowchart LR
    Plugin["Plugin"]
    Endpoint["Endpoint Contract"]
    Integration["Integration"]
    Router["Host Router"]
 
    Plugin --> Endpoint
    Endpoint --> Integration
    Integration --> Router

HonoX など特定の Route Framework を Plugin 本体へ直接組み込まないでください。

SEO へ参加する場合は seo を使用します。

たとえば Metadata や Feed に Plugin が情報を追加できます。

Application Route 側へ Plugin 固有 SEO Logic を再実装しないことが重要です。

19. Diagnostics

Plugin 固有の問題は addDiagnostics から報告します。

ts
addDiagnostics(context) {
  return [
    {
      // Diagnostic contract
    },
  ];
}

診断結果は可能な限り Structured Data として返してください。

Plugin が、

ts
console.log(...)

で独自の CLI Output を作るのではなく、Diagnostics または Logger を使用します。

20. Plugin Cache

context.cache は Plugin ごとに分離された Build-time Cache です。

保存するデータには次の条件があります。

  • 再生成できる
  • JSON Serializable
  • Plugin Namespace 内で完結する
  • 壊れていても安全に Cache Miss として扱える

cacheVersion を使って Cache Format の互換性を管理できます。

Write は Atomic に行います。

Diagram source
text
flowchart TD
    Work["Plugin Work"]
    Cache{"有効なCache?"}
 
    Work --> Cache
 
    Cache -->|Yes| Reuse["Reuse"]
    Cache -->|No| Generate["Regenerate"]

これは Runtime Database ではありません。

Cloudflare Workers などの永続 Storage として使用しないでください。

21. Logger / Tracer

Plugin では Framework の Logger / Tracer を利用できます。

ts
context.logger.info("...");
 
await context.tracer.span(
  "plugin.example.work",
  {
    plugin: "example",
  },
  async () => {
    // work
  },
);

Logger は処理内容を記録し、Tracer は処理時間などを Structured Trace として記録します。

Profiler はこの Trace を利用するため、Plugin ごとに独自の Stopwatch や Profiling System を作る必要はありません。

History

1 changesCollapseExpand
1 + ---
2 + title: Content パイプラインと拡張ポイント
3 + sidebar:
4 + label: パイプラインと拡張ポイント
5 + order: 20
6 + ---
7 + # Content パイプラインと拡張ポイント
8 +
9 + このページは [プラグイン作成の詳細](../plugin-system.ja.md) の一部で、Content を扱う拡張ポイント(Markdown / HTML Pipeline、Content Graph、Renderer、Page Type など)を扱います。
10 +
11 + ## 10. Markdown / HTML Pipeline
12 +
13 + Markdown や HTML の意味を変換する場合は remark / rehype を利用します。
14 +
15 + 単純な Plugin なら、
16 +
17 + ```ts id="twapvp"
18 + definePlugin({
19 + name: "example",
20 +
21 + remarkPlugins: [
22 + remarkExample,
23 + ],
24 +
25 + rehypePlugins: [
26 + rehypeExample,
27 + ],
28 + });
29 + ```
30 +
31 + と宣言できます。
32 +
33 + Pipeline 自体を構成する必要がある場合は、
34 +
35 + ```ts id="5fj1xn"
36 + definePlugin({
37 + name: "example",
38 +
39 + extendMarkdownPipeline(pipeline) {
40 + pipeline.use(remarkExample);
41 + },
42 +
43 + extendHtmlPipeline(pipeline) {
44 + pipeline.use(rehypeExample);
45 + },
46 + });
47 + ```
48 +
49 + を使用します。
50 +
51 + Markdown / HTML の意味変換を Application Component に持ち込まず、Plugin の Pipeline 処理として実装するのが基本です。
52 +
53 +
54 + ## 11. Content Graph
55 +
56 + Content Graph を拡張する場合は `extendContentGraph` を使用します。
57 +
58 + ```mermaid id="kjw3kt"
59 + flowchart LR
60 + Manifest["Manifest"]
61 + Graph["Content Graph"]
62 + Plugin["extendContentGraph"]
63 + Result["Extended Graph"]
64 +
65 + Manifest --> Graph
66 + Graph --> Plugin
67 + Plugin --> Result
68 + ```
69 +
70 + Backlinks や Graph 系機能を実装するために、Plugin が Filesystem を再走査しないでください。
71 +
72 + すでに解決された Manifest / Content Graph を利用します。
73 +
74 +
75 + ## 12. Public Location
76 +
77 + Content の公開 URL を変更する Plugin では `resolveContentLocations` を使用します。
78 +
79 + 最初に Core が Default Location を解決します。
80 +
81 + ```text id="dug8by"
82 + index
83 + → /
84 +
85 + その他
86 + → /{slug}
87 + ```
88 +
89 + その後、解決済み Plugin Order に従って Location Hook が実行されます。
90 +
91 + ```mermaid id="rbm5j4"
92 + flowchart LR
93 + Content["Content"]
94 + Default["Default Resolver"]
95 + PluginA["Plugin A"]
96 + PluginB["Plugin B"]
97 + Location["ContentPublicLocation"]
98 +
99 + Content --> Default
100 + Default --> PluginA
101 + PluginA --> PluginB
102 + PluginB --> Location
103 + ```
104 +
105 + 結果は `ContentPublicLocation` として、
106 +
107 + - Manifest
108 + - Content Graph
109 + - Markdown Pipeline
110 +
111 + などから利用されます。
112 +
113 + URL Strategy は Plugin が所有します。
114 +
115 + Core は特定 Plugin の URL 規則を知りません。
116 +
117 + Consumer は最終的な、
118 +
119 + ```ts id="b9m3p8"
120 + entry.permalink
121 + ```
122 +
123 + を利用します。
124 +
125 + Location を解決できない場合は、slug へ暗黙的に fallback せず明示的な Error とします。
126 +
127 +
128 + ## 13. Renderers
129 +
130 + `renderers` は記事本文内の特殊な Content Target を HTML へ変換します。
131 +
132 + たとえば、
133 +
134 + ```text id="o4apio"
135 + Canvas
136 + Bases
137 + Excalidraw
138 + Attachment
139 + Media
140 + ```
141 +
142 + などです。
143 +
144 + Renderer Context には、
145 +
146 + ```text id="pbh0ss"
147 + kind
148 + path
149 + raw
150 + label
151 + url
152 + embed
153 + ```
154 +
155 + と通常の `PluginContext` が含まれます。
156 +
157 + ```mermaid id="m5n3av"
158 + flowchart TD
159 + Target["Content Target"]
160 + Renderer{"このRendererが処理する?"}
161 +
162 + Target --> Renderer
163 + Renderer -->|Yes| HTML["HTML"]
164 + Renderer -->|No| Next["次のRenderer"]
165 + ```
166 +
167 + 処理対象でなければ `null` を返します。
168 +
169 + これによって複数 Renderer が同じ Pipeline に参加できます。
170 +
171 +
172 + ## 14. Page Types
173 +
174 + 独立した URL を持つ画面を Plugin が提供する場合は `pageTypes` を使用します。
175 +
176 + たとえば、
177 +
178 + ```text id="9tlwkd"
179 + /explore
180 + /report
181 + /tags/example
182 + ```
183 +
184 + などです。
185 +
186 + Page Type は、
187 +
188 + - 一意な `id`
189 + - SSG 用 `paths`
190 + - 必要に応じた `priority`
191 + - `resolve`
192 +
193 + を持ちます。
194 +
195 + Resolver は Framework 非依存の HTML Body または `null` を返します。
196 +
197 + 必要なら、
198 +
199 + ```text id="pjgd7n"
200 + title
201 + description
202 + headTags
203 + ```
204 +
205 + も返せます。
206 +
207 + ただし Document Frame は Site Application が所有します。
208 +
209 + ```mermaid id="dkbx6s"
210 + flowchart LR
211 + Plugin["Plugin Page Type"]
212 + Core["Core Resolver"]
213 + Integration["HonoX Integration"]
214 + Site["Site Document Frame"]
215 +
216 + Plugin --> Core
217 + Core --> Integration
218 + Integration --> Site
219 + ```
220 +
221 + Taxonomy の一覧や Explorer のような独立画面には Page Type を使用します。
222 +
223 + Canvas、Bases、Excalidraw のような記事本文への埋め込みには Renderer を使用します。
224 +
225 + Plugin が HonoX Route File や Document Frame を所有しないことが重要です。
226 +
227 + Page Type の詳しい仕組みは [Page System](../page-system.ja.md) を参照してください。
228 +
229 +
230 + ## 15. Assets
231 +
232 + Plugin 固有の Stylesheet は Plugin Package 内に置き、`assets` から公開します。
233 +
234 + ```ts id="b4qrvi"
235 + assets: [
236 + {
237 + pluginName: "example",
238 + kind: "style",
239 + moduleSpecifier:
240 + "@riebeckite/plugin-example/style.css",
241 + },
242 + ]
243 + ```
244 +
245 + Integration がこの Module Specifier を解決して Browser へ届けます。
246 +
247 + ```mermaid id="pbj8dg"
248 + flowchart LR
249 + CSS["Plugin style.css"]
250 + Asset["assets"]
251 + Integration["Integration"]
252 + Browser["Browser"]
253 +
254 + CSS --> Asset
255 + Asset --> Integration
256 + Integration --> Browser
257 + ```
258 +
259 + Plugin 固有 CSS を `apps/web` にコピーしたり、Browser から `/node_modules` を直接参照させたりしないでください。
260 +
261 +
262 + ## 16. CSS Hooks
263 +
264 + 再利用可能な UI を Plugin が描画する場合は、最外要素に Stable Root Hook を付けます。
265 +
266 + Plugin / Feature Hook は、
267 +
268 + ```text id="59lz7m"
269 + rr-<feature>
270 + ```
271 +
272 + とします。
273 +
274 + たとえば、
275 +
276 + ```text id="gj2f5a"
277 + rr-search
278 + rr-callout
279 + rr-query
280 + rr-code
281 + ```
282 +
283 + です。
284 +
285 + 内部要素では BEM を利用できます。
286 +
287 + ```text id="9lq6se"
288 + rr-search
289 + rr-search__input
290 + rr-search__result
291 + rr-search--loading
292 + ```
293 +
294 + Namespace の役割は次のとおりです。
295 +
296 + | Namespace | 用途 |
297 + | --- | --- |
298 + | `rb-*` | Framework の構造 Hook |
299 + | `--rb-*` | Framework の Semantic Token |
300 + | `rr-*` | Plugin / Feature Hook |
301 + | `--rr-*` | Plugin 固有 Token |
302 +
303 + Plugin の出力を `rb-*` Namespace に置かないでください。
304 +
305 + 既存 Class がある場合は削除せず、`rr-*` Hook を追加します。
306 +
307 + また、
308 +
309 + ```text id="21g4wg"
310 + rr-feature__*
311 + rr-feature--*
312 + ```
313 +
314 + は原則として内部実装です。
315 +
316 + Theme から利用してよい子孫 Class だけを Public Hook として文書化してください。
317 +
318 +
319 + ## 17. Client Entries
320 +
321 + Browser 上で初期化処理が必要な場合だけ `clientEntries` を使用します。
322 +
323 + ```ts id="a8cjlk"
324 + clientEntries: [
325 + {
326 + pluginName: "example",
327 + moduleSpecifier:
328 + "@riebeckite/plugin-example/client",
329 + exportName: "initExample",
330 +
331 + publicConfig: {
332 + selector: ".example",
333 + },
334 + },
335 + ]
336 + ```
337 +
338 + ```mermaid id="wqsskl"
339 + flowchart LR
340 + Plugin["Plugin"]
341 + Client["Client Entry"]
342 + Build["Integration"]
343 + Browser["Browser"]
344 +
345 + Plugin --> Client
346 + Client --> Build
347 + Build --> Browser
348 + ```
349 +
350 + SSR / Build-time だけで完結する Plugin に Client JavaScript を追加しないでください。
351 +
352 + `publicConfig` は Browser へ渡されるため、
353 +
354 + - Token
355 + - Credential
356 + - Secret
357 + - Private Service URL
358 +
359 + などを含めてはいけません。
360 +
361 + Plugin の `options` が自動的に Client へ渡されることもありません。
362 +
363 +
364 + ## 18. Endpoints / SEO
365 +
366 + HTTP Endpoint を提供する場合は `endpoints` Contract を使用します。
367 +
368 + ```mermaid id="brq9cr"
369 + flowchart LR
370 + Plugin["Plugin"]
371 + Endpoint["Endpoint Contract"]
372 + Integration["Integration"]
373 + Router["Host Router"]
374 +
375 + Plugin --> Endpoint
376 + Endpoint --> Integration
377 + Integration --> Router
378 + ```
379 +
380 + HonoX など特定の Route Framework を Plugin 本体へ直接組み込まないでください。
381 +
382 + SEO へ参加する場合は `seo` を使用します。
383 +
384 + たとえば Metadata や Feed に Plugin が情報を追加できます。
385 +
386 + Application Route 側へ Plugin 固有 SEO Logic を再実装しないことが重要です。
387 +
388 +
389 + ## 19. Diagnostics
390 +
391 + Plugin 固有の問題は `addDiagnostics` から報告します。
392 +
393 + ```ts id="1xq5te"
394 + addDiagnostics(context) {
395 + return [
396 + {
397 + // Diagnostic contract
398 + },
399 + ];
400 + }
401 + ```
402 +
403 + 診断結果は可能な限り Structured Data として返してください。
404 +
405 + Plugin が、
406 +
407 + ```ts id="c4x89a"
408 + console.log(...)
409 + ```
410 +
411 + で独自の CLI Output を作るのではなく、Diagnostics または Logger を使用します。
412 +
413 +
414 + ## 20. Plugin Cache
415 +
416 + `context.cache` は Plugin ごとに分離された Build-time Cache です。
417 +
418 + 保存するデータには次の条件があります。
419 +
420 + - 再生成できる
421 + - JSON Serializable
422 + - Plugin Namespace 内で完結する
423 + - 壊れていても安全に Cache Miss として扱える
424 +
425 + `cacheVersion` を使って Cache Format の互換性を管理できます。
426 +
427 + Write は Atomic に行います。
428 +
429 + ```mermaid id="42mkl4"
430 + flowchart TD
431 + Work["Plugin Work"]
432 + Cache{"有効なCache?"}
433 +
434 + Work --> Cache
435 +
436 + Cache -->|Yes| Reuse["Reuse"]
437 + Cache -->|No| Generate["Regenerate"]
438 + ```
439 +
440 + これは Runtime Database ではありません。
441 +
442 + Cloudflare Workers などの永続 Storage として使用しないでください。
443 +
444 +
445 + ## 21. Logger / Tracer
446 +
447 + Plugin では Framework の Logger / Tracer を利用できます。
448 +
449 + ```ts id="j7ad6k"
450 + context.logger.info("...");
451 +
452 + await context.tracer.span(
453 + "plugin.example.work",
454 + {
455 + plugin: "example",
456 + },
457 + async () => {
458 + // work
459 + },
460 + );
461 + ```
462 +
463 + Logger は処理内容を記録し、Tracer は処理時間などを Structured Trace として記録します。
464 +
465 + Profiler はこの Trace を利用するため、Plugin ごとに独自の Stopwatch や Profiling System を作る必要はありません。
466 +