Structured Markdown components without framework lock-in
Write Markdown with validated, structured blocks. Render it anywhere. Built for content written by humans, CMSes, and LLMs.
$ pnpm add @contentbit/core @contentbit/blocksor 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/moValidation 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 nowshadcn 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 — 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-packregistry: https://contentbit.dev/r/{name}.json