Color mode

HonoX Integration

@riebeckite/honox は、Riebeckite Core と HonoX / Vite を接続する integration です。

Core はコンテンツやプラグインの処理を担当しますが、HonoX の route や Vite の build 方法については知りません。

その間を接続するのが @riebeckite/honox です。

Diagram source
text
flowchart LR
    A["Riebeckite Core<br/>Content / Plugin / Manifest"]
    B["@riebeckite/honox<br/>Integration"]
    C["HonoX / Vite<br/>Application"]
 
    A --> B
    B --> C

主に次の処理を担当します。

  • application root / config の解決
  • Vite の development / build
  • SSG の設定
  • plugin / theme の style entry 生成
  • client entry の生成
  • Riebeckite のコンテンツと HonoX application の接続

これにより、通常の Site は Riebeckite 内部の Vite / HonoX 設定を毎回組み立てる必要がありません。

基本的な使い方

通常は vite.config.ts で riebeckiteVite() を登録します。

ts
import { riebeckiteVite } from "@riebeckite/honox";
import { defineConfig } from "vite";
 
export default defineConfig({
  plugins: [honox({ ... }), ...riebeckiteVite(), build()],
});

riebeckiteVite() は通常の Site 向けの higher-level helper です。

Riebeckite の Vite plugin を追加するだけでなく、次の設定もまとめて行います。

  • SSG entry の設定
  • extension mapping
  • SSR に必要な external dependency の設定
  • plugin / theme の生成 entry の接続

HonoX plugin、deployment 用 build plugin、Tailwind など、Site 自身が必要とする Vite plugin と組み合わせて利用できます。

Root と Config

riebeckiteVite() では、必要に応じて次の場所を指定できます。

Option 意味
appRoot Site application の基準ディレクトリ
configRoot Riebeckite config を探す基準
configFile 使用する config file
workspaceRoot monorepo 開発時の workspace root

通常は指定する必要はありません。

appRoot の既定値は Vite root、configRoot の既定値は appRoot です。

Riebeckite config は configRoot を基準に読み込みます。

一方、

ts
content: {
  directory: "./content",
}

のような content directory は appRoot を基準に解決します。

resolveHonoxApplication() は、これらの root と解決済み config をまとめて返します。

CLI と Vite がこの共通モデルを利用することで、それぞれが異なる方法で application を解決しないようにしています。

workspaceRoot

workspaceRoot は、Riebeckite 自体を monorepo で開発するときに source package alias を利用するための設定です。

npm から Riebeckite をインストールした通常の Site では必要ありません。

その場合は Site 自身の node_modules から package が解決されます。

.riebeckite に生成されるファイル

Integration は application 内の

text
app/.riebeckite/

へ、plugin や theme を接続するためのファイルを生成します。

たとえば plugin style や theme style です。client module は .riebeckite には生成されず、virtual module として提供されます。

Diagram source
text
flowchart LR
    A["Installed Plugins / Themes"]
    B["@riebeckite/honox"]
    C["app/.riebeckite/"]
    D["Site Application"]
 
    A --> B
    B -->|"generated entries"| C
    C --> D

.riebeckite は integration が管理する生成物です。

Site の source code として直接編集しないでください。

Bootstrap module

generated Site は Framework 所有の bootstrap module を import します。解決済み config は virtual:riebeckite/config、構成済みの content runtime は virtual:riebeckite/content です。そのため app/config.ts、app/content.ts、app/constants/paths.ts は生成されません。SSG entry の app/server.ts はこの2つを re-export し、riebeckiteSsg はそこから manifest を見つけます。

Vite の外で動く script(tsx で起動する Node script など)は @riebeckite/honox/runtime の resolveHonoxConfig で同じ config を解決できます。

Lower-level API

より細かく integration を制御したい場合は、lower-level API も利用できます。

  • riebeckite
  • riebeckiteSsg
  • riebeckiteSsgExtensionMap
  • createRiebeckiteSsg

通常の Site では riebeckiteVite() を利用し、独自の build integration が必要な場合のみ lower-level API を利用してください。

Routing と SSG

HonoX の runtime routing と静的生成では、同じ URL が同じページとして扱われる必要があります。

特に catch-all route がある場合、SSG の route 列挙に注意が必要です。

Riebeckite はこのために2つの helper を提供します。

contentRouteSsgParams

ts
contentRouteSsgParams(routePath, params)

hono/ssg の ssgParams の代わりとして使用します。

この helper は、その route 自身に属する params だけを返します。

たとえば、

text
/:slug{.+}

という catch-all route があっても、

text
/tags/:slug{.+}

に属するページまで横取りしません。

ssgEnumerableHandler

ts
ssgEnumerableHandler(handler)

next() を使って sibling route に処理を渡す handler を、SSG の列挙対象として残すための helper です。

Hono は middleware 形式の handler を通常 SSG の列挙対象から外すため、この差を補います。

Plugin Page

Plugin は通常の content とは別に、独自のページを提供できます。

その場合は、

