---
title: Customization
description: Override components, add interactive islands, mount custom pages, install registry components, or eject entirely when you need full control.
---

## Component overrides

Add a `components.ts` (or `components.tsx`) to your project root and export `defineComponents`. The `mdx` map either **replaces** a built-in component or **adds** a new one — available in every `.mdx` page with no import.

```ts components.ts lineNumbers
import { defineComponents } from "blume";
import Callout from "./components/Callout.astro";
import Pricing from "./components/Pricing.astro";

export default defineComponents({
  mdx: {
    Callout, // replace the built-in Callout
    Pricing, // add a new <Pricing /> component
  },
});
```

Keys are the names you write in MDX (`<Callout>`, `<Pricing>`). Use the `.tsx` filename when you import React components.

### Reference form

Every override — in `mdx`, `layout`, or `islands` — accepts three forms:

```ts components.ts
import { defineComponents } from "blume";
import Callout from "./components/Callout.astro";

export default defineComponents({
  mdx: {
    Callout, // 1. an imported component
    Note: "./components/Note.astro", // 2. a path string (resolved from the project root)
    Chart: { component: "./components/Chart.tsx", client: "load" }, // 3. a descriptor
  },
});
```

The **descriptor** form adds a hydration mode so an interactive React/Vue/Svelte component ships its JavaScript and comes alive on the client. Without a `client` mode a framework component renders as static HTML — Blume prints a build warning when it spots one, since that's usually a mistake.

| `client` | Hydrates |
| --- | --- |
| `"load"` | Immediately on page load |
| `"idle"` | When the main thread is idle |
| `"visible"` | When scrolled into view |
| `"media"` | When a `media` query matches (add `media: "(min-width: 40rem)"`) |
| `"only"` | Client only, never server-rendered |

