How to Use DatoCMS's Awesome 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.
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 highlighing, 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` and `thematicBreak`, and they are presented within an array. Within each of these 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
he 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 haeding tag.
For code nodes, you need a custom component like [prism-react-renderer](c) 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 `links` we need to explicitly query for whatever inner fields from the record we need. 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 } } } } }`;
In order to render special nodes, we need 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 NextJS. 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.
<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.