# contentbit (Content Blocks) > Structured Markdown components for LLM-written content. LLMs write plain > Markdown with directive blocks (:::callout, :::comparison, ...). The library > parses to a source-mapped AST, validates against per-block schemas with > file:line:col diagnostics, and renders through React, Astro, or a > plain-Markdown fallback. npm scope: @contentbit. CLI: > `npx contentbit@latest init`. ## Start here - [Authoring rules](https://contentbit.dev/contentbit-guide.md): the generated block-authoring instructions — paste these into a system prompt before writing Content Blocks documents - [Full documentation as one file](https://contentbit.dev/llms-full.txt) ## Docs - [Get started with programmatic SEO](https://contentbit.dev/docs): Scaffold Contentbit, define one page family, create a brief, and validate the finished page. - [Changelog](https://contentbit.dev/docs/changelog): What shipped in each release. - [Stability and compatibility](https://contentbit.dev/docs/compatibility): The interfaces Contentbit 1.x will keep stable and how changes are classified. - [LLM & agent authoring](https://contentbit.dev/docs/concepts/llm-authoring): How generated authoring instructions keep schemas, docs, prompts, and validation aligned. - [Rendering model](https://contentbit.dev/docs/concepts/rendering-model): How parsing, validation, Markdown rendering, and framework adapters fit together. - [Block syntax moved](https://contentbit.dev/docs/concepts/syntax): The syntax page now lives in the Reference section. - [Why contentbit?](https://contentbit.dev/docs/concepts/why-contentbit): The content model behind typed Markdown blocks, validation, renderers, and LLM-agent workflows. - [Why not MDX?](https://contentbit.dev/docs/concepts/why-not-mdx): MDX embeds components. Content Blocks defines a content protocol. - [Adopt an existing content library](https://contentbit.dev/docs/guides/adoption): Inspect an existing Markdown library, find integrity issues, and generate reviewable Contentbit setup proposals without modifying files. - [LLM-agent integration](https://contentbit.dev/docs/guides/agents): Give Codex, Claude Code, Cursor, Copilot, and other LLM agents a fast validated writing loop. - [Astro](https://contentbit.dev/docs/guides/astro): Render validated documents with .astro components — renderer only, one validation path. - [Defining custom blocks](https://contentbit.dev/docs/guides/custom-blocks): Add project-specific blocks in under 20 lines with defineBlock. - [Content doctor](https://contentbit.dev/docs/guides/doctor): Run validation, link checks, content-quality heuristics, and optional SEO contract checks as one ranked repair plan. - [Internal linking](https://contentbit.dev/docs/guides/internal-linking): Build and validate a frontmatter-authored internal-link graph with contentbit links. - [Plug in your Markdown library](https://contentbit.dev/docs/guides/markdown): One function connects Content Blocks to react-markdown, marked, markdown-it, or remark. Step-by-step for each. - [Start from the production Astro template](https://contentbit.dev/docs/guides/production-astro): Use astro-speedrun-seo when you want Contentbit inside a complete multilingual SEO site. - [Build a programmatic SEO workflow](https://contentbit.dev/docs/guides/programmatic-seo): Define a page family, brief a writer or agent, inspect the result, and enforce the contract in CI. - [Configure a Contentbit project](https://contentbit.dev/docs/guides/project-config): Add one shared config so Contentbit commands use the same content, blocks, links, and SEO contracts. - [Render documents](https://contentbit.dev/docs/guides/renderers): Render a validated contentbit document in React, Astro, or plain Markdown. - [SEO briefs and contracts](https://contentbit.dev/docs/guides/seo-briefs): Define page contracts and print agent-ready briefs for existing or planned pages. - [Document stats & auditing](https://contentbit.dev/docs/guides/stats): analyzeDocument and contentbit stats — outlines, block usage, link domains, and validation summaries as JSON. - [Content Studio](https://contentbit.dev/docs/guides/studio): Browse content health, previews, stats, links, backlinks, keywords, and SEO briefs in a read-only local web app. - [Release checklist](https://contentbit.dev/docs/maintainers/release-checklist): The gates to run before publishing contentbit packages and starters. - [Block reference](https://contentbit.dev/docs/reference/blocks): Every built-in block, generated from the registry. - [CLI reference](https://contentbit.dev/docs/reference/cli): Commands, options, shared flags, and exit behavior for the contentbit CLI. - [Configuration reference](https://contentbit.dev/docs/reference/configuration): Fields, defaults, discovery, paths, and command overrides for contentbit.config.*. - [Block syntax](https://contentbit.dev/docs/reference/syntax): Directive fences, props, child blocks, pipe rows, Markdown bodies, and raw HTML behavior. - [Release checklist moved](https://contentbit.dev/docs/release-checklist): The release checklist now lives in the Maintainers section. ## Blog - ['contentbit 0.2.0: LLM agents write validated content'](https://contentbit.dev/blog/contentbit-0-2-0): One command installs LLM-agent skills that fetch the live authoring guide, write, and validate until clean. Plus document stats, an Astro renderer, and fully documented props. - ["contentbit 0.3.0: internal links your content can't outgrow"](https://contentbit.dev/blog/contentbit-0-3-0): Author each page's place in the graph in frontmatter; contentbit derives backlinks, validates every reference, and heals renames with --fix. Internal linking that fails CI instead of your readers. - [Why LLMs write broken Markdown (and how to make it impossible)](https://contentbit.dev/blog/llm-markdown-that-cannot-break): LLMs write Markdown that looks right and breaks in production. Prompting alone never fixes it. A validation contract does. ## Packages - @contentbit/core: parser, validator, registry, authoring-guide generation - @contentbit/blocks: the generic block pack - @contentbit/react: headless React renderer - contentbit: CLI — init, validate, stats, render, instructions, agents --- # Block authoring rules (generated from the registry) # Content block authoring rules - write regular Markdown by default; use blocks only when they improve scanning or structure - never invent block names — only the blocks documented below exist - follow each block's syntax exactly; props use {key="value"} on the open line - close every :::block with a line containing only ::: - validate generated content and fix diagnostics rather than bypassing them ## 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. ``` --- # Get started with programmatic SEO URL: https://contentbit.dev/docs This tutorial builds one search-focused page from a reusable contract. You will scaffold Contentbit, plan the page before its file exists, write the draft, and run the same quality gate an agent or CI job can use later. You will finish with: 1. A project-wide Contentbit config. 2. A reusable page-family contract. 3. An agent-ready brief for one planned page. 4. A Markdown page that passes strict SEO checks. 5. A local Studio view for reviewing the result. > Only need structured Markdown and rendering? Run `contentbit init` without > `--seo`, then continue with [Render documents](/docs/guides/renderers). ## 1. Scaffold the project Run `init` with SEO support from the root of an existing React, Astro, or Markdown project: ```bash pnpm dlx contentbit@latest init --seo ``` ```bash npx contentbit@latest init --seo ``` ```bash bunx contentbit@latest init --seo ``` ```bash yarn dlx contentbit@latest init --seo ``` `init` detects the framework and package manager, asks a few setup questions, and wires a working content project. Add `-y` to accept the detected defaults. The scaffold has two configuration layers: - `contentbit.config.ts` tells every command which content, block registry, and SEO config to use. - `contentbit.seo.config.ts` describes page families and pages you plan to publish. The block registry, renderer, example content, Studio script, and agent instructions are ready too. This tutorial focuses on the SEO path first. The remaining examples use the `contentbit` binary directly. If your shell does not expose local package binaries, prefix commands with `pnpm exec`, `npx`, `bunx`, or `yarn exec`. ## 2. Define one page family For this tutorial, replace the generated `contentbit.seo.config.ts` with one small contract: ```ts import { defineSeoConfig } from '@contentbit/core' export default defineSeoConfig({ pageTypes: { quickstart: { requiredFrontmatter: ['type', 'intent', 'keywords.primary'], requiredSections: [ { id: 'overview', headings: ['Overview'] }, { id: 'workflow', headings: ['Workflow'] }, { id: 'outcomes', headings: ['Outcomes'] }, ], recommendedBlocks: ['steps'], }, }, pages: { 'content/getting-started/safer-content-workflows.md': { type: 'quickstart', key: 'safer-content-workflows', slug: 'safer-content-workflows', title: 'Safer content workflows', intent: 'product education', keywords: { primary: 'content workflow', secondary: ['content validation', 'AI content workflow'], }, }, }, }) ``` The `quickstart` type is the reusable family contract. The entry under `pages` plans a specific page before its Markdown file exists. Real projects usually keep several page types, such as alternatives, integrations, comparisons, and glossary entries. Starting with one makes it easy to see what the contract controls. ## 3. Generate the page brief Ask Contentbit for the planned page: ```bash contentbit brief safer-content-workflows ``` The brief combines the target page with its family contract. It includes the primary keyword, required sections, recommended blocks, and acceptance checks. Use JSON when a coding agent or script will consume the brief: ```bash contentbit brief safer-content-workflows --json ``` At this point, the plan exists but the content file does not. That is useful: the writer gets the intended structure before drafting rather than trying to repair an arbitrary page afterward. ## 4. Create the page Create `content/getting-started/safer-content-workflows.md`: ```md --- type: quickstart intent: product education keywords: primary: content workflow secondary: [content validation, AI content workflow] --- # Safer content workflows Programmatic content can move quickly without becoming unpredictable. A shared plan gives each page a clear purpose, repeatable structure, and review process before anyone starts drafting. ## Overview A reliable content workflow gives writers and agents the same structure before they draft and the same checks before they publish. The contract keeps those requirements visible throughout planning, writing, and review. ## Workflow :::steps 1. Plan the page with a reusable contract covering its intent, headings, and useful blocks. 2. Generate the brief so the writer understands the target before drafting. 3. Validate the finished content and repair structural problems before review. ::: ## Outcomes Every page follows the intended structure, uses registered content blocks, and can be checked locally or in CI. Writers spend less time reconstructing page requirements, while reviewers get consistent evidence that the draft is ready. ``` You can also give the brief to an agent instead of writing the file manually: ```text Write the planned safer-content-workflows page from its Contentbit brief. Preserve the target facts, satisfy every acceptance check, and run Contentbit Doctor before you finish. ``` The installed agent instructions teach supported tools to fetch the live block guide and repair diagnostics without copied schemas in the prompt. ## 5. Run the quality gate Check the new page against its family contract: ```bash contentbit doctor content/getting-started/safer-content-workflows.md --strict-seo ``` Doctor checks the page contract, frontmatter, headings, structured blocks, and general content health in one pass. `--strict-seo` returns a failing exit code when a required SEO finding remains. Try changing `## Outcomes` to `## Results` and run the command again. The report will point to the missing required section. Restore the heading and rerun the gate until it exits cleanly. ## 6. Review the page in Studio Open Studio on the same page: ```bash contentbit studio content/getting-started/safer-content-workflows.md ``` Studio gives you a human view over the same project scan. Open the page to see: - the rendered preview and source Markdown - the original SEO brief and acceptance checks - validation and content-health findings - keywords, document stats, and block usage You now have the complete loop: plan, brief, write, inspect, and enforce. ## 7. Apply the workflow to your project The tutorial used one small page family and one targeted file. In a real content library, the next step is to classify existing folders with `pageDefaults`, add planned pages, and run Doctor over the full configured content set. Continue with: - [Build a programmatic SEO workflow](/docs/guides/programmatic-seo) for page defaults, agents, CI, and multilingual projects. - [SEO briefs and contracts](/docs/guides/seo-briefs) for every page-family field. - [Project configuration](/docs/guides/project-config) for shared content, link, registry, and SEO settings. - [Production Astro template](/docs/guides/production-astro) when you want a complete multilingual reference implementation instead of wiring the host application yourself. - [LLM-agent integration](/docs/guides/agents) for the automated writing and repair loop. - [Render documents](/docs/guides/renderers) to publish validated content in React, Astro, or Markdown. - [Content Studio](/docs/guides/studio) and [Content doctor](/docs/guides/doctor) for deeper review and audit options. --- # Changelog URL: https://contentbit.dev/docs/changelog Terse by design - each release links its announcement post when there is one. ## Next - **First-run proof.** Standalone Astro and TanStack starters track the current contentbit package version and exercise validate, links, doctor, build, and smoke gates. Their first-run docs also include Studio as the human inspection step. ## 0.7.0 - **Shared project configuration.** Commands discover the nearest `contentbit.config.*`, so content globs, custom blocks, link resolution, and SEO contracts live in one place instead of package-script flags. See [Configure a Contentbit project](/docs/guides/project-config). - **Existing-project adoption.** `contentbit adopt` scans a Markdown library and prints a read-only integrity report plus reviewable configuration proposals. See [Adopt an existing content library](/docs/guides/adoption). - **Faster editing feedback.** `contentbit doctor --watch` reruns the ranked repair plan as content changes, while `--strict-warnings` and `--strict-seo` remain the CI gates. - **Stronger rendering contracts.** Core exposes explicit processed and validated document types, strict compile helpers, and inferred block props for renderer integrations. - **Clearer CLI ergonomics.** Commands validate incompatible options, prop diagnostics point to the offending key, unknown props include suggestions, and `contentbit agents --check` reports stale integrations without writing. - **Lightweight CLI installs.** Studio is a separate development dependency; `contentbit init` installs it automatically for new projects. ## 0.6.1 - **Published package hardening.** The CLI and Studio bundles no longer expose the private `@contentbit/project` workspace package as a runtime dependency. Packed manifests are checked before release. ## 0.6.0 - **SEO contracts.** `contentbit.seo.config.ts` can define reusable page types, planned pages, required sections, required blocks, link requirements, and strict SEO checks. See [Programmatic SEO workflows](/docs/guides/programmatic-seo). - **SEO briefs.** New `contentbit brief` command prints an agent-ready writing contract for existing or planned pages, in Markdown or JSON. See [SEO briefs and contracts](/docs/guides/seo-briefs). - **SEO-aware Doctor and Studio.** Doctor folds SEO findings into the repair plan, and Studio shows SEO counts plus per-document Brief views. ## 0.5.0 - **One project scan.** `scanContentProject()` now runs frontmatter parsing, directive parsing, validation, stats, link graph, and ranked findings through a shared core pipeline. - **Headless renderers.** React and Astro renderers no longer ship built-in styled components. Apps provide their own block components and Markdown prose renderer, while shadcn packs provide editable styled UI. - **Custom registry control.** `--no-generic-blocks` lets a project registry own the full block set, including names that overlap the generic pack. ## 0.4.0 - **Content Studio.** New `contentbit studio` command launches a read-only local TanStack dashboard for content health counters, search, sortable files, previews, stats, diagnostics, links, backlinks, keywords, and block usage. `contentbit init` adds a `studio` script for `pnpm studio`. See [Content Studio](/docs/guides/studio). - **Content doctor.** New `contentbit doctor` command reads matched content once and prints a ranked repair plan across block validation, internal-link diagnostics, thin sections, block-less long documents, and missing image alt text. Use `--json` for agents and CI. See [Content doctor](/docs/guides/doctor). ## 0.3.0 [Read the announcement →](/blog/contentbit-0-3-0) - **Internal linking.** New `contentbit links` command builds `.contentbit/link-index.json` from frontmatter `slug`, `linksTo`, `aliases`, and `keywords`, derives backlinks, validates broken slug references, and heals alias references with `--fix`. `contentbit validate` runs link checks automatically when matched files declare slugs. See [Internal linking](/docs/guides/internal-linking). - **LLM link context.** `contentbit agents` teaches generated skills and `AGENTS.md` blocks to load the link index, choose existing slugs, and validate the full graph when authoring linked content. ## 0.2.0 All packages now share one version number — this is the first synchronized release. [Read the announcement →](/blog/contentbit-0-2-0) - **LLM-agent integration.** New `contentbit agents` command (also run by `init`, skip with `--no-agents`): installs the `contentbit-author` and `contentbit-audit` Claude Code skills and manages a fenced instruction block in `AGENTS.md` for every other LLM agent. Skills hold no schemas — they read everything from the CLI at runtime, so custom blocks are picked up automatically. See [LLM-agent integration](/docs/guides/agents). - **Document stats.** `analyzeDocument` in `@contentbit/core` and a `contentbit stats` command: heading outlines with per-section word counts, block usage, link domains, and a validation summary, as JSON. Accepts multiple files and globs. See [Document stats & auditing](/docs/guides/stats). - **`@contentbit/astro`.** Renderer-only Astro integration: render validated documents with `.astro` components, per-block overrides included. See [Astro](/docs/guides/astro). - **Every prop, documented.** Generated authoring guides now list every prop from the block's schema — name, type (enums spelled out), required/optional, default, and description — so LLMs that never guess props can discover all of them. See [LLM-agent integration](/docs/guides/agents). ## 0.1.0 Initial release: directive-block parser with source-mapped AST, block registry with per-block schemas, line-level validation diagnostics, the generic block pack, React and static-HTML renderers, the shadcn styled pack, and the `contentbit` CLI. --- # Stability and compatibility URL: https://contentbit.dev/docs/compatibility Contentbit is approaching 1.0. Releases before 1.0 may still refine an interface in a minor version, with the change documented in the package changelogs. The 1.0 release starts the compatibility policy below. ## Stable throughout 1.x The following are compatibility commitments once 1.0 ships: - accepted directive syntax and the meaning of an already-valid document - public package exports documented under `@contentbit/core`, `@contentbit/blocks`, `@contentbit/react`, and `@contentbit/astro` - `contentbit.config.*` and `contentbit.seo.config.*` property names and defaults - diagnostic codes, CLI exit-code categories, and versioned JSON result shapes - renderer interfaces for validated documents and registered block components A 1.x minor release may add optional configuration, commands, result fields, blocks, exports, or diagnostics. Additions must not make an existing clean project fail its current command without an explicit stricter option. Removing or renaming a stable export, changing the interpretation of valid syntax, changing a configuration default incompatibly, or changing an existing diagnostic or exit-code contract requires a major release. ## Versioned machine output Machine-readable output includes a `schemaVersion` where callers need a durable contract. Consumers should reject unknown major schema versions and ignore unknown additive fields. Human terminal formatting is not a machine interface; use `--json` for automation. ## Project and runtime support - The supported Node.js floor is declared in each published package's `engines` field. - React and Astro compatibility is declared through renderer peer dependencies. - Patch releases support the same runtime and peer ranges as their containing minor unless a security or upstream platform constraint makes that impossible. - A dropped runtime or framework major is announced in advance and shipped in a Contentbit major release once 1.0 is stable. ## Deliberately flexible surfaces The Studio visual design and route layout, generated agent-guidance prose, and the editable shadcn registry styles may evolve during 1.x. Their documented commands, file ownership rules, and data contracts remain stable, but their presentation is not a compatibility promise. ## Reference implementations The minimal Astro and TanStack starters in the Contentbit repository are standalone compatibility fixtures. They verify installation, rendering, content checks, and build output against every package change. [`astro-speedrun-seo`](https://github.com/agonist/astro-speedrun-seo) is the production-shaped Astro reference template. It demonstrates multilingual routes, programmatic page sets, metadata, structured data, feeds, sitemaps, social images, agent guidance, and strict Contentbit checks in a separate consumer repository. --- # LLM & agent authoring URL: https://contentbit.dev/docs/concepts/llm-authoring The block definitions that validate content also generate instructions for whatever writes it. That is the core idea: schemas, docs, and prompts come from one registry, so they do not drift apart. This page explains the model. To install the agent files, see [LLM-agent integration](/docs/guides/agents). ## One registry, two audiences ```ts const guide = registry.toAuthoringGuide({ audience: 'llm', // or 'human' includeExamples: true, }) ``` The human audience is readable block documentation. The LLM audience adds operational ground rules before the block list: - write regular Markdown by default; use blocks only when they improve scanning or structure - never invent block names; only documented blocks exist - follow each block's syntax exactly - close every `:::block` with a line containing only `:::` - validate generated content and fix diagnostics rather than bypassing them Each block contributes its description, content shape, use/avoid rules, a verified example, and a per-prop reference: every prop's name, type (enum values spelled out), required or optional, default, and `.describe()` text. That last part makes "never guess props" realistic. Optional props that an example does not use are still discoverable. Custom blocks registered by your project are included automatically. ## Generated output is disposable Generated guides are not the source of truth. The registry is. Regenerate the guide whenever an agent, docs page, or prompt needs current instructions: ```bash contentbit instructions --audience llm --out content-blocks-guide.md contentbit docs --out docs/blocks.md ``` The installed `AGENTS.md` block and Claude Code skills follow the same pattern: they ask the CLI for current instructions at runtime instead of carrying copied schemas. ## Validation closes the loop Generation is only useful when output is checked: ```bash contentbit validate "content/**/*.md" ``` Diagnostics use source positions, stable codes, and fix hints an LLM can act on: ```text recipes/dough.md:31:1 error CB_CHILD_COUNT :::tabs needs at least 2 children, found 1. ``` That means an agent does not need a private mental model of the schema. It can read the live guide, write content, run validation, and feed diagnostics back into the next edit until the document is clean. For existing content, [`contentbit doctor`](/docs/guides/doctor) adds a repair order across validation, links, and content health. [`contentbit stats`](/docs/guides/stats) adds raw measurements an agent can use as context. ## Custom registries Point CLI commands at a registry module when your project adds blocks: ```bash contentbit validate "content/**/*.md" --registry ./blocks/registry.ts contentbit instructions --audience llm --registry ./blocks/registry.ts ``` The generic pack loads first by default, then your registry adds project blocks. Pass `--no-generic-blocks` only when your registry intentionally owns the full block set. ## Boundaries Generated instructions help agents write valid structured Markdown. They do not decide your editorial strategy, verify facts, or replace human review. Pair the contentbit loop with whatever content, product, and compliance checks your team already needs. For command details, see the [CLI reference](/docs/reference/cli). --- # Rendering model URL: https://contentbit.dev/docs/concepts/rendering-model Contentbit has one job before render: turn Markdown with directive blocks into a validated document tree. Framework adapters consume that tree. Your app still owns the prose renderer, component styling, routing, and page chrome. ## Pipeline The runtime shape is: ```text Markdown source -> parseDocument() -> validateDocument(registry) -> ContentBlocks / renderToMarkdown() ``` Parsing finds block fences, props, rows, child blocks, and Markdown segments. Validation applies the registry. Rendering happens only after that. ## Prose belongs to your Markdown pipeline Contentbit is not a full Markdown renderer. It extracts block structure and then hands prose back to one function you provide: - React: `renderMarkdown: (md: string) => ReactNode` - Astro: `renderMarkdown: (md: string) => string | Promise` - Plain Markdown: `renderToMarkdown()` returns Markdown directly That includes paragraphs between blocks and Markdown bodies inside blocks. Without a real `renderMarkdown` function, rich Markdown such as `**bold**` or links will use the minimal escaped fallback. For setup steps, see [Plug in your Markdown library](/docs/guides/markdown). ## Block UI belongs to your app The registry validates content. It does not import React, Astro, Tailwind, or your design system. Framework adapters map block names to components: ```tsx } /> ``` Styled packs are distributed through the shadcn registry so components land as editable project files. They are a convenience layer, not a hidden runtime. ## Invalid blocks stay visible Validation should usually run before render, in CI and in local scripts. Runtime renderers still need an invalid-block policy because preview tools and local dev often inspect broken work in progress. React exposes a `fallback` component. Astro exposes `onInvalid` with `strict`, `annotated`, and `fallback` modes. Use loud development behavior and strict build gates when publishing. ## Render targets Use the renderer that matches the surface: - **React:** headless `ContentBlocks` with accessible generic defaults and component overrides. - **Astro:** `.astro` components with validated props, `` content, and `ctx.renderMarkdown`. - **Plain Markdown:** `renderToMarkdown()` for email, search indexing, exports, and AI context. The source document does not need to know which target will consume it. --- # Block syntax moved URL: https://contentbit.dev/docs/concepts/syntax The syntax page has moved to [Block syntax](/docs/reference/syntax). Concept docs explain why contentbit works the way it does. Syntax details are reference material, so they now live beside the CLI and block reference. --- # Why contentbit? URL: https://contentbit.dev/docs/concepts/why-contentbit Contentbit exists for teams that like Markdown, but need generated Markdown to be safer than plain text. It keeps normal prose in Markdown and adds a small typed block layer for structured sections such as comparisons, callouts, steps, metrics, tabs, and FAQs. That gives you a contract: 1. Authors write readable Markdown. 2. Blocks carry typed props and structured bodies. 3. Validation runs before render. 4. Renderers receive a validated document instead of raw guesses. 5. LLM agents read the same registry that validation uses. ## Markdown stays the authoring format Contentbit does not replace Markdown. Paragraphs, headings, lists, code fences, and links remain ordinary Markdown. Blocks are for moments where prose needs a shape a renderer or agent can rely on: ```md :::comparison{left="Manual review" right="contentbit"} - Broken props | Found after publish | Found before render - Agent prompts | Drift over time | Generated from the registry ::: ``` The source stays readable in Git. The rendered result can become React components, Astro components, or plain Markdown fallback output. ## The registry is the source of truth Every block definition lives in a registry. A definition names the block, describes its props, defines its content model, and includes authoring guidance. That same definition drives: - validation diagnostics - runtime renderer dispatch - generated human docs - generated LLM instructions - Studio previews and content health views This is the key design choice. You do not maintain separate schemas, docs, and prompts. When the registry changes, the rest of the system can ask it for the current truth. ## Validation is the feedback loop Invalid block syntax or invalid props never need to reach a reader. Contentbit reports diagnostics in a stable shape: ```text content/pricing.md:12:1 error CB_PROPS_INVALID :::callout props invalid: type must be one of note|tip|warning|important|tldr. hint: Did you mean type="warning"? ``` That format works for people, CI, and LLM agents. The model does not need to guess what went wrong. It can read the line, code, message, and hint, fix the source, and validate again. ## Renderers stay host-owned Contentbit parses and validates the block layer. It does not try to own your whole site renderer. Your app still chooses how normal Markdown prose renders, how components are styled, and how URLs are built. That separation keeps content portable. The same validated document can render through React, Astro, or `renderToMarkdown()` without changing the source file. For the details, see [Rendering model](/docs/concepts/rendering-model). ## LLM agents get live instructions LLM-authored Markdown breaks when prompts drift away from the schema. Contentbit avoids that by generating agent instructions from the registry. An agent can run: ```bash contentbit instructions --audience llm --registry ./blocks/registry.ts ``` The result lists the actual blocks, props, examples, use cases, and avoid rules for this project. After writing, the agent runs `contentbit validate` and fixes diagnostics until the content is clean. That loop is deliberately simple: read the live guide, write Markdown, validate, repair, repeat. --- # Why not MDX? URL: https://contentbit.dev/docs/concepts/why-not-mdx MDX is excellent when trusted developers want arbitrary JSX inside content. Content Blocks solves a different problem: - content may be generated by LLMs - content may be generated, localized, or edited outside the codebase - content should remain Markdown-shaped and portable - authors should not need React or JSX knowledge - teams want validation before render - teams want renderer choice instead of React lock-in MDX embeds components. Content Blocks defines a content protocol. ## Use MDX when - authors are developers - content lives close to app code - arbitrary React power is desired - build-time JSX execution is acceptable ## Use Content Blocks when - content is generated, imported, translated, or managed outside the app - strict block schemas matter - line-level content errors are valuable - content should render outside React - LLMs or agent skills need clear authoring instructions ## The practical differences | | MDX | Content Blocks | | ------------------- | ----------------------------- | ---------------------------------------- | | Authoring surface | JSX + Markdown | Markdown + constrained directives | | Validation | None (compile errors at best) | Schemas with `file:line:col` diagnostics | | Execution | Compiles to JS, runs code | Never executes code | | Render targets | React | React, Astro, plain Markdown | | Safe for LLM output | Risky | Designed for it | A document written with Content Blocks stays readable in any plain-text editor and degrades gracefully: strip the renderer and the content still makes sense as Markdown. The system can interoperate with MDX later (an MDX plugin or renderer is on the roadmap), but it does not depend on MDX as the primary model. --- # Adopt an existing content library URL: https://contentbit.dev/docs/guides/adoption Use `contentbit adopt` before adding Contentbit to an established project. It scans Markdown, reports the current health findings, infers a link-resolution strategy, shows locale coverage for stable page keys, and prints setup files to review. It never writes source files or configuration. ```bash contentbit adopt "content/**/*.{md,mdx}" --dry-run ``` With no glob, adoption looks for `content/**/*.{md,mdx}` from the current directory. Pass explicit globs when content lives elsewhere: ```bash contentbit adopt "docs/**/*.md" "blog/**/*.mdx" --json ``` ## What adoption infers - frontmatter coverage for `slug`, `key`, `locale`, titles, descriptions, and declared `linksTo` relationships - a link strategy: global slugs, same-locale slugs, or locale-aware stable keys - locale coverage for every key that has localized pages - observed `type` values, common sections, and consistently present frontmatter as baseline SEO contract proposals The terminal report includes a suggested `contentbit.config.ts`, high-confidence per-file frontmatter additions (a slug and title only when absent), and a reviewable `contentbit.seo.config.ts` baseline when pages use `type`. The baseline only includes observed common requirements; decide stricter section and block requirements with the people who own the content. ## Review the integrity report Adoption uses the same scan as Doctor and Studio. Before accepting the config proposal, review duplicate titles and descriptions, missing or multiple H1s, heading-level jumps, local route or Markdown links that do not resolve, and locale/key coverage findings. After you create and adapt the proposed config, run the normal daily loop: ```bash contentbit doctor --watch ``` The watch session reruns Doctor after changes in the directories that supplied the initial scan. Use ordinary `contentbit doctor --strict-warnings` in CI. --- # LLM-agent integration URL: https://contentbit.dev/docs/guides/agents Use this guide to install or refresh the agent files that make contentbit part of your normal LLM writing loop. After setup, the agent reads live block instructions from the CLI, writes Markdown, validates the result, and fixes diagnostics until the document is clean. If you already ran `contentbit init`, try this now: > write a blog post about our new dark mode > audit my content For an existing project, install or refresh the integration first: ```bash contentbit agents ``` The failed validation in the middle of this session is expected. It is the feedback loop doing its job. ## What gets installed `contentbit init` installs the integration automatically (skip with `--no-agents`). In an existing project, or after upgrading the CLI: ```bash contentbit agents ``` This manages: - **`AGENTS.md`**: a fenced block between `` and `` markers with compact instructions any LLM agent reads (Cursor, Codex, Copilot, ...). Your content outside the markers is never touched; re-running refreshes only the block. - **Claude Code skills**: when a `.claude/` directory exists (or forced with `--claude` when it doesn't), two skills land in `.claude/skills/`: - `contentbit-author`: the writing loop: fetch the authoring guide, write, validate, fix diagnostics until exit 0. - `contentbit-audit`: the health check: run `doctor` across content, flag validation issues, internal-link problems, thin sections, block-less long documents, and missing image alt text, in that order. Pass `--no-agents-md` to manage only the skills. Run `contentbit agents --check` to see what would be created, refreshed, or skipped before writing files. Project configuration matters too. Generated instructions tell the LLM agent to use the nearest `contentbit.config.ts`, which holds the canonical content glob, registry, link fields, and SEO config. ## Use the writing loop The author skill follows the same loop you would run by hand: ```bash # 1. the live guide, including your custom blocks contentbit instructions --audience llm # 2. if the project uses link frontmatter, load the current graph contentbit links # 3. write the document, then validate until clean contentbit validate content/new-post.md ``` Both skills use `contentbit.config.ts`, so they adapt to your layout without copying project arguments into every command. If the project has `contentbit.seo.config.ts` and the task is a search-targeted page, give the agent the page brief before it writes: ```bash contentbit brief --json ``` The brief is the structure contract: target facts, intent, keywords, required sections, required blocks, required links, related pages, and acceptance checks. After the edit, run `doctor --strict-seo` over the same glob. Generated instructions document every block prop, enum value, default, and description. That is why the agent can follow a "never guess props" rule. For the model behind this, see [LLM authoring](/docs/concepts/llm-authoring). ## Use the audit loop The audit skill starts with `doctor`, which prints a ranked repair plan and can also emit JSON: ```bash contentbit doctor contentbit doctor --json ``` Use `stats` when you need raw per-document metrics: ```bash contentbit stats ``` Each stats entry carries a heading outline with per-section word counts, block usage by name, link domains, and a validation summary. Full field reference and recipes: [Document stats & auditing](/docs/guides/stats). Doctor behavior: [Content doctor](/docs/guides/doctor). For a human review pass over the same content set, open the read-only Studio: ```bash pnpm studio ``` Studio is for browsing previews, links, backlinks, keywords, and findings. Agents should keep using `doctor --json` and `stats` when they need stable data. For internal-link health, use the graph command next to stats: ```bash contentbit links contentbit links --fix ``` The first command writes `.contentbit/link-index.json` with resolved `linksTo` and derived `linkedFrom`. The second safely rewrites `linksTo` values that still point at known aliases after a slug rename. The author skill uses that index as context when sibling documents declare `slug`, `linksTo`, `aliases`, or `keywords`, and asks new linked pages to include `keywords.primary` and `keywords.secondary` search-intent phrases for future link selection. Full workflow: [Internal linking](/docs/guides/internal-linking). ## Add contentbit to your own skills You may already have skills for your content workflows, such as a blog-post or changelog skill. Add contentbit as a context source and a quality gate with this instruction: ```markdown Before writing, run `contentbit instructions --audience llm` and follow it exactly. Never guess block syntax. If sibling documents use internal-link frontmatter, run `contentbit links` and choose `linksTo` values from the generated index. After writing, run `contentbit validate` and fix every diagnostic (`file:line:col severity CODE message`, with `hint:` lines) until it exits 0. ``` For data-informed writing, feed `contentbit doctor --json` or `contentbit stats` output in as well — for example, have the skill check sibling documents' outlines and block usage so new content matches the structure of what already works. For the full SEO agent loop, see [Programmatic SEO workflows](/docs/guides/programmatic-seo). This works in any LLM agent that can run shell commands. The CLI is the interface; skills, rules files, and prompts are thin adapters over it. For flags and command syntax, see the [CLI reference](/docs/reference/cli). --- # Astro URL: https://contentbit.dev/docs/guides/astro `@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`): ```bash npx contentbit@latest init ``` It scaffolds the full loop: - `blocks/registry.ts` — a custom `quote` block, shared with the validate CLI - `blocks/QuoteBlock.astro` — its `.astro` component - `content/example.md` — a starter document - `src/content.config.ts` — an `articles` collection over `content/` using Astro's builtin glob loader - `src/pages/example.astro` — a rendered example page - the `content:check` script and the [LLM-agent integration](/docs/guides/agents) Wiring it by hand instead: ```bash pnpm add @contentbit/core @contentbit/blocks @contentbit/astro ``` ## Render a page The pattern is the same as every other target — parse, validate, render — in component frontmatter: ```astro --- 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) --- ``` 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: ```astro --- 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 } --- ``` 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 ``, and two reserved props are passed alongside them: - `node` — the full validated AST node, including parsed `node.data` - `ctx` — the render context, currently `{ renderMarkdown }` ```astro --- // RichNoteBlock.astro import type { AstroBlockContext } from '@contentbit/astro' import type { ValidatedBlockNode } from '@contentbit/core' interface Props { node: ValidatedBlockNode ctx: AstroBlockContext title?: string } const { node, ctx, title } = Astro.props const data = node.data as { markdown: string } const html = await ctx.renderMarkdown(data.markdown) --- ``` ```astro ``` Because props were already validated against the block's schema, your component can use them without defensive parsing. Use `` for nested content and `ctx.renderMarkdown(...)` for Markdown strings stored in `node.data`. ## Options - **`renderMarkdown`** — `(md: string) => string | Promise` for prose and block bodies. It is also passed to block components as `ctx.renderMarkdown`, so copied shadcn components and your custom components share the same host-owned Markdown pipeline. The minimal fallback is exported as `defaultRenderMarkdown` from `@contentbit/astro`. - **`components`** — block-name to `.astro` component 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), or `fallback` (escaped body as prose). The Astro default is `annotated`, which keeps invalid blocks loud during dev. ## Validate in CI `init` adds the gate for you: ```json { "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: ```bash pnpm dlx shadcn@latest add @contentbit/astro-pack ``` Components land in your project as editable `.astro` source files. You own them after install. --- # Defining custom blocks URL: https://contentbit.dev/docs/guides/custom-blocks Every block is a `BlockDefinition`: a name, a props schema, a content model, and authoring guidance. The registry is the single source of truth — the same definition validates content, drives renderers, and generates LLM instructions. Define a block today and every [LLM agent wired to this project](/docs/guides/agents) writes it correctly on its next run, because skills read the registry live through the CLI. ## A complete custom block ```ts import { childBlocks, defineBlock, pipeRows } from '@contentbit/core' import { z } from 'zod' export const pricingTableBlock = defineBlock({ name: 'pricing-table', description: 'Compares product plans.', props: z.object({ currency: z.enum(['usd', 'eur', 'gbp']).default('usd'), }), content: pipeRows({ columns: ['plan', 'price', 'features'], minRows: 2, }), authoring: { useWhen: ['Comparing SaaS pricing plans'], avoidWhen: ['Listing non-pricing feature differences'], example: `:::pricing-table{currency="usd"} - Starter | $0 | Basic usage - Pro | $12/mo | Teams and automation :::`, }, }) ``` Register it next to the generic pack: ```ts const registry = createBlockRegistry().use(genericBlocks()).add(pricingTableBlock) ``` That's it — `validateDocument` now enforces the schema, `toAuthoringGuide()` includes the block, and any renderer can map `pricing-table` to an implementation. ## The fields - **name** — lowercase kebab-case, unique per registry. Duplicates throw. - **props** — a zod schema for the open-line props. Defaults are applied during validation; violations become `CB_PROPS_INVALID` diagnostics with the zod path. Generated authoring guides document every prop — name, type with enum values spelled out, required/optional, default, and any `.describe()` text — so describing props pays off: your LLM agent reads those descriptions. - **content** — a content model (below) that parses the body into typed `data`. - **childOnly** — set `true` for blocks that only make sense inside a parent (like `tab` inside `tabs`). - **interactive** — a hint to renderers that the block needs client behavior. - **authoring** — `useWhen` / `avoidWhen` / `example`, consumed by the generated authoring guide. Keep the example valid: it's worth testing that every example passes your own registry. ## Content models Four helpers cover most blocks: ```ts markdownBody({ minLength: 10 }) // free Markdown body pipeRows({ columns: ['qty', 'name', 'note'], optionalColumns: 1 }) // pipe rows listItems({ marker: 'ordered', minItems: 2 }) // bullet | ordered | signed childBlocks({ allowed: ['tab'], minChildren: 2 }) // nested child blocks ``` Each helper reports diagnostics with the exact body line, so authors see `content.md:14:1` instead of "somewhere in this block". For unusual shapes, provide your own content model: any object with `kind`, `describe()`, and `parse(node, report)` works. ## Rendering custom blocks Renderers are adapter-side maps — block definitions never reference framework code: ```tsx // React import { defineBlockComponent, defineBlockComponents } from '@contentbit/react' const PricingTable = defineBlockComponent(pricingTableBlock, ({ node }) => { // Inferred: node.props.currency and node.data.rows need no casts. return }) const components = defineBlockComponents([pricingTableBlock], { 'pricing-table': PricingTable, }) ``` `defineBlockComponent` carries the block name, Zod output props, and content model data across the renderer seam. `defineBlockComponents` also rejects misspelled block-name keys. ```astro // Astro — a plain .astro component; props arrive validated, body via ``` --- # Content doctor URL: https://contentbit.dev/docs/guides/doctor `contentbit doctor` is the audit command to run before opening files by hand. It reads matched content once, validates registered blocks, checks frontmatter links, analyzes document structure, and prints the issues in repair order. ```bash contentbit doctor ``` The content glob, registry, link fields, and SEO config come from `contentbit.config.ts`. Explicit globs and flags override it for one run. The default report is meant for humans: ```text contentbit doctor 3 file(s): 1 errors, 1 warnings, 2 suggestions 2 linked page(s), 1 link(s), 1 orphan(s) Repair plan: 1. [error] validation CB_PROPS_INVALID content/bad.md:8:1 - comparison: prop "left" Required 2. [warning] links CB_LINK_ORPHAN content/orphan.md:1:1 - page "orphan" has no inbound links 3. [info] stats CB_THIN_SECTION content/guide.md:24:1 - section "Deploy" has 3 words ``` ## What it checks - **Validation errors** from the same registry path used by `contentbit validate`. - **Internal-link errors and warnings** from `slug`, `linksTo`, `aliases`, locale/key options, and the same resolver modes used by `contentbit links`. - **Thin sections** below `--min-section-words` (default `25`). - **Block-less long documents** with at least 250 prose words and no structured blocks. - **Missing image alt text** from Markdown image syntax. - **Content integrity**: duplicate frontmatter titles or descriptions, missing or multiple H1s on slugged pages, heading-level jumps, and unresolved local route/Markdown links or heading anchors. Multilingual libraries also flag missing stable keys and incomplete locale variants for a key. - **SEO contract findings** when `contentbit.seo.config.ts` exists or `--seo-config` is passed. `doctor` is read-only. It does not write `.contentbit/link-index.json`, and it does not run `links --fix`. If aliases are present and the link graph has no errors, the text report suggests the fix command explicitly. ## Watch while editing Run Doctor continuously during an edit session: ```bash contentbit doctor --watch ``` It prints the first report immediately, then reruns after a change in a directory that supplied an initially matched content file. Watch mode is local feedback only; use the ordinary command with strict flags for CI. ## JSON for agents and CI Use `--json` when an LLM agent or pipeline needs stable data: ```bash contentbit doctor --json ``` The shape is: ```json { "files": 3, "summary": { "errors": 1, "warnings": 1, "suggestions": 2 }, "linkGraph": { "pages": 2, "links": 1, "orphans": 1 }, "findings": [ { "severity": "error", "source": "validation", "code": "CB_PROPS_INVALID", "file": "content/bad.md", "line": 8, "column": 1, "message": "comparison: prop \"left\" Required" } ] } ``` Exit codes match its job as a health gate: - `0` when there are no error findings. - `1` when there are error findings, or when `--strict-warnings` is passed and warnings are present. - `1` when `--strict-seo` is passed and SEO warnings are present. - `2` when no files or globs were provided, or no files matched. ## How it fits Use `doctor` to decide what to fix first. Use `validate` when you only need the blocking syntax/schema gate. Use `links` when you need to write the link index or heal alias references. Use `stats` when you need raw per-document metrics. Use [Programmatic SEO workflows](/docs/guides/programmatic-seo) when you want to plan pages with briefs and enforce SEO contracts in CI. For the full option list, see the [CLI reference](/docs/reference/cli). --- # Internal linking URL: https://contentbit.dev/docs/guides/internal-linking Internal links are content metadata, not renderer behavior. Author each page's place in the graph in frontmatter, let contentbit derive backlinks, and fail CI when a slug drifts after a rename. ```yaml --- slug: beginner-pizza-dough linksTo: - cold-fermentation-pizza - detroit-pizza-dough aliases: - intro-pizza-dough keywords: primary: how to make pizza dough secondary: [easy pizza dough recipe, homemade pizza dough] --- ``` Only `slug`, `linksTo`, `aliases`, and `keywords` belong in source files. `linkedFrom` is derived and written only to `.contentbit/link-index.json`. ## Build the index Point `links` at the same content glob you validate: ```bash contentbit links "content/**/*.md" ``` The command reads frontmatter, resolves aliases, derives backlinks, writes `.contentbit/link-index.json`, and prints a summary: ```text 12 page(s), 34 link(s), 1 orphan(s): 0 errors, 1 warnings index written to /site/.contentbit/link-index.json ``` The JSON artifact is intentionally small and stable, so an LLM agent or pipeline can load it before writing a page: ```json { "pages": [ { "slug": "beginner-pizza-dough", "path": "/repo/content/beginner.md", "title": "Beginner Pizza Dough", "linksTo": ["cold-fermentation-pizza"], "linkedFrom": ["pizza-flour-guide"], "aliases": ["intro-pizza-dough"], "keywords": { "primary": "how to make pizza dough", "secondary": ["easy pizza dough recipe"] } } ], "aliases": { "intro-pizza-dough": "beginner-pizza-dough" } } ``` ## Validate links `contentbit validate` runs the cross-file link checks automatically whenever any matched file declares a `slug`: ```bash contentbit validate "content/**/*.md" --registry ./blocks/registry.ts ``` Use the full content glob for linked documents. Validating only one linked file does not give the checker the rest of the graph. The link diagnostics use the same `file:line:col severity CODE message` shape as block validation: - `CB_LINK_UNRESOLVED` for a `linksTo` target that is not a slug or alias. - `CB_SLUG_DUPLICATE` for two pages with the same `slug`. - `CB_ALIAS_CONFLICT` for aliases that collide with another alias or real slug. - `CB_LINK_LOCALE_MISSING` for a target that exists in another locale but not the current locale. - `CB_LINK_CROSS_LOCALE` for an explicit object target that intentionally points across locales. - `CB_LINK_SELF` for a page linking to itself. - `CB_LINK_ORPHAN` for a page with no inbound links. ## Multilingual projects Use the resolver mode that matches your URL model: ```bash # Same translated slug in every locale: /en/blog/espresso and /fr/blog/espresso contentbit links "content/**/*.md" --link-resolve same-locale-slug # Translated slugs with stable content keys: contentbit links "content/**/*.md" --link-resolve same-locale-key # Prefer stable keys, but allow same-locale slug links for simple projects: contentbit links "content/**/*.md" --link-resolve prefer-same-locale-key-fallback-slug ``` For translated slugs, author a stable `key` and a locale-specific `slug`: ```yaml --- locale: fr slug: regler-espresso key: espresso-dial-in linksTo: - espresso-grinder-settings --- ``` The matching English page can use the same `key` with its own slug: ```yaml --- locale: en slug: dialing-in-espresso key: espresso-dial-in linksTo: - espresso-grinder-settings --- ``` The generated index keeps locale, slug, and key together so an app can build URLs with its own route helper, such as `/{locale}/blog/{slug}`. To point across locales intentionally, use an object target: ```yaml linksTo: - locale: en slug: dialing-in-espresso ``` That emits a warning instead of silently hiding the cross-locale edge. ## Heal renames When a page changes slug, keep the old slug in `aliases` on the destination page: ```yaml --- slug: cold-fermentation-pizza aliases: - overnight-pizza-dough --- ``` Then let `--fix` rewrite stale outbound references: ```bash contentbit links "content/**/*.md" --fix ``` `--fix` is deliberately narrow. It only rewrites values inside `linksTo` that point at known aliases. It does not remove aliases, invent related pages, change body Markdown, or write `linkedFrom` to source files. ## Agent workflow `contentbit agents` teaches installed skills and `AGENTS.md` to use the link graph when it exists: 1. Run `contentbit links ` before writing to read current slugs, aliases, keywords, and backlinks. 2. Pick `linksTo` values from existing slugs; never guess a slug from a title. 3. For new linked pages, include `keywords.primary` and `keywords.secondary` with search-intent phrases that help future LLM agents choose related pages. 4. Run `contentbit links --fix` after edits when aliases are present. 5. Run `contentbit validate --registry ` 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.