A lightweight, markdown-first Storybook alternative. Author your
documentation as plain Markdown; reference component stories that live in
adjacent .tsx / .ts / .vue / .js files via directives. The build emits
a static, Starlight-style HTML site with full-text search, dark mode, an
llms.txt mirror, SEO tags, and a sitemap — plus portable, drop-in
embeds of any of your stories.
📖 Read the docs → · 🧩 React / Vue / Web Components · 🔍 Pagefind search · 📦 Portable story embeds
Status: pre-1.0. The public API (
defineConfig,build/dev/bundle, the adapter contract, directives, frontmatter, theme tokens) is documented and largely stable, but minor releases may still break things until v1.0 freezes it.
- Why Markbook
- Install
- Quick start
- Core concepts
- CLI
- Configuration
- What Markbook deliberately doesn't ship
- Packages
- Repository layout
- Contributing
- License
- 📄 Markdown is the source of truth. Every page is a
.mdfile; the HTML site and thellms.txtmirror are two views of one AST. No MDX, no JSX in your prose, no JSON sidecars, no JS templates to learn. - 🧩 Component stories, optional. Drop a
:::storydirective into any page to mount a React, Vue, or web-component example. Skip the directive — and the adapter — for a pure docs/marketing site. - 🔍 Search + SEO by default. Pagefind builds a full-text index
at build time. Canonical, Open Graph, Twitter Card,
sitemap.xml, androbots.txtare emitted automatically. - 🎨 Four layers of customization. Token overrides → opt out of base CSS → swap the HTML shell with your own layouts → post-process the final HTML. Each layer is opt-in; reach for the smallest one that solves your problem.
- 📦 Portable stories.
markbook bundleproduces self-contained ESM embeds (drop a<script type="module">on any page) or publishable npm packages with the framework as a peer dependency — stories that work anywhere. - ⚡ Fast dev loop. Vite under the hood. ~80 ms regeneration on a small site, including a full Pagefind re-index, with hot reload across markdown, CSS, layouts, and story files.
- 🛠️ Extensible directives. Beyond the three built-ins, register your own
:::namehandlers frommarkbook.config.ts— admonitions, video embeds, diagram renderers, any reusable markdown vocabulary your team needs.
# Core + CLI (markdown-only sites need nothing else)
npm install -D markbook @markbook/core
pnpm add -D markbook @markbook/core
yarn add -D markbook @markbook/coreFor live component stories, add the matching adapter and its runtime:
# React — adapter (dev) + react/react-dom runtime
npm install -D @markbook/adapter-react && npm install react react-dom
pnpm add -D @markbook/adapter-react && pnpm add react react-dom
yarn add -D @markbook/adapter-react && yarn add react react-dom
# Vue — adapter (dev) + vue runtime
npm install -D @markbook/adapter-vue && npm install vue
pnpm add -D @markbook/adapter-vue && pnpm add vue
yarn add -D @markbook/adapter-vue && yarn add vue
# Web components — no adapter dependency, no runtime
npm install -D @markbook/adapter-wc
pnpm add -D @markbook/adapter-wc
yarn add -D @markbook/adapter-wcEach block lists the npm / pnpm / yarn form of the same command — use whichever package manager your project uses.
Using an AI coding agent? Run
markbook skills installto drop Markbook-specific skills (markbook-init,markbook-add-component-page,markbook-bulk-generate,markbook-style,markbook-bundle-story) into.claude//.codex//.opencode//.agents/so your agent can scaffold pages and generate stories. See the CLI README.
A minimal markdown-only site:
my-site/
├─ pages/
│ └─ index.md
└─ markbook.config.ts
markbook.config.ts:
import { defineConfig } from '@markbook/core';
export default defineConfig({
title: 'My Project',
description: 'A short blurb about the site.',
});pages/index.md:
---
title: Welcome
description: The home page of my site.
---
# Hello, world
This is **markdown**. It becomes HTML — with search, dark mode, and a TOC.Then:
npx markbook dev # live dev server with HMR → http://localhost:5173
npx markbook build # static site in dist/
npx markbook preview # serve dist/ over HTTP → http://localhost:4173Markbook reads from
pages/ordocs/(configurable viacontentDir). Don't opendist/*.htmlviafile://— Pagefind loads its runtime through dynamicimport(), which browsers block onfile://. Usemarkbook preview.
Add an adapter to the config and reference a story file from markdown:
// markbook.config.ts
import { defineConfig } from '@markbook/core';
import { reactAdapter } from '@markbook/adapter-react/config';
export default defineConfig({
title: 'My Components',
adapter: reactAdapter(),
});// pages/Button/Button.stories.tsx
import { Button } from '../../../src/Button';
export default () => <Button variant="primary">Click me</Button>;<!-- pages/index.md -->
# Button
:::story{src=./Button/Button.stories.tsx}
:::Markbook mounts the live component where the directive was, with a Shiki-highlighted "Show code" disclosure underneath.
Each .md file under contentDir becomes a page. Subdirectories become
sidebar nav groups; H2/H3 headings become the on-this-page TOC. Frontmatter
controls per-page behavior:
| Field | Type | Purpose |
|---|---|---|
title |
string |
Page title (falls back to the first H1, then the file id). |
description |
string |
Muted lede under the H1; used for <meta name="description">. |
order |
number |
Sidebar position within the nav group (lower = earlier). |
template |
string |
Wrap the page in a markdown template from templatesDir. |
layout |
string | false |
Pick an HTML layout from layoutsDir, or false to force the built-in shell. |
component / componentExport |
string |
Target component for :::props. |
ogImage |
string |
Per-page Open Graph image (overrides config.ogImage). |
Markbook recognizes :::name{attr=value} (container) and ::name{attr=value}
(leaf) blocks on top of standard markdown — the syntax comes from
remark-directive; Markbook
layers a registry + dispatcher on top.
Three built-ins (tightly integrated with internal pipelines; cannot be overridden):
:::story{src=… [export=…] [id=…]}— mount a single story (the file's default export, or a namedexport).:::stories{src=… [only=A,B] [exclude=C]}— mount every named runtime export of a CSF-v3 story file, discovered via TypeScript AST analysis, one card per export.:::props— render a props table for a React component (frontmattercomponent:), generated from its TypeScript types viareact-docgen-typescript. The table is mirrored intollms.txttoo.
User directives — register your own from markbook.config.ts:
import { defineConfig, escapeAttribute } from '@markbook/core';
export default defineConfig({
directives: {
youtube: ({ attributes }) =>
`<iframe src="https://youtube.com/embed/${escapeAttribute(attributes.id ?? '')}" allowfullscreen></iframe>`,
callout: ({ attributes, innerHtml }) =>
`<aside class="callout callout-${attributes.type ?? 'info'}">${innerHtml ?? ''}</aside>`,
},
});Handlers can be async, read files (with dev-mode dependency tracking), return a
plain-markdown fallback for llms.txt, and live in their own modules. The
htmlTemplate(new URL('./callout.html', import.meta.url)) helper lets directive
markup live in a real .html file with {{ key }} substitution instead of
inline template literals. See the
custom directives guide.
Three thin adapters mount stories into placeholder elements; the core engine knows nothing about any framework.
| Adapter | Mounts | Runtime |
|---|---|---|
@markbook/adapter-react |
React components | react, react-dom (peer) |
@markbook/adapter-vue |
Vue 3 components | vue (peer) |
@markbook/adapter-wc |
Custom elements | none — vanilla DOM |
A story file is a regular component file. One story per file is the
convention (default export); multiple named exports fan out via :::stories.
Storybook CSF v3 object exports are supported:
export const Primary = {
render: (args) => <Button {...args}>Click me</Button>,
args: { variant: 'primary', disabled: false },
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary'] },
disabled: { control: 'boolean' },
},
parameters: { layout: 'centered' }, // centered | padded | fullscreen
};args— initial prop values. The React adapter renders an interactive controls panel under the story so readers can tweak props live.argTypes— control hints (text/number/boolean/select); inferred fromargswhen omitted.- Decorators — wrap every story in shared providers (theme, i18n, router)
via
reactAdapter({ decorators: ['./preview.tsx', './theme.tsx'] }). Applied outer-to-inner:['A', 'B']→<A><B><Story/></B></A>.
See the adding-stories guide.
markbook bundle packages stories as portable artifacts that work outside the
docs site:
markbook bundle # all stories → self-mounting ESM embeds
markbook bundle my-button # one story by id
markbook bundle --mode package # publishable npm package directories
markbook bundle --isolation shadow # wrap each mount in an open shadow rootembedmode →dist/embed/<slug>.js. Drop a placeholder anywhere:The bundled CSS is baked in and injected at mount time (into<div data-markbook-embed="my-button"></div> <script type="module" src="https://cdn.example.com/embed/my-button.js"></script>
document.head, or the shadow root with--isolation shadowso host-page CSS can't leak in).packagemode →dist/packages/<slug>/, a publishable npm package with the framework declared as a peer dependency.
Four escalating layers — use the smallest that solves your problem:
css— inline CSS files after the built-in chrome. Override the--mb-*theme tokens (--mb-bg,--mb-fg,--mb-accent,--mb-content-width, …) to rebrand without touching templates. A[data-theme="dark"]block re-declares the color tokens; the toggle is wired by an inline boot script.disableBaseCss— drop the built-in stylesheet entirely. The.markbook-*class names anddata-*hooks stay stable so you can restyle from scratch.layoutsDir+layout— replace the whole HTML shell with your own.htmllayouts, using{{ content }},{{ head }},{{ bodyEnd }},{{ search }},{{ themeToggle }},{{ pageActions }},{{ title }},{{ frontmatter.x }}, … placeholders (validated — unknown placeholders and a missing/duplicate{{ content }}throw).transformHtml(html, page)— an async escape hatch that post-processes each page's final HTML.
- Search — Pagefind indexes the built output (and the dev server).
Cmd/Ctrl+Kor/focuses the search box. - SEO — set
siteUrlto emit<link rel="canonical">,og:url,sitemap.xml, androbots.txt. Open Graph + Twitter Card +theme-color+color-schememeta are always injected. - llms.txt — every build emits a top-level
/llms.txtindex plus per-page plain-markdown mirrors at/llms/<page>.txt, surfaced via "View / Copy as Markdown" buttons on each page.
npx markbook <command> [options]| Command | Purpose |
|---|---|
build |
Build the static site to outDir (parse → layout → Vite bundle → llms.txt → sitemap → Pagefind). |
dev |
Vite dev server with HMR across markdown, CSS, layouts, and story files. --port, --host. |
preview |
Serve the built dist/ over HTTP (verify production output). |
bundle [storyId] |
Bundle one/all stories. --mode embed|package, --isolation shadow. |
skills install / skills list |
Manage the agent skills shipped in the npm package. |
Common flags: -c, --config <path> and --root <path>. Full details in the
CLI reference.
markbook.config.{ts,mts,js,mjs} exports a MarkbookConfig via defineConfig:
import { defineConfig } from '@markbook/core';
import { reactAdapter } from '@markbook/adapter-react/config';
export default defineConfig({
// Layout
contentDir: 'pages', // default 'docs'
outDir: 'dist',
publicDir: 'public', // static assets copied to the output root
// Identity + SEO
title: 'My Component Library',
description: 'A small set of accessible primitives.',
siteUrl: 'https://my-components.example', // enables canonical/OG/sitemap
themeColor: '#7c3aed',
ogImage: 'https://my-components.example/og.png',
// Customization
css: ['./brand.css'],
// disableBaseCss: true,
// layoutsDir: 'layouts', layout: 'default',
// transformHtml: async (html, page) => html,
// Component stories (omit for markdown-only sites)
adapter: reactAdapter({ decorators: ['./preview.tsx'] }),
// User directives
directives: { /* youtube, callout, … */ },
// Optional: "Open in playground" buttons (CodeSandbox / StackBlitz)
// playground: { providers: ['codesandbox', 'stackblitz'] },
dev: { port: 5173 },
});Every field, with defaults, lives in the
config reference and
in packages/core/README.md.
- No MDX. Markdown is markdown. To embed a component, use a story directive —
your component file stays a regular
.tsxyour tooling already understands. - No theme engine. Customize via CSS tokens or replace the shell. No theme-prop API, no provider hierarchy, no plugin framework to learn.
- No bundled UI framework. Markbook itself is plain HTML + minified IIFE boot scripts. Bring React/Vue for stories if you want them; the engine doesn't care.
| Package | Purpose |
|---|---|
markbook |
The markbook CLI (build, dev, preview, bundle, skills). |
@markbook/core |
Markdown parser, builder, dev server, embed bundler, directive registry. |
@markbook/adapter-react |
Mount React stories (+ controls + decorators). |
@markbook/adapter-vue |
Mount Vue 3 stories (+ decorators). |
@markbook/adapter-wc |
Mount vanilla web components (no runtime). |
@markbook/adapter-shared |
Shared browser runtime for the adapters (internal; see ADR-0026). |
packages/
core/ — markdown + builder + dev server + embed bundler + directives
cli/ — `markbook` binary (cac + jiti)
adapter-react/ — React mount + controls + decorators
adapter-vue/ — Vue 3 mount + decorators
adapter-wc/ — web-components mount (no framework runtime)
adapter-shared/ — shared pure-DOM runtime for the adapters
examples/
react-demo/ — Pixie component library — the canonical dogfood
vue-demo/ — Counter component in Vue
wc-demo/ — <click-counter> custom element
static-demo/ — Skyline: a markdown-only docs site, no adapter
marketing-demo/ — Cumulus: a marketing site with a fully custom layout
markbook-site/ — the official Markbook website (hybrid layout, custom :::callout)
embed-host/ — external consumer of the React demo's embed bundles
pnpm install # bootstrap the workspace
pnpm build # compile every @markbook/* package (tsc -b, topological)
pnpm test # @markbook/core + CLI Vitest suites
pnpm typecheck # tsc --noEmit across packages (resolves from source — no prior build needed)
pnpm lint # biome check
pnpm examples:dev # every example dev server in parallel (ports 5173+)
pnpm examples:build # build every exampleConventions live in AGENTS.md; architectural decisions in
DECISIONS.md; the running development journal in
PROGRESS.md; planned work in ROADMAP.md.
Personal project — license TBD before v1.0.