diff --git a/docs/README.md b/docs/README.md
deleted file mode 100644
index c86671d..0000000
--- a/docs/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# docs
-
-This is a Waku application generated with [Create Fumadocs](https://github.com/fuma-nama/fumadocs).
-
-Run development server:
-
-```bash
-npm run dev
-# or
-pnpm dev
-# or
-yarn dev
-```
diff --git a/docs/biome.json b/docs/biome.json
deleted file mode 100644
index 1381c95..0000000
--- a/docs/biome.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
- "vcs": {
- "enabled": true,
- "clientKind": "git",
- "useIgnoreFile": true
- },
- "files": {
- "ignoreUnknown": true,
- "includes": [
- "**",
- "!node_modules",
- "!.source"
- ]
- },
- "formatter": {
- "enabled": true,
- "indentStyle": "space",
- "indentWidth": 2
- },
- "linter": {
- "enabled": true,
- "rules": {
- "recommended": true
- },
- "domains": {
- "react": "recommended"
- }
- },
- "assist": {
- "actions": {
- "source": {
- "organizeImports": "on"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/docs/content/02-structure.mdx b/docs/content/02-structure.mdx
new file mode 100644
index 0000000..e69de29
diff --git a/docs/content/03-faq.mdx b/docs/content/03-faq.mdx
new file mode 100644
index 0000000..605427c
--- /dev/null
+++ b/docs/content/03-faq.mdx
@@ -0,0 +1,162 @@
+---
+title: FAQ
+description: Common questions about Blume — how it compares to other documentation tools, and why a Markdown formatter might collapse your callout directives.
+sidebar:
+ label: FAQ
+---
+
+Answers to questions that come up often. Missing one? [Open an issue](https://github.com/haydenbleasel/blume/issues) or ask the in-page assistant.
+
+## How is Blume different from Mintlify, Fumadocs, and others?
+
+Most documentation tools sit at one of two extremes. **Managed platforms** like Mintlify give you a polished result fast, but the build and hosting are their service — you author inside their system and deploy to their infrastructure. **Component libraries and starters** like Fumadocs, Nextra, or Docusaurus are open-source and flexible, but they hand you an application (a Next.js or React project) that you scaffold, wire up, and maintain before and after you write a word.
+
+Blume takes a third path: **the framework is the template.** You point it at a folder of Markdown and it generates and drives the whole site — navigation, search, theming, Open Graph images, SEO, and AI endpoints — with no app to own. It's fully open-source and self-hostable, so there's no managed service and no vendor lock-in, but there's also no boilerplate to maintain.
+
+| | Blume | Mintlify | Fumadocs / Nextra / Docusaurus |
+| --- | --- | --- | --- |
+| **Model** | Zero-config framework; content only | Hosted platform | Library + app you scaffold |
+| **Source** | Open-source (MIT) | Closed core | Open-source |
+| **Hosting** | Anywhere — static or a server function | Their managed infrastructure | Anywhere; you build and deploy |
+| **You maintain** | Your Markdown | Your Markdown + platform config | Your Markdown + the app around it |
+| **Rendering** | Astro; core theme ships zero client JS | Their runtime | React/Next.js runtime |
+| **AI features** | `llms.txt`, raw Markdown, Ask AI, MCP — built in, no hosted service | Built in (hosted) | Bring your own |
+
+A few consequences worth calling out:
+
+- **You own the output.** `blume build` produces a plain site you host on Vercel, Netlify, Cloudflare, S3, or your own box. Nothing phones home.
+- **No lock-in, two ways out.** Your content is portable Markdown, and `blume eject` turns the project into a standalone Astro app that still uses the `blume` package when you want full control.
+- **Fast by default.** The core theme is React-free and renders static HTML, so pages score well on Core Web Vitals without tuning. You opt into server features (Ask AI, MCP) only when you need them.
+- **Type-safe configuration.** `blume.config.ts` and every `meta.ts` are real TypeScript validated by a schema — not loosely-typed YAML.
+
+:::note
+This isn't "better than everything" — managed platforms and full frameworks are the right call when you want a hosted product or maximum control over the app. Blume is for teams who want a production-grade docs site without owning either the platform or the plumbing.
+:::
+
+See [Why Blume exists](/docs) for the longer version.
+
+## Is Blume free and open-source?
+
+Yes — Blume is MIT-licensed and free. You install the `blume` package, keep your content in your own repository, and host the build wherever you like. There's no paid tier, no per-seat pricing, and no account to sign up for. The source lives on [GitHub](https://github.com/haydenbleasel/blume).
+
+## Do I need to know Astro, React, or Tailwind?
+
+No. A folder of Markdown is a complete site — navigation, search, and theming are inferred or set with a handful of tokens. You only reach for the underlying stack when you want to customize: [interactive islands](/docs/content/islands) (React), [component overrides](/docs/configuration/customization), or [theme tokens](/docs/configuration/theming) (Tailwind). Even then, [`blume.config.ts`](/docs/configuration) is typed, so your editor guides you.
+
+## Can I use React components and MDX?
+
+Yes. Any page can be `.md` or `.mdx`, and MDX lets you drop in the [built-in components](/docs/content/components) with no imports. You can also add your own `.tsx`/`.jsx` [islands](/docs/content/islands) — Blume auto-enables React only for the pages that use them, so the core theme stays JavaScript-free everywhere else.
+
+## Where can I deploy it?
+
+Anywhere. `blume build` outputs static HTML by default, which you can serve from any static host or CDN — Vercel, Netlify, Cloudflare Pages, GitHub Pages, S3, or your own server. Server-only features (Ask AI, the MCP server, on-demand rendering) switch the build to a server function through an adapter for Vercel, Node, Netlify, or Cloudflare. See [Deployment](/docs/deployment).
+
+## Does search need a hosted service?
+
+No. [Orama](/docs/configuration/search) builds a local index that works in both dev and production with nothing to host or pay for. For very large sites, [Pagefind](/docs/configuration/search) is one flag away. Either way the index ships as part of your site.
+
+## How do I customize the look?
+
+Start with [theme tokens](/docs/configuration/theming) — accent color, fonts, radius, and a `theme.css` for anything else Tailwind can express. Go further by [overriding built-in components](/docs/configuration/customization) or adding [custom pages](/docs/configuration/customization#custom-pages). When you want the Astro project itself, [`blume eject`](/docs/reference/cli) hands you a standalone app that still uses the `blume` package.
+
+## Why is oxfmt / Ultracite collapsing my directives?
+
+If you format your Markdown with [Ultracite](https://www.ultracite.ai) (which runs oxlint + [oxfmt](https://oxc.rs)) — as Blume itself does — you may notice that container directives get flattened onto a single line after a format pass:
+
+```md
+:::note
+Regenerate the project with blume dev.
+:::
+```
+
+becomes
+
+```md
+:::note Regenerate the project with blume dev. :::
+```
+
+Once the opening `:::note` fence is joined to the prose, it's no longer a directive, so it renders as literal text instead of a [callout](/docs/content/syntax#callouts).
+
+### Why it happens
+
+This is a bug in oxfmt's Markdown formatter (inherited from Prettier's Markdown printer — see [prettier/prettier#19040](https://github.com/prettier/prettier/pull/19040)). When it wraps prose, it treats the `:::` fence lines as ordinary text and joins them with the adjacent line, breaking the directive. It affects every container directive type — `:::note`, `:::tip`, `:::info`, `:::warning`, `:::danger`, `:::success`.
+
+We reported it upstream in [oxc-project/oxc#24096](https://github.com/oxc-project/oxc/issues/24096); until it's fixed there, the patch below is the workaround.
+
+### The fix
+
+Patch oxfmt so it preserves the line break that sits directly against a `:::` fence. Blume ships the same fix in its own repo, and you can apply it in any project.
+
+1. Save the patch as `patches/oxfmt@0.61.0.patch`:
+
+ ```diff patches/oxfmt@0.61.0.patch
+ diff --git a/dist/markdown-ZuiQU4Xe.js b/dist/markdown-ZuiQU4Xe.js
+ index 566b9e6d27f36061d64b93736e238e871e1ee2b2..82d0595acc010807c2939fc4a1717dde887a8555 100644
+ --- a/dist/markdown-ZuiQU4Xe.js
+ +++ b/dist/markdown-ZuiQU4Xe.js
+ @@ -4875,7 +4875,43 @@ function lu(e, t, r) {
+ case "sentence": return Oh(e, r);
+ case "word": return t.parser !== "mdx" ? zh(e, t) : Uh(e);
+ case "whitespace": {
+ - let { next: a } = e, u = a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !NE(e) && !(t.proseWrap === "preserve" && RE(e)) ? "never" : t.proseWrap;
+ + let { next: a, previous: oxfmtFencePrev } = e;
+ + // Preserve line breaks that sit directly against a `:::` container
+ + // directive fence, so `proseWrap: "never"` keeps the opening/closing
+ + // fence on their own lines instead of joining them into the prose (which
+ + // breaks the directive). Ordinary prose still wraps per proseWrap.
+ + // See prettier/prettier#19040.
+ + let oxfmtIsFence = (w) => w != null && typeof w.value === "string" && w.value.startsWith(":::");
+ + // A titled directive (`:::warning[Heads up]`) parses its `[title]` as a
+ + // linkReference between two sentence nodes at the paragraph level: the
+ + // fence word ends the sentence before the reference, and the body's
+ + // leading newline opens the sentence after it. So when this whitespace
+ + // starts its sentence, climb to the paragraph and check whether the two
+ + // preceding siblings are a (link) reference and a sentence ending in a
+ + // `:::` fence word.
+ + let oxfmtPrevIsTitledFence = !1;
+ + if (oxfmtFencePrev == null && e.index === 0 && e.grandparent != null && Array.isArray(e.grandparent.children)) {
+ + let oxfmtSibs = e.grandparent.children, oxfmtSentIdx = oxfmtSibs.indexOf(e.parent);
+ + if (oxfmtSentIdx >= 2) {
+ + let oxfmtLink = oxfmtSibs[oxfmtSentIdx - 1], oxfmtBefore = oxfmtSibs[oxfmtSentIdx - 2];
+ + let oxfmtLastWord = oxfmtBefore && oxfmtBefore.type === "sentence" && Array.isArray(oxfmtBefore.children) ? oxfmtBefore.children[oxfmtBefore.children.length - 1] : null;
+ + oxfmtPrevIsTitledFence = oxfmtLink != null && (oxfmtLink.type === "linkReference" || oxfmtLink.type === "link") && oxfmtIsFence(oxfmtLastWord);
+ + }
+ + }
+ + // The plain-markdown parser keeps a titled fence's `[title]` as literal
+ + // words, so the whole directive is one sentence. For a newline
+ + // whitespace, walk back to the start of its visual line within the
+ + // sentence; a line led by a `:::` word is a fence whose break must stay.
+ + if (!oxfmtPrevIsTitledFence && e.node.value.includes("\n") && e.parent != null && Array.isArray(e.parent.children)) {
+ + let oxfmtLineFirst = null;
+ + for (let oxfmtJ = e.index - 1; oxfmtJ >= 0; oxfmtJ--) {
+ + let oxfmtSib = e.parent.children[oxfmtJ];
+ + if (oxfmtSib.type === "whitespace" && typeof oxfmtSib.value === "string" && oxfmtSib.value.includes("\n")) break;
+ + oxfmtLineFirst = oxfmtSib;
+ + }
+ + oxfmtPrevIsTitledFence = oxfmtIsFence(oxfmtLineFirst);
+ + }
+ + let u = oxfmtIsFence(oxfmtFencePrev) || oxfmtPrevIsTitledFence || oxfmtIsFence(a) ? "preserve" : a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !NE(e) && !(t.proseWrap === "preserve" && RE(e)) ? "never" : t.proseWrap;
+ return ou(e, n.value, u, !1, t);
+ }
+ case "emphasis": {
+ ```
+
+2. Register it with your package manager's `patchedDependencies`. With Bun or pnpm, add to `package.json`:
+
+ ```json package.json
+ {
+ "patchedDependencies": {
+ "oxfmt@0.61.0": "patches/oxfmt@0.61.0.patch"
+ }
+ }
+ ```
+
+3. Reinstall so the patch is applied:
+
+ ```package-install
+ bun install
+ ```
+
+:::warning[Version-pinned]
+The patch targets a specific oxfmt build — its diff references a file whose name is hashed per release (`dist/markdown-*.js`). When you bump oxfmt, regenerate the patch (e.g. `bun patch oxfmt`) or check whether the upstream fix has landed and the patch is no longer needed.
+:::
diff --git a/docs/content/coding/index.mdx b/docs/content/coding/index.mdx
new file mode 100644
index 0000000..2907dd7
--- /dev/null
+++ b/docs/content/coding/index.mdx
@@ -0,0 +1,13 @@
+---
+title: 'Coding'
+sidebar:
+ label: Overview
+ icon: arrow-right-circle
+ order: 1
+---
+
+## Description
+
+Given two integer arrays `nums1` and `nums2`, return the minimum number of operations required to make both arrays equal.
+
+You have the following three choices to modify two arrays:
diff --git a/docs/content/coding/two-pointers/index.mdx b/docs/content/coding/two-pointers/index.mdx
new file mode 100644
index 0000000..9d83436
--- /dev/null
+++ b/docs/content/coding/two-pointers/index.mdx
@@ -0,0 +1,14 @@
+---
+title: 'Two Pointers'
+sidebar:
+ label: Overview
+ icon: arrow-right-circle
+ badge: Easy
+ order: 1
+---
+
+## Description
+
+Given two integer arrays `nums1` and `nums2`, return the minimum number of operations required to make both arrays equal.
+
+You have the following three choices to modify two arrays:
diff --git a/docs/content/coding/two-pointers/problem.mdx b/docs/content/coding/two-pointers/problem.mdx
new file mode 100644
index 0000000..e69de29
diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx
deleted file mode 100644
index 6c70e42..0000000
--- a/docs/content/docs/index.mdx
+++ /dev/null
@@ -1,31 +0,0 @@
----
-title: Hello World
-description: Your favourite docs framework.
-icon: Rocket
----
-
-Hey there! Fumadocs is a docs framework that works on Waku!
-
-## Heading
-
-Hello World!
-
-
-
-
-
-
-### CodeBlock
-
-```ts
-console.log('Hello World');
-```
-
-#### Table
-
-| Head | Description |
-| ------------------------------- | ----------------------------------- |
-| `hello` | Hello World |
-| very **important** | Hey |
-| _Surprisingly_ | Fumadocs |
-| very long text that looks weird | hello world hello world hello world |
diff --git a/docs/content/docs/test.mdx b/docs/content/docs/test.mdx
deleted file mode 100644
index 68e1f4c..0000000
--- a/docs/content/docs/test.mdx
+++ /dev/null
@@ -1,12 +0,0 @@
----
-title: Test
-description: This is another page
----
-
-Hello World again!
-
-## Installation
-
-```npm
-npm i fumadocs-core fumadocs-ui
-```
diff --git a/docs/content/index.mdx b/docs/content/index.mdx
new file mode 100644
index 0000000..bb643ac
--- /dev/null
+++ b/docs/content/index.mdx
@@ -0,0 +1,10 @@
+---
+title: Introduction
+description: Welcome to your new Blume docs.
+---
+
+# Introduction
+
+Welcome to **Blume** — markdown-first docs powered by Astro and Vite.
+
+Edit `content/index.mdx` to get started, then run `blume dev`.
diff --git a/docs/content/system-design/index.mdx b/docs/content/system-design/index.mdx
new file mode 100644
index 0000000..2907dd7
--- /dev/null
+++ b/docs/content/system-design/index.mdx
@@ -0,0 +1,13 @@
+---
+title: 'Coding'
+sidebar:
+ label: Overview
+ icon: arrow-right-circle
+ order: 1
+---
+
+## Description
+
+Given two integer arrays `nums1` and `nums2`, return the minimum number of operations required to make both arrays equal.
+
+You have the following three choices to modify two arrays:
diff --git a/docs/src/components/mdx.tsx b/docs/src/components/mdx.tsx
deleted file mode 100644
index a640575..0000000
--- a/docs/src/components/mdx.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import defaultMdxComponents from 'fumadocs-ui/mdx';
-import type { MDXComponents } from 'mdx/types';
-
-export function getMDXComponents(components?: MDXComponents) {
- return {
- ...defaultMdxComponents,
- ...components,
- } satisfies MDXComponents;
-}
-
-export const useMDXComponents = getMDXComponents;
-
-declare global {
- type MDXProvidedComponents = ReturnType;
-}
diff --git a/docs/src/components/provider.tsx b/docs/src/components/provider.tsx
deleted file mode 100644
index fb30046..0000000
--- a/docs/src/components/provider.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-'use client';
-import type { ReactNode } from 'react';
-import { RootProvider } from 'fumadocs-ui/provider/waku';
-
-export function Provider({ children }: { children: ReactNode }) {
- return {children};
-}
diff --git a/docs/src/lib/cn.ts b/docs/src/lib/cn.ts
deleted file mode 100644
index 241ffb3..0000000
--- a/docs/src/lib/cn.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { cn } from 'cnfast';
diff --git a/docs/src/lib/layout.shared.tsx b/docs/src/lib/layout.shared.tsx
deleted file mode 100644
index 88b9ca8..0000000
--- a/docs/src/lib/layout.shared.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
-import { appName, gitConfig } from './shared';
-
-export function baseOptions(): BaseLayoutProps {
- return {
- nav: {
- // JSX supported
- title: appName,
- },
- githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`,
- };
-}
diff --git a/docs/src/lib/shared.ts b/docs/src/lib/shared.ts
deleted file mode 100644
index 4fa88de..0000000
--- a/docs/src/lib/shared.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export const appName = 'Waku';
-export const docsRoute = '/docs';
-export const docsImageRoute = '/og/docs';
-export const docsContentRoute = '/llms.mdx/docs';
-
-// fill this with your actual GitHub info, for example:
-export const gitConfig = {
- user: 'fuma-nama',
- repo: 'fumadocs',
- branch: 'main',
-};
diff --git a/docs/src/lib/source.ts b/docs/src/lib/source.ts
deleted file mode 100644
index 2e25e58..0000000
--- a/docs/src/lib/source.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { loader } from 'fumadocs-core/source';
-import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
-import { docsContentRoute, docsImageRoute, docsRoute } from './shared';
-import { defineDocs } from 'fumadocs-mdx/macro';
-import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
-
-const docs = defineDocs({
- dir: 'content/docs',
- docs: {
- schema: pageSchema,
- postprocess: {
- includeProcessedMarkdown: true,
- },
- },
- meta: {
- schema: metaSchema,
- },
-});
-
-export const source = loader({
- source: docs.toFumadocsSource(),
- baseUrl: docsRoute,
- plugins: [lucideIconsPlugin()],
-});
-
-export function getPageImageUrl(page: (typeof source)['$inferPage']) {
- const segments = [...page.slugs, 'image.webp'];
-
- return '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/');
-}
-
-export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
- const segments = [...page.slugs, 'content.md'];
-
- return {
- segments,
- url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
- };
-}
-
-export async function getLLMText(page: (typeof source)['$inferPage']) {
- const processed = await page.data.getText('processed');
-
- return `# ${page.data.title} (${page.url})
-
-${processed}`;
-}
diff --git a/docs/src/pages/(home)/404.tsx b/docs/src/pages/(home)/404.tsx
deleted file mode 100644
index 0a9b00d..0000000
--- a/docs/src/pages/(home)/404.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { DefaultNotFound as default } from 'fumadocs-ui/layouts/home/not-found';
diff --git a/docs/src/pages/(home)/_layout.tsx b/docs/src/pages/(home)/_layout.tsx
deleted file mode 100644
index dbecdd5..0000000
--- a/docs/src/pages/(home)/_layout.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { ReactNode } from 'react';
-import { baseOptions } from '@/lib/layout.shared';
-import { HomeLayout } from 'fumadocs-ui/layouts/home';
-
-export default function Layout({ children }: { children: ReactNode }) {
- return {children};
-}
diff --git a/docs/src/pages/(home)/index.tsx b/docs/src/pages/(home)/index.tsx
deleted file mode 100644
index 58a364a..0000000
--- a/docs/src/pages/(home)/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Link } from 'waku';
-
-export default function Home() {
- return (
-