Color mode

Plugin System

Riebeckite Plugin は、Riebeckite の機能を Core や Site Application に直接組み込まず、再利用可能な形で追加するための仕組みです。

Plugin では、たとえば次のような機能を追加できます。

  • Markdown / HTML の変換
  • コンテンツの処理
  • 独自形式の埋め込み表示
  • 独立したページ
  • CSS
  • Browser 上の処理
  • HTTP Endpoint
  • SEO
  • Diagnostics
  • Content Graph の拡張

Plugin は必要な機能だけを実装します。

すべての API を使う必要はありません。

まず何を使うか決める

Plugin を作るときは、最初に 目的に合った最小の Extension Point を選びます。

Diagram source
text
flowchart TD
    Q{"何を追加したい?"}
 
    Q -->|"Markdown / HTMLの意味変換"| Pipeline["remark / rehype<br/>Pipeline"]
    Q -->|"Content処理の特定段階へ参加"| Hook["Content Hooks"]
    Q -->|"特定形式を表示"| Renderer["renderers"]
    Q -->|"独立したページ"| Page["pageTypes"]
    Q -->|"CSS"| Asset["assets"]
    Q -->|"Browser処理"| Client["clientEntries"]
    Q -->|"HTTP API"| Endpoint["endpoints"]
    Q -->|"SEO"| SEO["seo"]
    Q -->|"Graph情報"| Graph["extendContentGraph"]
    Q -->|"問題を報告"| Diagnostics["addDiagnostics"]

たとえば Canvas や Excalidraw を記事内へ表示するなら renderers、/explore のような独立したページを提供するなら pageTypes を使います。

独立画面が必要だからといって、Plugin 固有の HonoX route を追加するわけではありません。

最小の Plugin

最も小さい Plugin は次のように作れます。

ts
import { definePlugin } from "@riebeckite/core";
 
export function examplePlugin() {
  return definePlugin({
    name: "example",
  });
}

Plugin に設定を持たせる場合は、factory の引数として受け取ります。

ts
type ExampleOptions = {
  enabled?: boolean;
};
 
export function examplePlugin(
  options: ExampleOptions = {},
) {
  return definePlugin({
    name: "example",
    options,
  });
}

definePlugin() が Plugin の共通 contract を提供します。

Plugin が持てる機能

RiebeckitePlugin には、大きく次の Extension Point があります。

分類 主な API
基本情報 name, enabled, order, options
依存関係 provides, requires, optional
設定検証 validateOptions
Cache cacheVersion, context.cache
Lifecycle setup, buildStart, buildEnd, dispose
Content onConfigResolved, onContentLoaded, onPostParsed, onPostProcessed, onManifestCreated
公開先 resolveContentLocations
Markdown / HTML remarkPlugins, rehypePlugins, extendMarkdownPipeline, extendHtmlPipeline
Graph extendContentGraph
Diagnostics addDiagnostics
埋め込み表示 renderers
独立ページ pageTypes
Browser assets, clientEntries
HTTP endpoints
SEO seo

Plugin はこの中から必要なものだけを使用してください。

Plugin の有効化

Plugin は riebeckite.config.ts の plugins へ追加します。

ts
plugins: [
  myPlugin(),
]

条件付きで有効化することもできます。

ts
plugins: [
  condition && myPlugin(),
]

false、null、undefined は Plugin の解決時に除外されます。

また、

ts
enabled: false

の Plugin も実行対象になりません。

Plugin の順序と依存関係

単純な実行順は order で指定できます。

ただし、Plugin 同士に実際の依存関係がある場合は order ではなく Capability を使用します。

ts
definePlugin({
  name: "consumer",
 
  provides: [
    "example.output",
  ],
 
  requires: [
    "content.graph",
  ],
 
  optional: [
    "example.optional",
  ],
});

それぞれの意味は次のとおりです。

Field 意味
provides この Plugin が提供する機能
requires 必ず必要な機能
optional あれば利用する機能

Resolver は依存関係から Plugin の実行順を決定します。

Diagram source
text
flowchart LR
    Provider["Provider Plugin<br/>provides: content.graph"]
    Consumer["Consumer Plugin<br/>requires: content.graph"]
 
    Provider --> Consumer

次のような状態は Configuration Error になります。

  • 必須 Capability が存在しない
  • 同じ Capability を複数 Plugin が提供する
  • 依存関係が循環している

Capability の解決に失敗した場合は PluginDependencyError(@riebeckite/core から import 可能)を投げます。kind と pluginName から原因を特定できます。

依存関係のない Plugin については、入力順を可能な限り維持します。

Options Validation

TypeScript の型だけでは、実行時に渡される設定値が必ず正しいとは保証できません。

必要な Plugin は validateOptions を実装できます。

Validation は、

text
Plugin Options
      ↓
validateOptions
      ↓
Structured Issues
      ↓
Config Validation

という流れで扱われます。

Validator は 設定の検証だけを行ってください。

ここで、

  • filesystem scan
  • Build
  • Cache write
  • 外部状態の変更

などを行わないでください。

問題は structured issue として返し、Riebeckite の Config Validation がまとめて表示できるようにします。

Plugin Context

Plugin は Framework の機能を PluginContext から受け取ります。

基本的な Context は概念的に次のようなものです。

ts
type PluginContext = {
  config?: ResolvedRiebeckiteConfig;
  contentIndex: Map<string, string>;
  diagnostics: Diagnostic[];
  cache: PluginCache;
  output: GeneratedOutputSink;
  logger: Logger;
  tracer: Tracer;
  contentSource?: ContentSource;
};

Hook によって、

  • slug
  • markdown
  • content
  • manifest
  • entries
  • Public Location の入力

などが追加されます。

Plugin 内で global singleton を作るより、Context から Framework Service を受け取ることを優先してください。

Lifecycle

Plugin には Framework 全体の Lifecycle と、Content 処理の Lifecycle があります。

Framework Lifecycle は、

text
setup
buildStart
buildEnd
dispose

があります。

dispose は Plugin が確保した resource の解放に使用します。

setup、buildStart、onConfigResolved、Content 処理、buildEnd は、1つの ContentManager につき一度だけ実行されます。buildEnd は Diagnostics の収集後に完成した Manifest を受け取る唯一の終端 Hook です。dispose は解決済み Plugin の逆順で実行されます。

名前付き Lifecycle / Content Hook でエラーが発生した場合は PluginHookError(@riebeckite/core から import 可能)として上位へ伝播させます。message は Plugin "<name>" failed during "<hook>" 形式で、元の Error は cause に保持されます。Content Hook で発生した場合は、対象 file が path(例: note.md)として設定されます。

Content Lifecycle

Content 処理は概念的に次の順番で進みます。

Diagram source
text
flowchart TD
    Config["Config Resolved"]
    Loaded["Content Loaded"]
    Location["Public Location Resolved"]
    Parsed["Post Parsed"]
    Processed["Post Processed"]
    Graph["Content Graph"]
    Manifest["Manifest Created"]
 
    Config --> Location
    Location --> Loaded
    Loaded --> Parsed
    Parsed --> Processed
    Processed --> Graph
    Graph --> Manifest

全体の順序は setup → buildStart → onConfigResolved → Public Location 解決 → onContentLoaded → Markdown / HTML Pipeline → onPostParsed → onPostProcessed → extendContentGraph → onManifestCreated → Diagnostics → buildEnd です。

代表的な Hook として、

text
onConfigResolved
onContentLoaded
onPostParsed
onPostProcessed
onManifestCreated

があります。

必要な段階の Hook だけを使用します。

後段ですでに得られる情報を前段で独自に再構築しないでください。

Markdown / HTML Pipeline

Markdown や HTML の意味を変換する場合は Pipeline を使用します。

簡単な remark / rehype Plugin なら直接宣言できます。

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

Pipeline 自体を構成する必要がある場合は Extension API を利用します。

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

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

処理済み Content の Build Dependency

Incremental Build の無効化は Core の責務です。Plugin は独自に affected content を計算せず、processedContentCache で契約を宣言します。

ts
definePlugin({
  name: "citations",
  processedContentCache: {
    version: "citations-v1",
    dependencyMode: "tracked",
  },
  extendMarkdownPipeline(pipeline, context) {
    pipeline.use(remarkCitations, { contentSource: context.contentSource });
  },
});
  • none は、処理結果が Content 本体、frontmatter、options、宣言した version だけに依存することを表します。
  • tracked は、Pipeline が他の Content や file を読む場合に使います。context.contentSource 経由で読めば、Core が dependency を記録し、利用側だけを再処理します。たとえば citations Plugin は readContentSourceEntry(context.contentSource, path) で BibTeX file を読みます。
  • unsafe は処理済み Content の永続的な再利用を無効にします。Content 処理を行う Plugin が契約を宣言しない場合も、同じ安全側の全 Content 再処理になります。

