コンテンツにスキップ
Blume
Esc
↑↓移動↵開く⌘Jプレビュー
このページの内容

アイランド

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 つのアイランドが同じ名前に解決される場合 — Counter.tsx と Counter.vue、あるいは別々のサブフォルダにある 2 つの Counter.tsx ファイルなど — Blume はファイルパス順で最初のものを残し、もう一方をビルド警告とともに無視します。

ページからブラウザでコードを実行する方法は、アイランド以外にありません。<button onClick={() => open()}> のように、MDX 内の通常の HTML 要素に付けた JavaScript のイベントハンドラは一切実行されません: ページは静的 HTML であるため、関数はテキストとして属性に書き込まれるだけです。Blume はこれを該当する行で BLUME_MDX_EVENT_HANDLER として警告します。そのマークアップをアイランドに移し、ページではそのアイランドを使ってください。

components.ts でのアイランド登録

アイランドを他のコンポーネントと同じ場所に置きたい場合や、ファイル名とは異なる名前を付けたい場合は、defineComponents で client モードを指定した mdx エントリとして登録します。アイランドとはつまり、ハイドレートされる MDX コンポーネントにすぎません。各エントリはすべての MDX ページで利用でき、islands/ のファイルと同じ名前のエントリはそのファイルを置き換えます。

import { defineComponents } from "blume";
import Counter from "./widgets/Counter.tsx";

export default defineComponents({
  mdx: {
    Counter: { component: Counter, client: "visible" }, // <Counter /> in any MDX page, hydrated
  },
});

コンポーネントは import 経由でもパス文字列でも参照でき、アイランドごとにハイドレーションモードを選べます("media" の場合は media クエリも併せて指定します):

export default defineComponents({
  mdx: {
    Chart: { component: "./widgets/Chart.tsx", client: "only" },
  },
});

フォルダとは異なり、デフォルト値はありません: client を指定しない mdx エントリは静的 HTML としてレンダリングされ、そのエントリが React、Vue、Svelte のコンポーネントである場合は Blume が警告を出します。Blume は components.ts を静的に読み取るため、エントリは import したコンポーネント、パス文字列、または { component, client, media } のオブジェクトリテラルのいずれかでなければなりません — 受け付けられる形式と、それ以外の場合に発生するエラーについては カスタマイズ を参照してください。

ハイドレーション

デフォルトでは、アイランドは 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 を必要とするライブラリ(チャート、エディタ)
"media" (components.ts のみ) CSS メディアクエリに一致したとき 特定の画面サイズでのみ動作する UI(モバイル専用メニューなど)

"media" にはクエリを併せて指定する必要があるため、components.ts のエントリ でのみ使用できます — { component: Menu, client: "media", media: "(max-width: 50em)" }。これを宣言した islands/ ファイルは、警告とともに "visible" にフォールバックします。

フレームワーク

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 } — 設定済みの検索プロバイダーに問い合わせます
useAssistant() { ask, messages, loading, reset } — アシスタントエンドポイントからストリーミングします

useBlume() と usePage() は、アイランドがマウントされるまで null を返します(サーバーとクライアントが同じ最初のフレームをレンダリングするため) — ガードを入れてください。このスナップショットは React を配信するページでのみ出力されるため、完全に静的なサイトでは一切コストがかかりません。

PageLayout で構築した カスタムページ では、そこにあるアイランドが読み取れるように clientData を渡します:

<PageLayout
  clientData={{ config: data.config, navigation: data.navigation, page: { route: "/", title: "Home" } }}
  {/* …other props… */}
/>

最終更新

このページは役に立ちましたか?