Skip to main content

Rich text

<Copy.Rich field="body" />

<Copy.Rich> is the one exception to plain-text output. A client writing a blog post cannot work with a single unformatted string, so that content is stored as Markdown.

body: |
We started in **2019** with one idea.

- Ship small
- Ship often

Read the [full story](/about).

The allowlist

Markdown is rendered straight to React elements against a strict allowlist:

Allowed
Paragraphs<p>
Emphasis**bold**, *italic*
Links[text](href)
Listsordered and unordered
Headings# through ######
Blockquotes>
Inline code`code`

Anything else — raw HTML, images, tables, code fences with attributes — is dropped, and its text kept. A client pasting styled content from a word processor gets their words, not their markup.

Why there is nothing to sanitise

No HTML string is ever produced. The Markdown AST is walked and React elements are constructed directly, so there is no dangerouslySetInnerHTML anywhere in the library and no sanitiser that could miss a case.

That is a structural guarantee rather than a filtering one: a bypass would have to make the Markdown parser emit an allowlisted node type that renders someone else's script, which the node types do not permit. The test suite covers this with XSS bypass attempts.

Link hrefs are still checked — javascript: and data: URLs do not survive.

Styling the output

<Copy.Rich> renders the elements with no classes of its own. Style them from the parent:

<div className="prose">
<Copy.Rich field="body" />
</div>

In editor mode the output is wrapped in a <div> carrying the editing anchor. That wrapper does not exist in production, so do not write CSS that depends on it.

When to use plain text instead

Use <Copy> for anything that is a single string: a heading, a label, a subtitle, a button. Rich text costs a Markdown parse per field and hands the client formatting controls they do not need on a hero heading.

A rough rule: if you would be annoyed to find a bulleted list inside it, it is not a rich text field.