tracked の dependency は Content 処理中に取得します。Core が content/file の identity を永続化し、逆引き index から次回 Build で affected content を決定します。この用途で vault を独自に走査したり、Plugin 固有の Incremental Build state を保存したりしないでください。Framework API 経由で dependency を追跡できない場合は unsafe を使います。広い再処理は許容されますが、古い結果の再利用は許容されません。

これは Output Dependency とは別の契約です。pageTypes[].outputDependencies と context.output.emit(..., { dependencies }) は、再生成が必要な page や生成 file を表します。ここでは content、tag、folder、global、unknown を使います。unknown は安全側として全 Output の再生成を要求します。

Generated output の path は物理出力 path です。Generated output を Content、redirect、plugin page の route と衝突させないでください。衝突した場合は route を上書きせず、Core が build を失敗させます。

Public Location

Plugin は resolveContentLocations を使って、コンテンツの公開先を変更できます。

最初に Core が標準の公開先を計算します。

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

その後、Plugin が順番に Public Location を解決します。

Diagram source
text
flowchart LR
    Content["Content"]
    Default["Default Location"]
    P1["Plugin A"]
    P2["Plugin B"]
    Result["ContentPublicLocation"]
 
    Content --> Default
    Default --> P1
    P1 --> P2
    P2 --> Result

結果は ContentPublicLocation として Manifest、Content Graph、Markdown Pipeline などから利用されます。

URL strategy 自体は Plugin の責務です。

たとえば、

  • identity field
  • frontmatter ID
  • hash
  • URL path
  • redirect

などの規則は Plugin が定義できます。

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

Consumer は最終的に解決された、

ts
entry.permalink

を利用します。

slug から URL を再構築したり、特定 Plugin が有効かどうかで URL を分岐したりしないでください。

Public Location が解決できない場合も、slug へ暗黙的に fallback せず明示的なエラーとして扱います。

Renderers

renderers は、特定の Content Target を Plugin 固有の HTML へ変換する仕組みです。

たとえば、

  • Canvas
  • Excalidraw
  • Media
  • Attachment

のような埋め込み表示に利用できます。

Diagram source
text
flowchart LR
    Target["Content Target"]
    Renderer["Plugin Renderer"]
    HTML["HTML"]
 
    Target --> Renderer
    Renderer -->|"対応する"| HTML
    Renderer -->|"対応しない"| Next["次のRenderer"]

Renderer Context には、

text
kind
path
raw
label
url
embed

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

対象でなければ null を返し、他の Renderer に処理を委ねられるようにします。

Body Slots

Plugin は、route を追加したり document shell を書き換えたりせずに、Site が所有する article layout の名前付き位置へ HTML fragment を提供できます。slot 名の contract は Core の ContentBodySlot が定義し、どの slot をどこに描画するかは Site が決めます。

標準の article layout は次の slot を認識します。

Slot 位置
article.header article header の直後
article.metadata title / meta block の後
article.aside article aside 内
article.before-content 本文の前
article.after-content 本文の後
article.footer article footer 内

ContentBodySlot は他の文字列も受け付けるため、独自 Site は追加の slot 名を定義できます。

fragment は @riebeckite/core の appendContentBodySlot で提供します。通常は onManifestCreated などの manifest hook から呼び出します。

ts
import { appendContentBodySlot } from "@riebeckite/core";
 
appendContentBodySlot(entry, "article.after-content", "<section>...</section>");

空の fragment は無視され、fragment は解決済み Plugin 順に蓄積されます。先に処理された Plugin の contribution は保持され、新しい fragment が末尾へ追加されます。

関連コンテンツ、履歴、ナビゲーション、backlinks など記事末尾の section には article.footer を使います。Plugin ごとに order を設定して順序を固定し、route や CSS で並べ替えません。

Site は entry.bodySlots を読み、各値を描画するかどうかと描画位置を決めます。描画の仕組みは @riebeckite/honox/ui の公開 ContentSlot primitive に任せられます。

tsx
// app/components/article/article.tsx
import { ContentSlot } from "@riebeckite/honox/ui";
 
<ContentSlot slots={props.bodySlots} name="article.after-content" />

ContentSlot は slot lookup、存在しない slot や空 slot の扱い、HTML fragment の描画、data-slot の付与を担当する公開 API です。Site 固有 class は class / className で追加します。slot は Site 自身の renderer が描画を選んだときだけ描画され、独自 slot 名は Site が描画を選ぶまで何もしません。slots を直接読んだり、任意の要素で包んだり、同じ slot を複数回描画する escape hatch も残っています。Plugin は代わりに Hono JSX component を export して Site に配置を任せることもできます。詳しくは UI の提供方法 を参照してください。

参照アプリと scaffold の starter は標準 slot を消費します。Plugin は提供し、Site が描画します。Plugin が route、shell、描画順を変更することはありません。route レベルの contract は Body Slots を参照してください。

Manifest の collection と公開境界

Manifest を受け取る Hook(onManifestCreated、page resolver、renderer)では、 次の3つの entry collection を使い分けます。

  • manifest.entries — draft と scheduled を含む全 entry。公開ページや discovery UI へ描画しないでください。
  • manifest.publicEntries — 到達可能な entry(public と unlisted)。 sitemap など、到達可能な全 URL を網羅する出力に使います。unlisted を 含む点に注意してください。
  • manifest.discoverableEntries — discovery surface に表示してよい entry (public のみ)。関連記事、新着、tag ページ、検索 index など、読者が 一覧から辿る UI にはこれを使います。

frontmatter から可視性を再判定したり、publishAt を再実装したりしないで ください。Hook が分岐を必要とする場合は、解決済みの entry.publishing (visibility、routable、discoverable)を読みます。それ以外は、判断を すでに含む collection を選んでください。公開方針は Configuration で設定します。

Page Types

pageTypes は Plugin が独立したページを提供するための仕組みです。

たとえば、

text
/explore
/report

のようなページです。

ts
definePlugin({
  name: "example-pages",
 
  pageTypes: [
    {
      id: "example.report",
      paths: ["/report"],
 
      resolve: ({ pathname, manifest }) =>
        pathname === "/report"
          ? {
              type: "example.report",
              pathname,
              body: `<p>${manifest.discoverableEntries.length}</p>`,
            }
          : null,
    },
  ],
});

Plugin が Application Route を直接追加する必要はありません。

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

Page Type は、

  • 一意な ID
  • SSG path
  • resolver
  • 必要に応じた priority

を宣言します。

Page は body のほか、

text
title
description
headTags
language

も返せます。

ただし、それらを最終 HTML のどこへ描画するかは Site Application が決めます。

詳しくは Page System を参照してください。

Build Dependency

processedContentCache は、Core が Plugin の処理済み Content を Build 間で再利用できるかを宣言する契約です。cacheVersion と context.cache とは別のものです。

ts
processedContentCache: {
  version: "example-v1",
  dependencyMode: "tracked",
}
  • none: source Content、frontmatter、options、宣言した version だけに依存する変換です。
  • tracked: 他の Content や file を Core 経由で読む変換です。context.contentSource、readContentSourceEntry、renderContent、renderNoteEmbed を使うと、Core が ContentDependencyTracker により Content/file 読み取りを自動記録します。filesystem を直接読んではいけません。
  • unsafe: Git、network、時刻、process state など、Core が追跡できない入力です。処理済み Content の永続 Cache 再利用を安全側で無効にします。

Content Dependency は再処理する source Content を決め、Output Dependency は再出力するファイルを決めます。両者は別の契約です。Page Type は outputDependencies を宣言します。onManifestCreated で既存の manifest entry HTML を更新する Plugin は、root の outputDependencies を宣言すると各 Content Output に加算されます。

ts
outputDependencies: [{ type: "global" }]
 
pageTypes: [{
  id: "example.report",
  paths: ["/report"],
  outputDependencies: [{ type: "tag", tag: "release" }],
  resolve: () => null,
}]

対象を特定できる場合は content、tag、folder を使います。manifest 全体に依存する集合変換は global を使います。表現できない入力だけに unknown を使ってください。unknown は安全側で再生成し、全 Output の再生成を要求します。依存を宣言しない Generated Output も unknown です。

Content Graph

Content Graph を拡張する場合は、

text
extendContentGraph

を使用します。

たとえば Backlinks や Graph 系 Plugin が独自に filesystem を scan してリンク関係を再構築するのではなく、既存の Manifest / Content Graph を利用します。

Diagram source
text
flowchart LR
    Manifest["Manifest Entries"]
    Graph["Content Graph"]
    Plugin["Plugin Extension"]
    Extended["Extended Graph"]
 
    Manifest --> Graph
    Graph --> Plugin
    Plugin --> Extended

Content System がすでに解決した情報を再利用することが重要です。

Assets

Plugin 固有の CSS は Plugin package 内に置き、assets で公開します。

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

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

package 名が @riebeckite/plugin-<name> の場合は createStyleAsset() / createClientEntry() が @riebeckite/plugin-<name>/style.css と @riebeckite/plugin-<name>/client を組み立てます。それ以外の名前(site 内 Plugin や任意名の第三者 package)では、自身の exports が公開する specifier を assets / clientEntries に明示してください。

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

