Areca Docs

Rendering rich text

Render Areca rich text with the SDK's document, Markdown and HTML helpers, and style every element with your own React components.

Rich text fields arrive in a snapshot as a RichTextValue object. The SDK gives you three helpers for it: richTextDocument returns a parsed document for a TanStack Markdown renderer, richTextMarkdown returns the Markdown source, and richTextHtml returns a static HTML string. For a React site, use richTextDocument with the Markdown component. This lets you control how every element looks.

Install the renderer

The SDK does not render anything itself. Install the TanStack Markdown renderer alongside it, at the version the SDK depends on: @tanstack/markdown 1.0.0.

pnpm add @tanstack/markdown@1.0.0

The rich text value

Each rich text field in a snapshot has four properties:

PropertyTypeWhat it is
markdownstringThe canonical Markdown source. Use it when you need to re-parse with your own options.
documentMarkdownDocumentThe Markdown parsed into a tree. This is what you render.
htmlstringA static HTML rendering of the same tree.
jsonunknownThe editor's internal format. Do not render it.

Helpers

All three helpers accept RichTextValue | null | undefined, so you can pass a field without checking it first.

HelperReturnsEmpty or missing value
richTextDocument(value)MarkdownDocumentAn empty document ({ type: 'root', children: [] })
richTextMarkdown(value)string''
richTextHtml(value)string''

richTextDocument also handles older snapshots. Publications made before the CMS sent document carry only markdown. In that case the helper parses the Markdown for you, so older content keeps rendering.

Import the helpers from the rich-text entry point:

app/lib/rich-text.ts
import { richTextDocument, richTextHtml, richTextMarkdown } from '@areca-cms/site-sdk/rich-text'

The main @areca-cms/site-sdk entry point exports the same functions.

Render with React

Pass the document to Markdown from @tanstack/markdown/react. The example below styles a small set of elements and leaves the rest as the renderer's defaults.

components/article-body.tsx
import { Markdown } from '@tanstack/markdown/react'
import type { MarkdownComponents } from '@tanstack/markdown/react'
import { richTextDocument } from '@areca-cms/site-sdk/rich-text'
import type { RichTextValue } from '@areca-cms/site-sdk'

const components = {
  h2(props) {
    return <h2 {...props} className="mt-12 text-2xl font-semibold" />
  },
  p(props) {
    return <p {...props} className="mt-5 text-lg leading-8" />
  },
  a(props) {
    const external = /^https?:\/\//i.test(props.href ?? '')
    return (
      <a
        {...props}
        rel={external ? 'noopener noreferrer' : props.rel}
        target={external ? '_blank' : props.target}
      />
    )
  },
  img(props) {
    return <img {...props} loading="lazy" className="h-auto w-full rounded-xl" />
  },
} satisfies MarkdownComponents

export function ArticleBody({ body }: { body: RichTextValue | null | undefined }) {
  return <Markdown components={components}>{richTextDocument(body)}</Markdown>
}

The components map

components is an object whose keys are HTML element names (h1 to h6, p, a, img, ul, ol, li, blockquote, pre, code, hr, and so on). Each value is a React component that receives the element's props and children. Spread the props, add your classes or wrappers, and you have full control over the output. Any element you leave out uses the renderer's default.

Use satisfies MarkdownComponents, as shown above, so TypeScript checks the element names and props.

Static HTML

If you are not using React, use richTextHtml to insert the static HTML:

const html = richTextHtml(article.data.body)

The HTML is generated from the same tree as document, so the two always match. It does not go through your components map, so you cannot restyle elements with it. Prefer richTextDocument with a renderer whenever you can.

Code blocks

Editors can mark inline code and add fenced code blocks with a language. The CMS publishes both in each of the three properties:

