` until clean.
For projects without generated LLM-agent files, add the same loop to your own skill or
rules file. The CLI stays the source of truth.
---
# Plug in your Markdown library
URL: https://contentbit.dev/docs/guides/markdown
Content Blocks is **not** a Markdown renderer. It parses the block layer (`:::callout`,
`::tab`, pipe rows) and hands every prose segment to one function you provide:
- paragraphs between blocks
- Markdown bodies inside blocks
- Markdown strings stored in parsed block data
React returns a `ReactNode`. Astro returns an HTML string or promise. If you do
not provide this function, the built-in default only escapes text and wraps
paragraphs in `` tags. `**bold**`, links, and lists will show as literal
characters.
For the bigger picture, see [Rendering model](/docs/concepts/rendering-model).
## What your library sees (and never sees)
```text
your markdown file what reaches YOUR renderer
───────────────────── ──────────────────────────
Some intro **prose**. → "Some intro **prose**."
← blocks never reach it
:::callout{type="tip"}
Inline *formatting* too. → "Inline *formatting* too."
:::
```
Your library is only ever called with plain Markdown strings. It never sees `:::`
lines, props, or pipe rows — no plugins or escaping rules needed on its side.
## React + react-markdown
**Step 1 — install:**
```bash
pnpm add react-markdown
```
**Step 2 — create one component** (style the elements however your app does):
```tsx
// components/markdown.tsx
import ReactMarkdown from 'react-markdown'
export function Markdown({ source }: { source: string }) {
return {source}
}
```
**Step 3 — pass it once:**
```tsx
import { ContentBlocks } from '@contentbit/react'
import { Markdown } from '@/components/markdown'
}
/>
```
Done. Every prose segment and every block body now renders through react-markdown.
(This site's playground uses exactly this setup.)
## React + an existing remark/MDX pipeline
If your app already renders Markdown (a blog or content front end), reuse that component —
the contract is just `(md: string) => ReactNode`:
```tsx
} />
```
## Plain Markdown target
`renderToMarkdown` needs nothing — its output *is* Markdown:
```ts
import { renderToMarkdown } from '@contentbit/core'
import { genericMarkdownRenderers } from '@contentbit/blocks'
const md = renderToMarkdown(result.document, { renderers: genericMarkdownRenderers })
```
## Gotchas
- **The default is intentionally minimal.** Escaped text + `` paragraphs, nothing
else. If formatting "doesn't work", you haven't wired this page yet.
- **One function covers everything.** Prose between blocks and Markdown bodies inside
blocks (`callout`, `tab`, `faq-item`) all flow through the same `renderMarkdown`.
Astro block components receive that function as `ctx.renderMarkdown`.
- **Security split:** Content Blocks escapes everything *it* renders (props, rows,
titles). Whatever your `renderMarkdown` returns is your responsibility.
- **Headings inside prose work normally** — but keep document structure (H1/H2) in
your page chrome, not in block bodies.
---
# Start from the production Astro template
URL: https://contentbit.dev/docs/guides/production-astro
Use [`astro-speedrun-seo`](https://github.com/agonist/astro-speedrun-seo) when
you need a production-shaped Astro application, not only a renderer example.
It is maintained as a separate consumer repository so its CI catches the same
installation and integration problems that an application will encounter.
The template includes:
- a fully static Astro site with English and French routes
- typed Contentbit blocks and strict content validation
- page-family contracts, briefs, Doctor, Studio, and agent guidance
- canonical URLs, hreflang, Open Graph, Twitter metadata, and JSON-LD
- build-time social images, sitemap, robots.txt, llms.txt, and localized feeds
- a typed programmatic glossary section and locale fallback behavior
## Start a project
Create a repository from the GitHub template, then run:
```bash
pnpm install
pnpm dev
```
Before changing content, run the complete gate once:
```bash
pnpm check
```
That establishes a clean baseline across formatting, linting, types, tests,
content validation, Doctor policy, and the production build.
## Make the first production changes
1. Set the site URL, name, and social account in
`apps/web/src/site.config.ts`.
2. Replace the example authors and posts under `apps/web/src/content/`.
3. Replace the glossary dataset in `apps/web/src/data/glossary.ts`, or remove
that example section.
4. Update locale dictionaries and remove locales you do not plan to ship.
5. Run `pnpm content:doctor`, inspect the site with
`pnpm content:studio`, then finish with `pnpm check`.
## Understand repository ownership
Contentbit owns portable content contracts, parsing, validation, briefs, the
link graph, and review tooling. The template owns host behavior: routes,
metadata, structured data, feeds, social images, locale fallback, and visual
components. The boundary is intentional—SEO behavior stays testable in the
application without becoming a Contentbit runtime requirement.
The small starters inside the Contentbit repository are package compatibility
fixtures. Use this separate template as the example for production application
architecture.
## Continue learning
The template's README contains a recommended path through its executable
onboarding articles. Return to the Contentbit docs for portable workflows:
- [Programmatic SEO workflow](/docs/guides/programmatic-seo)
- [Content Doctor](/docs/guides/doctor)
- [Content Studio](/docs/guides/studio)
- [Astro rendering](/docs/guides/astro)
- [Stability and compatibility](/docs/compatibility)
---
# Build a programmatic SEO workflow
URL: https://contentbit.dev/docs/guides/programmatic-seo
This guide takes one page family from configuration to a repeatable publishing
gate. By the end, you can brief a page before its Markdown file exists, give the
same plan to a writer or agent, and catch structural or linking gaps before the
page ships.
The working loop is:
1. Connect Contentbit to the project.
2. Define the page family.
3. Plan one page.
4. Generate its writing brief.
5. Inspect the finished draft.
6. Run the publishing gate.
## Before you start
If Contentbit is not installed yet, scaffold the project with SEO support:
```bash
npx contentbit@latest init --seo
```
This creates `contentbit.config.ts` and a starter
`contentbit.seo.config.ts`. You can use the same steps below in an existing
installation by adding those files yourself.
## 1. Connect the project
Put the shared command settings in `contentbit.config.ts`:
```ts
import { defineContentConfig } from '@contentbit/core'
export default defineContentConfig({
content: 'content/**/*.md',
registry: './blocks/registry.ts',
seo: './contentbit.seo.config.ts',
})
```
Contentbit now knows which files to scan, which block registry to load, and
where the SEO contracts live. The commands in the rest of this guide do not
need repeated glob, registry, or SEO config flags.
Run a first scan to confirm the project loads:
```bash
contentbit doctor
```
It is normal to see SEO findings at this point. The next steps define what the
pages should satisfy.
## 2. Define the page family
A page type describes the structure shared by every page in a family. This
example defines glossary pages:
```ts
// contentbit.seo.config.ts
import { defineSeoConfig } from '@contentbit/core'
export default defineSeoConfig({
pageTypes: {
glossary: {
requiredFrontmatter: ['name', 'summary', 'seoKeywords.primary'],
requiredSections: [
{ id: 'definition', headings: ['What it means', 'Definition'] },
{ id: 'why-it-matters', headings: ['Why it matters'] },
],
recommendedBlocks: ['quick-ref', 'faq'],
minOutgoingLinks: 2,
},
},
pageDefaults: [
{
pathPrefix: 'content/glossary/',
type: 'glossary',
intent: 'definition',
},
],
})
```
`pageDefaults` applies the glossary contract to existing files in that folder.
Use explicit page plans when a page does not exist yet or needs its own target
keywords and links.
For every available contract field, see
[SEO briefs and contracts](/docs/guides/seo-briefs).
## 3. Plan one page
Add the first planned page to the same SEO config:
```ts
// Add this beside pageTypes and pageDefaults.
pages: {
'content/glossary/recipe-import/en.md': {
type: 'glossary',
key: 'glossary/recipe-import',
slug: 'recipe-import',
title: 'Recipe import',
intent: 'definition',
keywords: {
primary: 'recipe import',
secondary: ['recipe importer', 'save recipes from websites'],
},
linksTo: ['glossary/recipe-manager', 'guides/save-recipes-from-websites'],
},
},
```
The file does not need to exist yet. Contentbit can now resolve the page by its
stable key or slug and combine its target facts with the glossary contract.
Check that the plan resolves:
```bash
contentbit brief glossary/recipe-import
```
You should see the page target, required sections, recommended blocks, required
links, related pages, and acceptance checks.
## 4. Brief the writer
Use the Markdown brief for a human or an editorial ticket:
```bash
contentbit brief glossary/recipe-import
```
Use JSON when a coding agent or script will consume it:
```bash
contentbit brief glossary/recipe-import --json
```
Give the brief to the writer as the contract for this page. A short agent
instruction is enough:
```text
Write the planned glossary/recipe-import page from this Contentbit brief.
Preserve the target facts, include every required section, satisfy the required
links, and run contentbit doctor before you finish.
```
The brief defines the page structure. The live registry explains the syntax for
any structured blocks:
```bash
contentbit instructions --audience llm
```
This separation keeps the page plan focused while ensuring the agent never has
to guess block names, props, or body formats.
## 5. Inspect the draft
Once the writer creates the Markdown file, open Studio:
```bash
contentbit studio
```
Open the page and check four views:
1. **Preview** for the rendered page.
2. **Brief** for the original target and acceptance checks.
3. **Findings** for structural, block, and SEO issues.
4. **Links** for outgoing links and backlinks.
The live frontmatter becomes the source for page facts such as `title`, `slug`,
`intent`, `keywords`, and `linksTo`. The page plan continues to supply anything
the file does not override.
## 6. Run the publishing gate
Run Doctor after the draft exists:
```bash
contentbit doctor
```
The report combines normal content health checks with the SEO contract. It can
flag:
- missing required frontmatter
- missing required sections
- missing required or recommended blocks
- missing required links
- too few outgoing links or backlinks
- invalid block markup, thin sections, and missing image alt text
Fix required findings, then run the strict gate:
```bash
contentbit doctor --strict-seo
```
The command exits with a failure when required SEO findings remain, so the same
check works for a local handoff, an agent repair loop, and CI.
## 7. Add the gate to CI
Keep the package scripts short because `contentbit.config.ts` already carries
the project settings:
```json
{
"scripts": {
"content:doctor": "contentbit doctor --strict-seo",
"studio": "contentbit studio"
}
}
```
Run `content:doctor` in the same CI job that builds the site. A page that no
longer satisfies its family contract will fail before publishing.
## Multilingual page families
For translated slugs, configure one link strategy for every command:
```ts
// contentbit.config.ts
export default defineContentConfig({
content: 'content/**/*.md',
registry: './blocks/registry.ts',
seo: './contentbit.seo.config.ts',
links: {
resolve: 'same-locale-key',
localeField: 'locale',
keyField: 'key',
slugField: 'slug',
},
})
```
Translated pages share a stable key and keep their own locale-specific slug:
```yaml
---
key: glossary/recipe-import
locale: fr
slug: import-recettes
linksTo:
- glossary/recipe-manager
seoKeywords:
primary: import de recettes
---
```
Contentbit keeps each localized SEO page separate while resolving links within
the right locale. `seoKeywords` is normalized as keyword data when `keywords`
is absent.
## If something does not resolve
- **No SEO config found:** confirm that `contentbit.config.ts` points to
`contentbit.seo.config.ts`.
- **Brief target not found:** try the page's stable `key`, then its `slug`.
- **Page has no type:** add a matching `pageDefaults` prefix or set `type` in an
explicit page plan.
- **Links resolve in the wrong locale:** put the shared resolver under `links`
in `contentbit.config.ts` so every command uses the same strategy.
## What to read next
- [SEO briefs and contracts](/docs/guides/seo-briefs) for the full config reference.
- [Content Studio](/docs/guides/studio) for the local review interface.
- [Content doctor](/docs/guides/doctor) for findings and exit behavior.
- [Internal linking](/docs/guides/internal-linking) for aliases, backlinks, and resolver modes.
- [LLM-agent integration](/docs/guides/agents) for project-local agent instructions.
---
# Configure a Contentbit project
URL: https://contentbit.dev/docs/guides/project-config
Use `contentbit.config.ts` to stop repeating project flags across package
scripts, local commands, Studio, and agent workflows. `contentbit init` creates
this file for new projects. For an existing setup, add it at the project root:
```ts
import { defineContentConfig } from '@contentbit/core'
export default defineContentConfig({
content: 'content/**/*.{md,mdx}',
registry: './blocks/registry.ts',
genericBlocks: true,
links: {
resolve: 'same-locale-key',
localeField: 'locale',
keyField: 'key',
slugField: 'slug',
},
seo: './contentbit.seo.config.ts',
})
```
Paths and globs are relative to the config file. Contentbit searches upward
from the current directory for `contentbit.config.ts`, `.mts`, `.mjs`, or
`.js`, which makes the nearest config win in a monorepo.
With a config present, daily commands need no repeated project flags:
```bash
contentbit validate
contentbit doctor
contentbit studio
contentbit stats
contentbit links
contentbit instructions --audience llm
contentbit brief
```
Explicit files, globs, `--registry`, `--seo-config`, link fields, and
`--no-generic-blocks` override the config for one invocation. Existing
projects that keep the full invocation in package scripts continue to work;
config discovery takes precedence when both are present.
## Check the discovered project
Run a command from the project root without a glob or registry flag:
```bash
contentbit validate --strict-warnings
```
If the command scans the expected files, the other project-aware commands will
use the same configuration. In a monorepo, run from the package that owns the
nearest config.
Use an explicit option only for a one-off override. For example:
```bash
contentbit doctor "drafts/**/*.md" --no-seo
```
`@contentbit/studio` is installed separately from the base CLI so validation
and one-shot `init` installs stay lightweight. `contentbit init` installs both
development packages for you. When adding Studio manually:
```bash
pnpm add -D @contentbit/studio
```
See the [configuration reference](/docs/reference/configuration) for every
field, default, discovery rule, and override.
---
# Render documents
URL: https://contentbit.dev/docs/guides/renderers
Renderers consume a validated document. Parse and validate first, then hand the
result to the target that matches your app:
```ts
import { genericBlocks } from '@contentbit/blocks'
import { assertValidDocument, compileDocument, createBlockRegistry } from '@contentbit/core'
const registry = createBlockRegistry().use(genericBlocks())
const document = assertValidDocument(compileDocument(markdown, registry), 'article.md')
```
For the design behind this split, see [Rendering model](/docs/concepts/rendering-model).
## React
```tsx
import { ContentBlocks } from '@contentbit/react'
}
fallback={MyInvalidBlock}
/>
```
`@contentbit/react` renders Markdown segments through `renderMarkdown`, calls
your block components by name, and uses `fallback` for invalid or unregistered
blocks in development surfaces.
For pre-made components, install the shadcn pack. The files land in your app and
are yours to edit:
```bash
pnpm dlx shadcn@latest add @contentbit/generic-pack
```
## Astro
```astro
---
import { ContentBlocks } from '@contentbit/astro/components'
import QuoteBlock from '../blocks/QuoteBlock.astro'
---
```
Per-block overrides are plain `.astro` components: validated props arrive as
component props, nested content via ``, and the raw AST node as the
reserved `node` prop. They also receive `ctx`, currently `{ renderMarkdown }`,
so Markdown strings inside `node.data` can use the same site pipeline as normal
prose. The `onInvalid` default is `annotated`, which keeps invalid blocks loud
during dev. Full guide: [Astro](/docs/guides/astro).
For pre-made Astro components:
```bash
pnpm dlx shadcn@latest add @contentbit/astro-pack
```
## Plain Markdown fallback
```ts
import { renderToMarkdown } from '@contentbit/core'
import { genericMarkdownRenderers } from '@contentbit/blocks'
const md = renderToMarkdown(document, { renderers: genericMarkdownRenderers })
```
Comparisons become Markdown tables, key metrics become bold-value bullets, tabs
become headed sections. Useful for email, exports, search indexing, and any
environment without a component runtime. Blocks without a renderer degrade to their
raw body, so information is not lost.
## Markdown prose
React and Astro renderers both need a Markdown prose function. Wire that once
with [Plug in your Markdown library](/docs/guides/markdown), then reuse it for
paragraphs between blocks and Markdown bodies inside blocks.
---
# SEO briefs and contracts
URL: https://contentbit.dev/docs/guides/seo-briefs
SEO contracts describe what a page must satisfy. Briefs turn those contracts
plus the current content inventory into a writer-ready plan.
Use this page as a reference while editing `contentbit.seo.config.ts`. To build
the workflow from start to finish, follow
[Build a programmatic SEO workflow](/docs/guides/programmatic-seo).
## How the config files fit together
The project config tells Contentbit where the SEO config lives:
```ts
// contentbit.config.ts
import { defineContentConfig } from '@contentbit/core'
export default defineContentConfig({
content: 'content/**/*.md',
registry: './blocks/registry.ts',
seo: './contentbit.seo.config.ts',
})
```
The SEO config holds page-family contracts and individual page plans. Keeping
the two concerns separate lets every CLI command share the same project paths
without mixing them into the editorial model.
## Config file
Create `contentbit.seo.config.ts` at the project root, or point to another path
from `contentbit.config.ts`. Keep the file small at first. Define one page type,
add folder defaults, then add explicit plans where a writer needs a brief before
the Markdown file exists.
```ts
import { defineSeoConfig } from '@contentbit/core'
export default defineSeoConfig({
pageTypes: {
alternative: {
requiredFrontmatter: ['type', 'intent', 'keywords.primary'],
requiredSections: [
{ id: 'overview', headings: ['Overview', 'Summary'] },
{ id: 'alternatives', headings: ['Best alternatives', 'Top alternatives'] },
{ id: 'comparison', headings: ['Comparison', 'Feature comparison'] },
],
requiredBlocks: ['comparison'],
recommendedBlocks: ['faq', 'key-metrics'],
requiredLinksTo: ['seo-tools-comparison'],
minOutgoingLinks: 2,
minIncomingLinks: 1,
},
},
pageDefaults: [
{
pathPrefix: 'content/alternatives/',
type: 'alternative',
intent: 'commercial',
},
],
pages: {
'ahrefs-alternatives': {
type: 'alternative',
key: 'ahrefs-alternatives',
slug: 'ahrefs-alternatives',
title: 'Ahrefs Alternatives',
intent: 'commercial',
keywords: { primary: 'ahrefs alternatives' },
linksTo: ['seo-tools-comparison'],
},
},
})
```
## Page types
`pageTypes` are reusable contracts. Use them for page families such as docs
pages, glossary entries, alternatives pages, comparison pages, product pages, or
blog posts.
| Field | Meaning |
| --- | --- |
| `requiredFrontmatter` | Dot paths that must exist after frontmatter normalization. |
| `requiredSections` | Headings that must appear in the document outline. Each section can provide allowed heading variants. |
| `requiredBlocks` | Block names that must appear at least once. |
| `recommendedBlocks` | Block names that produce informational suggestions when absent. |
| `requiredLinksTo` | Link targets that must appear in `linksTo`. |
| `minOutgoingLinks` | Minimum number of outgoing SEO links. |
| `minIncomingLinks` | Minimum number of backlinks from other scanned pages. |
Frontmatter is normalized before SEO checks run. If a project uses
`seoKeywords`, contentbit also exposes that data as `keywords` for page facts.
That means a contract can require the source field (`seoKeywords.primary`) or
the normalized field (`keywords.primary`), depending on the convention you want
writers to see in the file.
## Page defaults
`pageDefaults` classify existing files by path. Use them when a folder already
means something:
```ts
pageDefaults: [
{ pathPrefix: 'content/docs/', type: 'docs-page', intent: 'documentation' },
{ pathPrefix: 'content/blog/', type: 'blog-post', intent: 'publication' },
]
```
Defaults fill in missing facts. Existing frontmatter and explicit planned page
entries still win.
## Planned pages
`pages` can describe pages before Markdown exists. That is the part that makes
briefs useful for agents:
```ts
pages: {
'content/alternatives/semrush.md': {
type: 'alternative',
key: 'semrush-alternatives',
slug: 'semrush-alternatives',
title: 'Semrush Alternatives',
keywords: { primary: 'semrush alternatives' },
linksTo: ['seo-tools-comparison'],
},
}
```
When the file is later created, contentbit merges the plan with the live
frontmatter and stats. Existing frontmatter wins for page facts such as `title`,
`slug`, `key`, `intent`, `keywords`, and `linksTo`.
## Print a brief
```bash
contentbit brief semrush-alternatives
contentbit brief semrush-alternatives "content/**/*.md"
contentbit brief semrush-alternatives "content/**/*.md" --json
```
Targets resolve by `key` first, then by `slug`, then by internal page id. With a
project config, `brief` scans the configured content paths. If the page is only
planned, the brief still prints.
The Markdown brief is meant to paste into an agent or editorial task. The JSON
brief is better for tools.
## Brief contents
A brief includes:
- target page facts: id, source, key, slug, title, type, intent, and keywords
- the matching page-type contract
- required sections, blocks, and links
- SEO findings for the target page
- related pages from the same type or link neighborhood
- acceptance checks the writer should satisfy before handoff
Studio shows the same data in the document Brief view, with copy and download
actions for Markdown and JSON.
## Doctor integration
When an SEO config exists, `doctor` includes SEO findings in the normal repair
plan:
```bash
contentbit doctor "content/**/*.md" --strict-seo
contentbit doctor "content/**/*.md" --no-seo
```
Use `--strict-seo` in CI when missing contract requirements should fail the
build. Use `--no-seo` when you only want validation, link, and stats checks.
## Multilingual pages
Use the same link resolver options for `brief`, `doctor`, `studio`, and
`links`.
```bash
contentbit brief glossary/recipe-import "content/**/*.md" \
--link-resolve same-locale-key \
--locale-field locale \
--key-field key \
--slug-field slug
```
For translated slugs, share a stable `key` across locales and localize `slug`.
Contentbit keeps localized SEO pages separate while still resolving links by
same-locale key.
---
# Document stats & auditing
URL: https://contentbit.dev/docs/guides/stats
`contentbit doctor` is the ranked audit command: validation, links, and content
health in one repair plan. `contentbit stats` is the raw read tool next to it:
it analyzes documents and prints JSON to stdout, always exiting 0 — even when
documents have validation errors. Use stats when you need measurements to feed a
custom report, dashboard, or LLM prompt.
## Try it
Edit the source — the report recomputes as you type. Notice the per-section
word counts: that's the thin-section detector.
{`## Getting started
Install the package, run init, open the example page. Three steps,
two minutes, no configuration.
:::callout{type="tip" title="Shortcut"}
\`npx contentbit@latest init -y\` skips every prompt.
:::
## Deployment
Coming soon.
:::steps
1. Build the site.
2. Push to your host.
:::
Read the [full guide](https://contentbit.dev/docs) for the details.`}
## The CLI
Point `stats` at files or globs (quote globs so your shell doesn't expand them):
```bash
contentbit stats "content/**/*.md" --registry ./blocks/registry.ts
```
If your registry owns the full block set and includes names from the generic
pack, add `--no-generic-blocks`.
One matched file prints a single stats object; multiple files print an array.
Validation runs against the same registry `validate` uses and lands in a
`validation` summary per file — skip it with `--no-validate`. No files matched
is the only failure mode (exit 2).
A report, trimmed to the interesting fields:
```json
{
"file": { "path": "content/getting-started.md", "lines": 102 },
"length": { "words": 601, "readingMinutes": 4, "approxTokens": 1164 },
"outline": [
{ "level": 2, "text": "Getting started", "line": 20, "words": 127 },
{ "level": 2, "text": "Deployment", "line": 37, "words": 2 }
],
"blocks": { "total": 7, "byName": { "callout": 1, "steps": 1 }, "maxDepth": 2 },
"links": { "total": 3, "external": 1, "internal": 2, "domains": ["contentbit.dev"] },
"validation": { "errors": 0, "warnings": 0 }
}
```
## What's in the report
- **`outline`** — every ATX heading with its level, source line, and the prose
word count from that heading until the next one. A section with 2 words is a
stub; the outline finds it instantly.
- **`blocks`** — total count, `byName` usage, nesting depth, and per-instance
source lines. An empty `byName` on a long page usually means missed structure:
prose that wants to be steps, a comparison, or an FAQ.
- **`links`** — external/internal split plus the deduplicated external
`domains` list. Good for spotting link rot targets and unexpected domains.
This is inline Markdown link analysis. For the frontmatter-authored internal
graph with `slug`, `linksTo`, aliases, and backlinks, use
[`contentbit links`](/docs/guides/internal-linking).
- **`length`** — prose words only (frontmatter, code, and markup syntax are
excluded), reading minutes, and a rough token estimate.
- **`frontmatter`**, **`images`** (with `missingAlt`), **`code`**, and
**`structure`** (lists, tables, blockquotes) round out the picture.
## The library API
The same analysis is one import, with no filesystem access and nothing
environment-specific:
```ts
import { analyzeDocument } from '@contentbit/core'
const stats = analyzeDocument(source, { path: 'content/post.md' })
stats.outline // [{ level, text, line, words }, ...]
```
`analyzeDocument` takes the raw source string and needs no registry. The CLI
adds the `validation` summary by running the validate pipeline alongside it —
do the same if you need both:
```ts
import { compileDocument } from '@contentbit/core'
const result = compileDocument(source, registry)
```
The live demo above is exactly this — `analyzeDocument` plus
`validateDocument`, running in your browser.
## Auditing with an LLM agent
The `contentbit-audit` skill installed by [`contentbit agents`](/docs/guides/agents)
starts with [`contentbit doctor`](/docs/guides/doctor) for a ranked report, then
uses stats when raw metrics are useful. Doctor's priority order is worth
stealing for your own tooling:
1. **Validation errors and warnings** — broken content ships broken pages.
2. **Thin documents** — outline sections with very low word counts.
3. **Block-less documents** — empty `blocks.byName` where sibling documents use blocks.
4. **Missing or inconsistent frontmatter** compared to siblings.
5. **Structural imbalance** — skipped heading levels, single-section walls of text.
Ask your LLM agent to "audit my content" and this is the loop it runs.
## Recipes
JSON to stdout means `jq` does the rest:
```bash
# Rank files by word count, shortest first
contentbit stats "content/**/*.md" | jq 'sort_by(.length.words) | .[] | {path: .file.path, words: .length.words}'
# Files with no blocks at all
contentbit stats "content/**/*.md" | jq '.[] | select(.blocks.total == 0) | .file.path'
# Every external domain you link to
contentbit stats "content/**/*.md" | jq '[.[].links.domains[]] | unique'
```
---
# Content Studio
URL: https://contentbit.dev/docs/guides/studio
`contentbit studio` opens a local dashboard over your Markdown content. It is
the human-friendly view next to [`contentbit doctor`](/docs/guides/doctor):
doctor prints the ranked repair plan; Studio lets you browse the same project
health, preview documents, inspect links, and review SEO briefs without jumping
between files.
New projects created with `contentbit init` include:
```bash
pnpm studio
```
The generated `contentbit.config.ts` supplies the content glob and registry, so
the script expands to:
```bash
contentbit studio
```
Run the command directly when you want a custom glob or registry:
```bash
contentbit studio "content/**/*.md" --registry ./blocks/registry.ts --no-open
```
Studio is a separate development package so the base CLI stays lightweight.
`init` installs it automatically; for manual setups run
`pnpm add -D @contentbit/studio`.
## What you get
The first screen is a dense content dashboard:
- health counters for files, findings, blocks, and link graph coverage
- search and status filters
- sortable file table
- block usage
- keyword coverage from `keywords.primary` and `keywords.secondary`
- internal-link summary for pages, links, and orphans
Open a document to inspect:
- rendered preview using the React renderer
- source Markdown
- word, section, image, link, and reading-time stats
- outgoing links and backlinks
- keywords
- SEO brief details when `contentbit.seo.config.ts` is loaded
- validation, link, and content-health findings
The document Brief view shows the same contract that `contentbit brief` prints:
target facts, required sections, required links, related pages, findings,
acceptance checks, and raw Markdown/JSON with copy and download actions. Use it
when a human editor wants to inspect the page plan before handing work to an
agent or writer.
By default, generic blocks render through `@contentbit/react`. Custom blocks are
still validated from your registry, and Studio automatically looks for project
components next to the registry (`blocks/components.tsx` from `contentbit init`,
or `components.ts`, `preview.tsx`, and `renderers.tsx`). If no component exists,
Studio falls back to a safe labelled block that shows the body as Markdown.
## Options
```bash
contentbit studio [--registry ] [--port ] [--host ] [--no-open]
```
Link resolver options match `contentbit links` and `contentbit doctor`:
```bash
contentbit studio "content/**/*.md" \
--registry ./blocks/registry.ts \
--seo-config ./contentbit.seo.config.ts \
--link-resolve prefer-same-locale-key-fallback-slug \
--locale-field locale \
--key-field key \
--slug-field slug \
--default-locale en
```
Defaults:
- host: `127.0.0.1`
- port: first free port starting at `4377`
- browser: opens automatically unless `--no-open` is passed
- generic blocks: definitions and preview components load automatically unless
`--no-generic-blocks` is passed
Use `--no-generic-blocks` when your registry intentionally owns the whole block
set, including names such as `quick-ref` that exist in the generic pack.
For the full option list, see the [CLI reference](/docs/reference/cli).
For a complete SEO planning loop, see
[Programmatic SEO workflows](/docs/guides/programmatic-seo).
## Read-only scope
Studio v1 never mutates content. It does not:
- rewrite source files
- write `.contentbit/link-index.json`
- run `contentbit links --fix`
- watch files or refresh automatically
Use the Refresh button after editing files. Use `contentbit links --fix`
explicitly when you want alias references rewritten.
Future Studio capabilities may include editing, live file watching, alias
fixing, and richer per-project preview configuration.
---
# Release checklist
URL: https://contentbit.dev/docs/maintainers/release-checklist
Use this checklist before publishing packages or updating the starters. CI runs
the same core gates, but doing them locally catches release drift before a PR
waits on Actions.
## Package gates
```bash
pnpm check
```
`check` is the authoritative package gate. It verifies synchronized package and
standalone-starter manifest/lockfile versions, lint and formatting, builds,
tests, packed package manifests, and both complete starter gates.
After publishing a new package version, update the standalone consumers and
their lockfiles before changing starter code:
```bash
pnpm starter:sync
```
This is deliberately post-publication: release PRs cannot install the package
version they are preparing to publish.
## Starter gates
Run each starter from its own directory because each is intentionally standalone:
```bash
cd starter/astro
pnpm install --frozen-lockfile
pnpm check
```
```bash
cd starter/tanstack
pnpm install --frozen-lockfile
pnpm check
```
The smoke checks assert the `/blog` and `/blog/{slug}` demo, contentbit metadata,
keyword usage, generated backlinks, first-run script names, contentbit dependency
versions, and starter build output. They are not a replacement for visual
review, but they catch broken demo wiring quickly.
For visual review, run `pnpm studio` in the starter and open
`http://localhost:4377`, then stop the server before continuing.
## Content and docs
- Regenerate docs artifacts after docs changes with `pnpm --filter @contentbit/site prebuild`.
- Confirm internal-link docs cover the active locale strategy when changing
resolver behavior.
- Keep starter article keywords present in the article body, not only in
frontmatter.
- When changing Astro rendering behavior, confirm the prose path still belongs to
Astro/Sätteri and contentbit remains the block validation/rendering layer.
---
# Block reference
URL: https://contentbit.dev/docs/reference/blocks
## callout
Highlighted note, tip, warning, important, or TLDR box.
Props:
- type: one of note|tip|warning|important|tldr (optional, default: note)
- title: string (optional)
Content: Markdown body
Use when:
- Practical advice that prevents a common mistake (tip)
- Context the reader must not miss (note/important)
- Something that ruins the result if ignored (warning)
- A 1-3 sentence summary at the top of a section (tldr)
Avoid when:
- Regular prose that is not a standout remark
- More than one callout in the same section
- Content longer than ~3 sentences
Example:
```md
:::callout{type="tip" title="Worth knowing"}
Always weigh flour — volume measures drift by 20%.
:::
```
## steps
Ordered process steps with visual numbering.
Content: Ordered list: `1. item`
Use when:
- Sequential processes where order matters
- 3+ steps that benefit from numbering
Avoid when:
- Unordered tips — use a bullet list
- A single step — write it in prose
Example:
```md
:::steps
1. Combine flour and water
2. Rest 20 minutes
3. Add salt and mix
:::
```
## key-metrics
Scannable stat cards: a large value with a short label.
Content: List rows: `- value | label`
Use when:
- Surfacing 2-8 numbers the reader should remember
- Replacing a stats paragraph
Avoid when:
- Values without a clear unit or context
- Long textual descriptions
Example:
```md
:::key-metrics
- 42% | Conversion lift
- 18ms | Median parse time
:::
```
## quick-ref
Compact key/value reference card.
Content: List rows: `- key | value`
Use when:
- Facts a reader will return to (temperatures, ratios, durations)
- At-a-glance summaries near the top of a guide
Avoid when:
- Narrative content
- Comparing options — use comparison instead
Example:
```md
:::quick-ref
- Hydration | 65%
- Proof time | 2h at 24°C
:::
```
## comparison
Side-by-side comparison of exactly two options.
Props:
- left: string (required)
- right: string (required)
Content: List rows: `- label | left | right`
Use when:
- Comparing exactly two things across attributes
- Replacing a two-column Markdown table
Avoid when:
- Comparing more than two things
- Only one row of comparison
Example:
```md
:::comparison{left="Option A" right="Option B"}
- Speed | Fast | Slow
- Setup | Simple | Advanced
:::
```
## pros-cons
Paired advantages (+) and disadvantages (-) of one option.
Content: Signed list: `+ positive` / `- negative`
Use when:
- Evaluating a single option's trade-offs
- Summarizing a review
Avoid when:
- Comparing two options — use comparison
- Only positives or only negatives
Example:
```md
:::pros-cons
+ Cheap to run
+ Fast setup
- No offline mode
:::
```
## tabs
Tabbed switcher for alternative methods or variants — the reader picks one.
Content: Child blocks: `::tab`
Use when:
- Two or more alternative methods for the same task
- Variants by equipment, audience, or budget
Avoid when:
- Side-by-side comparison — use comparison
- Sequential content — use steps
Example:
```md
:::tabs
::tab{title="Fast"}
Use this when time matters.
::tab{title="Cheap"}
Use this when budget matters.
:::
```
## tab
One tab panel with a title. (child block — only inside a parent that allows it)
Props:
- title: string (required)
Content: Markdown body
Use when:
- Only inside :::tabs
Avoid when:
- Anywhere outside :::tabs
Example:
```md
::tab{title="Stand Mixer"}
Use the dough hook on speed 2.
```
## faq
Frequently asked questions with expandable answers.
Content: Child blocks: `::faq-item`
Use when:
- Real questions readers ask about the topic
- End-of-article FAQ sections
Avoid when:
- Content that is not literally question/answer
- A single trivial question
Example:
```md
:::faq
::faq-item{question="Can I freeze it?"}
Yes, up to 3 months.
:::
```
## faq-item
One question/answer pair. (child block — only inside a parent that allows it)
Props:
- question: string (required)
Content: Markdown body
Use when:
- Only inside :::faq
Avoid when:
- Anywhere outside :::faq
Example:
```md
::faq-item{question="Can I freeze it?"}
Yes, up to 3 months.
```
---
# CLI reference
URL: https://contentbit.dev/docs/reference/cli
Use `contentbit` for project setup, validation, content health reports,
rendering, internal-link indexes, generated authoring docs, and LLM-agent
integration.
Put shared project settings in `contentbit.config.ts`:
```ts
import { defineContentConfig } from '@contentbit/core'
export default defineContentConfig({
content: 'content/**/*.md',
registry: './blocks/registry.ts',
})
```
Commands discover the nearest config by walking upward. Explicit options and
globs override it for one invocation. Quote explicit globs so your shell does
not expand them before contentbit receives them:
```bash
contentbit validate "content/**/*.md" --registry ./blocks/registry.ts
```
Without a config, `validate`, `doctor`, `studio`, and `links` retain backward
compatibility by reading the nearest matching package script.
## Commands
```text
contentbit [options]
```
| Command | Purpose |
| --- | --- |
| `init` | Scaffold contentbit into a project. |
| `adopt` | Inspect an existing Markdown library and propose read-only setup. |
| `validate` | Check block syntax, block schemas, and internal links. |
| `brief` | Print an agent-ready SEO brief. |
| `doctor` | Print a ranked content repair plan. |
| `studio` | Open the local read-only content dashboard. |
| `stats` | Print document stats as JSON. |
| `render` | Render one file to plain Markdown. |
| `instructions` | Print generated LLM or human authoring instructions. |
| `docs` | Print generated human block docs. |
| `agents` | Install or refresh coding-agent guidance. |
| `links` | Build or fix the internal-link index. |
## init
Scaffold packages, starter content, a block registry, renderer files, scripts,
and agent instructions.
```bash
contentbit init [-t react|markdown|astro] [--md ] [-y] [--seo]
```
Options:
| Option | Meaning |
| --- | --- |
| `-t, --target ` | Force the target instead of detecting it. |
| `--md ` | Choose the Markdown library wiring when supported. |
| `-y, --yes` | Accept defaults. |
| `--cwd ` | Run setup in another directory. |
| `--no-install` | Write files without installing packages. |
| `--no-page` | Skip generated example route/page files. |
| `--no-styled` | Skip styled component pack setup. |
| `--no-agents` | Skip `AGENTS.md` and Claude Code skills. |
| `--seo` | Scaffold `contentbit.seo.config.ts`. |
## validate
Check content and print `file:line:col` diagnostics.
```bash
contentbit validate [globs...] [--registry ] [--strict-warnings]
```
Options:
| Option | Meaning |
| --- | --- |
| `--registry ` | Load custom blocks from a registry module. |
| `--no-generic-blocks` | Do not load the generic block pack first. |
| `--strict-warnings` | Exit 1 when warnings are present. |
| Link resolver options | See [Shared link options](#shared-link-options). |
Exit codes:
- `0` when no errors are found.
- `1` when errors are found.
- `1` when warnings are found and `--strict-warnings` is passed.
- `2` for command input problems such as missing or unmatched globs.
## adopt
Inspect an established Markdown library and print reviewable configuration
proposals. This command is always read-only: it never creates, changes, or
renames files.
```bash
contentbit adopt [globs...] [--dry-run] [--json]
```
Without a glob, it scans `content/**/*.{md,mdx}`. The report counts common
frontmatter fields, infers a locale-aware link strategy when appropriate,
shows stable-key locale coverage, and includes a proposed `contentbit.config.ts`.
Projects with observed page `type` values also receive a baseline SEO contract
inferred from consistently present frontmatter and sections. Review that
baseline before enforcing it. See [Adopt an existing content library](/docs/guides/adoption).
| Option | Meaning |
| --- | --- |
| `--registry ` | Load custom block definitions while scanning. |
| `--no-generic-blocks` | Do not load the generic block pack first. |
| `--dry-run` | Explicitly confirm the report-only workflow; adoption is always read-only. |
| `--json` | Print stable machine-readable JSON. |
## brief
Print a page brief from `contentbit.seo.config.ts`, existing content, or both.
```bash
contentbit brief [globs...] [--json]
```
Options:
| Option | Meaning |
| --- | --- |
| `--registry ` | Load custom blocks before checking required blocks. |
| `--no-generic-blocks` | Do not load the generic block pack first. |
| `--seo-config ` | Use a non-default SEO config file. |
| `--json` | Print machine-readable JSON. |
| Link resolver options | See [Shared link options](#shared-link-options). |
Targets resolve by page `key` first, then by `slug`, then by internal page id.
Use [SEO briefs and contracts](/docs/guides/seo-briefs) for config details and
[Programmatic SEO workflows](/docs/guides/programmatic-seo) for the full
writing loop.
## doctor
Print a ranked repair plan across validation, internal links, document stats,
image alt text, and optional SEO contracts.
```bash
contentbit doctor [globs...] [--watch] [--registry ] [--json]
```
Options:
| Option | Meaning |
| --- | --- |
| `--registry ` | Load custom block definitions. |
| `--no-generic-blocks` | Do not load the generic block pack first. |
| `--strict-warnings` | Exit 1 when warnings are present. |
| `--strict-seo` | Treat required SEO contract findings as build failures. |
| `--watch` | Rerun when a scanned content directory changes. |
| `--json` | Print stable JSON for agents and CI. |
| `--min-section-words ` | Change the thin-section threshold. |
| `--seo-config ` | Use a non-default SEO config file. |
| `--no-seo` | Ignore SEO config even when one exists. |
| Link resolver options | See [Shared link options](#shared-link-options). |
Use [Content doctor](/docs/guides/doctor) for workflow guidance.
## studio
Open the local read-only dashboard for previews, diagnostics, links, backlinks,
keywords, and stats.
```bash
contentbit studio [globs...] [--registry ] [--no-open]
```
Options:
| Option | Meaning |
| --- | --- |
| `--registry ` | Load custom blocks and preview components. |
| `--port ` | Choose the first port to try. |
| `--host ` | Bind to a custom host. |
| `--no-open` | Start the server without opening a browser. |
| `--no-generic-blocks` | Do not load generic blocks or preview components. |
| `--min-section-words ` | Change the thin-section threshold. |
| `--seo-config ` | Use a non-default SEO config file. |
| `--no-seo` | Ignore SEO config even when one exists. |
| Link resolver options | See [Shared link options](#shared-link-options). |
Use [Content Studio](/docs/guides/studio) for workflow guidance. When an SEO
config is loaded, document pages also include a read-only Brief view with
copy/download actions for Markdown and JSON.
## stats
Print raw document stats as JSON.
```bash
contentbit stats [--registry ]
```
Options:
| Option | Meaning |
| --- | --- |
| `--registry ` | Include validation summaries from this registry. |
| `--no-generic-blocks` | Do not load the generic block pack first. |
| `--no-validate` | Analyze Markdown only, without loading a registry. |
One matched file prints one JSON object. Multiple files print an array. No
matched files is a command input error.
## render
Render one file to plain Markdown.
```bash
contentbit render [--out ]
```
Options:
| Option | Meaning |
| --- | --- |
| `--target ` | Render target. Today this is `markdown`. |
| `--registry ` | Load custom block definitions before rendering. |
| `--no-generic-blocks` | Do not load the generic block pack first. |
| `--out ` | Write output to a file instead of stdout. |
## instructions
Print generated authoring instructions from the block registry.
```bash
contentbit instructions [--audience llm|human] [--out ]
```
Options:
| Option | Meaning |
| --- | --- |
| `--audience ` | `llm` by default, or `human`. |
| `--no-examples` | Omit block examples. |
| `--registry ` | Load custom blocks. |
| `--no-generic-blocks` | Do not load the generic block pack first. |
| `--out ` | Write the guide to a file. |
## docs
Print generated human block docs from the block registry.
```bash
contentbit docs [--registry ] [--out ]
```
Options:
| Option | Meaning |
| --- | --- |
| `--registry ` | Load custom blocks. |
| `--no-generic-blocks` | Do not load the generic block pack first. |
| `--out ` | Write docs to a file. |
The site's [Block reference](/docs/reference/blocks) is generated from this kind
of registry data.
## agents
Install or refresh agent guidance.
```bash
contentbit agents [--claude] [--check|--dry-run] [--no-agents-md]
```
Options:
| Option | Meaning |
| --- | --- |
| `--claude` | Install Claude Code skills even when `.claude/` is absent. |
| `--check` | Show what would be created, refreshed, or skipped without writing files. |
| `--dry-run` | Alias for `--check`. |
| `--no-agents-md` | Skip `AGENTS.md` updates. |
| `--cwd ` | Run in another directory. |
`AGENTS.md` is updated between `` and
`` markers. Content outside those markers is left alone.
## links
Build `.contentbit/link-index.json` from frontmatter slugs, aliases, and
`linksTo` values.
```bash
contentbit links [globs...] [--fix] [--out ]
```
Options:
| Option | Meaning |
| --- | --- |
| `--fix` | Rewrite stale alias references inside `linksTo`. |
| `--out ` | Write the index somewhere other than `.contentbit/link-index.json`. |
| Link resolver options | See [Shared link options](#shared-link-options). |
`--fix` is narrow. It only rewrites `linksTo` values that point at known aliases.
## Shared registry options
Commands that validate or render blocks load the generic block pack by default.
Pass a registry when your project adds custom blocks:
```bash
contentbit validate "content/**/*.md" --registry ./blocks/registry.ts
```
Use `--no-generic-blocks` when the registry intentionally owns the complete
block set, including generic names such as `callout` or `comparison`.
## Shared link options
`validate`, `brief`, `doctor`, `studio`, and `links` accept the same internal
link resolver options:
| Option | Meaning |
| --- | --- |
| `--link-resolve ` | Choose how `linksTo` targets resolve. |
| `--locale-field ` | Frontmatter field for locale. |
| `--slug-field ` | Frontmatter field for slug. |
| `--key-field ` | Frontmatter field for stable content key. |
| `--default-locale ` | Locale to assume when a page omits one. |
Resolver modes:
| Mode | Use when |
| --- | --- |
| `global-slug` | Slugs are globally unique. |
| `same-locale-slug` | Each locale has its own slug set. |
| `same-locale-key` | Translations share stable keys and have localized slugs. |
| `prefer-same-locale-key-fallback-slug` | Prefer keys, but allow same-locale slug links. |
See [Internal linking](/docs/guides/internal-linking) for examples.
---
# Configuration reference
URL: https://contentbit.dev/docs/reference/configuration
`contentbit.config.*` is the shared project interface used by validation,
Doctor, Studio, links, briefs, generated instructions, and agent guidance.
```ts
import { defineContentConfig } from '@contentbit/core'
export default defineContentConfig({
content: 'content/**/*.{md,mdx}',
registry: './blocks/registry.ts',
genericBlocks: true,
links: {
resolve: 'same-locale-key',
localeField: 'locale',
keyField: 'key',
slugField: 'slug',
defaultLocale: 'en',
},
seo: './contentbit.seo.config.ts',
})
```
## File discovery
Contentbit walks upward from the current directory and uses the nearest
`contentbit.config.ts`, `.mts`, `.mjs`, or `.js`. This lets each package in a
monorepo own its content boundary. Paths and globs are resolved relative to the
configuration file, not the shell's current directory.
## Top-level fields
| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `content` | `string \| string[]` | Required | One or more Markdown or MDX files/globs. `defineContentConfig` normalizes a string to an array. |
| `registry` | `string` | None | Module that default-exports custom block definitions. |
| `genericBlocks` | `boolean` | `true` | Include the built-in generic block pack before the custom registry. |
| `links` | `LinkResolverOptions` | Global slug resolution | Shared page identity and locale fields for the internal-link graph. |
| `seo` | `string \| false` | Automatic SEO config discovery | Path to an SEO contract module, or `false` to disable discovery. |
## Link resolver fields
| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `resolve` | `global-slug \| same-locale-slug \| same-locale-key \| prefer-same-locale-key-fallback-slug` | `global-slug` | How a `linksTo` target identifies a page. |
| `localeField` | `string` | `locale` | Frontmatter field containing the page locale. |
| `slugField` | `string` | `slug` | Frontmatter field containing the public slug. |
| `keyField` | `string` | `key` | Frontmatter field containing stable translation identity. |
| `defaultLocale` | `string` | None | Locale assigned when a participating page omits its locale field. |
Use `same-locale-slug` when translated pages have distinct identities but may
reuse a slug. Use `same-locale-key` when translations share a stable key. Use
the fallback mode when key-based links are preferred but legacy slug targets
must continue to resolve.
## Command overrides
Explicit files, globs, `--registry`, `--seo-config`, link-field flags, and
`--no-generic-blocks` override the discovered config for one invocation.
Commands that accept `--no-seo` can disable the configured SEO contract for one
run.
When no config exists, project-aware commands can reuse a matching package
script for backward compatibility. Once a config exists, config discovery wins
over package-script inference.
For the setup procedure, see [Configure a Contentbit project](/docs/guides/project-config).
For every command option, see the [CLI reference](/docs/reference/cli).
---
# Block syntax
URL: https://contentbit.dev/docs/reference/syntax
## Blocks
Blocks use fenced directive syntax. A block opens with three or more colons plus
a kebab-case name, and closes with a line containing only colons:
```md
:::block-name{prop="value" count=3 enabled=true}
Block body
:::
```
Rules:
- block names are lowercase kebab-case
- props are optional
- a block closes with `:::` on its own trimmed line
- nested blocks are allowed only when the parent block permits them
- unknown blocks produce diagnostics; they are never silently ignored
- block delimiters inside fenced code blocks are literal text
- a closing fence matches the innermost open block
- use a longer fence such as `::::` on the outer block to disambiguate deep nesting
## Props
Props use a constrained grammar so generated content stays portable across
renderers:
```md
{title="Quick Reference" count=3 variant="compact" featured=true}
```
Supported values:
- quoted strings: `title="Hello \"world\""`
- numbers: `count=3`, `ratio=-1.5`
- booleans: `featured=true`
- bare-key boolean shorthand: `featured` means `featured=true`
- bare identifiers for enums: `variant=compact`
Not supported: JavaScript expressions, object literals, arrays, function
references, and JSX.
## Child blocks
Child blocks use two-colon syntax and close implicitly at the next sibling or
the parent close:
```md
:::tabs
::tab{title="Fast"}
Use this when time matters.
::tab{title="Cheap"}
Use this when budget matters.
:::
```
Three-colon blocks can nest inside child bodies. They must be balanced before
the child ends.
{[
':::tabs',
'::tab{title="Fast"}',
'Use this when time matters.',
'::tab{title="Cheap"}',
'Use this when budget matters.',
':::',
].join('\n')}
## Pipe rows
Many blocks take rows of pipe-separated cells:
```md
:::key-metrics
- 42% | Conversion lift
- 18ms | Median parse time
:::
```
Each block defines its own columns and limits, so diagnostics name the exact
expectation. Escape a literal pipe inside a cell as `\|`.
```md
:::key-metrics
- 42% | Conversion lift
- 18ms | Median parse time
:::
```
When rows do not match the block's column count, validation fails before render:
```md
:::key-metrics
- 42% | Conversion lift | extra column
:::
```
## Markdown bodies
Blocks such as `callout` accept regular Markdown content:
```md
:::callout{type="tip" title="Worth knowing"}
You can use **Markdown** inside this block.
:::
```
The parser preserves the body verbatim. Your app's Markdown pipeline renders it
through the `renderMarkdown` function you provide.
## Raw HTML
Raw HTML is disabled by default everywhere. If your project needs raw HTML, opt
in at the renderer level and own sanitization there.
---
# Release checklist moved
URL: https://contentbit.dev/docs/release-checklist
The release checklist has moved to
[Maintainers: Release checklist](/docs/maintainers/release-checklist).
It is maintainer-facing project documentation, so it now lives outside the main
user learning path.