For interactive components you use across many pages, the [`islands` group](/docs/content/islands#registering-islands-in-componentsts) is a shorthand for the descriptor form with `client: "visible"`.

### Typing an override

When you replace a built-in, import its prop type from `blume/components` so your component matches the contract — the types are derived from the components themselves, so they never drift:

```tsx components/Callout.tsx
import type { CalloutProps } from "blume/components";

export default function Callout(props: CalloutProps) {
  // …your own callout, same props as the built-in
}
```

Prop types are exported for the content components (`CalloutProps`, `CardProps`, `TabsProps`, `StepsProps`, `BadgeProps`, and more).

## Layout slots

The `layout` map replaces a piece of Blume's chrome with your own component. Each override receives the same props as the built-in it replaces, so you can wrap the default or start from scratch.

```ts components.ts
import { defineComponents } from "blume";
import Footer from "./components/Footer.astro";
import Logo from "./components/Logo.astro";

export default defineComponents({
  layout: {
    Logo, // brand mark + title in the header
    Footer, // site-wide footer (no built-in — renders only when set)
  },
});
```

Wired slots:

| Slot | Replaces | Props |
| --- | --- | --- |
| `Layout` | The entire page shell (`RootLayout`) | Everything the built-in layout receives, plus the `layout` map |
| `Header` | The top navigation bar | `site`, `logo`, `navigation`, `route`, `searchEnabled`, … |
| `Logo` | The brand link (mark + title) in the header | `site`, `logo` |
| `Search` | The header search trigger + modal | `navigation`, `strings`, `locale`, `askEnabled` |
| `Sidebar` | The primary navigation tree | `items`, `currentRoute` |
| `MobileNav` | The nav inside the mobile drawer (defaults to `Sidebar`) | `items`, `currentRoute` |
| `Breadcrumbs` | The breadcrumb trail | `crumbs` |
| `TableOfContents` | The on-this-page outline | `headings`, `title`, `variant` |
| `Pagination` | The prev/next footer links | `prev`, `next`, `strings` |
| `PageHeader` | An injection point above the article (no built-in) | `page`, `headings`, `route` |
| `PageFooter` | An injection point below the article (no built-in) | `page`, `headings`, `route` |
| `Footer` | A site-wide footer after the content grid (no built-in) | `site`, `navigation`, `ui` |

`PageHeader`, `PageFooter`, and `Footer` have no built-in component — they render nothing until you set them, which makes them handy injection points for a promo banner, a "last updated" note, or a marketing footer.

Layout slots accept the same [three reference forms](#reference-form) as MDX overrides, so a slot can be a path string or a hydrated descriptor (`{ component, client }`) when you want an interactive header or footer.

## Interactive islands

For interactive UI (React, Vue, or Svelte), drop a component into an `islands/` folder and use it in any MDX page — Blume hydrates it for you, no wrapper or registration needed:

```tsx islands/Counter.tsx lineNumbers
import { useState } from "react";

export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>Clicked {n}</button>;
}
```

```mdx page.mdx
Use it anywhere: <Counter />
```

See [Islands](/docs/content/islands) for hydration strategies and framework setup.

## Custom pages

Add `.astro` files under your `pages/` folder to mount fully custom routes alongside your docs — a landing page, a pricing page, or a hand-built index. They keep their location, so relative imports and `getStaticPaths` work as usual, and they can read your config, navigation, and routes from the `blume:data` module.

See [Custom Pages](/docs/advanced/custom-pages) for the full guide.

## Registry

`blume add` copies a Blume-maintained component into your project as **source** — you own it and can edit it freely. Run it with no arguments to list what's available:

```bash
blume add
```

Install a layout slot (header, sidebar, breadcrumbs, table of contents, pagination, or feedback) or any content component (callout, card, tabs, steps, accordion, and more):

```bash
blume add callout
blume add pagination
```

The copy imports the rest of the framework from `blume/*`, so it renders exactly like the built-in until you change it. `blume add` prints the `defineComponents` snippet to register it — content components under `mdx`, layout pieces under `layout`.

## Astro integrations

Add any Astro integration from the top-level `integrations` array in `blume.config.ts`. Install the integration in your site first; Blume does not add it to the generated runtime's dependencies or manage its Astro compatibility.

```bash
npm install @astrojs/sitemap
```

```ts blume.config.ts lineNumbers
import sitemap from "@astrojs/sitemap";
import { defineConfig } from "blume";

export default defineConfig({
  integrations: [
    sitemap({
      filter: (page) => !page.includes("/drafts/"),
    }),
  ],
});
```

Blume keeps its built-in integrations in their existing order, then appends your entries in declaration order. It does not sort or deduplicate them, so two integrations with the same `name` both run. Blume validates that `integrations` is an array, while Astro validates each entry and reports invalid integrations.

Because Blume loads your integrations by re-importing `blume.config.ts` from the generated Astro config rather than copying the instances, the config module evaluates twice per run — once when Blume reads your config and once when Astro loads it. Keep integration factories side-effect free (return the integration; don't write files or open connections at construction) so the second evaluation is harmless.

The same integrations run in `blume dev` and `blume build`. Editing `blume.config.ts` during `blume dev` regenerates the hidden Astro config and triggers a config restart; if you don't see an edited integration take effect, restart `blume dev`. Blume can't tell which config edits affect integrations, so once `integrations` is non-empty, every edit to `blume.config.ts` — even to an unrelated field — restarts the dev server rather than hot-applying. Blume only tracks the contents of `blume.config.ts`, so editing a separate file it imports won't trigger that regeneration on its own — restart `blume dev` after such edits. If you eject, the owned `astro.config.mjs` keeps a relative bridge to `blume.config.ts`, so the configured integrations continue to run; you can later move them directly into the Astro config as part of taking full ownership.

## Eject

When you want full control, eject the generated runtime into a standalone Astro project:

```bash
blume eject --yes
```

Eject is a one-way step: the hidden `.blume/` runtime becomes a normal Astro app you own and can modify directly. The `blume` package stays importable, so you keep its components, theme, and Markdown processors.

### What eject leaves behind

After ejecting, your `build` script runs plain `astro build` — the site itself builds the same, but the artifacts `blume build` layered on top are no longer produced. The eject command warns about the ones your config actually uses. To keep them:

- **Pagefind search index** — with `search.provider: "pagefind"`, the search UI loads the index from the built site, so search breaks in production until you index it yourself. Install `pagefind` as a devDependency and index after each build: `"build": "astro build && pagefind --site dist"`.
- **Hosted search sync** — a hosted provider's index is no longer pushed on build; re-upload your search records after each build with the provider's API or CLI.
- **sitemap.xml** — recreate it with the standard [@astrojs/sitemap](https://docs.astro.build/en/guides/integrations-guide/sitemap/) integration.
- **robots.txt** — ship your own as `public/robots.txt`.
- **llms.txt / llms-full.txt and agent-readability.json** — write them by hand (or generate them in a build step of your own) and serve them from `public/`.
- **Platform redirect files** — `_redirects` and `vercel.json` are no longer emitted for static builds. Your redirects still work as Astro-generated meta-refresh pages, or you can move them into your host's own config.
