Color mode

This page is part of Plugins in Depth and covers content processing and the extension points.

Content processing and extension points

3-6. Markdown / HTML pipeline

Simple remark/rehype plugins are declared as arrays.

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

To compose the pipeline itself:

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

Semantic Markdown/HTML transformation belongs in the plugin; do not bring AST processing into application components.

3-7. Content graph and public locations

extendContentGraph extends the graph before or after construction within the framework contract. For backlinks or graph features, do not rescan the filesystem yourself — use the existing Content Graph / Manifest contract.

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

resolveContentLocations replaces the public location of content entries. Core first applies the default resolver (resolveDefaultContentLocation: index → /, otherwise /{slug}), then runs each plugin hook in resolved plugin order, and keeps the result as ContentPublicLocation in the manifest / graph / pipeline. URL strategy is the plugin's responsibility. An unresolved location is an explicit error, not a slug fallback.

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

Consumers read the resolved entry.permalink.

3-8. Renderers

renderers transform a content target into plugin-specific HTML. Examples include Canvas, Bases, Excalidraw, Attachment, and Media. The context includes kind, path, raw, label, url, embed, plus the usual PluginContext. Return null when the target is not yours so other renderers are tried.

Diagram source
text
flowchart TD
    Target["Content Target"]
    Renderer{"Does this renderer handle it?"}
 
    Target --> Renderer
    Renderer -->|Yes| HTML["HTML"]
    Renderer -->|No| Next["Next renderer"]

3-9. Page Types

pageTypes provides an independent screen through the site's generic catch-all route. Examples include /explore, /report, and /tags/example. A type has a globally unique id, static or manifest-derived paths, an optional priority, and resolve. It returns a framework-independent HTML body or null. It may also describe title, description, and headTags; the site document frame renders those values.

Use a Page Type for a page such as a taxonomy listing or explorer. Use a renderer for an article embed such as Canvas, Bases, or Excalidraw. Plugins do not add HonoX route files or own the document frame.

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 IDs are validated at runtime and must be unique. If more than one type resolves a request, the greatest priority wins; a tie is an error. Use the resolved manifest passed to the resolver, reading manifest.publicEntries or manifest.discoverableEntries rather than manifest.entries. The application wires resolveRiebeckiteRoute and pluginPageSsgParams into its catch-all route; the scaffold does this already. See Page System for the full rendering flow and Plugin API for the contract.

3-10. Assets

Put plugin stylesheets inside the plugin package and declare the module specifier in assets.

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

Do not copy plugin CSS into apps/web, and never reference /node_modules directly from the browser. The integration resolves the module specifier and delivers it to the browser:

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

In-site plugins: a plugin does not have to be published. Define it inside the site with definePlugin and pass it to plugins. Resolution order, dependency resolution, pipeline hooks, and diagnostics use the same contract as a package.

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

createStyleAsset() and createClientEntry() build @riebeckite/plugin-<name>/... specifiers, so they fit only a package literally named that way. An unpublished plugin, or a published package under any other name, must provide an explicit moduleSpecifier the host bundler can resolve.

3-11. CSS hooks

Plugin CSS lives in the plugin package and reaches the browser through assets. When a plugin renders an independent, reusable feature, put a stable root hook on the outermost element.

  • Name plugin/feature hooks rr-<feature> (rr-search, rr-callout, rr-query, rr-code, …). Below the root, use the BEM shape rr-<feature>, rr-<feature>__element, rr-<feature>--modifier.
  • Keep existing classes on the same element. rr- hooks are additions, so existing selectors and site overrides keep working.
  • Do not put plugin output in the rb- namespace. rb-* classes and --rb-* tokens belong to the framework's structural hooks and semantic design tokens. Plugin-specific tokens are --rr-*, with --rb-* as fallback.
  • rr-<feature>__* and rr-<feature>--* are internal implementation details. Document only the descendants you want themes to style.
Namespace Purpose
rb-* Framework structural hooks
--rb-* Framework semantic tokens
rr-* Plugin / feature hooks
--rr-* Plugin-specific tokens

3-12. Client entries

Use clientEntries only when browser initialization is required.

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

