Skip to main content

Markdown template engine

Personalize a Markdown document with typed application data.

npm install @react-markdown-kit/renderer @react-markdown-kit/template
import Markdown from '@react-markdown-kit/renderer'
import { template } from '@react-markdown-kit/template'

<Markdown extensions={[template({ data: { user: { name: 'Chatis' } } })]}>
{'# Hello {{user.name}}'}
</Markdown>

template() is a plugin, not an engine you call: the renderer parses with your preset and the plugin fills the tree in. compileMarkdown runs the same extension outside React, in a service, a worker, a CLI, an email job or a PDF pipeline.

The source never changes, only the output
Authored template never changes
# Account review for {{customer.name}}

Renews {{renewsAt | date:"long"}}.

| Item | Amount |
| --- | ---: |
| Plan | {{amounts.plan \| currency:"USD"}} |
Resolved for Acme changes

Account review for Acme Industrial

Renews November 1, 2026.

ItemAmount
Plan$4,800.00
Data passed to template()
{
  "customer": {
    "name": "Acme Industrial"
  },
  "renewsAt": "2026-11-01",
  "amounts": {
    "plan": 4800
  }
}

Data cannot inject structure

This is the part that matters when authored documents meet runtime data.

Values are placed structurally into a parsed syntax tree. They are never substituted into source text and re-parsed. A customer named **Administrator** renders as those literal characters.

Hostile values stay literal
Authored template never changes
# Hello {{name}}

Signed, {{name}}.
Resolved for Ordinary changes

Hello Dana Okafor

Signed, Dana Okafor.

Data passed to template()
{
  "name": "Dana Okafor"
}

No value can create a heading, a table row, a link destination, an HTML tag or a code fence. That holds after the output is serialized back to Markdown and re-parsed downstream, which is where a naive engine leaks.

Typed, then validated

TypeScript generics catch mistakes at compile time.

template<ReportData>({ data })

Runtime schemas catch them at the boundary, through Standard Schema, so Zod, Valibot and ArkType all work with no adapter and the package depends on none of them.

template({ data, schema: ReportSchema })

A missing required value is an error diagnostic and renders nothing (or your fallback). It never produces a document that looks publishable with a blank where a number should be.

Formatting and localization

Six built-in formatters: number, currency, percent, date, time and datetime. Currency always carries an explicit code, because inferring it from a locale is how invoices go out in the wrong denomination.

Formatting follows the locale and timeZone you pass; the authored source stays yours to choose per language.

Formatting and localization

It renders through the same renderer

Resolution happens inside the renderer's own compile step, so there is no stringify and reparse in between, and a compiled document can be cached and handed to <Markdown document> later.

const document = compileMarkdown(source, { preset, extensions: [template({ data })] })

Next

Template docs · The editor demo · Security model