Formhtmlmarkdowndocument
Inline code<code>npm install</code>, not highlighted`npm install`an inlineCode node
Code block<pre class="tm-code" data-lang="ts"><code class="language-ts"> with highlighted <span>sa fence: ```tsa code node with lang and value, not highlighted

Highlighting happens at publish time, so html already has the token classes. Each token is a <span class="th-token th-*">: th-token is on every token, and the second class names its kind (th-keyword, th-string, th-comment and so on). The published HTML has no colours or inline styles. Your CSS chooses them.

The language ids are:

IdLanguage
ts, tsxTypeScript, TSX
js, jsxJavaScript, JSX
html, css, jsonHTML, CSS, JSON
shellShell (bash, sh and zsh are stored as shell)
python, php, go, rustPython, PHP, Go, Rust
sql, yaml, markdown, diffSQL, YAML, Markdown, Diff
plaintextNo language: a fence without a name, shown without colours

A code block in any other language is kept as written and shown without colours.

Colour the code

@tanstack/highlight generates the CSS for these classes. Install it in your website project. The SDK does not depend on it:

pnpm add @tanstack/highlight
app/lib/code-theme.ts
import { createThemeCss } from '@tanstack/highlight/theme'
import { githubDarkTheme } from '@tanstack/highlight/themes/github-dark'
import { githubLightTheme } from '@tanstack/highlight/themes/github-light'

export const codeThemeCss = createThemeCss({
  light: githubLightTheme,
  dark: githubDarkTheme,
  codeBlockSelector: '.tm-code',
})

createThemeCss returns a CSS string. Save it as a stylesheet and import it from your global CSS. Light colours apply on :root, and dark colours apply under .dark. If your site switches themes another way, pass lightSelector or darkSelector. With codeBlockSelector: '.tm-code', the generated rules also give code blocks their padding, background and horizontal scrolling.

To use your own colours, skip createThemeCss and write rules for the token classes:

app/styles/code.css
.tm-code { overflow-x: auto; padding: 1rem; background: #f6f8fa; }
.th-keyword { color: #cf222e; }
.th-string { color: #0a7f64; }
.th-comment { color: #6e7781; font-style: italic; }

The token classes are th-token, th-attr, th-code-inline, th-command, th-comment, th-deleted, th-function, th-heading, th-inserted, th-keyword, th-link, th-literal, th-meta, th-number, th-operator, th-property, th-selector, th-string, th-tag, th-type and th-variable. Inline code is not highlighted, so style code in your own CSS.

Highlight document yourself

If you render richTextDocument with Markdown, the code blocks come out without token spans. To colour them, create a highlighter with the languages you need and pass it to the renderer. createTanStackMarkdownHighlighter adapts it to the renderer's highlighter option:

components/article-body.tsx
import { Markdown } from '@tanstack/markdown/react'
import { createHighlighter } from '@tanstack/highlight/core'
import { createTanStackMarkdownHighlighter } from '@tanstack/highlight/markdown'
import { js } from '@tanstack/highlight/languages/js'
import { ts } from '@tanstack/highlight/languages/ts'
import { richTextDocument } from '@areca-cms/site-sdk/rich-text'
import type { RichTextValue } from '@areca-cms/site-sdk'

const highlightCode = createTanStackMarkdownHighlighter(
  createHighlighter({ languages: [ts, js] }),
)

export function ArticleBody({ body }: { body: RichTextValue | null | undefined }) {
  return <Markdown highlighter={highlightCode}>{richTextDocument(body)}</Markdown>
}

Import only the languages you use. The package root imports every grammar, which makes the bundle much larger. Keep the CSS from the previous step, because the classes are the same.

Safety

  • Editors choose from a fixed set of formatting. The body can hold headings, lists, block quotes, code blocks, tables, images, bold, italic and links. It cannot hold arbitrary HTML, so there is no <script> tag to pass through.
  • Only render document, markdown or html. Never render the json property. It is the editor's internal format and is not meant for the page.
  • Check link targets if you need stricter rules. The SDK does not rewrite or filter link addresses in rich text. Links can be http, https or mailto, and your a component receives the href as written. If you only accept https or relative links, enforce that in your component.
  • Keep the renderer version in step with the SDK. A renderer at a different version may not understand the document tree. Upgrade the SDK and the renderer together.
  • SEO helpers for head tags and JSON-LD from the same entry.
  • Reading content for how to get the entry that holds the rich text field.
  • Field types for the richtext field and its settings.

On this page