ts
resolveContentRoute(manifest, path)

ではなく、

ts
resolveRiebeckiteRoute(content, path)

を使用します。

SSG params には、

ts
pluginPageSsgParams(content)

を追加します。

生成された Site の catch-all route は、これらをまとめた resolveRiebeckiteContentRequest(c, content) を使用します。この helper が content / Plugin Page / redirect / not-found を解決し、htmlLanguage と headTags を context へ設定するため、Site は返された結果を自身の composition に渡すだけで済みます。root / も同じ mechanics を共有する resolveRiebeckiteHomeRequest(c, content) で解決します。

Route resolver は次の順序で URL を解決します。

Diagram source
text
flowchart TD
    A["Request Path"]
    B{"Plugin Page?"}
    C["Plugin Page"]
    D{"Content?"}
    E["Content"]
    F{"Redirect?"}
    G["Redirect"]
    H["Not Found"]
 
    A --> B
    B -->|Yes| C
    B -->|No| D
    D -->|Yes| E
    D -->|No| F
    F -->|Yes| G
    F -->|No| H

Plugin Page の body は意図的に文字列として扱います。

Site が持つ既存の document frame 内へ描画し、page.headTags も Site の frame へ渡します。

この仕組みにより、Plugin が独自ページを提供するためだけに HonoX の route file を追加する必要はありません。

UI Primitive

@riebeckite/honox/ui は UI framework ではありません。

Site が独自のデザインを作りながら、Riebeckite と共通の HTML 構造を利用するための小さな primitive set です。

公開されている主な component は次のとおりです。

  • Article
  • ArticleLayout
  • ArticleHeader
  • ArticleContent
  • ArticleBody
  • PageBody
  • ArticleMeta
  • ArticleFooter
  • ContentSlot
  • Sidebar

対応する *Props 型も公開されています。ContentSlot には hasSlot(slots, name) という純粋 helper が対応し、ARTICLE_SLOT 定数が標準 slot 名を提供します。

Stable Styling Hooks

各 primitive は次の class を stable styling hook として提供します。

Component Class
Article rb-article
ArticleLayout rb-article-layout
ArticleHeader rb-article-header
ArticleContent rb-article-body
ArticleBody rb-article-content
ArticleMeta rb-article-meta
ArticleFooter rb-article-footer
Sidebar rb-sidebar

ArticleBody はレンダリング済み Markdown 本文を .rb-article-content として描画し、Markdown typography はこの wrapper にのみ適用されます。plugin component の見出しは plugin 自身が所有します。

ContentSlot は slots map から slot 名で HTML fragment を取り出し、data-slot を付けて描画します。存在しない slot、空文字、whitespace のみの slot は何も描画しません。class / className で Site 固有 class を追加できます。slot 名から semantic 要素を推測するような暗黙の mapping は行いません。

Primitive が担当するのは主に、

  • semantic HTML
  • stable styling hook
  • class / className の合成
  • hook を成立させる構造 CSS

です。rb-* hook を成立させる構造 CSS は @riebeckite/honox/style.css にあり、生成された .riebeckite/framework-styles.css 経由で Site に読み込まれます。

一方、

  • 記事本文の見た目
  • metadata の表示形式
  • navigation の配置
  • card
  • page layout の composition
  • island
  • Site 固有の visual design と override

は Site Application が管理します。

使用例

tsx
import {
  Article,
  ArticleBody,
  ArticleContent,
  ArticleLayout,
  ContentSlot,
} from "@riebeckite/honox/ui";
 
<Article class="site-article">
  <ArticleLayout>
    <ContentSlot
      slots={bodySlots}
      name="article.aside"
      class="site-article__aside"
    />
    <ArticleContent>
      <ContentSlot slots={bodySlots} name="article.header" />
      <ContentSlot slots={bodySlots} name="article.metadata" />
      <ArticleBody html={post.html ?? ""} />
    </ArticleContent>
  </ArticleLayout>
</Article>;

ArticleHeader と ArticleContent は、children と HTML input prop のどちらか一方だけを受け取ります。レンダリング済み Markdown 本文は ArticleBody に渡してください。ArticleContent html={...} は後方互換のために残っていますが非推奨です。

Primitive は composition point として使用し、構造は Framework の hook CSS が、見た目は Site 側が定義してください。

また、

text
@riebeckite/honox/src/

以下を直接 import しないでください。

公開 API として記載されていない内部 component に依存することも避けてください。

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 を使う必要はありません。

Not-found と Error

not-found の処理は HonoX 標準の app/routes/_404.tsx です。Riebeckite が「見つからない」と判断し、HonoX がその response を Site の _renderer.tsx を通して描画し直すため、status は 404 のまま、not-found 画面の見た目は Site が所有します。

tsx
// app/routes/_404.tsx
import type { NotFoundHandler } from "hono";
 
const handler: NotFoundHandler = (c) => {
  c.status(404);
  return c.render(<main class="not-found">Page not found</main>);
};
 
export default handler;

