The DatoCMS Blog
How to Use DatoCMS's Structured Text Field in a NextJS App
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-datocmsor
npm install react-datocmsThe 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:
itemLinknodes that point to other records instead of URLsinlineItemnodes that let us embed a reference to a record in between textblocknodes
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.
- What are structured text fields
- Querying a structured text field
- How to convert the result of the query to HTML within NextJS
- How to style a structured text component
- 1. Apply CSS classes to the parent div
- 2. Create custom render rules
- Rendering content that is not text (blocks and links to internal records)
- Rendering special nodes
- Custom render rules for internal records
- Custom render rules for inLine items
- Custom render rules for blocks
- Using a custom component to gather reusable custom render rules