Color mode

Page System

The Page System lets a plugin provide a standalone page without taking over an application route. It is a capability on the normal RiebeckitePlugin contract, not a second kind of plugin.

Responsibilities

Layer Responsibility
Core Page Type contract, validation, path enumeration, resolution, and conflicts
Plugin Page Type ID, public paths, and a framework-independent HTML body
HonoX integration Generic route resolver and SSG parameter helper
Site application Catch-all route, document frame, metadata, and safe HTML rendering
Theme Tokens and stable hooks; no knowledge of a Page Type ID is required

PluginPageContext.manifest is the resolved manifest. Its entries still contains draft and scheduled, so a Page Type must read publicEntries (routable) or discoverableEntries (public only) for page output instead.

Rendering pipeline

Before Page Types, a standalone plugin feature needed a dedicated route:

text
request -> plugin-specific application route -> plugin component -> document frame

With the Page System, plugins participate through one generic route:

text
build: plugin Page Types -> public paths -> SSG parameters
request: catch-all route -> resolveRiebeckiteRoute
                           -> plugin page | content | redirect
plugin page -> site document frame -> theme CSS and plugin client entries

The route and frame remain site-owned. Plugins return a body fragment and optional title, description, headTags, and language; the site decides how those values are represented in the document.

Authoring a Page Type

Use pageTypes when the feature is an independent screen. Use renderers for content embeds such as Canvas, Bases, and Excalidraw: they belong inside an article and do not need a page of their own.

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

IDs are globally unique. A request with multiple matching Page Types selects the greatest priority; equal priorities are an explicit error. Declare paths for every static page and derive dynamic paths from manifest.publicEntries when SSG must emit them.

HonoX application wiring

Every HonoX site that uses plugin pages needs the generic catch-all route. The scaffolded site already includes it. The root / is resolved by resolveRiebeckiteHomeRequest(c, content), which shares the same mechanics:

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-specific article composition */);
  },
);

Keep the frame's HTML policy at the application boundary. A plugin must only return HTML it is responsible for generating; an application must not treat untrusted request input as a page body.

For the complete fields and runtime validation rules, see the Plugin API.

History

1 changesCollapseExpand
1 + # Page System
2 +
3 + The Page System lets a plugin provide a standalone page without taking over an
4 + application route. It is a capability on the normal `RiebeckitePlugin`
5 + contract, not a second kind of plugin.
6 +
7 + ## Responsibilities
8 +
9 + | Layer | Responsibility |
10 + | --- | --- |
11 + | Core | Page Type contract, validation, path enumeration, resolution, and conflicts |
12 + | Plugin | Page Type ID, public paths, and a framework-independent HTML body |
13 + | HonoX integration | Generic route resolver and SSG parameter helper |
14 + | Site application | Catch-all route, document frame, metadata, and safe HTML rendering |
15 + | Theme | Tokens and stable hooks; no knowledge of a Page Type ID is required |
16 +
17 + `PluginPageContext.manifest` is the resolved manifest. Its `entries` still
18 + contains `draft` and `scheduled`, so a Page Type must read `publicEntries`
19 + (routable) or `discoverableEntries` (public only) for page output instead.
20 +
21 + ## Rendering pipeline
22 +
23 + Before Page Types, a standalone plugin feature needed a dedicated route:
24 +
25 + ```text
26 + request -> plugin-specific application route -> plugin component -> document frame
27 + ```
28 +
29 + With the Page System, plugins participate through one generic route:
30 +
31 + ```text
32 + build: plugin Page Types -> public paths -> SSG parameters
33 + request: catch-all route -> resolveRiebeckiteRoute
34 + -> plugin page | content | redirect
35 + plugin page -> site document frame -> theme CSS and plugin client entries
36 + ```
37 +
38 + The route and frame remain site-owned. Plugins return a body fragment and
39 + optional `title`, `description`, `headTags`, and `language`; the site decides how
40 + those values are represented in the document.
41 +
42 + ## Authoring a Page Type
43 +
44 + Use `pageTypes` when the feature is an independent screen. Use `renderers` for
45 + content embeds such as Canvas, Bases, and Excalidraw: they belong inside an
46 + article and do not need a page of their own.
47 +
48 + ```ts
49 + import { definePlugin } from "@riebeckite/core";
50 +
51 + export function reportPlugin() {
52 + return definePlugin({
53 + name: "report",
54 + pageTypes: [{
55 + id: "example.report",
56 + paths: ["/report"],
57 + resolve: ({ pathname, manifest }) => pathname === "/report"
58 + ? {
59 + type: "example.report",
60 + pathname,
61 + title: "Report",
62 + body: `<p>${manifest.publicEntries.length} published entries</p>`,
63 + }
64 + : null,
65 + }],
66 + });
67 + }
68 + ```
69 +
70 + IDs are globally unique. A request with multiple matching Page Types selects
71 + the greatest `priority`; equal priorities are an explicit error. Declare
72 + `paths` for every static page and derive dynamic paths from
73 + `manifest.publicEntries` when SSG must emit them.
74 +
75 + ## HonoX application wiring
76 +
77 + Every HonoX site that uses plugin pages needs the generic catch-all route. The
78 + scaffolded site already includes it. The root `/` is resolved by
79 + `resolveRiebeckiteHomeRequest(c, content)`, which shares the same mechanics:
80 +
81 + ```tsx
82 + import {
83 + contentRouteSsgParams,
84 + resolveRiebeckiteContentRequest,
85 + riebeckiteSsgParams,
86 + } from "@riebeckite/honox/server";
87 + import { PageBody } from "@riebeckite/honox/ui";
88 + import { createRoute } from "honox/factory";
89 +
90 + export default createRoute(
91 + contentRouteSsgParams("/:slug{.+}", () => riebeckiteSsgParams(content)),
92 + async (c) => {
93 + const resolved = await resolveRiebeckiteContentRequest(c, content);
94 +
95 + if (resolved.kind === "response") return resolved.response;
96 +
97 + if (resolved.kind === "page") {
98 + return c.render(<PageBody html={resolved.page.body} />);
99 + }
100 +
101 + return c.render(/* site-specific article composition */);
102 + },
103 + );
104 + ```
105 +
106 + Keep the frame's HTML policy at the application boundary. A plugin must only
107 + return HTML it is responsible for generating; an application must not treat
108 + untrusted request input as a page body.
109 +
110 + For the complete fields and runtime validation rules, see the
111 + [Plugin API](../reference/plugin-api.en.md#pages).
112 +