Do not add client JavaScript to plugins that work purely at SSR/build time. publicConfig is passed to the client initializer and recorded in the manifest so static hosts can use it. Plugin options are not passed to the client automatically. Never register tokens, credentials, or private service URLs as public config.

3-13. Endpoints and SEO

HTTP endpoints use the endpoints contract. Do not embed route-framework implementations in the plugin itself; the integration connects the endpoint contract to the host router.

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

seo lets a plugin participate in SEO processing (metadata/feeds). Do not reimplement plugin-specific SEO logic in application routes.

3-14. Diagnostics

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

Return diagnostics as structured data whenever possible. Use Diagnostics / Logger instead of console.log from the plugin.

3-15. Plugin cache

context.cache is a build-time cache isolated per plugin.

  • Store only regenerable values
  • JSON-serializable values
  • Do not reference across plugin namespaces
  • Switch compatibility with cacheVersion
  • Treat corrupt caches as safe misses
  • Writes are atomic
  • Do not use it as a runtime database
Diagram source
text
flowchart TD
    Work["Plugin work"]
    Cache{"Valid cache?"}
 
    Work --> Cache
    Cache -->|Yes| Reuse["Reuse"]
    Cache -->|No| Regenerate["Regenerate"]

It is not Cloudflare Workers persistent storage.

3-16. Logger / tracer

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

The profiler uses structured traces; plugins do not need their own stopwatch/reporting system.

History

