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.
Using the RichText component
Section titled “Using the RichText component”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');Supported node types
Section titled “Supported node types”renderRichText() handles these TipTap node types out of the box:
| Node type | HTML 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 /> |
Text marks
Section titled “Text marks”Inline formatting is stored as marks on text nodes:
| Mark | HTML output |
|---|---|
bold | <strong> |
italic | <em> |
underline | <u> |
strike | <s> |
code | <code> |
link | <a href="..."> (supports class and rel attributes) |
subscript | <sub> |
superscript | <sup> |
Link attributes
Section titled “Link attributes”Links in rich text support optional attributes beyond the URL, configured via the Advanced section of the link dialog in the admin UI.
| Attribute | Type | Description |
|---|---|---|
href | string | Link URL (required) |
target | string | Link target — set via the “Open in new tab” checkbox |
class | string | CSS class(es) for styling, e.g. btn btn-primary |
rel | string | Relationship 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.
Styling links as buttons
Section titled “Styling links as buttons”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 } } ]}SEO link attributes
Section titled “SEO link attributes”The rel attribute controls how search engines treat the link:
| Value | Use case |
|---|---|
nofollow | Don’t pass link equity (e.g. user-submitted or untrusted links) |
sponsored | Paid or sponsored links |
ugc | User-generated content |
noopener noreferrer | Security — auto-applied for new-tab links |
Multiple values can be combined with spaces: nofollow sponsored.
Image attributes
Section titled “Image attributes”Image nodes support these attributes:
| Attribute | Type | Description |
|---|---|---|
src | string | Image URL |
alt | string | Alt text |
title | string | Title text (optional) |
width | string | CSS width (e.g. "50%") |
float | string | "none", "left", "right", or "center" |
caption | string | Figure caption text (optional) |
href | string | Link URL — wraps the image in <a> (optional) |
linkTarget | string | Link 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.
Custom renderers
Section titled “Custom renderers”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" }}TipTap JSON structure
Section titled “TipTap JSON structure”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" } } ]}