Astro
Render validated documents with .astro components — renderer only, one validation path.
@contentbit/astro is a renderer, deliberately nothing more. There is no
content-loader integration: Astro's own content collections load your Markdown,
your Astro Markdown processor renders prose, contentbit validates structured
block islands where they are used, and contentbit validate covers the same
files in CI. One validation path, without replacing Astro's Markdown pipeline.
Setup
init detects Astro from your dependencies (or force it with -t astro):
npx contentbit@latest initIt scaffolds the full loop:
blocks/registry.ts— a customquoteblock, shared with the validate CLIblocks/QuoteBlock.astro— its.astrocomponentcontent/example.md— a starter documentsrc/content.config.ts— anarticlescollection overcontent/using Astro's builtin glob loadersrc/pages/example.astro— a rendered example page- the
content:checkscript and the LLM-agent integration
Wiring it by hand instead:
pnpm add @contentbit/core @contentbit/blocks @contentbit/astroRender a page
The pattern is the same as every other target — parse, validate, render — in component frontmatter:
---
import { genericBlocks } from '@contentbit/blocks'
import { assertValidDocument, compileDocument, createBlockRegistry } from '@contentbit/core'
import { getEntry } from 'astro:content'
import { ContentBlocks } from '@contentbit/astro/components'
const entry = await getEntry('articles', 'example')
if (!entry?.body) throw new Error('Entry not found.')
const registry = createBlockRegistry().use(genericBlocks())
const document = assertValidDocument(compileDocument(entry.body, registry), entry.id)
---
<ContentBlocks document={document} />Static pages render at build time, so this is a real safety net: invalid blocks fail the build, not the reader.
ContentBlocks is intentionally headless. It walks the validated document,
renders Markdown through renderMarkdown, and dispatches blocks to the
components you pass. The pre-made block UI lives in the shadcn registry as
editable .astro files, not inside the package.
Use Astro/Sätteri for prose
Astro 7 uses Sätteri for Markdown and MDX. Contentbit can stay complementary by delegating Markdown fragments back to that host renderer:
---
import { createSatteriMarkdownProcessor } from '@astrojs/markdown-satteri'
import { ContentBlocks } from '@contentbit/astro/components'
const processor = await createSatteriMarkdownProcessor({
features: { directive: true, headingAttributes: true, math: true },
})
const renderMarkdown = async (markdown: string) => {
const result = await processor.render(markdown)
return result.code
}
---
<ContentBlocks document={document} renderMarkdown={renderMarkdown} />The contentbit parser still finds and validates block directives, but normal Markdown syntax, heading ids, smart punctuation, math, and other enabled prose features stay owned by Astro.
Component overrides
Map block names to your own .astro components. Validated props arrive as
component props, nested block content arrives via <slot />, and two reserved
props are passed alongside them:
node— the full validated AST node, including parsednode.datactx— the render context, currently{ renderMarkdown }
---
// RichNoteBlock.astro
import type { AstroBlockContext } from '@contentbit/astro'
import type { ValidatedBlockNode } from '@contentbit/core'
interface Props {
node: ValidatedBlockNode<unknown>
ctx: AstroBlockContext
title?: string
}
const { node, ctx, title } = Astro.props
const data = node.data as { markdown: string }
const html = await ctx.renderMarkdown(data.markdown)
---
<aside>
{title ? <h3>{title}</h3> : null}
<div set:html={html} />
</aside><ContentBlocks document={document} components={{ 'rich-note': RichNoteBlock }} />Because props were already validated against the block's schema, your component
can use them without defensive parsing. Use <slot /> for nested content and
ctx.renderMarkdown(...) for Markdown strings stored in node.data.
Options
renderMarkdown—(md: string) => string | Promise<string>for prose and block bodies. It is also passed to block components asctx.renderMarkdown, so copied shadcn components and your custom components share the same host-owned Markdown pipeline. The minimal fallback is exported asdefaultRenderMarkdownfrom@contentbit/astro.components— block-name to.astrocomponent map. This is the only block UI path in the Astro runtime.onInvalid— what to do with a block that failed validation:strict(throw),annotated(visible dev box), orfallback(escaped body as prose). The Astro default isannotated, which keeps invalid blocks loud during dev.
Validate in CI
init adds the gate for you:
{ "scripts": { "content:check": "contentbit validate" } }The content glob and registry live once in contentbit.config.ts.
The CLI strips frontmatter before validating — the same thing Astro does when
it hands you entry.body — so the CLI and your pages always judge identical
content.
Styled components
A Tailwind-styled pack for Astro is distributed through the shadcn registry:
pnpm dlx shadcn@latest add @contentbit/astro-packComponents land in your project as editable .astro source files. You own
them after install.