route resolver(resolveRiebeckiteContentRequest、resolveRiebeckiteHomeRequest、Plugin Page の解決)と、asset-like な path を content route に入れない extension guard は @riebeckite/honox に残ります。Site がこれらを再実装することはありません。解決されるのは公開済みで routable な content だけなので、not-found response に draft・未来公開・非公開 content の metadata が載ることもありません。

runtime error は Hono の error 処理を使います。app/routes/_error.tsx が無ければ、標準の handler が error をログに記録し、500 Internal Server Error を返します。visitor 向けの画面が必要な場合に限り、Site は _error.tsx(ErrorHandler)を追加できます。config、plugin、build の失敗は開発者向けであり、成功したページに変換してはいけません。

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

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

Integration の境界

Riebeckite の routing では、すでに解決された公開 URL を使用します。

基本的には、

text
byPermalink
    ↓
redirects

の順で request を解決します。

filesystem path やディレクトリ構造から公開 URL を逆算しません。

また、slug はコンテンツを内部で検索するためのキーです。

実際に公開される URL は、解決済みの permalink です。

責務のまとめ

Riebeckite 全体では、次のように責務を分離します。

Diagram source
text
flowchart LR
    Core["Core<br/>Content / Manifest / Plugin API"]
    Integration["HonoX Integration<br/>Vite / SSG / Route Resolution"]
    Plugin["Plugin<br/>Content Extension / Page / Asset / Client Entry"]
    Site["Site Application<br/>Route / Shell / UI / Island / CSS"]
 
    Core --> Integration
    Plugin --> Core
    Integration --> Site
    Plugin -. "提供した情報を<br/>Site が配置" .-> Site

HonoX / Vite / Cloudflare 固有の処理は integration または Site Application に閉じます。

Core は HonoX routing を所有しません。

Plugin はページ、アセット、client entry などを提供できますが、Site 全体の route composition や UI 構造は所有しません。

Core はコンテンツを扱い、Integration は HonoX と接続し、Plugin は機能を提供し、Site が最終的な表示を決める、という境界を維持してください。

build state の扱いについては Build system、package ごとの責務については Architecture を参照してください。

History