1 changesCollapseExpand
1 + ---
2 + title: Content processing and extension points
3 + sidebar:
4 + label: Content processing and extension points
5 + order: 20
6 + ---
7 +
8 + This page is part of [Plugins in Depth](../plugin-system.md) and covers content processing and the extension points.
9 +
10 + # Content processing and extension points
11 +
12 + ## 3-6. Markdown / HTML pipeline
13 +
14 + Simple remark/rehype plugins are declared as arrays.
15 +
16 + ```ts
17 + definePlugin({
18 + name: "example",
19 + remarkPlugins: [remarkExample],
20 + rehypePlugins: [rehypeExample],
21 + });
22 + ```
23 +
24 + To compose the pipeline itself:
25 +
26 + ```ts
27 + definePlugin({
28 + name: "example",
29 + extendMarkdownPipeline(pipeline, context) {
30 + pipeline.use(remarkExample);
31 + },
32 + extendHtmlPipeline(pipeline) {
33 + pipeline.use(rehypeExample);
34 + },
35 + });
36 + ```
37 +
38 + Semantic Markdown/HTML transformation belongs in the plugin; do not bring AST processing into application components.
39 +
40 + ## 3-7. Content graph and public locations
41 +
42 + `extendContentGraph` extends the graph before or after construction within the framework contract. For backlinks or graph features, do not rescan the filesystem yourself — use the existing Content Graph / Manifest contract.
43 +
44 + ```mermaid
45 + flowchart LR
46 + Manifest["Manifest"]
47 + Graph["Content Graph"]
48 + Plugin["extendContentGraph"]
49 + Result["Extended Graph"]
50 +
51 + Manifest --> Graph
52 + Graph --> Plugin
53 + Plugin --> Result
54 + ```
55 +
56 + `resolveContentLocations` replaces the public location of content entries. Core first applies the default resolver (`resolveDefaultContentLocation`: `index` → `/`, otherwise `/{slug}`), then runs each plugin hook in resolved plugin order, and keeps the result as `ContentPublicLocation` in the manifest / graph / pipeline. URL strategy is the plugin's responsibility. An unresolved location is an **explicit error**, not a slug fallback.
57 +
58 + ```mermaid
59 + flowchart LR
60 + Content["Content"]
61 + Default["Default Resolver"]
62 + PluginA["Plugin A"]
63 + PluginB["Plugin B"]
64 + Location["ContentPublicLocation"]
65 +
66 + Content --> Default
67 + Default --> PluginA
68 + PluginA --> PluginB
69 + PluginB --> Location
70 + ```
71 +
72 + Consumers read the resolved `entry.permalink`.
73 +
74 + ## 3-8. Renderers
75 +
76 + `renderers` transform a content target into plugin-specific HTML. Examples include Canvas, Bases, Excalidraw, Attachment, and Media. The context includes `kind`, `path`, `raw`, `label`, `url`, `embed`, plus the usual `PluginContext`. Return `null` when the target is not yours so other renderers are tried.
77 +
78 + ```mermaid
79 + flowchart TD
80 + Target["Content Target"]
81 + Renderer{"Does this renderer handle it?"}
82 +
83 + Target --> Renderer
84 + Renderer -->|Yes| HTML["HTML"]
85 + Renderer -->|No| Next["Next renderer"]
86 + ```
87 +
88 + ## 3-9. Page Types
89 +
90 + `pageTypes` provides an independent screen through the site's generic catch-all route. Examples include `/explore`, `/report`, and `/tags/example`. A type has a globally unique `id`, static or manifest-derived `paths`, an optional `priority`, and `resolve`. It returns a framework-independent HTML body or `null`. It may also describe `title`, `description`, and `headTags`; the site document frame renders those values.
91 +
92 + Use a Page Type for a page such as a taxonomy listing or explorer. Use a renderer for an article embed such as Canvas, Bases, or Excalidraw. Plugins do not add HonoX route files or own the document frame.
93 +
94 + ```mermaid
95 + flowchart LR
96 + Plugin["Plugin Page Type"]
97 + Core["Core Resolver"]
98 + Integration["HonoX Integration"]
99 + Site["Site Document Frame"]
100 +
101 + Plugin --> Core
102 + Core --> Integration
103 + Integration --> Site
104 + ```
105 +
106 + Page Type IDs are validated at runtime and must be unique. If more than one type resolves a request, the greatest priority wins; a tie is an error. Use the resolved manifest passed to the resolver, reading `manifest.publicEntries` or `manifest.discoverableEntries` rather than `manifest.entries`. The application wires `resolveRiebeckiteRoute` and `pluginPageSsgParams` into its catch-all route; the scaffold does this already. See [Page System](../page-system.md) for the full rendering flow and [Plugin API](../../reference/plugin-api.md#pages) for the contract.
107 +
108 + ## 3-10. Assets
109 +
110 + Put plugin stylesheets inside the plugin package and declare the module specifier in `assets`.
111 +
112 + ```ts
113 + assets: [{
114 + pluginName: "example",
115 + kind: "style",
116 + moduleSpecifier: "@riebeckite/plugin-example/style.css",
117 + }]
118 + ```
119 +
120 + Do not copy plugin CSS into `apps/web`, and never reference `/node_modules` directly from the browser. The integration resolves the module specifier and delivers it to the browser:
121 +
122 + ```mermaid
123 + flowchart LR
124 + CSS["Plugin style.css"]
125 + Asset["assets"]
126 + Integration["Integration"]
127 + Browser["Browser"]
128 +
129 + CSS --> Asset
130 + Asset --> Integration
131 + Integration --> Browser
132 + ```
133 +
134 + **In-site plugins**: a plugin does not have to be published. Define it inside the site with `definePlugin` and pass it to `plugins`. Resolution order, dependency resolution, pipeline hooks, and diagnostics use the same contract as a package.
135 +
136 + ```ts
137 + // site/extensions/local-plugin.ts
138 + return definePlugin({
139 + name: "site-local",
140 + assets: [{
141 + pluginName: "site-local",
142 + kind: "style",
143 + moduleSpecifier: "/extensions/plugin.css",
144 + }],
145 + });
146 + ```
147 +
148 + `createStyleAsset()` and `createClientEntry()` build `@riebeckite/plugin-<name>/...` specifiers, so they fit only a package literally named that way. An unpublished plugin, or a published package under any other name, must provide an explicit `moduleSpecifier` the host bundler can resolve.
149 +
150 + ## 3-11. CSS hooks
151 +
152 + Plugin CSS lives in the plugin package and reaches the browser through `assets`. When a plugin renders an independent, reusable feature, put a stable root hook on the outermost element.
153 +
154 + - Name plugin/feature hooks `rr-<feature>` (`rr-search`, `rr-callout`, `rr-query`, `rr-code`, …). Below the root, use the BEM shape `rr-<feature>`, `rr-<feature>__element`, `rr-<feature>--modifier`.
155 + - Keep existing classes on the same element. `rr-` hooks are additions, so existing selectors and site overrides keep working.
156 + - Do not put plugin output in the `rb-` namespace. `rb-*` classes and `--rb-*` tokens belong to the framework's structural hooks and semantic design tokens. Plugin-specific tokens are `--rr-*`, with `--rb-*` as fallback.
157 + - `rr-<feature>__*` and `rr-<feature>--*` are internal implementation details. Document only the descendants you want themes to style.
158 +
159 + | Namespace | Purpose |
160 + | --- | --- |
161 + | `rb-*` | Framework structural hooks |
162 + | `--rb-*` | Framework semantic tokens |
163 + | `rr-*` | Plugin / feature hooks |
164 + | `--rr-*` | Plugin-specific tokens |
165 +
166 + ## 3-12. Client entries
167 +
168 + Use `clientEntries` only when browser initialization is required.
169 +
170 + ```ts
171 + clientEntries: [{
172 + pluginName: "example",
173 + moduleSpecifier: "@riebeckite/plugin-example/client",
174 + exportName: "initExample",
175 + publicConfig: { selector: ".example" },
176 + }]
177 + ```
178 +
179 + ```mermaid
180 + flowchart LR
181 + Plugin["Plugin"]
182 + Client["Client Entry"]
183 + Integration["Integration"]
184 + Browser["Browser"]
185 +
186 + Plugin --> Client
187 + Client --> Integration
188 + Integration --> Browser
189 + ```
190 +
191 + Do not add client JavaScript to plugins that work purely at SSR/build time. `publicConfig` is passed to the client initializer and recorded in the manifest so static hosts can use it. Plugin `options` are not passed to the client automatically. Never register tokens, credentials, or private service URLs as public config.
192 +
193 + ## 3-13. Endpoints and SEO
194 +
195 + HTTP endpoints use the `endpoints` contract. Do not embed route-framework implementations in the plugin itself; the integration connects the endpoint contract to the host router.
196 +
197 + ```mermaid
198 + flowchart LR
199 + Plugin["Plugin"]
200 + Endpoint["Endpoint Contract"]
201 + Integration["Integration"]
202 + Router["Host Router"]
203 +
204 + Plugin --> Endpoint
205 + Endpoint --> Integration
206 + Integration --> Router
207 + ```
208 +
209 + `seo` lets a plugin participate in SEO processing (metadata/feeds). Do not reimplement plugin-specific SEO logic in application routes.
210 +
211 + ## 3-14. Diagnostics
212 +
213 + ```ts
214 + addDiagnostics(context) {
215 + return [{
216 + // follows the Diagnostic contract
217 + }];
218 + }
219 + ```
220 +
221 + Return diagnostics as structured data whenever possible. Use Diagnostics / Logger instead of `console.log` from the plugin.
222 +
223 + ## 3-15. Plugin cache
224 +
225 + `context.cache` is a **build-time cache** isolated per plugin.
226 +
227 + - Store only regenerable values
228 + - JSON-serializable values
229 + - Do not reference across plugin namespaces
230 + - Switch compatibility with `cacheVersion`
231 + - Treat corrupt caches as safe misses
232 + - Writes are atomic
233 + - Do not use it as a runtime database
234 +
235 + ```mermaid
236 + flowchart TD
237 + Work["Plugin work"]
238 + Cache{"Valid cache?"}
239 +
240 + Work --> Cache
241 + Cache -->|Yes| Reuse["Reuse"]
242 + Cache -->|No| Regenerate["Regenerate"]
243 + ```
244 +
245 + It is not Cloudflare Workers persistent storage.
246 +
247 + ## 3-16. Logger / tracer
248 +
249 + ```ts
250 + context.logger.info("...");
251 + await context.tracer.span("plugin.example.work", { plugin: "example" }, async () => {
252 + // work
253 + });
254 + ```
255 +
256 + The profiler uses structured traces; plugins do not need their own stopwatch/reporting system.
257 +