# Structured Markdown components without framework lock-in

> Source: <https://contentbit.dev/>
> Published: 2026-06-12 07:20:01+00:00

# 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 dlx contentbit@latest init`

or see [a complete article](/blog/llm-markdown-that-cannot-break/) 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.

``` js
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.

[All blocks →](/blocks/)

```
:::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