1 changesCollapseExpand
1 + # HonoX Integration
2 +
3 + `@riebeckite/honox` は、Riebeckite Core と HonoX / Vite を接続する integration です。
4 +
5 + Core はコンテンツやプラグインの処理を担当しますが、HonoX の route や Vite の build 方法については知りません。
6 +
7 + その間を接続するのが `@riebeckite/honox` です。
8 +
9 + ```mermaid
10 + flowchart LR
11 + A["Riebeckite Core<br/>Content / Plugin / Manifest"]
12 + B["@riebeckite/honox<br/>Integration"]
13 + C["HonoX / Vite<br/>Application"]
14 +
15 + A --> B
16 + B --> C
17 + ```
18 +
19 + 主に次の処理を担当します。
20 +
21 + - application root / config の解決
22 + - Vite の development / build
23 + - SSG の設定
24 + - plugin / theme の style entry 生成
25 + - client entry の生成
26 + - Riebeckite のコンテンツと HonoX application の接続
27 +
28 + これにより、通常の Site は Riebeckite 内部の Vite / HonoX 設定を毎回組み立てる必要がありません。
29 +
30 + ## 基本的な使い方
31 +
32 + 通常は `vite.config.ts` で `riebeckiteVite()` を登録します。
33 +
34 + ```ts
35 + import { riebeckiteVite } from "@riebeckite/honox";
36 + import { defineConfig } from "vite";
37 +
38 + export default defineConfig({
39 + plugins: [honox({ ... }), ...riebeckiteVite(), build()],
40 + });
41 + ```
42 +
43 + `riebeckiteVite()` は通常の Site 向けの higher-level helper です。
44 +
45 + Riebeckite の Vite plugin を追加するだけでなく、次の設定もまとめて行います。
46 +
47 + - SSG entry の設定
48 + - extension mapping
49 + - SSR に必要な external dependency の設定
50 + - plugin / theme の生成 entry の接続
51 +
52 + HonoX plugin、deployment 用 build plugin、Tailwind など、Site 自身が必要とする Vite plugin と組み合わせて利用できます。
53 +
54 + ## Root と Config
55 +
56 + `riebeckiteVite()` では、必要に応じて次の場所を指定できます。
57 +
58 + | Option | 意味 |
59 + | --- | --- |
60 + | `appRoot` | Site application の基準ディレクトリ |
61 + | `configRoot` | Riebeckite config を探す基準 |
62 + | `configFile` | 使用する config file |
63 + | `workspaceRoot` | monorepo 開発時の workspace root |
64 +
65 + 通常は指定する必要はありません。
66 +
67 + `appRoot` の既定値は Vite root、`configRoot` の既定値は `appRoot` です。
68 +
69 + Riebeckite config は `configRoot` を基準に読み込みます。
70 +
71 + 一方、
72 +
73 + ```ts
74 + content: {
75 + directory: "./content",
76 + }
77 + ```
78 +
79 + のような content directory は `appRoot` を基準に解決します。
80 +
81 + `resolveHonoxApplication()` は、これらの root と解決済み config をまとめて返します。
82 +
83 + CLI と Vite がこの共通モデルを利用することで、それぞれが異なる方法で application を解決しないようにしています。
84 +
85 + ### `workspaceRoot`
86 +
87 + `workspaceRoot` は、Riebeckite 自体を monorepo で開発するときに source package alias を利用するための設定です。
88 +
89 + npm から Riebeckite をインストールした通常の Site では必要ありません。
90 +
91 + その場合は Site 自身の `node_modules` から package が解決されます。
92 +
93 + ## `.riebeckite` に生成されるファイル
94 +
95 + Integration は application 内の
96 +
97 + ```text
98 + app/.riebeckite/
99 + ```
100 +
101 + へ、plugin や theme を接続するためのファイルを生成します。
102 +
103 + たとえば plugin style や theme style です。client module は `.riebeckite` には生成されず、virtual module として提供されます。
104 +
105 + ```mermaid
106 + flowchart LR
107 + A["Installed Plugins / Themes"]
108 + B["@riebeckite/honox"]
109 + C["app/.riebeckite/"]
110 + D["Site Application"]
111 +
112 + A --> B
113 + B -->|"generated entries"| C
114 + C --> D
115 + ```
116 +
117 + `.riebeckite` は integration が管理する生成物です。
118 +
119 + **Site の source code として直接編集しないでください。**
120 +
121 + ## Bootstrap module
122 +
123 + generated Site は Framework 所有の bootstrap module を import します。解決済み config は `virtual:riebeckite/config`、構成済みの content runtime は `virtual:riebeckite/content` です。そのため `app/config.ts`、`app/content.ts`、`app/constants/paths.ts` は生成されません。SSG entry の `app/server.ts` はこの2つを re-export し、`riebeckiteSsg` はそこから manifest を見つけます。
124 +
125 + Vite の外で動く script(`tsx` で起動する Node script など)は `@riebeckite/honox/runtime` の `resolveHonoxConfig` で同じ config を解決できます。
126 +
127 + ## Lower-level API
128 +
129 + より細かく integration を制御したい場合は、lower-level API も利用できます。
130 +
131 + - `riebeckite`
132 + - `riebeckiteSsg`
133 + - `riebeckiteSsgExtensionMap`
134 + - `createRiebeckiteSsg`
135 +
136 + 通常の Site では `riebeckiteVite()` を利用し、独自の build integration が必要な場合のみ lower-level API を利用してください。
137 +
138 + ## Routing と SSG
139 +
140 + HonoX の runtime routing と静的生成では、同じ URL が同じページとして扱われる必要があります。
141 +
142 + 特に catch-all route がある場合、SSG の route 列挙に注意が必要です。
143 +
144 + Riebeckite はこのために2つの helper を提供します。
145 +
146 + ### `contentRouteSsgParams`
147 +
148 + ```ts
149 + contentRouteSsgParams(routePath, params)
150 + ```
151 +
152 + `hono/ssg` の `ssgParams` の代わりとして使用します。
153 +
154 + この helper は、その route 自身に属する params だけを返します。
155 +
156 + たとえば、
157 +
158 + ```text
159 + /:slug{.+}
160 + ```
161 +
162 + という catch-all route があっても、
163 +
164 + ```text
165 + /tags/:slug{.+}
166 + ```
167 +
168 + に属するページまで横取りしません。
169 +
170 + ### `ssgEnumerableHandler`
171 +
172 + ```ts
173 + ssgEnumerableHandler(handler)
174 + ```
175 +
176 + `next()` を使って sibling route に処理を渡す handler を、SSG の列挙対象として残すための helper です。
177 +
178 + Hono は middleware 形式の handler を通常 SSG の列挙対象から外すため、この差を補います。
179 +
180 + ## Plugin Page
181 +
182 + Plugin は通常の content とは別に、独自のページを提供できます。
183 +
184 + その場合は、
185 +
186 + ```ts
187 + resolveContentRoute(manifest, path)
188 + ```
189 +
190 + ではなく、
191 +
192 + ```ts
193 + resolveRiebeckiteRoute(content, path)
194 + ```
195 +
196 + を使用します。
197 +
198 + SSG params には、
199 +
200 + ```ts
201 + pluginPageSsgParams(content)
202 + ```
203 +
204 + を追加します。
205 +
206 + 生成された Site の catch-all route は、これらをまとめた `resolveRiebeckiteContentRequest(c, content)` を使用します。この helper が content / Plugin Page / redirect / not-found を解決し、`htmlLanguage` と `headTags` を context へ設定するため、Site は返された結果を自身の composition に渡すだけで済みます。root `/` も同じ mechanics を共有する `resolveRiebeckiteHomeRequest(c, content)` で解決します。
207 +
208 + Route resolver は次の順序で URL を解決します。
209 +
210 + ```mermaid
211 + flowchart TD
212 + A["Request Path"]
213 + B{"Plugin Page?"}
214 + C["Plugin Page"]
215 + D{"Content?"}
216 + E["Content"]
217 + F{"Redirect?"}
218 + G["Redirect"]
219 + H["Not Found"]
220 +
221 + A --> B
222 + B -->|Yes| C
223 + B -->|No| D
224 + D -->|Yes| E
225 + D -->|No| F
226 + F -->|Yes| G
227 + F -->|No| H
228 + ```
229 +
230 + Plugin Page の body は意図的に文字列として扱います。
231 +
232 + Site が持つ既存の document frame 内へ描画し、`page.headTags` も Site の frame へ渡します。
233 +
234 + この仕組みにより、Plugin が独自ページを提供するためだけに HonoX の route file を追加する必要はありません。
235 +
236 + # UI Primitive
237 +
238 + `@riebeckite/honox/ui` は UI framework ではありません。
239 +
240 + Site が独自のデザインを作りながら、Riebeckite と共通の HTML 構造を利用するための小さな primitive set です。
241 +
242 + 公開されている主な component は次のとおりです。
243 +
244 + - `Article`
245 + - `ArticleLayout`
246 + - `ArticleHeader`
247 + - `ArticleContent`
248 + - `ArticleBody`
249 + - `PageBody`
250 + - `ArticleMeta`
251 + - `ArticleFooter`
252 + - `ContentSlot`
253 + - `Sidebar`
254 +
255 + 対応する `*Props` 型も公開されています。`ContentSlot` には `hasSlot(slots, name)` という純粋 helper が対応し、`ARTICLE_SLOT` 定数が標準 slot 名を提供します。
256 +
257 + ## Stable Styling Hooks
258 +
259 + 各 primitive は次の class を stable styling hook として提供します。
260 +
261 + | Component | Class |
262 + | --- | --- |
263 + | `Article` | `rb-article` |
264 + | `ArticleLayout` | `rb-article-layout` |
265 + | `ArticleHeader` | `rb-article-header` |
266 + | `ArticleContent` | `rb-article-body` |
267 + | `ArticleBody` | `rb-article-content` |
268 + | `ArticleMeta` | `rb-article-meta` |
269 + | `ArticleFooter` | `rb-article-footer` |
270 + | `Sidebar` | `rb-sidebar` |
271 +
272 + `ArticleBody` はレンダリング済み Markdown 本文を `.rb-article-content` として描画し、Markdown typography はこの wrapper にのみ適用されます。plugin component の見出しは plugin 自身が所有します。
273 +
274 + `ContentSlot` は `slots` map から slot 名で HTML fragment を取り出し、`data-slot` を付けて描画します。存在しない slot、空文字、whitespace のみの slot は何も描画しません。`class` / `className` で Site 固有 class を追加できます。slot 名から semantic 要素を推測するような暗黙の mapping は行いません。
275 +
276 + Primitive が担当するのは主に、
277 +
278 + - semantic HTML
279 + - stable styling hook
280 + - `class` / `className` の合成
281 + - hook を成立させる構造 CSS
282 +
283 + です。`rb-*` hook を成立させる構造 CSS は `@riebeckite/honox/style.css` にあり、生成された `.riebeckite/framework-styles.css` 経由で Site に読み込まれます。
284 +
285 + 一方、
286 +
287 + - 記事本文の見た目
288 + - metadata の表示形式
289 + - navigation の配置
290 + - card
291 + - page layout の composition
292 + - island
293 + - Site 固有の visual design と override
294 +
295 + は Site Application が管理します。
296 +
297 + ## 使用例
298 +
299 + ```tsx
300 + import {
301 + Article,
302 + ArticleBody,
303 + ArticleContent,
304 + ArticleLayout,
305 + ContentSlot,
306 + } from "@riebeckite/honox/ui";
307 +
308 + <Article class="site-article">
309 + <ArticleLayout>
310 + <ContentSlot
311 + slots={bodySlots}
312 + name="article.aside"
313 + class="site-article__aside"
314 + />
315 + <ArticleContent>
316 + <ContentSlot slots={bodySlots} name="article.header" />
317 + <ContentSlot slots={bodySlots} name="article.metadata" />
318 + <ArticleBody html={post.html ?? ""} />
319 + </ArticleContent>
320 + </ArticleLayout>
321 + </Article>;
322 + ```
323 +
324 + `ArticleHeader` と `ArticleContent` は、children と HTML input prop のどちらか一方だけを受け取ります。レンダリング済み Markdown 本文は `ArticleBody` に渡してください。`ArticleContent html={...}` は後方互換のために残っていますが非推奨です。
325 +
326 + Primitive は composition point として使用し、構造は Framework の hook CSS が、見た目は Site 側が定義してください。
327 +
328 + また、
329 +
330 + ```text
331 + @riebeckite/honox/src/
332 + ```
333 +
334 + 以下を直接 import しないでください。
335 +
336 + 公開 API として記載されていない内部 component に依存することも避けてください。
337 +
338 + # Site Application の責務
339 +
340 + Riebeckite Site は、最終的には通常の HonoX application です。
341 +
342 + `@riebeckite/honox` は content と build を接続しますが、実際にユーザーが見る UI の設計は Site が管理します。通常の HonoX で編集する手順は [サイトのカスタマイズ](../guides/customizing-your-site.md) を参照してください。
343 +
344 + ```mermaid
345 + flowchart TD
346 + A["Riebeckite Core<br/>content / manifest / plugin"]
347 + B["@riebeckite/honox<br/>build / routing integration"]
348 + C["Site Application"]
349 +
350 + C --> D["app/routes/<br/>URL / page composition"]
351 + C --> E["app/components/<br/>Site UI"]
352 + C --> F["app/islands/<br/>Interactive UI"]
353 + C --> G["app/style.css<br/>Visual Design"]
354 +
355 + A --> B
356 + B --> C
357 + ```
358 +
359 + 主なディレクトリの責務は次のとおりです。
360 +
361 + | ディレクトリ | Site が持つ責務 |
362 + | --- | --- |
363 + | `app/routes/` | URL処理、ページ構成、redirect、response metadata |
364 + | `app/components/` | Site 固有の UI |
365 + | `app/islands/` | 対話 UI と client-side state |
366 + | `app/style.css` | 色、layout、typography、extension style |
367 +
368 + ## Site Shell
369 +
370 + ```text
371 + app/routes/_renderer.tsx
372 + ```
373 +
374 + は Site 全体の shell です。
375 +
376 + ここでは主に、
377 +
378 + - document head
379 + - navigation
380 + - page chrome
381 + - application client entry
382 +
383 + などを管理します。
384 +
385 + Route は `ContentManager` からコンテンツを取得し、
386 +
387 + ```ts
388 + resolveRiebeckiteRoute(content, c.req.path)
389 + ```
390 +
391 + で request URL を解決します。
392 +
393 + その結果をどの component tree で表示するかは Site が決定します。
394 +
395 + Riebeckite repository にある `apps/web` は実装例の1つであり、外部 Site が同じ layout を使う必要はありません。
396 +
397 + ## Not-found と Error
398 +
399 + not-found の処理は HonoX 標準の `app/routes/_404.tsx` です。Riebeckite が「見つからない」と判断し、HonoX がその response を Site の `_renderer.tsx` を通して描画し直すため、status は `404` のまま、not-found 画面の見た目は Site が所有します。
400 +
401 + ```tsx
402 + // app/routes/_404.tsx
403 + import type { NotFoundHandler } from "hono";
404 +
405 + const handler: NotFoundHandler = (c) => {
406 + c.status(404);
407 + return c.render(<main class="not-found">Page not found</main>);
408 + };
409 +
410 + export default handler;
411 + ```
412 +
413 + route resolver(`resolveRiebeckiteContentRequest`、`resolveRiebeckiteHomeRequest`、Plugin Page の解決)と、asset-like な path を content route に入れない extension guard は `@riebeckite/honox` に残ります。Site がこれらを再実装することはありません。解決されるのは公開済みで routable な content だけなので、not-found response に draft・未来公開・非公開 content の metadata が載ることもありません。
414 +
415 + runtime error は Hono の error 処理を使います。`app/routes/_error.tsx` が無ければ、標準の handler が error をログに記録し、`500 Internal Server Error` を返します。visitor 向けの画面が必要な場合に限り、Site は `_error.tsx`(`ErrorHandler`)を追加できます。config、plugin、build の失敗は開発者向けであり、成功したページに変換してはいけません。
416 +
417 + # Plugin と Site の境界
418 +
419 + Plugin は Site に情報や UI fragment を提供できます。
420 +
421 + ただし、**最終的にどこへ描画するかは Site が決定します。**
422 +
423 + ```mermaid
424 + flowchart LR
425 + A["Plugin"]
426 + B["Manifest"]
427 + C["Site Route"]
428 + D["Site Shell / Component"]
429 +
430 + A -->|"headTags / bodySlots / page"| B
431 + B --> C
432 + C -->|"placement"| D
433 + ```
434 +
435 + ## Head Tags
436 +
437 + Plugin が document head に情報を追加したい場合は、
438 +
439 + ```ts
440 + ContentManifestEntry.headTags
441 + ```
442 +
443 + へ `meta` / `link` / `script` を記述します。
444 +
445 + Plugin 自身が `<head>` を描画するわけではありません。
446 +
447 + `resolveRiebeckiteContentRequest` / `resolveRiebeckiteHomeRequest` が、解決した entry の `headTags` を route context へ設定します。`_renderer.tsx` がそれを読み取って描画します。
448 +
449 + ```tsx
450 + import { PluginHeadTags } from "@riebeckite/honox/ui";
451 +
452 + const headTags = c.get("headTags") ?? [];
453 +
454 + <head>
455 + <PluginHeadTags tags={headTags} />
456 + </head>;
457 + ```
458 +
459 + つまり、
460 +
461 + ```text
462 + Plugin
463 + ↓ headTags を提供
464 + Framework resolver
465 + ↓ context へ設定
466 + _renderer.tsx
467 + ↓
468 + <head> に描画
469 + ```
470 +
471 + という関係です。
472 +
473 + たとえば `@riebeckite/plugin-discord-embed` は、この仕組みを使って `theme-color` を提供します。
474 +
475 + Plugin は `<head>` 自体や tag の並び順を所有しません。
476 +
477 + ## RiebeckiteHead と PluginHeadTags
478 +
479 + `@riebeckite/honox/ui` より公開される 2 つの primitive は、head composition の責務分離を明確にします。
480 +
481 + ### `RiebeckiteHead`
482 +
483 + ```tsx
484 + import { RiebeckiteHead } from "@riebeckite/honox/ui";
485 +
486 + <RiebeckiteHead title="My Site" headTags={[]} />
487 + ```
488 +
489 + Framework が次の標準的な head contents を描画します。
490 +
491 + - `<meta charset="utf-8">`
492 + - `<meta name="viewport" content="width=device-width, initial-scale=1.0">`
493 + - `<title>`(title prop が提供する値)
494 + - `<link rel="icon" href="/favicon.ico">`(faviconHref プロップで上書き可能、null で省略可)
495 + - `<ColorModeScript />`(colorModeScript プロップで制御、default true)
496 + - stylesheet entries(stylesheets プロップ、default `["/app/style.css"]`)
497 + - client script entry(clientSrc プロップ、default `"/app/client.ts"`、null で省略可)
498 + - `PluginHeadTag` values の変換(headTags プロップ)
499 + - 子要素(children prop)は標準の後に追加
500 +
501 + `RiebeckiteHead` は `<head>` 要素自身を描画しません。Site は `<head>` の ownership を保持し、その中に `RiebeckiteHead` を配置できます。
502 +
503 + ### `PluginHeadTags`
504 +
505 + ```tsx
506 + import { PluginHeadTags } from "@riebeckite/honox/ui";
507 +
508 + <PluginHeadTags tags={headTagsFromManifest} />
509 + ```
510 +
511 + `PluginHeadTag` values (meta / link / script) を JSX 要素に変換します。`RiebeckiteHead` を使わず、Site が自分で head を組み立てる際に使用します。
512 +
513 + ### 使用例
514 +
515 + Site が `<head>` 所有権を維持しつつ標準 head をFrameworkに任せる場合:
516 +
517 + ```tsx
518 + import { RiebeckiteHead, ThemeRoot } from "@riebeckite/honox/ui";
519 +
520 + export default jsxRenderer(({ children }, c) => (
521 + <ThemeRoot
522 + theme={config.theme}
523 + lang={c.get("htmlLanguage") ?? config.site.locale}
524 + >
525 + <head>
526 + <RiebeckiteHead
527 + title={config.site.title}
528 + headTags={c.get("headTags") ?? []}
529 + />
530 + <meta name="custom-site-value" content="..." />
531 + </head>
532 + <body class="riebeckite-page rb-site">{children}</body>
533 + </ThemeRoot>
534 + );
535 + ```
536 +
537 + 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 が所有権を持ちます。
538 +
539 + Plugin が head tags を提供する場合は、既存の `headTags` メカニズムはそのまま機能します。`RiebeckiteHead` と `headTags` は併用可能です。
540 +
541 + ## Body Slots
542 +
543 + 本文の途中へ Plugin の HTML を表示したい場合は、
544 +
545 + ```ts
546 + ContentManifestEntry.bodySlots
547 + ```
548 +
549 + を使用します。
550 +
551 + Plugin は slot 名と HTML fragment を提供します。
552 +
553 + たとえば、
554 +
555 + ```text
556 + properties
557 + ```
558 +
559 + という slot があれば、Route は slot object を article component へ渡し、Site は、
560 +
561 + ```tsx
562 + <Article
563 + content={post}
564 + bodySlots={route.entry.bodySlots}
565 + />
566 + ```
567 +
568 + のように article component 内の任意の位置へ配置できます。
569 +
570 + ここでの `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 が描画を選ぶまで何も表示しません。
571 +
572 + Site はどの slot をどこへ置くかを選び、描画の仕組みは公開 `ContentSlot` primitive に任せます。
573 +
574 + ```tsx
575 + <ArticleContent>
576 + <ContentSlot slots={bodySlots} name="article.header" />
577 + <ContentSlot
578 + slots={bodySlots}
579 + name="article.metadata"
580 + class="site-article__metadata"
581 + />
582 + <ArticleBody html={post.html ?? ""} />
583 + </ArticleContent>
584 + ```
585 +
586 + `ContentSlot` は slot lookup、存在しない slot や空 slot の扱い、HTML fragment の描画、`data-slot` の付与を担当します。Site が `dangerouslySetInnerHTML` を直接書く必要はありません。順序、可視性、Site 固有 class、独自 slot 名は引き続き Site が所有します。`slots` を直接読んだり、任意の wrapper で包んだり、同じ slot を複数回描画する escape hatch も残っています。
587 +
588 + Plugin が route や shell の構造を書き換える必要はありません。
589 +
590 + `@riebeckite/plugin-properties` では、
591 +
592 + ```ts
593 + render: "slot"
594 + ```
595 +
596 + を指定すると `properties` slot を提供します。
597 +
598 + `render: "html"` は従来どおり、生成 HTML の先頭または末尾へ直接挿入します。
599 +
600 + 記事末尾の Plugin section は `article.footer` に集約します。article component ではこの slot を一度だけ描画し、fragment の順序は解決済み Plugin の `order` で決めます。空の contribution は DOM node を生成しません。
601 +
602 + # 独自 Site を作る
603 +
604 + 外部 Site でも、公開 primitive を使いながら自由に component を構成できます。
605 +
606 + ```tsx
607 + import type { ContentBodySlots, PostContent } from "@riebeckite/core";
608 + import {
609 + Article,
610 + ArticleBody,
611 + ArticleContent,
612 + ArticleLayout,
613 + ContentSlot,
614 + } from "@riebeckite/honox/ui";
615 +
616 + export function SiteArticle({
617 + post,
618 + bodySlots,
619 + }: {
620 + post: PostContent;
621 + bodySlots?: ContentBodySlots;
622 + }) {
623 + return (
624 + <Article class="site-article">
625 + <ArticleLayout>
626 + <ArticleContent>
627 + <ContentSlot slots={bodySlots} name="article.header" />
628 + <ArticleBody html={post.html ?? ""} />
629 + <ContentSlot slots={bodySlots} name="article.footer" />
630 + </ArticleContent>
631 + </ArticleLayout>
632 + </Article>
633 + );
634 + }
635 + ```
636 +
637 + 見た目は Site の CSS で定義します。
638 +
639 + ```css
640 + @import "./.riebeckite/framework-styles.css";
641 + @import "./.riebeckite/plugin-styles.css";
642 + @import "./.riebeckite/theme-styles.css";
643 +
644 + .site-article {
645 + max-width: 48rem;
646 + margin: 0 auto;
647 + }
648 + ```
649 +
650 + `.riebeckite` 内の生成 CSS 自体を直接編集しないでください。
651 +
652 + ## Islands
653 +
654 + Island も通常の Site module として管理します。
655 +
656 + ```text
657 + app/islands/
658 + ```
659 +
660 + へ HonoX island を配置し、それを利用する route または component から import します。
661 +
662 + hydration や client-side state は Site 内で管理します。
663 +
664 + `app/client.ts` では、
665 +
666 + ```ts
667 + createClient();
668 + initRiebeckiteClient();
669 + ```
670 +
671 + の両方を初期化します。
672 +
673 + `initRiebeckiteClient()` は、インストールされている Plugin や Theme が提供する browser entry を起動するために使用されます。
674 +
675 + Plugin は client entry を提供できますが、
676 +
677 + - Site route
678 + - shell
679 + - component
680 + - island
681 + - CSS design
682 +
683 + そのものを所有してはいけません。
684 +
685 + # Integration の境界
686 +
687 + Riebeckite の routing では、すでに解決された公開 URL を使用します。
688 +
689 + 基本的には、
690 +
691 + ```text
692 + byPermalink
693 + ↓
694 + redirects
695 + ```
696 +
697 + の順で request を解決します。
698 +
699 + filesystem path やディレクトリ構造から公開 URL を逆算しません。
700 +
701 + また、`slug` はコンテンツを内部で検索するためのキーです。
702 +
703 + 実際に公開される URL は、解決済みの `permalink` です。
704 +
705 + ## 責務のまとめ
706 +
707 + Riebeckite 全体では、次のように責務を分離します。
708 +
709 + ```mermaid
710 + flowchart LR
711 + Core["Core<br/>Content / Manifest / Plugin API"]
712 + Integration["HonoX Integration<br/>Vite / SSG / Route Resolution"]
713 + Plugin["Plugin<br/>Content Extension / Page / Asset / Client Entry"]
714 + Site["Site Application<br/>Route / Shell / UI / Island / CSS"]
715 +
716 + Core --> Integration
717 + Plugin --> Core
718 + Integration --> Site
719 + Plugin -. "提供した情報を<br/>Site が配置" .-> Site
720 + ```
721 +
722 + HonoX / Vite / Cloudflare 固有の処理は integration または Site Application に閉じます。
723 +
724 + Core は HonoX routing を所有しません。
725 +
726 + Plugin はページ、アセット、client entry などを提供できますが、Site 全体の route composition や UI 構造は所有しません。
727 +
728 + **Core はコンテンツを扱い、Integration は HonoX と接続し、Plugin は機能を提供し、Site が最終的な表示を決める**、という境界を維持してください。
729 +
730 + build state の扱いについては [Build system](build-system.md)、package ごとの責務については [Architecture](architecture.md) を参照してください。
731 +