CSS Hooks

再利用可能な Plugin UI には、最外要素へ stable な CSS Hook を付けます。

Plugin の 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

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

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

既存の class がある場合、rr-* は置き換えではなく追加します。

Theme に公開する必要がある Hook だけを stable contract として文書化してください。

Plugin の default CSS は Theme CSS より先に読み込まれるため、Theme は Plugin package を変更せずに見た目を上書きできます。

Plugin の Dark 対応は --rb-* Semantic Token を参照するのが基本です。Semantic Token は Light、明示 Dark、System Dark のいずれでも正しく解決されます。Build 済み資産の反転など、状態そのもので分岐する必要がある場合だけ、Document Root でも埋め込み .rb-theme-root でも効くように Theme Root へスコープしてください。:is(:root, .rb-theme-root)[data-theme="dark"] <hook> と、System Dark 用の @media (prefers-color-scheme: dark) { :is(:root, .rb-theme-root):not([data-theme]) <hook> { ... } } です。html[data-theme="dark"] や :root:not([data-theme]) だけに依存すると埋め込み Theme Root を取りこぼします。Framework は .dark class を付与しないため、.dark を前提にしないでください。

詳しくは Theme System を参照してください。

Client Entries

Browser 上で JavaScript を実行する必要がある場合だけ clientEntries を使用します。

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

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

publicConfig は Browser へ渡される公開情報です。

そのため、

  • token
  • credential
  • private service URL
  • secret

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

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

HTTP Endpoints

Plugin が再利用可能な HTTP Endpoint を提供する場合は endpoints を使用します。

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

HonoX など特定の Router 実装を Plugin 本体へ直接埋め込まず、Integration が Endpoint Contract を Host Router へ接続します。

SEO

Plugin が metadata や feed などの SEO 処理へ参加する場合は seo Extension Point を使用します。

Plugin 固有の SEO logic を Application Route 側へ再実装しないでください。

Diagnostics

Plugin 固有の問題を報告する場合は addDiagnostics を使用します。

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

診断結果は可能な限り structured data として返します。

Plugin が直接、

ts
console.log(...)

で CLI 向けメッセージを出すのではなく、Diagnostics または Logger を利用してください。

Plugin Cache

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

Cache に保存する値は、

  • 再生成可能
  • JSON serializable
  • Plugin namespace 内で完結

している必要があります。

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

壊れた Cache は安全に Cache Miss として扱える設計にしてください。

Write は atomic に行います。

Diagram source
text
flowchart TD
    Plugin["Plugin"]
    Cache["Plugin Cache"]
    Valid{"利用可能?"}
 
    Plugin --> Cache
    Cache --> Valid
 
    Valid -->|Yes| Reuse["再利用"]
    Valid -->|No| Generate["再生成"]

これは Cloudflare Workers などの Runtime Database ではありません。

Logger / Tracer

Plugin から Framework の Observability を利用できます。

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

Logger は「何が起きたか」、Tracer は「どこに時間がかかったか」を記録します。

Profiler はこの structured trace を利用するため、Plugin が独自の stopwatch や profiling system を作る必要はありません。

Site 内だけで使う Plugin

Plugin は npm package として公開する必要はありません。

Site 内に Plugin を作ることもできます。

ts
// site/extensions/local-plugin.ts
 
return definePlugin({
  name: "site-local",
 
  assets: [
    {
      pluginName: "site-local",
      kind: "style",
      moduleSpecifier:
        "/extensions/plugin.css",
    },
  ],
});

そして riebeckite.config.ts の plugins へ追加します。

Site-local Plugin でも、

  • Dependency Resolution
  • Pipeline Hooks
  • Diagnostics
  • Renderer
  • Endpoint

などは package Plugin と同じ contract を利用します。

createStyleAsset() と createClientEntry() は @riebeckite/plugin-<name>/... の specifier しか組み立てないため、その名前で ない package(site 内 Plugin、別名の第三者 package)は Host Bundler が解決 できる moduleSpecifier を明示してください。

推奨 Package 構成

公開 Plugin は、たとえば次のように構成できます。

text
packages/plugins/example/
├─ index.ts
├─ components/        # component を export する場合のみ
├─ client.ts          # 必要な場合のみ
├─ style.css          # 必要な場合のみ
├─ package.json
└─ src/
   ├─ remark.ts
   ├─ rehype.ts
   ├─ renderer.ts
   └─ types.ts

すべての Plugin がこの構造を必要とするわけではありません。

Client JavaScript や CSS が不要なら、それらのファイルも不要です。

Repository 外で Plugin を配布する

外部 Plugin は Riebeckite monorepo の内部 path に依存しないようにします。

基本的には、

text
@riebeckite/core

の公開 API を利用します。

Plugin 自身が提供する、

text
./client
./components
./style.css

などは、自身の package.json の exports で公開します。

次のような import は避けてください。

ts
import {
  something,
} from "@riebeckite/core/src/...";

