Color mode

Site Application の責務

Riebeckite Site は、最終的には通常の HonoX application です。

@riebeckite/honox は content と build を接続しますが、実際にユーザーが見る UI の設計は Site が管理します。通常の HonoX で編集する手順は サイトのカスタマイズ を参照してください。

Diagram source
text
flowchart TD
    A["Riebeckite Core<br/>content / manifest / plugin"]
    B["@riebeckite/honox<br/>build / routing integration"]
    C["Site Application"]
 
    C --> D["app/routes/<br/>URL / page composition"]
    C --> E["app/components/<br/>Site UI"]
    C --> F["app/islands/<br/>Interactive UI"]
    C --> G["app/style.css<br/>Visual Design"]
 
    A --> B
    B --> C

主なディレクトリの責務は次のとおりです。

ディレクトリ Site が持つ責務
app/routes/ URL処理、ページ構成、redirect、response metadata
app/components/ Site 固有の UI
app/islands/ 対話 UI と client-side state
app/style.css 色、layout、typography、extension style

Site Shell

text
app/routes/_renderer.tsx

は Site 全体の shell です。

ここでは主に、

  • document head
  • navigation
  • page chrome
  • application client entry

などを管理します。

Route は ContentManager からコンテンツを取得し、

ts
resolveRiebeckiteRoute(content, c.req.path)

で request URL を解決します。

その結果をどの component tree で表示するかは Site が決定します。

Riebeckite repository にある apps/web は実装例の1つであり、外部 Site が同じ layout を使う必要はありません。

Plugin と Site の境界

Plugin は Site に情報や UI fragment を提供できます。

ただし、最終的にどこへ描画するかは Site が決定します。

Diagram source
text
flowchart LR
    A["Plugin"]
    B["Manifest"]
    C["Site Route"]
    D["Site Shell / Component"]
 
    A -->|"headTags / bodySlots / page"| B
    B --> C
    C -->|"placement"| D

Head Tags

Plugin が document head に情報を追加したい場合は、

ts
ContentManifestEntry.headTags

へ meta / link / script を記述します。

Plugin 自身が <head> を描画するわけではありません。

resolveRiebeckiteContentRequest / resolveRiebeckiteHomeRequest が、解決した entry の headTags を route context へ設定します。_renderer.tsx がそれを読み取って描画します。

tsx
import { PluginHeadTags } from "@riebeckite/honox/ui";
 
const headTags = c.get("headTags") ?? [];
 
<head>
  <PluginHeadTags tags={headTags} />
</head>;

つまり、

text
Plugin
  ↓ headTags を提供
Framework resolver
  ↓ context へ設定
_renderer.tsx
  ↓
<head> に描画

という関係です。

たとえば @riebeckite/plugin-discord-embed は、この仕組みを使って theme-color を提供します。

Plugin は <head> 自体や tag の並び順を所有しません。

RiebeckiteHead と PluginHeadTags

@riebeckite/honox/ui より公開される 2 つの primitive は、head composition の責務分離を明確にします。

RiebeckiteHead

tsx
import { RiebeckiteHead } from "@riebeckite/honox/ui";
 
<RiebeckiteHead title="My Site" headTags={[]} />

Framework が次の標準的な head contents を描画します。

  • <meta charset="utf-8">
  • <meta name="viewport" content="width=device-width, initial-scale=1.0">
  • <title>(title prop が提供する値)
  • <link rel="icon" href="/favicon.ico">(faviconHref プロップで上書き可能、null で省略可)
  • <ColorModeScript />(colorModeScript プロップで制御、default true)
  • stylesheet entries(stylesheets プロップ、default ["/app/style.css"])
  • client script entry(clientSrc プロップ、default "/app/client.ts"、null で省略可)
  • PluginHeadTag values の変換(headTags プロップ)
  • 子要素(children prop)は標準の後に追加

RiebeckiteHead は <head> 要素自身を描画しません。Site は <head> の ownership を保持し、その中に RiebeckiteHead を配置できます。

PluginHeadTags

tsx
import { PluginHeadTags } from "@riebeckite/honox/ui";
 
<PluginHeadTags tags={headTagsFromManifest} />

PluginHeadTag values (meta / link / script) を JSX 要素に変換します。RiebeckiteHead を使わず、Site が自分で head を組み立てる際に使用します。

使用例

Site が <head> 所有権を維持しつつ標準 head をFrameworkに任せる場合:

