Write Markdown with validated, structured blocks. Render it anywhere. Built for content written by humans, CMSes, and LLMs.
$ pnpm dlx contentbit@latest init
or see a complete article rendered by the library
01·The idea
Markdown in, components out #
Authors write directive blocks inside ordinary Markdown. The parser builds a source-mapped AST, the registry validates it, and your renderer of choice takes it from there. Below: the actual styled pack rendering live.
:::key-metrics- 65% | Hydration- 24h | Cold ferment- 250g | Ball weight- 450°C | Oven temp::: :::tabs::tab{title="Stand mixer"}Dough hook, speed 2, eight minutes. Stop when the dough **clears the bowl**.::tab{title="By hand"}Fold every 30 minutes, four times. Slower, same gluten.::: :::comparison{left="Fresh yeast" right="Instant"}- Amount | 9g | 3g- Where to buy | Bakeries | Everywhere- Flavor | Slightly richer | Neutral::: :::callout{type="tip" title="Same source, every target"}This panel is the real React pack. Prose runs through [react-markdown](https://github.com/remarkjs/react-markdown), exactly like your app would wire it.:::
Dough hook, speed 2, eight minutes. Stop when the dough clears the bowl.
| Fresh yeast | Instant | |
|---|---|---|
| Amount | 9g | 3g |
| Where to buy | Bakeries | Everywhere |
| Flavor | Slightly richer | Neutral |
02·The safety net
Errors with line numbers, not broken pages #
Validation runs before rendering: in your editor, your CI, or your agent loop. Diagnostics carry a code, a position, and a fix hint, so an LLM can repair its own output.
:::comparison{left="Basic"}
- Price | Free
:::
broken.md:1:1 error CB_PROPS_INVALIDcomparison: prop "right" Invalid input: expected string, received undefinedbroken.md:2:1 error CB_ROW_COLUMNS:::comparison rows require 3 columns (label | left | right). Found 2.hint: Format: - label | left | rightbroken.md:1:1 error CB_ROW_COUNT:::comparison needs at least 2 rows, found 0.
03·The system
One definition, every surface #
No framework lock-in
The content is a protocol. Renderers are adapters: React and static HTML today, plain Markdown always.
- Price | Free
- Price | Free | $12/mo
Validation before render
Every block has a schema. Bad content fails with file:line:col diagnostics, not broken pages.
## Dough basics
Weigh everything. Volume
measures drift by 20%.
:::callout{type="tip"}
Cold ferment for flavor.
:::
Still just Markdown
Documents stay readable in any text editor. Strip the renderer and the content still makes sense.
↳ generated from the registry
Made for generated content
The registry that validates content also writes the authoring instructions for LLMs, so prompts never drift from the rules.
components/
└─ content-blocks/
└─ tabs-block.tsx ← yours now
shadcn distribution
Styled components install as editable source files through a shadcn registry. You own them after install.
const pricingTable = defineBlock({
name: 'pricing-table',
props: z.object({ currency: z.enum(['usd', 'eur']) }),
content: pipeRows({ columns: ['plan', 'price'] }),
authoring: { useWhen: [...], example },
})
Extensible registry
A custom block is a name, a zod props schema, a content model, and authoring guidance, in under 20 lines. It validates, renders, and documents itself from that one definition.
04·The generic pack
Eight blocks that work in any niche #
Pick a block. The example is its real authoring guidance from the registry, the same text LLMs get, rendered live by the styled pack.
:::callout{type="tip" title="Worth knowing"}Always weigh flour — volume measures drift by 20%.:::
Use when: Practical advice that prevents a common mistake (tip)
Highlighted note, tip, warning, important, or TLDR box.
05·Styled pack
Install the components, own the code #
The React pack ships through a shadcn registry. Components land in your app as editable source files: Tailwind, your tokens, your rules.
$ pnpm dlx shadcn@latest add @contentbit/generic-pack
registry: https://contentbit.dev/r/{name}.json