アイランド
interactive なコンポーネントを islands/ に置くだけで、任意の MDX ページで使えます — 自動的にハイドレートされ、ページごとの import は不要です。
Blume はデフォルトでドキュメントを JavaScript ゼロ の静的 HTML としてレンダリングします。インタラクティブな要素が必要になったとき — ライブデモ、チャート、プレイグラウンドなど — に追加するのが アイランド です。アイランドとは、自分自身のための JS のみを、それを使っているページにのみ配信するフレームワークコンポーネントです。
islands/ の規約
プロジェクトルートの islands/ フォルダにコンポーネントを置きます。そのファイル名がコンポーネント名となり、import なしで 任意の .mdx ページで使えるようになります:
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Clicked {count}</button>;
}
Here's a live counter: <Counter />
ファイル名がコンポーネント名になるため、PascalCase の識別子でなければなりません — 使えるのは英字、数字、アンダースコアのみです(Counter.tsx → <Counter />)。小文字始まりのファイル名、ダッシュ・ドット・スペースを含む名前(Time-Picker.tsx など)、および同じ名前に解決される 2 つのアイランドは、ビルド警告とともにスキップされます。
components.ts でのアイランド登録
アイランドを他のコンポーネントと同じ場所に置きたい場合や、ファイル名とは異なる名前を付けたい場合は、defineComponents で登録します。islands グループは islands/ フォルダとまったく同じです: 各エントリはすべての MDX ページで利用でき、ハイドレートされます(デフォルトは client: "visible")。
import { defineComponents } from "blume";
import Counter from "./widgets/Counter.tsx";
export default defineComponents({
islands: {
Counter, // <Counter /> in any MDX page, hydrated
},
});
コンポーネントは import 経由でもパス文字列でも参照でき、ディスクリプタ形式を使えばアイランドごとにハイドレーションモードを設定できます:
export default defineComponents({
islands: {
Chart: { component: "./widgets/Chart.tsx", client: "only" },
},
});
ハイドレーション
デフォルトでは、アイランドは client:visible を使います: 読者がスクロールしてビューに入った時点でハイドレートされるため、アイランドだらけのページでも即座に読み込まれます。別の戦略を選ぶには、アイランドのファイル内で export const client を指定します:
// Skip server rendering entirely — for components that touch the DOM/window.
export const client = "only";
export default function Chart() {
/* ... */
}
client の値 |
ハイドレートのタイミング | 用途 |
|---|---|---|
"visible" (デフォルト) |
スクロールでビューに入ったとき | ほとんどのアイランド |
"load" |
ページ読み込み時に即座に | ファーストビュー、即応が必須の UI |
"idle" |
メインスレッドがアイドルになったとき | 緊急性の低いインタラクティブ要素 |
"only" |
クライアントのみ、サーバーレンダリングなし | window/document を必要とするライブラリ(チャート、エディタ) |
フレームワーク
React はそのまま動作します — プロジェクトに .tsx/.jsx のアイランドが含まれた瞬間、Blume が自動的に有効化します。
React Compiler は React が有効なときは常にデフォルトでオンになっているため、アイランドは自動的にメモ化されます — 手書きの useMemo/useCallback は不要です。Blume に同梱されているので、インストールするものはありません。オプトアウトは blume.config.ts で行います:
export default defineConfig({
react: { compiler: false },
});
Vue と Svelte もサポートされています。対応する Astro インテグレーションをインストールすれば、.vue または .svelte のアイランドを検出したときに Blume がレンダラーを接続します:
# Vue
npm install @astrojs/vue vue
# Svelte
npm install @astrojs/svelte svelte
<script setup>
import { ref } from "vue";
const on = ref(false);
</script>
<template>
<button @click="on = !on">{{ on ? "On" : "Off" }}</button>
</template>
MDX で渡したプロパティ(<Counter start={5} />)はコンポーネントに転送され、子要素(<Counter>label</Counter>)はデフォルトスロットとして届きます。
フック
アイランドは独立してハイドレートされるため、プロジェクトのデータを引き回すための React コンテキストは存在しません。代わりに blume/hooks が、レイアウトがページ内にシリアライズした小さなスナップショットを読み取ります — props のバケツリレーは不要です:
import { useBlume, usePage } from "blume/hooks";
export default function PageInfo() {
const blume = useBlume();
const page = usePage();
if (!(blume && page)) {
return null;
}
return (
<p>
You're reading <strong>{page.title}</strong> on {blume.config.title}.
</p>
);
}
| フック | 戻り値 |
|---|---|
useBlume() |
サイトの { config, navigation }、マウント前は null |
usePage() |
現在のページの { route, title }、マウント前は null |
useSearch() |
{ search, results, loading } — 設定済みの検索プロバイダーに問い合わせます |
useAskAI() |
{ ask, messages, loading, reset } — Ask AI エンドポイントからストリーミングします |
useBlume() と usePage() は、アイランドがマウントされるまで null を返します(サーバーとクライアントが同じ最初のフレームをレンダリングするため) — ガードを入れてください。このスナップショットは React を配信するページでのみ出力されるため、完全に静的なサイトでは一切コストがかかりません。
PageLayout で構築した カスタムページ では、そこにあるアイランドが読み取れるように clientData を渡します:
<PageLayout
clientData={{ config: data.config, navigation: data.navigation, page: { route: "/", title: "Home" } }}
{/* …other props… */}
/>