src/** は Public API ではありません。

また、Riebeckite monorepo 内にしか存在しない相対 path へ依存しないでください。

公式 Plugin も可能な限り同じ Public API の consumer として実装します。

Package 構成

公開 package では、Build 済み ESM と型定義を publish し、exports をその Build 成果物へ向けます。最小構成の package.json は次のとおりです。

json
{
  "name": "my-riebeckite-plugin",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "default": "./dist/index.js"
    },
    "./client": {
      "types": "./dist/client.d.ts",
      "import": "./dist/client.js",
      "default": "./dist/client.js"
    },
    "./style.css": "./style.css"
  },
  "files": ["dist", "style.css"],
  "scripts": {
    "build": "node build.mjs && tsc -p tsconfig.json",
    "prepack": "npm run build"
  },
  "dependencies": {
    "@riebeckite/core": "^0.0.19",
    "unist-util-visit": "^5.0.0"
  },
  "devDependencies": {
    "@types/mdast": "^4.0.0",
    "esbuild": "^0.28.0",
    "typescript": "^5.0.0"
  }
}

client.ts や style.css を持たない Plugin では、その subpath と files の entry を削除します。

JavaScript の entry point は esbuild で bundle し、型定義は tsc で emit します。どちらも一般的なツールで、Riebeckite 固有の build script は必要ありません。

build.mjs:

js
import { build } from "esbuild";
 
await build({
  entryPoints: ["index.ts", "client.ts"],
  outdir: "dist",
  bundle: true,
  format: "esm",
  platform: "neutral",
  packages: "external",
  external: ["@riebeckite/*"],
  logLevel: "warning",
});

entryPoints には、その Package が持つ entry だけを列挙します(client を持たないなら index.ts だけ)。

tsconfig.json:

json
{
  "compilerOptions": {
    "declaration": true,
    "emitDeclarationOnly": true,
    "outDir": "dist",
    "rootDir": ".",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "target": "ESNext",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["index.ts", "client.ts"]
}

prepack で build するようにしておけば、npm pack / npm publish は常に最新の成果物を同梱します。@riebeckite/core は dependencies に置くのが最も簡単です(Site 側の instance を共有したい場合は peerDependencies でもかまいません)。import する変換依存(unist-util-visit、unified、remark / rehype package など)は dependencies に宣言し、公開 package の exports を TypeScript の source(./index.ts)へ向けないでください。

配布する Package のテストは テスト を、CSS / Client entry の packaging は Assets と Client Entries を参照してください。

ESM

Riebeckite の package は NodeNext / ESM を前提とします。

Build 後に Node.js が実際に解決できる import を維持してください。

Development 時の TypeScript Loader が、

text
extensionless import

などを偶然解決できている状態へ依存しないことが重要です。

Plugin に置くもの・置かないもの

Plugin に置くものは、Riebeckite Site 間で再利用可能な機能です。

Diagram source
text
flowchart TD
    Feature{"この機能は何?"}
 
    Feature -->|"Framework共通のContent Model"| Core["Core"]
    Feature -->|"再利用可能なContent機能"| Plugin["Plugin"]
    Feature -->|"HonoX / Vite接続"| Integration["Integration"]
    Feature -->|"Site固有Route / Layout"| App["Application"]
    Feature -->|"見た目だけ"| Theme["Theme"]

Plugin に向いているものは、

  • Markdown / HTML の解釈
  • 再利用可能な Content Transformation
  • Plugin 固有 Renderer
  • 再利用可能な Browser Behavior
  • Plugin 固有 Diagnostics
  • Plugin 固有 Endpoint
  • SEO Extension
  • 独立した Plugin Page

などです。

一方、

text
Framework-wide Content Model
  → Core
 
HonoX / Vite 接続
  → Integration
 
Application 固有 Route / Layout
  → Site Application
 
見た目だけの変更
  → Theme

とします。

Plugin 設計の基本

Plugin System 全体は次のようになります。

Diagram source
text
flowchart LR
    Plugin["Plugin"]
 
    Plugin --> Pipeline["Content Pipeline"]
    Plugin --> Renderer["Renderer"]
    Plugin --> Page["Page Type"]
    Plugin --> Graph["Content Graph"]
    Plugin --> Diagnostics["Diagnostics"]
    Plugin --> Asset["Assets"]
    Plugin --> Client["Client Entry"]
    Plugin --> Endpoint["Endpoint"]
    Plugin --> SEO["SEO"]
 
    Pipeline --> Core["Core Contracts"]
    Renderer --> Core
    Page --> Core
    Graph --> Core
    Diagnostics --> Core
    Asset --> Core
    Client --> Core
    Endpoint --> Core
    SEO --> Core
 
    Core --> Integration["Integration"]
    Integration --> Site["Site Application"]

基本原則は、必要な最小の Extension Point を使い、すでに Framework が解決した情報を Plugin 側で再構築しないことです。

Plugin は再利用可能な機能を提供し、Core はそのための Contract を提供します。Integration は Framework や Platform へ接続し、Site Application が最終的な Route、Document、UI を所有します。

関連

History

1 changesCollapseExpand
1 + # Plugin System
2 +
3 + Riebeckite Plugin は、Riebeckite の機能を Core や Site Application に直接組み込まず、再利用可能な形で追加するための仕組みです。
4 +
5 + Plugin では、たとえば次のような機能を追加できます。
6 +
7 + - Markdown / HTML の変換
8 + - コンテンツの処理
9 + - 独自形式の埋め込み表示
10 + - 独立したページ
11 + - CSS
12 + - Browser 上の処理
13 + - HTTP Endpoint
14 + - SEO
15 + - Diagnostics
16 + - Content Graph の拡張
17 +
18 + Plugin は必要な機能だけを実装します。
19 +
20 + すべての API を使う必要はありません。
21 +
22 + # まず何を使うか決める
23 +
24 + Plugin を作るときは、最初に **目的に合った最小の Extension Point** を選びます。
25 +
26 + ```mermaid
27 + flowchart TD
28 + Q{"何を追加したい?"}
29 +
30 + Q -->|"Markdown / HTMLの意味変換"| Pipeline["remark / rehype<br/>Pipeline"]
31 + Q -->|"Content処理の特定段階へ参加"| Hook["Content Hooks"]
32 + Q -->|"特定形式を表示"| Renderer["renderers"]
33 + Q -->|"独立したページ"| Page["pageTypes"]
34 + Q -->|"CSS"| Asset["assets"]
35 + Q -->|"Browser処理"| Client["clientEntries"]
36 + Q -->|"HTTP API"| Endpoint["endpoints"]
37 + Q -->|"SEO"| SEO["seo"]
38 + Q -->|"Graph情報"| Graph["extendContentGraph"]
39 + Q -->|"問題を報告"| Diagnostics["addDiagnostics"]
40 + ```
41 +
42 + たとえば Canvas や Excalidraw を記事内へ表示するなら `renderers`、`/explore` のような独立したページを提供するなら `pageTypes` を使います。
43 +
44 + 独立画面が必要だからといって、Plugin 固有の HonoX route を追加するわけではありません。
45 +
46 + # 最小の Plugin
47 +
48 + 最も小さい Plugin は次のように作れます。
49 +
50 + ```ts
51 + import { definePlugin } from "@riebeckite/core";
52 +
53 + export function examplePlugin() {
54 + return definePlugin({
55 + name: "example",
56 + });
57 + }
58 + ```
59 +
60 + Plugin に設定を持たせる場合は、factory の引数として受け取ります。
61 +
62 + ```ts
63 + type ExampleOptions = {
64 + enabled?: boolean;
65 + };
66 +
67 + export function examplePlugin(
68 + options: ExampleOptions = {},
69 + ) {
70 + return definePlugin({
71 + name: "example",
72 + options,
73 + });
74 + }
75 + ```
76 +
77 + `definePlugin()` が Plugin の共通 contract を提供します。
78 +
79 + # Plugin が持てる機能
80 +
81 + `RiebeckitePlugin` には、大きく次の Extension Point があります。
82 +
83 + | 分類 | 主な API |
84 + | --- | --- |
85 + | 基本情報 | `name`, `enabled`, `order`, `options` |
86 + | 依存関係 | `provides`, `requires`, `optional` |
87 + | 設定検証 | `validateOptions` |
88 + | Cache | `cacheVersion`, `context.cache` |
89 + | Lifecycle | `setup`, `buildStart`, `buildEnd`, `dispose` |
90 + | Content | `onConfigResolved`, `onContentLoaded`, `onPostParsed`, `onPostProcessed`, `onManifestCreated` |
91 + | 公開先 | `resolveContentLocations` |
92 + | Markdown / HTML | `remarkPlugins`, `rehypePlugins`, `extendMarkdownPipeline`, `extendHtmlPipeline` |
93 + | Graph | `extendContentGraph` |
94 + | Diagnostics | `addDiagnostics` |
95 + | 埋め込み表示 | `renderers` |
96 + | 独立ページ | `pageTypes` |
97 + | Browser | `assets`, `clientEntries` |
98 + | HTTP | `endpoints` |
99 + | SEO | `seo` |
100 +
101 + Plugin はこの中から**必要なものだけ**を使用してください。
102 +
103 + # Plugin の有効化
104 +
105 + Plugin は `riebeckite.config.ts` の `plugins` へ追加します。
106 +
107 + ```ts
108 + plugins: [
109 + myPlugin(),
110 + ]
111 + ```
112 +
113 + 条件付きで有効化することもできます。
114 +
115 + ```ts
116 + plugins: [
117 + condition && myPlugin(),
118 + ]
119 + ```
120 +
121 + `false`、`null`、`undefined` は Plugin の解決時に除外されます。
122 +
123 + また、
124 +
125 + ```ts
126 + enabled: false
127 + ```
128 +
129 + の Plugin も実行対象になりません。
130 +
131 + # Plugin の順序と依存関係
132 +
133 + 単純な実行順は `order` で指定できます。
134 +
135 + ただし、Plugin 同士に実際の依存関係がある場合は `order` ではなく **Capability** を使用します。
136 +
137 + ```ts
138 + definePlugin({
139 + name: "consumer",
140 +
141 + provides: [
142 + "example.output",
143 + ],
144 +
145 + requires: [
146 + "content.graph",
147 + ],
148 +
149 + optional: [
150 + "example.optional",
151 + ],
152 + });
153 + ```
154 +
155 + それぞれの意味は次のとおりです。
156 +
157 + | Field | 意味 |
158 + | --- | --- |
159 + | `provides` | この Plugin が提供する機能 |
160 + | `requires` | 必ず必要な機能 |
161 + | `optional` | あれば利用する機能 |
162 +
163 + Resolver は依存関係から Plugin の実行順を決定します。
164 +
165 + ```mermaid
166 + flowchart LR
167 + Provider["Provider Plugin<br/>provides: content.graph"]
168 + Consumer["Consumer Plugin<br/>requires: content.graph"]
169 +
170 + Provider --> Consumer
171 + ```
172 +
173 + 次のような状態は Configuration Error になります。
174 +
175 + - 必須 Capability が存在しない
176 + - 同じ Capability を複数 Plugin が提供する
177 + - 依存関係が循環している
178 +
179 + Capability の解決に失敗した場合は `PluginDependencyError`(`@riebeckite/core` から import 可能)を投げます。`kind` と `pluginName` から原因を特定できます。
180 +
181 + 依存関係のない Plugin については、入力順を可能な限り維持します。
182 +
183 + # Options Validation
184 +
185 + TypeScript の型だけでは、実行時に渡される設定値が必ず正しいとは保証できません。
186 +
187 + 必要な Plugin は `validateOptions` を実装できます。
188 +
189 + Validation は、
190 +
191 + ```text
192 + Plugin Options
193 + ↓
194 + validateOptions
195 + ↓
196 + Structured Issues
197 + ↓
198 + Config Validation
199 + ```
200 +
201 + という流れで扱われます。
202 +
203 + Validator は **設定の検証だけ**を行ってください。
204 +
205 + ここで、
206 +
207 + - filesystem scan
208 + - Build
209 + - Cache write
210 + - 外部状態の変更
211 +
212 + などを行わないでください。
213 +
214 + 問題は structured issue として返し、Riebeckite の Config Validation がまとめて表示できるようにします。
215 +
216 + # Plugin Context
217 +
218 + Plugin は Framework の機能を `PluginContext` から受け取ります。
219 +
220 + 基本的な Context は概念的に次のようなものです。
221 +
222 + ```ts
223 + type PluginContext = {
224 + config?: ResolvedRiebeckiteConfig;
225 + contentIndex: Map<string, string>;
226 + diagnostics: Diagnostic[];
227 + cache: PluginCache;
228 + output: GeneratedOutputSink;
229 + logger: Logger;
230 + tracer: Tracer;
231 + contentSource?: ContentSource;
232 + };
233 + ```
234 +
235 + Hook によって、
236 +
237 + - `slug`
238 + - `markdown`
239 + - `content`
240 + - `manifest`
241 + - `entries`
242 + - Public Location の入力
243 +
244 + などが追加されます。
245 +
246 + Plugin 内で global singleton を作るより、Context から Framework Service を受け取ることを優先してください。
247 +
248 + # Lifecycle
249 +
250 + Plugin には Framework 全体の Lifecycle と、Content 処理の Lifecycle があります。
251 +
252 + Framework Lifecycle は、
253 +
254 + ```text
255 + setup
256 + buildStart
257 + buildEnd
258 + dispose
259 + ```
260 +
261 + があります。
262 +
263 + `dispose` は Plugin が確保した resource の解放に使用します。
264 +
265 + `setup`、`buildStart`、`onConfigResolved`、Content 処理、`buildEnd` は、1つの `ContentManager` につき一度だけ実行されます。`buildEnd` は Diagnostics の収集後に完成した Manifest を受け取る唯一の終端 Hook です。`dispose` は解決済み Plugin の逆順で実行されます。
266 +
267 + 名前付き Lifecycle / Content Hook でエラーが発生した場合は `PluginHookError`(`@riebeckite/core` から import 可能)として上位へ伝播させます。`message` は `Plugin "<name>" failed during "<hook>"` 形式で、元の Error は `cause` に保持されます。Content Hook で発生した場合は、対象 file が `path`(例: `note.md`)として設定されます。
268 +
269 + # Content Lifecycle
270 +
271 + Content 処理は概念的に次の順番で進みます。
272 +
273 + ```mermaid
274 + flowchart TD
275 + Config["Config Resolved"]
276 + Loaded["Content Loaded"]
277 + Location["Public Location Resolved"]
278 + Parsed["Post Parsed"]
279 + Processed["Post Processed"]
280 + Graph["Content Graph"]
281 + Manifest["Manifest Created"]
282 +
283 + Config --> Location
284 + Location --> Loaded
285 + Loaded --> Parsed
286 + Parsed --> Processed
287 + Processed --> Graph
288 + Graph --> Manifest
289 + ```
290 +
291 + 全体の順序は `setup` → `buildStart` → `onConfigResolved` → Public Location 解決 → `onContentLoaded` → Markdown / HTML Pipeline → `onPostParsed` → `onPostProcessed` → `extendContentGraph` → `onManifestCreated` → Diagnostics → `buildEnd` です。
292 +
293 + 代表的な Hook として、
294 +
295 + ```text
296 + onConfigResolved
297 + onContentLoaded
298 + onPostParsed
299 + onPostProcessed
300 + onManifestCreated
301 + ```
302 +
303 + があります。
304 +
305 + **必要な段階の Hook だけを使用します**。
306 +
307 + 後段ですでに得られる情報を前段で独自に再構築しないでください。
308 +
309 + # Markdown / HTML Pipeline
310 +
311 + Markdown や HTML の意味を変換する場合は Pipeline を使用します。
312 +
313 + 簡単な remark / rehype Plugin なら直接宣言できます。
314 +
315 + ```ts
316 + definePlugin({
317 + name: "example",
318 +
319 + remarkPlugins: [
320 + remarkExample,
321 + ],
322 +
323 + rehypePlugins: [
324 + rehypeExample,
325 + ],
326 + });
327 + ```
328 +
329 + Pipeline 自体を構成する必要がある場合は Extension API を利用します。
330 +
331 + ```ts
332 + definePlugin({
333 + name: "example",
334 +
335 + extendMarkdownPipeline(pipeline) {
336 + pipeline.use(remarkExample);
337 + },
338 +
339 + extendHtmlPipeline(pipeline) {
340 + pipeline.use(rehypeExample);
341 + },
342 + });
343 + ```
344 +
345 + Markdown AST や HTML AST の処理を Application Component に持ち込まず、Plugin の Pipeline 処理として実装するのが基本です。
346 +
347 + # 処理済み Content の Build Dependency
348 +
349 + Incremental Build の無効化は Core の責務です。Plugin は独自に affected content を計算せず、`processedContentCache` で契約を宣言します。
350 +
351 + ```ts
352 + definePlugin({
353 + name: "citations",
354 + processedContentCache: {
355 + version: "citations-v1",
356 + dependencyMode: "tracked",
357 + },
358 + extendMarkdownPipeline(pipeline, context) {
359 + pipeline.use(remarkCitations, { contentSource: context.contentSource });
360 + },
361 + });
362 + ```
363 +
364 + - `none` は、処理結果が Content 本体、frontmatter、options、宣言した version だけに依存することを表します。
365 + - `tracked` は、Pipeline が他の Content や file を読む場合に使います。`context.contentSource` 経由で読めば、Core が dependency を記録し、利用側だけを再処理します。たとえば citations Plugin は `readContentSourceEntry(context.contentSource, path)` で BibTeX file を読みます。
366 + - `unsafe` は処理済み Content の永続的な再利用を無効にします。Content 処理を行う Plugin が契約を宣言しない場合も、同じ安全側の全 Content 再処理になります。
367 +
368 + `tracked` の dependency は Content 処理中に取得します。Core が content/file の identity を永続化し、逆引き index から次回 Build で affected content を決定します。この用途で vault を独自に走査したり、Plugin 固有の Incremental Build state を保存したりしないでください。Framework API 経由で dependency を追跡できない場合は `unsafe` を使います。広い再処理は許容されますが、古い結果の再利用は許容されません。
369 +
370 + これは Output Dependency とは別の契約です。`pageTypes[].outputDependencies` と `context.output.emit(..., { dependencies })` は、再生成が必要な page や生成 file を表します。ここでは `content`、`tag`、`folder`、`global`、`unknown` を使います。`unknown` は安全側として全 Output の再生成を要求します。
371 +
372 + Generated output の path は物理出力 path です。Generated output を Content、redirect、plugin page の route と衝突させないでください。衝突した場合は route を上書きせず、Core が build を失敗させます。
373 +
374 + # Public Location
375 +
376 + Plugin は `resolveContentLocations` を使って、コンテンツの公開先を変更できます。
377 +
378 + 最初に Core が標準の公開先を計算します。
379 +
380 + ```text
381 + index
382 + → /
383 +
384 + その他
385 + → /{slug}
386 + ```
387 +
388 + その後、Plugin が順番に Public Location を解決します。
389 +
390 + ```mermaid
391 + flowchart LR
392 + Content["Content"]
393 + Default["Default Location"]
394 + P1["Plugin A"]
395 + P2["Plugin B"]
396 + Result["ContentPublicLocation"]
397 +
398 + Content --> Default
399 + Default --> P1
400 + P1 --> P2
401 + P2 --> Result
402 + ```
403 +
404 + 結果は `ContentPublicLocation` として Manifest、Content Graph、Markdown Pipeline などから利用されます。
405 +
406 + URL strategy 自体は Plugin の責務です。
407 +
408 + たとえば、
409 +
410 + - identity field
411 + - frontmatter ID
412 + - hash
413 + - URL path
414 + - redirect
415 +
416 + などの規則は Plugin が定義できます。
417 +
418 + Core は特定 Plugin の URL 規則を知りません。
419 +
420 + Consumer は最終的に解決された、
421 +
422 + ```ts
423 + entry.permalink
424 + ```
425 +
426 + を利用します。
427 +
428 + slug から URL を再構築したり、特定 Plugin が有効かどうかで URL を分岐したりしないでください。
429 +
430 + Public Location が解決できない場合も、slug へ暗黙的に fallback せず明示的なエラーとして扱います。
431 +
432 + # Renderers
433 +
434 + `renderers` は、特定の Content Target を Plugin 固有の HTML へ変換する仕組みです。
435 +
436 + たとえば、
437 +
438 + - Canvas
439 + - Excalidraw
440 + - Media
441 + - Attachment
442 +
443 + のような埋め込み表示に利用できます。
444 +
445 + ```mermaid
446 + flowchart LR
447 + Target["Content Target"]
448 + Renderer["Plugin Renderer"]
449 + HTML["HTML"]
450 +
451 + Target --> Renderer
452 + Renderer -->|"対応する"| HTML
453 + Renderer -->|"対応しない"| Next["次のRenderer"]
454 + ```
455 +
456 + Renderer Context には、
457 +
458 + ```text
459 + kind
460 + path
461 + raw
462 + label
463 + url
464 + embed
465 + ```
466 +
467 + などと通常の `PluginContext` が含まれます。
468 +
469 + 対象でなければ `null` を返し、他の Renderer に処理を委ねられるようにします。
470 +
471 + # Body Slots
472 +
473 + Plugin は、route を追加したり document shell を書き換えたりせずに、Site が所有する article layout の名前付き位置へ HTML fragment を提供できます。slot 名の contract は Core の `ContentBodySlot` が定義し、どの slot をどこに描画するかは Site が決めます。
474 +
475 + 標準の article layout は次の slot を認識します。
476 +
477 + | Slot | 位置 |
478 + | --- | --- |
479 + | `article.header` | article header の直後 |
480 + | `article.metadata` | title / meta block の後 |
481 + | `article.aside` | article aside 内 |
482 + | `article.before-content` | 本文の前 |
483 + | `article.after-content` | 本文の後 |
484 + | `article.footer` | article footer 内 |
485 +
486 + `ContentBodySlot` は他の文字列も受け付けるため、独自 Site は追加の slot 名を定義できます。
487 +
488 + fragment は `@riebeckite/core` の `appendContentBodySlot` で提供します。通常は `onManifestCreated` などの manifest hook から呼び出します。
489 +
490 + ```ts
491 + import { appendContentBodySlot } from "@riebeckite/core";
492 +
493 + appendContentBodySlot(entry, "article.after-content", "<section>...</section>");
494 + ```
495 +
496 + 空の fragment は無視され、fragment は解決済み Plugin 順に蓄積されます。先に処理された Plugin の contribution は保持され、新しい fragment が末尾へ追加されます。
497 +
498 + 関連コンテンツ、履歴、ナビゲーション、backlinks など記事末尾の section には `article.footer` を使います。Plugin ごとに `order` を設定して順序を固定し、route や CSS で並べ替えません。
499 +
500 + Site は `entry.bodySlots` を読み、各値を描画するかどうかと描画位置を決めます。描画の仕組みは `@riebeckite/honox/ui` の公開 `ContentSlot` primitive に任せられます。
501 +
502 + ```tsx
503 + // app/components/article/article.tsx
504 + import { ContentSlot } from "@riebeckite/honox/ui";
505 +
506 + <ContentSlot slots={props.bodySlots} name="article.after-content" />
507 + ```
508 +
509 + `ContentSlot` は slot lookup、存在しない slot や空 slot の扱い、HTML fragment の描画、`data-slot` の付与を担当する公開 API です。Site 固有 class は `class` / `className` で追加します。slot は Site 自身の renderer が描画を選んだときだけ描画され、独自 slot 名は Site が描画を選ぶまで何もしません。`slots` を直接読んだり、任意の要素で包んだり、同じ slot を複数回描画する escape hatch も残っています。Plugin は代わりに Hono JSX component を export して Site に配置を任せることもできます。詳しくは [UI の提供方法](../plugins/writing-a-plugin.md#ui-の提供方法) を参照してください。
510 +
511 + 参照アプリと scaffold の starter は標準 slot を消費します。Plugin は提供し、Site が描画します。Plugin が route、shell、描画順を変更することはありません。route レベルの contract は [Body Slots](../framework/honox-integration.md#body-slots) を参照してください。
512 +
513 + # Manifest の collection と公開境界
514 +
515 + Manifest を受け取る Hook(`onManifestCreated`、page resolver、renderer)では、
516 + 次の3つの entry collection を使い分けます。
517 +
518 + - `manifest.entries` — `draft` と `scheduled` を含む全 entry。公開ページや
519 + discovery UI へ描画しないでください。
520 + - `manifest.publicEntries` — 到達可能な entry(`public` と `unlisted`)。
521 + sitemap など、到達可能な全 URL を網羅する出力に使います。`unlisted` を
522 + 含む点に注意してください。
523 + - `manifest.discoverableEntries` — discovery surface に表示してよい entry
524 + (`public` のみ)。関連記事、新着、tag ページ、検索 index など、読者が
525 + 一覧から辿る UI にはこれを使います。
526 +
527 + `frontmatter` から可視性を再判定したり、`publishAt` を再実装したりしないで
528 + ください。Hook が分岐を必要とする場合は、解決済みの `entry.publishing`
529 + (`visibility`、`routable`、`discoverable`)を読みます。それ以外は、判断を
530 + すでに含む collection を選んでください。公開方針は
531 + [Configuration](./configuration.md) で設定します。
532 +
533 + # Page Types
534 +
535 + `pageTypes` は Plugin が独立したページを提供するための仕組みです。
536 +
537 + たとえば、
538 +
539 + ```text
540 + /explore
541 + /report
542 + ```
543 +
544 + のようなページです。
545 +
546 + ```ts
547 + definePlugin({
548 + name: "example-pages",
549 +
550 + pageTypes: [
551 + {
552 + id: "example.report",
553 + paths: ["/report"],
554 +
555 + resolve: ({ pathname, manifest }) =>
556 + pathname === "/report"
557 + ? {
558 + type: "example.report",
559 + pathname,
560 + body: `<p>${manifest.discoverableEntries.length}</p>`,
561 + }
562 + : null,
563 + },
564 + ],
565 + });
566 + ```
567 +
568 + Plugin が Application Route を直接追加する必要はありません。
569 +
570 + ```mermaid
571 + flowchart LR
572 + Plugin["Plugin Page Type"]
573 + Core["Core Resolver"]
574 + Integration["HonoX Integration"]
575 + Site["Site Document Frame"]
576 +
577 + Plugin --> Core
578 + Core --> Integration
579 + Integration --> Site
580 + ```
581 +
582 + Page Type は、
583 +
584 + - 一意な ID
585 + - SSG path
586 + - resolver
587 + - 必要に応じた `priority`
588 +
589 + を宣言します。
590 +
591 + Page は `body` のほか、
592 +
593 + ```text
594 + title
595 + description
596 + headTags
597 + language
598 + ```
599 +
600 + も返せます。
601 +
602 + ただし、それらを最終 HTML のどこへ描画するかは Site Application が決めます。
603 +
604 + 詳しくは [Page System](../framework/page-system.md) を参照してください。
605 +
606 + # Build Dependency
607 +
608 + `processedContentCache` は、Core が Plugin の処理済み Content を Build 間で再利用できるかを宣言する契約です。`cacheVersion` と `context.cache` とは別のものです。
609 +
610 + ```ts
611 + processedContentCache: {
612 + version: "example-v1",
613 + dependencyMode: "tracked",
614 + }
615 + ```
616 +
617 + - `none`: source Content、frontmatter、options、宣言した version だけに依存する変換です。
618 + - `tracked`: 他の Content や file を Core 経由で読む変換です。`context.contentSource`、`readContentSourceEntry`、`renderContent`、`renderNoteEmbed` を使うと、Core が `ContentDependencyTracker` により Content/file 読み取りを自動記録します。filesystem を直接読んではいけません。
619 + - `unsafe`: Git、network、時刻、process state など、Core が追跡できない入力です。処理済み Content の永続 Cache 再利用を安全側で無効にします。
620 +
621 + Content Dependency は再処理する source Content を決め、Output Dependency は再出力するファイルを決めます。両者は別の契約です。Page Type は `outputDependencies` を宣言します。`onManifestCreated` で既存の manifest entry HTML を更新する Plugin は、root の `outputDependencies` を宣言すると各 Content Output に加算されます。
622 +
623 + ```ts
624 + outputDependencies: [{ type: "global" }]
625 +
626 + pageTypes: [{
627 + id: "example.report",
628 + paths: ["/report"],
629 + outputDependencies: [{ type: "tag", tag: "release" }],
630 + resolve: () => null,
631 + }]
632 + ```
633 +
634 + 対象を特定できる場合は `content`、`tag`、`folder` を使います。manifest 全体に依存する集合変換は `global` を使います。表現できない入力だけに `unknown` を使ってください。`unknown` は安全側で再生成し、全 Output の再生成を要求します。依存を宣言しない Generated Output も `unknown` です。
635 +
636 + # Content Graph
637 +
638 + Content Graph を拡張する場合は、
639 +
640 + ```text
641 + extendContentGraph
642 + ```
643 +
644 + を使用します。
645 +
646 + たとえば Backlinks や Graph 系 Plugin が独自に filesystem を scan してリンク関係を再構築するのではなく、既存の Manifest / Content Graph を利用します。
647 +
648 + ```mermaid
649 + flowchart LR
650 + Manifest["Manifest Entries"]
651 + Graph["Content Graph"]
652 + Plugin["Plugin Extension"]
653 + Extended["Extended Graph"]
654 +
655 + Manifest --> Graph
656 + Graph --> Plugin
657 + Plugin --> Extended
658 + ```
659 +
660 + Content System がすでに解決した情報を再利用することが重要です。
661 +
662 + # Assets
663 +
664 + Plugin 固有の CSS は Plugin package 内に置き、`assets` で公開します。
665 +
666 + ```ts
667 + assets: [
668 + {
669 + pluginName: "example",
670 + kind: "style",
671 + moduleSpecifier:
672 + "@riebeckite/plugin-example/style.css",
673 + },
674 + ]
675 + ```
676 +
677 + Plugin CSS を `apps/web` へコピーしたり、Browser から `/node_modules` を直接参照させたりしないでください。
678 +
679 + package 名が `@riebeckite/plugin-<name>` の場合は `createStyleAsset()` /
680 + `createClientEntry()` が `@riebeckite/plugin-<name>/style.css` と
681 + `@riebeckite/plugin-<name>/client` を組み立てます。それ以外の名前(site 内
682 + Plugin や任意名の第三者 package)では、自身の `exports` が公開する
683 + specifier を `assets` / `clientEntries` に明示してください。
684 +
685 + ```mermaid
686 + flowchart LR
687 + Plugin["Plugin Package"]
688 + CSS["style.css"]
689 + Asset["assets"]
690 + Integration["Integration"]
691 + Browser["Browser"]
692 +
693 + Plugin --> CSS
694 + CSS --> Asset
695 + Asset --> Integration
696 + Integration --> Browser
697 + ```
698 +
699 + # CSS Hooks
700 +
701 + 再利用可能な Plugin UI には、最外要素へ stable な CSS Hook を付けます。
702 +
703 + Plugin の Hook は、
704 +
705 + ```text
706 + rr-<feature>
707 + ```
708 +
709 + という名前を使います。
710 +
711 + たとえば、
712 +
713 + ```text
714 + rr-search
715 + rr-callout
716 + rr-query
717 + rr-code
718 + ```
719 +
720 + です。
721 +
722 + 内部要素は BEM 形式を使用できます。
723 +
724 + ```text
725 + rr-search
726 + rr-search__input
727 + rr-search__result
728 + rr-search--loading
729 + ```
730 +
731 + Plugin 固有の出力を `rb-*` namespace に置かないでください。
732 +
733 + | Namespace | 用途 |
734 + | --- | --- |
735 + | `rb-*` | Framework の構造 Hook |
736 + | `--rb-*` | Framework の Semantic Design Token |
737 + | `rr-*` | Plugin / Feature Hook |
738 + | `--rr-*` | Plugin 固有 Token |
739 +
740 + 既存の class がある場合、`rr-*` は置き換えではなく追加します。
741 +
742 + Theme に公開する必要がある Hook だけを stable contract として文書化してください。
743 +
744 + Plugin の default CSS は Theme CSS より先に読み込まれるため、Theme は Plugin package を変更せずに見た目を上書きできます。
745 +
746 + Plugin の Dark 対応は `--rb-*` Semantic Token を参照するのが基本です。Semantic Token は Light、明示 Dark、System Dark のいずれでも正しく解決されます。Build 済み資産の反転など、状態そのもので分岐する必要がある場合だけ、Document Root でも埋め込み `.rb-theme-root` でも効くように Theme Root へスコープしてください。`:is(:root, .rb-theme-root)[data-theme="dark"] <hook>` と、System Dark 用の `@media (prefers-color-scheme: dark) { :is(:root, .rb-theme-root):not([data-theme]) <hook> { ... } }` です。`html[data-theme="dark"]` や `:root:not([data-theme])` だけに依存すると埋め込み Theme Root を取りこぼします。Framework は `.dark` class を付与しないため、`.dark` を前提にしないでください。
747 +
748 + 詳しくは [Theme System](./theme-api.md#stable-css-hooks) を参照してください。
749 +
750 + # Client Entries
751 +
752 + Browser 上で JavaScript を実行する必要がある場合だけ `clientEntries` を使用します。
753 +
754 + ```ts
755 + clientEntries: [
756 + {
757 + pluginName: "example",
758 + moduleSpecifier:
759 + "@riebeckite/plugin-example/client",
760 + exportName: "initExample",
761 + publicConfig: {
762 + selector: ".example",
763 + },
764 + },
765 + ]
766 + ```
767 +
768 + ```mermaid
769 + flowchart LR
770 + Plugin["Plugin"]
771 + Entry["Client Entry"]
772 + Build["Integration"]
773 + Browser["Browser Initializer"]
774 +
775 + Plugin --> Entry
776 + Entry --> Build
777 + Build --> Browser
778 + ```
779 +
780 + SSR / Build-time だけで完結する Plugin に Client JavaScript を追加しないでください。
781 +
782 + `publicConfig` は Browser へ渡される公開情報です。
783 +
784 + そのため、
785 +
786 + - token
787 + - credential
788 + - private service URL
789 + - secret
790 +
791 + などを含めてはいけません。
792 +
793 + Plugin の `options` が自動的に Browser へ渡されることもありません。
794 +
795 + # HTTP Endpoints
796 +
797 + Plugin が再利用可能な HTTP Endpoint を提供する場合は `endpoints` を使用します。
798 +
799 + ```mermaid
800 + flowchart LR
801 + Plugin["Plugin"]
802 + Contract["Endpoint Contract"]
803 + Integration["Integration"]
804 + Router["Host Router"]
805 +
806 + Plugin --> Contract
807 + Contract --> Integration
808 + Integration --> Router
809 + ```
810 +
811 + HonoX など特定の Router 実装を Plugin 本体へ直接埋め込まず、Integration が Endpoint Contract を Host Router へ接続します。
812 +
813 + # SEO
814 +
815 + Plugin が metadata や feed などの SEO 処理へ参加する場合は `seo` Extension Point を使用します。
816 +
817 + Plugin 固有の SEO logic を Application Route 側へ再実装しないでください。
818 +
819 + # Diagnostics
820 +
821 + Plugin 固有の問題を報告する場合は `addDiagnostics` を使用します。
822 +
823 + ```ts
824 + addDiagnostics(context) {
825 + return [
826 + {
827 + // Diagnostic contract
828 + },
829 + ];
830 + }
831 + ```
832 +
833 + 診断結果は可能な限り structured data として返します。
834 +
835 + Plugin が直接、
836 +
837 + ```ts
838 + console.log(...)
839 + ```
840 +
841 + で CLI 向けメッセージを出すのではなく、Diagnostics または Logger を利用してください。
842 +
843 + # Plugin Cache
844 +
845 + `context.cache` は Plugin ごとに分離された **Build-time Cache** です。
846 +
847 + Cache に保存する値は、
848 +
849 + - 再生成可能
850 + - JSON serializable
851 + - Plugin namespace 内で完結
852 +
853 + している必要があります。
854 +
855 + `cacheVersion` を使って Cache format の互換性を管理できます。
856 +
857 + 壊れた Cache は安全に Cache Miss として扱える設計にしてください。
858 +
859 + Write は atomic に行います。
860 +
861 + ```mermaid
862 + flowchart TD
863 + Plugin["Plugin"]
864 + Cache["Plugin Cache"]
865 + Valid{"利用可能?"}
866 +
867 + Plugin --> Cache
868 + Cache --> Valid
869 +
870 + Valid -->|Yes| Reuse["再利用"]
871 + Valid -->|No| Generate["再生成"]
872 + ```
873 +
874 + これは Cloudflare Workers などの Runtime Database ではありません。
875 +
876 + # Logger / Tracer
877 +
878 + Plugin から Framework の Observability を利用できます。
879 +
880 + ```ts
881 + context.logger.info("...");
882 +
883 + await context.tracer.span(
884 + "plugin.example.work",
885 + {
886 + plugin: "example",
887 + },
888 + async () => {
889 + // work
890 + },
891 + );
892 + ```
893 +
894 + Logger は「何が起きたか」、Tracer は「どこに時間がかかったか」を記録します。
895 +
896 + Profiler はこの structured trace を利用するため、Plugin が独自の stopwatch や profiling system を作る必要はありません。
897 +
898 + # Site 内だけで使う Plugin
899 +
900 + Plugin は npm package として公開する必要はありません。
901 +
902 + Site 内に Plugin を作ることもできます。
903 +
904 + ```ts
905 + // site/extensions/local-plugin.ts
906 +
907 + return definePlugin({
908 + name: "site-local",
909 +
910 + assets: [
911 + {
912 + pluginName: "site-local",
913 + kind: "style",
914 + moduleSpecifier:
915 + "/extensions/plugin.css",
916 + },
917 + ],
918 + });
919 + ```
920 +
921 + そして `riebeckite.config.ts` の `plugins` へ追加します。
922 +
923 + Site-local Plugin でも、
924 +
925 + - Dependency Resolution
926 + - Pipeline Hooks
927 + - Diagnostics
928 + - Renderer
929 + - Endpoint
930 +
931 + などは package Plugin と同じ contract を利用します。
932 +
933 + `createStyleAsset()` と `createClientEntry()` は
934 + `@riebeckite/plugin-<name>/...` の specifier しか組み立てないため、その名前で
935 + ない package(site 内 Plugin、別名の第三者 package)は Host Bundler が解決
936 + できる `moduleSpecifier` を明示してください。
937 +
938 + # 推奨 Package 構成
939 +
940 + 公開 Plugin は、たとえば次のように構成できます。
941 +
942 + ```text
943 + packages/plugins/example/
944 + ├─ index.ts
945 + ├─ components/ # component を export する場合のみ
946 + ├─ client.ts # 必要な場合のみ
947 + ├─ style.css # 必要な場合のみ
948 + ├─ package.json
949 + └─ src/
950 + ├─ remark.ts
951 + ├─ rehype.ts
952 + ├─ renderer.ts
953 + └─ types.ts
954 + ```
955 +
956 + すべての Plugin がこの構造を必要とするわけではありません。
957 +
958 + Client JavaScript や CSS が不要なら、それらのファイルも不要です。
959 +
960 + # Repository 外で Plugin を配布する
961 +
962 + 外部 Plugin は Riebeckite monorepo の内部 path に依存しないようにします。
963 +
964 + 基本的には、
965 +
966 + ```text
967 + @riebeckite/core
968 + ```
969 +
970 + の公開 API を利用します。
971 +
972 + Plugin 自身が提供する、
973 +
974 + ```text
975 + ./client
976 + ./components
977 + ./style.css
978 + ```
979 +
980 + などは、自身の `package.json` の `exports` で公開します。
981 +
982 + 次のような import は避けてください。
983 +
984 + ```ts
985 + import {
986 + something,
987 + } from "@riebeckite/core/src/...";
988 + ```
989 +
990 + `src/**` は Public API ではありません。
991 +
992 + また、Riebeckite monorepo 内にしか存在しない相対 path へ依存しないでください。
993 +
994 + 公式 Plugin も可能な限り同じ Public API の consumer として実装します。
995 +
996 + ## Package 構成
997 +
998 + 公開 package では、Build 済み ESM と型定義を publish し、`exports` をその
999 + Build 成果物へ向けます。最小構成の `package.json` は次のとおりです。
1000 +
1001 + ```json
1002 + {
1003 + "name": "my-riebeckite-plugin",
1004 + "version": "1.0.0",
1005 + "type": "module",
1006 + "main": "./dist/index.js",
1007 + "types": "./dist/index.d.ts",
1008 + "exports": {
1009 + ".": {
1010 + "types": "./dist/index.d.ts",
1011 + "import": "./dist/index.js",
1012 + "default": "./dist/index.js"
1013 + },
1014 + "./client": {
1015 + "types": "./dist/client.d.ts",
1016 + "import": "./dist/client.js",
1017 + "default": "./dist/client.js"
1018 + },
1019 + "./style.css": "./style.css"
1020 + },
1021 + "files": ["dist", "style.css"],
1022 + "scripts": {
1023 + "build": "node build.mjs && tsc -p tsconfig.json",
1024 + "prepack": "npm run build"
1025 + },
1026 + "dependencies": {
1027 + "@riebeckite/core": "^0.0.19",
1028 + "unist-util-visit": "^5.0.0"
1029 + },
1030 + "devDependencies": {
1031 + "@types/mdast": "^4.0.0",
1032 + "esbuild": "^0.28.0",
1033 + "typescript": "^5.0.0"
1034 + }
1035 + }
1036 + ```
1037 +
1038 + `client.ts` や `style.css` を持たない Plugin では、その subpath と `files` の entry を削除します。
1039 +
1040 + JavaScript の entry point は `esbuild` で bundle し、型定義は `tsc` で emit します。どちらも一般的なツールで、Riebeckite 固有の build script は必要ありません。
1041 +
1042 + `build.mjs`:
1043 +
1044 + ```js
1045 + import { build } from "esbuild";
1046 +
1047 + await build({
1048 + entryPoints: ["index.ts", "client.ts"],
1049 + outdir: "dist",
1050 + bundle: true,
1051 + format: "esm",
1052 + platform: "neutral",
1053 + packages: "external",
1054 + external: ["@riebeckite/*"],
1055 + logLevel: "warning",
1056 + });
1057 + ```
1058 +
1059 + `entryPoints` には、その Package が持つ entry だけを列挙します(client を持たないなら `index.ts` だけ)。
1060 +
1061 + `tsconfig.json`:
1062 +
1063 + ```json
1064 + {
1065 + "compilerOptions": {
1066 + "declaration": true,
1067 + "emitDeclarationOnly": true,
1068 + "outDir": "dist",
1069 + "rootDir": ".",
1070 + "module": "ESNext",
1071 + "moduleResolution": "Bundler",
1072 + "target": "ESNext",
1073 + "lib": ["ESNext", "DOM", "DOM.Iterable"],
1074 + "strict": true,
1075 + "skipLibCheck": true
1076 + },
1077 + "include": ["index.ts", "client.ts"]
1078 + }
1079 + ```
1080 +
1081 + `prepack` で build するようにしておけば、`npm pack` / `npm publish` は常に最新の成果物を同梱します。`@riebeckite/core` は `dependencies` に置くのが最も簡単です(Site 側の instance を共有したい場合は `peerDependencies` でもかまいません)。import する変換依存(`unist-util-visit`、`unified`、remark / rehype package など)は `dependencies` に宣言し、公開 package の `exports` を TypeScript の source(`./index.ts`)へ向けないでください。
1082 +
1083 + 配布する Package のテストは [テスト](../framework/testing.md#plugin-のテスト) を、CSS / Client entry の packaging は [Assets](#assets) と [Client Entries](#client-entries) を参照してください。
1084 +
1085 + # ESM
1086 +
1087 + Riebeckite の package は NodeNext / ESM を前提とします。
1088 +
1089 + Build 後に Node.js が実際に解決できる import を維持してください。
1090 +
1091 + Development 時の TypeScript Loader が、
1092 +
1093 + ```text
1094 + extensionless import
1095 + ```
1096 +
1097 + などを偶然解決できている状態へ依存しないことが重要です。
1098 +
1099 + # Plugin に置くもの・置かないもの
1100 +
1101 + Plugin に置くものは、**Riebeckite Site 間で再利用可能な機能**です。
1102 +
1103 + ```mermaid
1104 + flowchart TD
1105 + Feature{"この機能は何?"}
1106 +
1107 + Feature -->|"Framework共通のContent Model"| Core["Core"]
1108 + Feature -->|"再利用可能なContent機能"| Plugin["Plugin"]
1109 + Feature -->|"HonoX / Vite接続"| Integration["Integration"]
1110 + Feature -->|"Site固有Route / Layout"| App["Application"]
1111 + Feature -->|"見た目だけ"| Theme["Theme"]
1112 + ```
1113 +
1114 + Plugin に向いているものは、
1115 +
1116 + - Markdown / HTML の解釈
1117 + - 再利用可能な Content Transformation
1118 + - Plugin 固有 Renderer
1119 + - 再利用可能な Browser Behavior
1120 + - Plugin 固有 Diagnostics
1121 + - Plugin 固有 Endpoint
1122 + - SEO Extension
1123 + - 独立した Plugin Page
1124 +
1125 + などです。
1126 +
1127 + 一方、
1128 +
1129 + ```text
1130 + Framework-wide Content Model
1131 + → Core
1132 +
1133 + HonoX / Vite 接続
1134 + → Integration
1135 +
1136 + Application 固有 Route / Layout
1137 + → Site Application
1138 +
1139 + 見た目だけの変更
1140 + → Theme
1141 + ```
1142 +
1143 + とします。
1144 +
1145 + # Plugin 設計の基本
1146 +
1147 + Plugin System 全体は次のようになります。
1148 +
1149 + ```mermaid
1150 + flowchart LR
1151 + Plugin["Plugin"]
1152 +
1153 + Plugin --> Pipeline["Content Pipeline"]
1154 + Plugin --> Renderer["Renderer"]
1155 + Plugin --> Page["Page Type"]
1156 + Plugin --> Graph["Content Graph"]
1157 + Plugin --> Diagnostics["Diagnostics"]
1158 + Plugin --> Asset["Assets"]
1159 + Plugin --> Client["Client Entry"]
1160 + Plugin --> Endpoint["Endpoint"]
1161 + Plugin --> SEO["SEO"]
1162 +
1163 + Pipeline --> Core["Core Contracts"]
1164 + Renderer --> Core
1165 + Page --> Core
1166 + Graph --> Core
1167 + Diagnostics --> Core
1168 + Asset --> Core
1169 + Client --> Core
1170 + Endpoint --> Core
1171 + SEO --> Core
1172 +
1173 + Core --> Integration["Integration"]
1174 + Integration --> Site["Site Application"]
1175 + ```
1176 +
1177 + 基本原則は、**必要な最小の Extension Point を使い、すでに Framework が解決した情報を Plugin 側で再構築しないこと**です。
1178 +
1179 + Plugin は再利用可能な機能を提供し、Core はそのための Contract を提供します。Integration は Framework や Platform へ接続し、Site Application が最終的な Route、Document、UI を所有します。
1180 +
1181 + ## 関連
1182 +
1183 + - [Architecture](../framework/architecture.md)
1184 + - [Content System](../framework/content-system.md)
1185 + - [Page System](../framework/page-system.md)
1186 + - [Testing](../framework/testing.md)
1187 + - [Observability](../framework/observability.md)
1188 + - [Theme System](./theme-api.md)
1189 + - [Framework Reference](./README.md)
1190 +