Skip to main content

Template basics

npm install @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>

@react-markdown-kit/template is a plugin package. It has no engine of its own to call: template({ data }) is an extension, and the renderer runs it while parsing. Put it in an extensions prop for one render, or in a preset when a whole surface is personalized. Outside React, compileMarkdown runs the same extension and returns the resolved MarkdownDocument:

import { compileMarkdown } from '@react-markdown-kit/renderer'
import { template } from '@react-markdown-kit/template'

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

The plugin calls no React, so this works in a Node service, a worker, a CLI or an email job.

One template, many customers

The authored source on the left never changes. Only the data does.

Invoice header
Authored template never changes
# Invoice for {{customer.name}}

Amount due: **{{amount | currency:"USD"}}** by {{dueDate | date:"long"}}.

Account manager: {{owner.name}}.
Resolved for Acme changes

Invoice for Acme Industrial

Amount due: $4,250.00 by March 1, 2026.

Account manager: Dana Reyes.

Data passed to template()
{
  "customer": {
    "name": "Acme Industrial"
  },
  "amount": 4250,
  "dueDate": "2026-03-01",
  "owner": {
    "name": "Dana Reyes"
  }
}

Switching the dataset recompiles the same source with new data. The template text is never rewritten.

Options

OptionPurpose
dataThe values placeholders resolve to
schemaA Standard Schema validator; rejected data is an error
locale, timeZoneFormatting locale (default en-US) and zone (default UTC)
formattersFormatters on top of the built-ins and those other extensions contribute
variablesPer-path metadata: required, default, label, group
fallbackMarkdown shown instead when resolution fails. Default: nothing
onDiagnosticsCalled with every diagnostic, on success and on failure

Failure renders nothing

A missing required value, a rejected schema or an unsafe path is an error. On any error the document becomes fallback, or an empty document, never a report with a blank where the account number should be.

<Markdown
extensions={[
template({
data,
fallback: 'This report is temporarily unavailable.',
onDiagnostics: (diagnostics) => logger.warn('invoice', { diagnostics }),
}),
]}
>
{source}
</Markdown>

With compileMarkdown the diagnostics are also on the document:

const document = compileMarkdown(source, { extensions: [template({ data })] })
const failed = document.diagnostics.some((d) => d.severity === 'error')

Diagnostics also arrive on success. A warning such as an absent optional value is reported without failing the resolution.

Diagnostics

Every diagnostic has a stable code, a severity, a message, and usually the data path it concerns. Messages name the path and never the runtime value, so they are safe to log.

import { TEMPLATE_DIAGNOSTIC_CODES } from '@react-markdown-kit/template'

const missing = diagnostics.filter((item) => item.code === TEMPLATE_DIAGNOSTIC_CODES.requiredValue)
CodeSeverityMeaning
TEMPLATE_REQUIRED_VALUEerrorA required variable had no value in the data.
TEMPLATE_OPTIONAL_VALUE_MISSINGwarningA variable declared optional had no value and resolved to empty text.
TEMPLATE_VALUE_NOT_SCALARerrorThe value was an object, an array or a function.
TEMPLATE_UNKNOWN_FORMATTERerrorNo formatter is registered under that name.
TEMPLATE_UNSAFE_PATHerrorA path segment was __proto__, constructor or prototype.
TEMPLATE_PARTIAL_URLerrorA placeholder filled part of a URL instead of all of it.
TEMPLATE_SCHEMA_INVALIDerrorThe schema rejected the data.

The full list is exported as TEMPLATE_DIAGNOSTIC_CODES. New members may appear in a minor release; existing members never change meaning.

Values become text, never structure

Data is placed structurally into the parsed tree. It is never substituted into the source text and re-parsed, so Markdown punctuation inside a value stays literal.

A value that tries to be bold
Authored template never changes
# Hello {{user.name}}

Signed by {{user.name}}.
Resolved for Plain name changes

Hello Chatis

Signed by Chatis.

Data passed to template()
{
  "user": {
    "name": "Chatis"
  }
}

The second dataset renders the asterisks as characters. No value can create a heading, a table row, a link destination, an HTML tag or a code fence.

The security guide explains the rules behind this, including literal code contexts, escaped delimiters and the newline boundary.

Which Markdown the template speaks

The plugin has no dialect of its own. It resolves whatever tree the preset parsed, so one source has one meaning everywhere in the kit. Tables and task lists are the renderer's gfm(), exactly as for rendering:

import { gfmPreset } from '@react-markdown-kit/renderer/gfm'

<Markdown preset={gfmPreset} extensions={[template({ data })]}>{source}</Markdown>

Other plugins participate through the extension contract's template capability: mermaid() marks its payload literal, and any extension can contribute formatters.

Resolved Markdown text

When you need Markdown text rather than a rendered document, for an email body or a file on disk, serialize the compiled document:

import { compileMarkdown, documentToMarkdown } from '@react-markdown-kit/renderer'

const document = compileMarkdown(source, { extensions: [template({ data })] })
await writeFile('report.md', await documentToMarkdown(document))

Serializing re-escapes, so a value of **Administrator** comes back out as \*\*Administrator\*\* and keeps the literal reading it was given.

Finding placeholders without data

templateVariables() from the same package lifts every placeholder into a templateVariable node, so compiling with it and no data is an inspection:

import { templateVariables, isTemplateVariableNode } from '@react-markdown-kit/template'

const { tree } = compileMarkdown(source, { extensions: [templateVariables()] })
// walk `tree` for nodes where isTemplateVariableNode(node): path, formatter, argument

No conditions or loops in v1

There is no if, no each, and no expression language. Branching belongs in your application, which picks the data or picks the source.

Next