When working with Astro.js and Umbraco's Headless Delivery API, one challenge developers may face is dynamically rendering rich text content stored as JSON. Unlike traditional HTML content that can be injected using set:html
, structured JSON data requires a more flexible approach—especially when you need to integrate custom components for handling specific elements.
Astro’s set:html
directive is a common way to render raw HTML content safely. However, when dealing with structured JSON—such as Umbraco’s umb-rte-element
—this method becomes limiting. We need a way to parse the JSON structure and dynamically render the appropriate components while maintaining Astro's component-based architecture.
To handle this, we can create a recursive Astro component that processes the JSON and renders the appropriate elements using Astro components. The types in the following code were generated using the Umbraco.Community.DeliveryApiExtensions package, which helps define strong types for the JSON response. By using strong typing and community extensions like Umbraco.Community.DeliveryApiExtensions, we ensure safer and more predictable handling of JSON-based content.
Sample Json:
"blogRichText": {
"tag": "#root",
"attributes": {},
"elements": [
{
"tag": "p",
"attributes": {
"id": "someId",
"class": "some-class"
},
"elements": [
{
"text": "This is ",
"tag": "#text"
},
{
"tag": "strong",
"attributes": {},
"elements": [
{
"text": "bold text",
"tag": "#text"
}
]
},
{
"text": " with an object following",
"tag": "#text"
}
]
},
{
"tag": "umb-rte-block",
"attributes": {
"content-id": "705a6633-3e20-4f3a-be76-4b450a397608"
},
"elements": []
},
{
"tag": "p",
"attributes": {},
"elements": [
{
"text": "with some more text",
"tag": "#text"
}
]
}
],
"blocks": [
{
"content": {
"contentType": "angularCounter",
"id": "705a6633-3e20-4f3a-be76-4b450a397608",
"properties": {}
},
"settings": null
}
]
}
RichTextField.astro
---
import type { RichTextGenericElementModel , RichTextRootElementModel , RichTextTextElementModel } from"@/api/umbraco";
import {richTextFieldHasContent} from"./RichTextHelper"import RenderRichText from'./RenderRichText.astro';
interface Props {
richTextField: RichTextGenericElementModel | RichTextRootElementModel | RichTextTextElementModel | null | undefined;
}
const { richTextField } = Astro.props;
if (!richTextFieldHasContent(richTextField)) returnnull;
// If the field is the root element, grab the blocks for use in block elements.let blocks: any[] = [];
if (richTextField && richTextField.tag === '#root') {
const root = richTextField as RichTextRootElementModel;
blocks = root.blocks;
}
---
<divclass="rich-text-block container mx-auto px-[15px]"><RenderRichTextnode={richTextField}blocks={blocks}></div>
RenderRichText.astro:
---
// filepath: /src/components/richText/RenderRichText.astro
import RichTextFieldBlockItem from './RichTextFieldBlockItem.astro';
import type { ApiBlockItemModel, RichTextGenericElementModel, RichTextRootElementModel, RichTextTextElementModel } from "@/api/umbraco";
import RenderRichTextComponent from './RenderRichText.astro';
interface Props {
node: RichTextGenericElementModel | RichTextRootElementModel | RichTextTextElementModel | null | undefined;
blocks: ApiBlockItemModel[] | null | undefined;
}
const { node, blocks } = Astro.props;
if (!node) return null;
const voidElements = [
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr"
];
const isText = node.tag === '#text';
const textNode = isText ? nodeas RichTextTextElementModel : null;
const isRoot = node.tag === '#root';
const rootNode = isRoot ? nodeas RichTextRootElementModel : null;
const isBlock = node.tag === 'umb-rte-block';
const blockNode = isBlock ? nodeas RichTextGenericElementModel : null;
const block = isBlock ? blocks?.find((b) => b.content && b.content.id === blockNode?.attributes['content-id']) : null;
const isGeneric = !isText && !isRoot && !isBlock;
const genericNode = isGeneric ? nodeas RichTextGenericElementModel : null;
const GenericTag = genericNode?.tag|| 'div';
const isVoidElement = voidElements.includes(GenericTag);
---
{isText && <Fragment set:html={textNode?.text}></Fragment>}
{isRoot && rootNode?.elements.map((child, i) =>
<RenderRichTextComponent node={child} blocks={blocks} />)}
{isBlock && <RichTextFieldBlockItem block={block} />}
{isGeneric && isVoidElement && (
<GenericTag {...genericNode?.attributes} />
)}
{isGeneric && !isVoidElement && (
<GenericTag {...genericNode?.attributes}>
{genericNode?.elements.map((child, i) =>
)}
</GenericTag>
)}
umb-rte-element
tags, it passes the associated block data to a RichTextFieldBlockItem whic determines the astro component that should render the block.This approach gives Astro.js developers working with Umbraco's Headless Delivery API a structured way to render rich text without relying on set:html
and allows for easily consuming regular astro components for rendering .
If you're integrating Umbraco's Headless Delivery API with Astro.js, handling rich text JSON properly is key to a smooth developer experience. With this method, you can maintain a clean component-based structure while rendering dynamic content efficiently.
The team at Given Data LLC continuously monitors advances in the content management space, keeping us ahead of the competition. Urgent need? call us