Skip to content

Rich Text

WollyCMS stores rich text as TipTap JSON, not raw HTML. The @wollycms/astro package includes renderRichText() to convert this JSON to HTML at render time.

The simplest way to render rich text is with the built-in component:

---
import RichText from '@wollycms/astro/components/RichText.astro';
const { fields } = Astro.props;
---
<div class="prose">
<RichText content={fields.body} />
</div>

Or call renderRichText() directly for more control:

---
import { renderRichText } from '@wollycms/astro/helpers/richtext';
const { fields } = Astro.props;
const html = renderRichText(fields.body);
---
<div class="prose" set:html={html} />

Split-origin deployments (Cloudflare Workers, etc.)

Section titled “Split-origin deployments (Cloudflare Workers, etc.)”

If your CMS and Astro frontend run on different origins — common with Cloudflare Workers, where each is a separate Worker — inline images and media links in rich text will use relative paths like /api/content/media/42/original that won’t resolve on the frontend origin.

Pass a baseUrl prop to the RichText component so these paths get prefixed with your CMS domain:

<RichText content={fields.body} baseUrl="https://cms.example.com" />

Or when calling renderRichText() directly:

const html = renderRichText(fields.body, 'https://cms.example.com');

renderRichText() handles these TipTap node types out of the box:

Node typeHTML output
paragraph<p>
heading<h2><h6> (based on level attribute)
bulletList<ul>
orderedList<ol>
listItem<li>
blockquote<blockquote>
codeBlock<pre><code> (with optional language class)
image<img> (or <a><img></a> when linked)
table<table> with <tr>, <th>, <td>
horizontalRule<hr />
hardBreak<br />

Inline formatting is stored as marks on text nodes:

MarkHTML output
bold<strong>
italic<em>
underline<u>
strike<s>
code<code>
link<a href="..."> (supports class and rel attributes)
subscript<sub>
superscript<sup>

Links in rich text support optional attributes beyond the URL, configured via the Advanced section of the link dialog in the admin UI.

AttributeTypeDescription
hrefstringLink URL (required)
targetstringLink target — set via the “Open in new tab” checkbox
classstringCSS class(es) for styling, e.g. btn btn-primary
relstringRelationship attribute for SEO, e.g. nofollow, sponsored

When “Open in new tab” is checked and no rel value is provided, noopener noreferrer is added automatically for security.

The class attribute lets your theme style specific links differently. For example, to render a link as a button, an editor adds btn btn-primary in the CSS Class field, and the frontend theme provides the matching CSS:

/* Example theme styles for rich text link classes */
.prose a.btn {
display: inline-block;
padding: 0.6rem 1.5rem;
border-radius: 6px;
font-weight: 600;
text-decoration: none;
text-align: center;
}
.prose a.btn-primary {
background: var(--color-primary);
color: white;
}
.prose a.btn-outline {
border: 2px solid var(--color-primary);
color: var(--color-primary);
}

The classes are stored in the TipTap JSON and rendered as a class attribute on the <a> tag:

{
"type": "text",
"text": "Apply Now",
"marks": [
{
"type": "link",
"attrs": {
"href": "https://example.com/apply",
"class": "btn btn-primary",
"rel": null,
"target": null
}
}
]
}

The rel attribute controls how search engines treat the link:

ValueUse case
nofollowDon’t pass link equity (e.g. user-submitted or untrusted links)
sponsoredPaid or sponsored links
ugcUser-generated content
noopener noreferrerSecurity — auto-applied for new-tab links

Multiple values can be combined with spaces: nofollow sponsored.

Image nodes support these attributes:

AttributeTypeDescription
srcstringImage URL
altstringAlt text
titlestringTitle text (optional)
widthstringCSS width (e.g. "50%")
floatstring"none", "left", "right", or "center"
captionstringFigure caption text (optional)
hrefstringLink URL — wraps the image in <a> (optional)
linkTargetstringLink target, e.g. "_blank" (optional)

When an image has an href, the rendered output wraps it in a link:

<!-- Image without link -->
<img src="/api/content/media/42/original" alt="QR code" />
<!-- Image with link -->
<a href="https://example.com" target="_blank">
<img src="/api/content/media/42/original" alt="QR code" />
</a>

Editors can add links to images in the admin UI by clicking the image and then clicking the link button in the toolbar.

If you write your own renderer instead of using renderRichText(), make sure to handle image links. The href and linkTarget values are stored as attributes on the image node, not as marks:

{
"type": "image",
"attrs": {
"src": "/api/content/media/42/original",
"alt": "QR code",
"href": "https://example.com",
"linkTarget": "_blank"
}
}

Rich text content is stored as a JSON document with a doc root node. Here is an example showing the structure:

{
"type": "doc",
"content": [
{
"type": "heading",
"attrs": { "level": 2 },
"content": [
{ "type": "text", "text": "Welcome" }
]
},
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Visit our " },
{
"type": "text",
"text": "website",
"marks": [
{ "type": "link", "attrs": { "href": "https://example.com" } }
]
},
{ "type": "text", "text": " for details." }
]
},
{
"type": "image",
"attrs": {
"src": "/api/content/media/5/original",
"alt": "Logo",
"href": "https://example.com",
"linkTarget": "_blank"
}
}
]
}