Partners

The DatoCMS Blog

How to Use DatoCMS's Structured Text Field in a NextJS App

Posted on November 29th, 2021 by Natalia Baeza

At Cantiere Creativo, we are proud to be the place where DatoCMS was born, and we use Dato in a vast majority of our projects. In early 2021 Dato launched a revolutionary new feature that greatly enhances the experience of editors while inserting and managing content. In this post, we're going to walk through all you need to know as a developer to start using Structured Text in your website today.

What are structured text fields

The structured text field provides a WYSIWYG editor where you can format text, insert code blocks with syntax highlighting, insert links (to regular URLs or to internal records) and insert custom blocks (galleries, videos, CTAs, etc.) that can be embedded in the text and reordered with drag and drop. You can read the documentation here.

Querying a structured text field

Suppose, for example, that you have many blog posts, where each has a structured text field named content. Your query for the content of all blog posts would be:

query {
allBlogPosts {
title
content {
value
}
}
}

The query returns data in dast format, which will need to be converted to HTML in order to be rendered. The query above, for instance, might return something like this:

{
"data": {
"allBlogPosts": [
{
"content": {
"value": {
"schema": "dast",
"document": {
"type": "root",
"children": [
{
"type": "paragraph",
"children": [
"type": "span",
"value": "Hello world"
]
},
{
"type": "paragraph",
"children": [
"type": "span",
"value": "Lorem ipsum..."
]
}
]
}
}
}
}
]
}
}

The dast tree starts from a node that is called root and corresponds to the body in HTML. Like body, root can have children of different types. In particular, children nodes can be of type paragraph, heading, list, code, blockquote, block or thematicBreak, and they are presented within an array. Within each of these child nodes, other children can be included. You can find a full list of the children that can be included in each type of node, and of the attributes that can be passed for each, here.

How to convert the result of the query to HTML within NextJS

The react-datocms package gives us a ready-made React component to render Structured Text. You can install the package with

yarn add react-datocms

or

npm install react-datocms

The component takes only one data prop, and is used like this:

import { StructuredText } from "react-datocms";
export default function Home({ props }) {
return (
<div>
{props.data.allBlogPosts.map(blogPost => (
<article key={blogPost.id}>
<h6>{blogPost.title}</h6>
<StructuredText data={blogPost.content} />
</article>
))}
</div>
);
}

That's all; the component gets rendered in HTML with a default style.

How to style a structured text component

The component renders all nodes except for inline_item, item_link and block using a set of default rules. If you want to customize the style of elements inside the <StructuredText /> component, there are two options.

1. Apply CSS classes to the parent div

The first is to style from the parent div like this:

<div className="formatted-content">
<StructuredText data={blogPost.content} />
</div>

where the classes are defined to target specific elements inside the div:

.formatted-content p {
margin: 20px;
}
.formatted-content a {
color: white;
}

2. Create custom render rules

The second option is to use a custom render rule to override the default render rules.

Render rules are the transformation functions that the <StructuredText /> component uses to traverse the dast tree and convert each node from dast into JSX. To create custom render rules, we need to use the datocms-structured-text-utils package. This package is already included when we import react-datocms, so we don't need to install it separately.

From datocms-structured-text-utils we can import a typescript type for each of the different nodes, and a function to check that a node of a certain type is in scope (e.g. isHeading, isParagraph, isList, etc.).

For example, to add the class text-cyan-500 to all headings, we could do this:

import { renderRule, isHeading } from 'datocms-structured-text-utils';
<StructuredText
data={data.blogPost.content}
customRules={[
renderRule(
isHeading,
({ node, children, key }) => {
const Tag = `h${node.level}`;
return <Tag className="text-cyan-500" key={key}>{children}</Tag>;
},
),
]}
/>

Inside the renderRule function, we need to pass the typescript type guard (isHeading in the above example) and a transformation function (the second argument passed to renderRule). The transformation function gives us access to the node, and depending on the node it is, we have access to different things.

For example, the node in the above example is a heading, so we can go here to see what attributes we have access to for a heading. In this case we have access to level (from 1 to 6), and we also have access to children, which can be span, link, itemLink, and inlineItem. node.children gives us access to what the node contains inside, in dast format. In addition to node, we can also pass children to the transformation function; the value of children is the content of the node already converted into HTML.

In the above example, we create a Tag variable, set it equal to a string, and then use it as if it were a component which is an HTML heading tag.

For code nodes, you need a custom component like prism-react-renderer to add syntax highlight. See the documentation for custom render rules for more details.

There are various utility packages to work with StructuredText; they are listed here.

Rendering content that is not text (blocks and links to internal records)

Structured Text enables us to intersperse textual content with special types of nodes, namely:

  • itemLink nodes that point to other records instead of URLs

  • inlineItem nodes that let us embed a reference to a record in between text

  • block nodes

These special nodes require a specific query. If we insert blocks, we need to query not only for value (as with textual content) but also for blocks, and if we insert links to internal records, we need to query for links. In addition, within blocks or linkswe need to explicitly query for whatever inner fields from the record we require. We must also remember to always query the record's id.

This is what the query looks like:

const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
allBlogPosts(first: $limit) {
id
title
content {
value
blocks {
__typename
... on ImageBlockRecord {
id
image { url alt }
}
}
links {
__typename
... on BlogPostRecord {
id
slug
}
}
}
}
}`;

Rendering special nodes

In order to render special nodes, we require custom render rules; the <StructuredText /> component doesn't know how to render these special nodes by default.

Custom render rules for internal records

For links to internal records, we need to provide a renderLinkToRecord function. This function gives us access to the record and to children (the record's content). Suppose for example that we want to link to a record using the Link component from Next.js, We could do something like this:

<StructuredText
data={content}
renderLinkToRecord={({ record, children }) => {
return (
<Link href={`/pages/${record.slug}`}>
<a>{children}</a>
</Link>
);
}} />

Custom render rules for inLine items

For inlineItem nodes, you need to specify a renderInlineRecord rule like this:

<StructuredText
data={content}
renderInlineRecord={({ record }) => {
switch (record.__typename) {
case "BlogPostRecord":
return <a href={`/blog/${record.slug}`}>{record.title}</a>;
default:
return null;
}
}}
/>

Custom render rules for blocks

For block nodes, you need to specify a renderBlock rule, for example like this:

<StructuredText
data={content}
renderBlock={({ record }) => {
switch (record.__typename) {
case "ImageBlockRecord":
return <img src={record.image.url} alt={record.image.alt} />;
default:
return null;
}
}}
/>

You can consult the documentation on rendering special nodes here.

Using a custom component to gather reusable custom render rules

If a component is shared among different pages, with the same custom render rules, we can make a reusable component so that the rules are defined only once, and then share this custom component throughout our site.

For example, the Dato website uses a single <PostContent /> component anywhere where there is structured content with blocks. This component has all the custom render rules that are needed across the website. You can take a look at the component code here.