tsx
import { RiebeckiteHead, ThemeRoot } from "@riebeckite/honox/ui";
 
export default jsxRenderer(({ children }, c) => (
  <ThemeRoot
    theme={config.theme}
    lang={c.get("htmlLanguage") ?? config.site.locale}
  >
    <head>
      <RiebeckiteHead
        title={config.site.title}
        headTags={c.get("headTags") ?? []}
      />
      <meta name="custom-site-value" content="..." />
    </head>
    <body class="riebeckite-page rb-site">{children}</body>
  </ThemeRoot>
);

Frameworkは標準 head rendering メカニクス(charset、viewport、default title、favicon wiring、color-mode bootstrap、stylesheet/client entry wiring、PluginHeadTag 変換)と theme-root attribute 導出を担当し、Site は <head>/<body> 構成とカスタム meta/link/script の所有権を保持します。favicon FILE (/public/favicon.ico) は Site-owned のまま、default link wiring にのみ Framework が所有権を持ちます。

Plugin が head tags を提供する場合は、既存の headTags メカニズムはそのまま機能します。RiebeckiteHead と headTags は併用可能です。

Body Slots

本文の途中へ Plugin の HTML を表示したい場合は、

ts
ContentManifestEntry.bodySlots

を使用します。

Plugin は slot 名と HTML fragment を提供します。

たとえば、

text
properties

という slot があれば、Route は slot object を article component へ渡し、Site は、

tsx
<Article
  content={post}
  bodySlots={route.entry.bodySlots}
/>

のように article component 内の任意の位置へ配置できます。

ここでの Article は Site 自身の article component であり、同名の @riebeckite/honox/ui primitive ではありません。scaffold の starter は標準 slot を決まった位置へ描画します(article.aside、article.header、article.metadata、article.before-content、article.after-content、article.footer)。plugin 作者はこれらから選ぶか、Site に独自名の描画を依頼します。独自 slot は Site が描画を選ぶまで何も表示しません。

Site はどの slot をどこへ置くかを選び、描画の仕組みは公開 ContentSlot primitive に任せます。

tsx
<ArticleContent>
  <ContentSlot slots={bodySlots} name="article.header" />
  <ContentSlot
    slots={bodySlots}
    name="article.metadata"
    class="site-article__metadata"
  />
  <ArticleBody html={post.html ?? ""} />
</ArticleContent>

ContentSlot は slot lookup、存在しない slot や空 slot の扱い、HTML fragment の描画、data-slot の付与を担当します。Site が dangerouslySetInnerHTML を直接書く必要はありません。順序、可視性、Site 固有 class、独自 slot 名は引き続き Site が所有します。slots を直接読んだり、任意の wrapper で包んだり、同じ slot を複数回描画する escape hatch も残っています。

Plugin が route や shell の構造を書き換える必要はありません。

@riebeckite/plugin-properties では、

ts
render: "slot"

を指定すると properties slot を提供します。

render: "html" は従来どおり、生成 HTML の先頭または末尾へ直接挿入します。

記事末尾の Plugin section は article.footer に集約します。article component ではこの slot を一度だけ描画し、fragment の順序は解決済み Plugin の order で決めます。空の contribution は DOM node を生成しません。

独自 Site を作る

外部 Site でも、公開 primitive を使いながら自由に component を構成できます。

tsx
import type { ContentBodySlots, PostContent } from "@riebeckite/core";
import {
  Article,
  ArticleBody,
  ArticleContent,
  ArticleLayout,
  ContentSlot,
} from "@riebeckite/honox/ui";
 
export function SiteArticle({
  post,
  bodySlots,
}: {
  post: PostContent;
  bodySlots?: ContentBodySlots;
}) {
  return (
    <Article class="site-article">
      <ArticleLayout>
        <ArticleContent>
          <ContentSlot slots={bodySlots} name="article.header" />
          <ArticleBody html={post.html ?? ""} />
          <ContentSlot slots={bodySlots} name="article.footer" />
        </ArticleContent>
      </ArticleLayout>
    </Article>
  );
}

見た目は Site の CSS で定義します。

css
@import "./.riebeckite/framework-styles.css";
@import "./.riebeckite/plugin-styles.css";
@import "./.riebeckite/theme-styles.css";
 
.site-article {
  max-width: 48rem;
  margin: 0 auto;
}

.riebeckite 内の生成 CSS 自体を直接編集しないでください。

Islands

Island も通常の Site module として管理します。

text
app/islands/

へ HonoX island を配置し、それを利用する route または component から import します。

hydration や client-side state は Site 内で管理します。

app/client.ts では、

ts
createClient();
initRiebeckiteClient();

の両方を初期化します。

initRiebeckiteClient() は、インストールされている Plugin や Theme が提供する browser entry を起動するために使用されます。

Plugin は client entry を提供できますが、

  • Site route
  • shell
  • component
  • island
  • CSS design

そのものを所有してはいけません。

History

1 changesCollapseExpand
1 + ---
2 + title: Site Application の責務
3 + sidebar:
4 + label: Site Application の責務
5 + order: 20
6 + ---
7 + # Site Application の責務
8 +
9 + Riebeckite Site は、最終的には通常の HonoX application です。
10 +
11 + `@riebeckite/honox` は content と build を接続しますが、実際にユーザーが見る UI の設計は Site が管理します。通常の HonoX で編集する手順は [サイトのカスタマイズ](../../guides/customizing-your-site.ja.md) を参照してください。
12 +
13 + ```mermaid
14 + flowchart TD
15 + A["Riebeckite Core<br/>content / manifest / plugin"]
16 + B["@riebeckite/honox<br/>build / routing integration"]
17 + C["Site Application"]
18 +
19 + C --> D["app/routes/<br/>URL / page composition"]
20 + C --> E["app/components/<br/>Site UI"]
21 + C --> F["app/islands/<br/>Interactive UI"]
22 + C --> G["app/style.css<br/>Visual Design"]
23 +
24 + A --> B
25 + B --> C
26 + ```
27 +
28 + 主なディレクトリの責務は次のとおりです。
29 +
30 + | ディレクトリ | Site が持つ責務 |
31 + | --- | --- |
32 + | `app/routes/` | URL処理、ページ構成、redirect、response metadata |
33 + | `app/components/` | Site 固有の UI |
34 + | `app/islands/` | 対話 UI と client-side state |
35 + | `app/style.css` | 色、layout、typography、extension style |
36 +
37 + ## Site Shell
38 +
39 + ```text
40 + app/routes/_renderer.tsx
41 + ```
42 +
43 + は Site 全体の shell です。
44 +
45 + ここでは主に、
46 +
47 + - document head
48 + - navigation
49 + - page chrome
50 + - application client entry
51 +
52 + などを管理します。
53 +
54 + Route は `ContentManager` からコンテンツを取得し、
55 +
56 + ```ts
57 + resolveRiebeckiteRoute(content, c.req.path)
58 + ```
59 +
60 + で request URL を解決します。
61 +
62 + その結果をどの component tree で表示するかは Site が決定します。
63 +
64 + Riebeckite repository にある `apps/web` は実装例の1つであり、外部 Site が同じ layout を使う必要はありません。
65 +
66 + ## Plugin と Site の境界
67 +
68 + Plugin は Site に情報や UI fragment を提供できます。
69 +
70 + ただし、**最終的にどこへ描画するかは Site が決定します。**
71 +
72 + ```mermaid
73 + flowchart LR
74 + A["Plugin"]
75 + B["Manifest"]
76 + C["Site Route"]
77 + D["Site Shell / Component"]
78 +
79 + A -->|"headTags / bodySlots / page"| B
80 + B --> C
81 + C -->|"placement"| D
82 + ```
83 +
84 + ### Head Tags
85 +
86 + Plugin が document head に情報を追加したい場合は、
87 +
88 + ```ts
89 + ContentManifestEntry.headTags
90 + ```
91 +
92 + へ `meta` / `link` / `script` を記述します。
93 +
94 + Plugin 自身が `<head>` を描画するわけではありません。
95 +
96 + `resolveRiebeckiteContentRequest` / `resolveRiebeckiteHomeRequest` が、解決した entry の `headTags` を route context へ設定します。`_renderer.tsx` がそれを読み取って描画します。
97 +
98 + ```tsx
99 + import { PluginHeadTags } from "@riebeckite/honox/ui";
100 +
101 + const headTags = c.get("headTags") ?? [];
102 +
103 + <head>
104 + <PluginHeadTags tags={headTags} />
105 + </head>;
106 + ```
107 +
108 + つまり、
109 +
110 + ```text
111 + Plugin
112 + ↓ headTags を提供
113 + Framework resolver
114 + ↓ context へ設定
115 + _renderer.tsx
116 + ↓
117 + <head> に描画
118 + ```
119 +
120 + という関係です。
121 +
122 + たとえば `@riebeckite/plugin-discord-embed` は、この仕組みを使って `theme-color` を提供します。
123 +
124 + Plugin は `<head>` 自体や tag の並び順を所有しません。
125 +
126 + ### RiebeckiteHead と PluginHeadTags
127 +
128 + `@riebeckite/honox/ui` より公開される 2 つの primitive は、head composition の責務分離を明確にします。
129 +
130 + #### `RiebeckiteHead`
131 +
132 + ```tsx
133 + import { RiebeckiteHead } from "@riebeckite/honox/ui";
134 +
135 + <RiebeckiteHead title="My Site" headTags={[]} />
136 + ```
137 +
138 + Framework が次の標準的な head contents を描画します。
139 +
140 + - `<meta charset="utf-8">`
141 + - `<meta name="viewport" content="width=device-width, initial-scale=1.0">`
142 + - `<title>`(title prop が提供する値)
143 + - `<link rel="icon" href="/favicon.ico">`(faviconHref プロップで上書き可能、null で省略可)
144 + - `<ColorModeScript />`(colorModeScript プロップで制御、default true)
145 + - stylesheet entries(stylesheets プロップ、default `["/app/style.css"]`)
146 + - client script entry(clientSrc プロップ、default `"/app/client.ts"`、null で省略可)
147 + - `PluginHeadTag` values の変換(headTags プロップ)
148 + - 子要素(children prop)は標準の後に追加
149 +
150 + `RiebeckiteHead` は `<head>` 要素自身を描画しません。Site は `<head>` の ownership を保持し、その中に `RiebeckiteHead` を配置できます。
151 +
152 + #### `PluginHeadTags`
153 +
154 + ```tsx
155 + import { PluginHeadTags } from "@riebeckite/honox/ui";
156 +
157 + <PluginHeadTags tags={headTagsFromManifest} />
158 + ```
159 +
160 + `PluginHeadTag` values (meta / link / script) を JSX 要素に変換します。`RiebeckiteHead` を使わず、Site が自分で head を組み立てる際に使用します。
161 +
162 + #### 使用例
163 +
164 + Site が `<head>` 所有権を維持しつつ標準 head をFrameworkに任せる場合:
165 +
166 + ```tsx
167 + import { RiebeckiteHead, ThemeRoot } from "@riebeckite/honox/ui";
168 +
169 + export default jsxRenderer(({ children }, c) => (
170 + <ThemeRoot
171 + theme={config.theme}
172 + lang={c.get("htmlLanguage") ?? config.site.locale}
173 + >
174 + <head>
175 + <RiebeckiteHead
176 + title={config.site.title}
177 + headTags={c.get("headTags") ?? []}
178 + />
179 + <meta name="custom-site-value" content="..." />
180 + </head>
181 + <body class="riebeckite-page rb-site">{children}</body>
182 + </ThemeRoot>
183 + );
184 + ```
185 +
186 + Frameworkは標準 head rendering メカニクス(charset、viewport、default title、favicon wiring、color-mode bootstrap、stylesheet/client entry wiring、PluginHeadTag 変換)と theme-root attribute 導出を担当し、Site は `<head>`/`<body>` 構成とカスタム meta/link/script の所有権を保持します。favicon FILE (`/public/favicon.ico`) は Site-owned のまま、default link wiring にのみ Framework が所有権を持ちます。
187 +
188 + Plugin が head tags を提供する場合は、既存の `headTags` メカニズムはそのまま機能します。`RiebeckiteHead` と `headTags` は併用可能です。
189 +
190 + ### Body Slots
191 +
192 + 本文の途中へ Plugin の HTML を表示したい場合は、
193 +
194 + ```ts
195 + ContentManifestEntry.bodySlots
196 + ```
197 +
198 + を使用します。
199 +
200 + Plugin は slot 名と HTML fragment を提供します。
201 +
202 + たとえば、
203 +
204 + ```text
205 + properties
206 + ```
207 +
208 + という slot があれば、Route は slot object を article component へ渡し、Site は、
209 +
210 + ```tsx
211 + <Article
212 + content={post}
213 + bodySlots={route.entry.bodySlots}
214 + />
215 + ```
216 +
217 + のように article component 内の任意の位置へ配置できます。
218 +
219 + ここでの `Article` は Site 自身の article component であり、同名の `@riebeckite/honox/ui` primitive ではありません。scaffold の starter は標準 slot を決まった位置へ描画します(`article.aside`、`article.header`、`article.metadata`、`article.before-content`、`article.after-content`、`article.footer`)。plugin 作者はこれらから選ぶか、Site に独自名の描画を依頼します。独自 slot は Site が描画を選ぶまで何も表示しません。
220 +
221 + Site はどの slot をどこへ置くかを選び、描画の仕組みは公開 `ContentSlot` primitive に任せます。
222 +
223 + ```tsx
224 + <ArticleContent>
225 + <ContentSlot slots={bodySlots} name="article.header" />
226 + <ContentSlot
227 + slots={bodySlots}
228 + name="article.metadata"
229 + class="site-article__metadata"
230 + />
231 + <ArticleBody html={post.html ?? ""} />
232 + </ArticleContent>
233 + ```
234 +
235 + `ContentSlot` は slot lookup、存在しない slot や空 slot の扱い、HTML fragment の描画、`data-slot` の付与を担当します。Site が `dangerouslySetInnerHTML` を直接書く必要はありません。順序、可視性、Site 固有 class、独自 slot 名は引き続き Site が所有します。`slots` を直接読んだり、任意の wrapper で包んだり、同じ slot を複数回描画する escape hatch も残っています。
236 +
237 + Plugin が route や shell の構造を書き換える必要はありません。
238 +
239 + `@riebeckite/plugin-properties` では、
240 +
241 + ```ts
242 + render: "slot"
243 + ```
244 +
245 + を指定すると `properties` slot を提供します。
246 +
247 + `render: "html"` は従来どおり、生成 HTML の先頭または末尾へ直接挿入します。
248 +
249 + 記事末尾の Plugin section は `article.footer` に集約します。article component ではこの slot を一度だけ描画し、fragment の順序は解決済み Plugin の `order` で決めます。空の contribution は DOM node を生成しません。
250 +
251 + ## 独自 Site を作る
252 +
253 + 外部 Site でも、公開 primitive を使いながら自由に component を構成できます。
254 +
255 + ```tsx
256 + import type { ContentBodySlots, PostContent } from "@riebeckite/core";
257 + import {
258 + Article,
259 + ArticleBody,
260 + ArticleContent,
261 + ArticleLayout,
262 + ContentSlot,
263 + } from "@riebeckite/honox/ui";
264 +
265 + export function SiteArticle({
266 + post,
267 + bodySlots,
268 + }: {
269 + post: PostContent;
270 + bodySlots?: ContentBodySlots;
271 + }) {
272 + return (
273 + <Article class="site-article">
274 + <ArticleLayout>
275 + <ArticleContent>
276 + <ContentSlot slots={bodySlots} name="article.header" />
277 + <ArticleBody html={post.html ?? ""} />
278 + <ContentSlot slots={bodySlots} name="article.footer" />
279 + </ArticleContent>
280 + </ArticleLayout>
281 + </Article>
282 + );
283 + }
284 + ```
285 +
286 + 見た目は Site の CSS で定義します。
287 +
288 + ```css
289 + @import "./.riebeckite/framework-styles.css";
290 + @import "./.riebeckite/plugin-styles.css";
291 + @import "./.riebeckite/theme-styles.css";
292 +
293 + .site-article {
294 + max-width: 48rem;
295 + margin: 0 auto;
296 + }
297 + ```
298 +
299 + `.riebeckite` 内の生成 CSS 自体を直接編集しないでください。
300 +
301 + ### Islands
302 +
303 + Island も通常の Site module として管理します。
304 +
305 + ```text
306 + app/islands/
307 + ```
308 +
309 + へ HonoX island を配置し、それを利用する route または component から import します。
310 +
311 + hydration や client-side state は Site 内で管理します。
312 +
313 + `app/client.ts` では、
314 +
315 + ```ts
316 + createClient();
317 + initRiebeckiteClient();
318 + ```
319 +
320 + の両方を初期化します。
321 +
322 + `initRiebeckiteClient()` は、インストールされている Plugin や Theme が提供する browser entry を起動するために使用されます。
323 +
324 + Plugin は client entry を提供できますが、
325 +
326 + - Site route
327 + - shell
328 + - component
329 + - island
330 + - CSS design
331 +
332 + そのものを所有してはいけません。
333 +