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:
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:
1{2 "data": {3 "allBlogPosts": [4 {5 "content": {6 "value": {7 "schema": "dast",8 "document": {9 "type": "root",10 "children": [11 {12 "type": "paragraph",13 "children": [14 "type": "span",15 "value": "Hello world"16 ]17 },18 {19 "type": "paragraph",20 "children": [21 "type": "span",22 "value": "Lorem ipsum..."23 ]24 }25 ]26 }27 }28 }29 }30 ]31 }32}
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:
1import { StructuredText } from "react-datocms";2
3export default function Home({ props }) {4 return (5 <div>6 {props.data.allBlogPosts.map(blogPost => (7 <article key={blogPost.id}>8 <h6>{blogPost.title}</h6>9 <StructuredText data={blogPost.content} />10 </article>11 ))}12 </div>13 );14}
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:
1import { renderRule, isHeading } from 'datocms-structured-text-utils';2
3<StructuredText4 data={data.blogPost.content}5 customRules={[6 renderRule(7 isHeading,8 ({ node, children, key }) => {9 const Tag = `h${node.level}`;10 return <Tag className="text-cyan-500" key={key}>{children}</Tag>;11 },12 ),13 ]}14/>
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 URLsinlineItem
nodes that let us embed a reference to a record in between textblock
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 links
we 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:
1const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {2 allBlogPosts(first: $limit) {3 id4 title5 content {6 value7 blocks {8 __typename9 ... on ImageBlockRecord {10 id11 image { url alt }12 }13 }14 links {15 __typename16 ... on BlogPostRecord {17 id18 slug19 }20 }21 }22 }23}`;
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:
1<StructuredText2 data={content}3 renderInlineRecord={({ record }) => {4 switch (record.__typename) {5 case "BlogPostRecord":6 return <a href={`/blog/${record.slug}`}>{record.title}</a>;7 default:8 return null;9 }10 }}11/>
Custom render rules for blocks
For block
nodes, you need to specify a renderBlock
rule, for example like this:
1<StructuredText2 data={content}3 renderBlock={({ record }) => {4 switch (record.__typename) {5 case "ImageBlockRecord":6 return <img src={record.image.url} alt={record.image.alt} />;7 default:8 return null;9 }10 }}11/>
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.