Islands
Drop an interactive component into islands/ and use it in any MDX page — hydrated automatically, no per-page import.
Blume renders your docs as static HTML with zero JavaScript by default. When you need something interactive — a live demo, a chart, a playground — you add an island: a framework component that ships JS only for itself, only on the pages that use it.
The islands/ convention
Drop a component into an islands/ folder at your project root. Its filename becomes a component you can use in any .mdx page, with no import:
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 />
The filename is the component name, so it must be a PascalCase identifier — letters, digits, and underscores only (Counter.tsx → <Counter />). Lowercase filenames, names with dashes/dots/spaces (like Time-Picker.tsx), and two islands that resolve to the same name are skipped with a build warning.
Registering islands in components.ts
If you’d rather keep islands next to the rest of your components — or give them a different name than the file — register them with defineComponents. The islands group is exactly like the islands/ folder: each entry is available in every MDX page and hydrates (defaulting to client: "visible").
import { defineComponents } from "blume";
import Counter from "./widgets/Counter.tsx";
export default defineComponents({
islands: {
Counter, // <Counter /> in any MDX page, hydrated
},
});
Reference the component by import or by a path string, and set a hydration mode per island with the descriptor form:
export default defineComponents({
islands: {
Chart: { component: "./widgets/Chart.tsx", client: "only" },
},
});
Hydration
By default an island uses client:visible: it hydrates when the reader scrolls it into view, so a page full of islands still loads instantly. Opt into a different strategy with an export const client in the island file:
// Skip server rendering entirely — for components that touch the DOM/window.
export const client = "only";
export default function Chart() {
/* ... */
}
client value |
Hydrates | Use it for |
|---|---|---|
"visible" (default) |
When scrolled into view | Most islands |
"load" |
Immediately on page load | Above-the-fold, must-be-instant UI |
"idle" |
When the main thread is idle | Non-urgent interactivity |
"only" |
Client only, never server-rendered | Libraries that need window/document (charts, editors) |
Frameworks
React works out of the box — Blume turns it on automatically the moment your project contains a .tsx/.jsx island.
The React Compiler is on by default whenever React is enabled, so your islands are auto-memoized — no hand-written useMemo/useCallback needed. It ships with Blume; there’s nothing to install. Opt out in blume.config.ts:
export default defineConfig({
react: { compiler: false },
});
Vue and Svelte are supported too; install the matching Astro integration and Blume wires up the renderer when it sees a .vue or .svelte island:
# 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>
Props you pass in MDX (<Counter start={5} />) are forwarded to the component, and children (<Counter>label</Counter>) arrive as the default slot.
Hooks
Islands hydrate on their own, so there’s no React context to thread project data through. Instead, blume/hooks reads a small snapshot the layout serializes into the page — no props to drill:
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>
);
}
| Hook | Returns |
|---|---|
useBlume() |
{ config, navigation } for the site, or null before mount |
usePage() |
{ route, title } for the current page, or null before mount |
useSearch() |
{ search, results, loading } — query the configured search provider |
useAskAI() |
{ ask, messages, loading, reset } — stream from the Ask AI endpoint |
useBlume() and usePage() return null until the island mounts (so server and client render the same first frame) — guard for it. The snapshot is emitted only on pages that ship React, so a fully static site pays nothing.
On custom pages built with PageLayout, pass clientData so islands there can read it:
<PageLayout
clientData={{ config: data.config, navigation: data.navigation, page: { route: "/", title: "Home" } }}
{/* …other props… */}
/>