Rich text rendering without dangerouslySetInnerHTML
If you have an existing Sanity project, you may prefer to read the documentation on Portable Text for details on how to set it up in your application.
What's covered in this guide
You'll create a new free Sanity project using the Next.js template, publish new documents with Sanity Studio and explore the benefits of Portable Text.
Create a new project
From the command line, create a new free Sanity project using the Next.js template.
If you don't already have an account with Sanity, you'll be prompted to create one.
npm create sanity@latest -- --template sanity-io/sanity-template-nextjs-clean --coupon=text-guide
Now inside this directory, start the development server.
npm run dev
You should now have two apps running:
- http://localhost:3000 – Your Next.js app
- http://localhost:3333 – Your Sanity Studio
These apps are separately configured in the following directories:
├─ /nextjs-app
└─ /studio
The Sanity Studio is configured with code, edits you make to the configuration will render immediately in the development environment.
Content in Sanity Studio is written to a schemaless, cloud-hosted Dataset in what we call the Content Lake.
The Next.js app fetches content using Sanity Client with the GROQ query language.
Enter the Studio
Open http://localhost:3333 and log in to your Sanity Studio. The first tab that will open is the Presentation tool.
The Presentation tool is used to view your Next.js front end (http://localhost:3000) within an Iframe so that you can interact with your content.
💡 To see how Presentation can be used for interactive live preview of draft content, see the guide in this series on Visual Editing.
Publish a new post
Click "+ Create" from the top menu and select "Post."
The post
document type currently has a few required fields. You will need to give this new document a
- Title
- Slug
- Cover Image
- and alternative text for the image
Once the Publish button is enabled, publish the post.
Open the post in Presentation
At the top of the document, you will see a box saying "Used on 2 pages" which you can open to show the locations of the current document in the front end.
Click the first location and you'll see the front end open side-by-side with the document editor. Make a change to the document and press Publish. You'll see the document update again, without a reload!

Authoring Portable Text
Scroll to the Content field in the document editor and start entering some text. There's formatting controls like you would expect from an WYSIWYG editor—but you're not authoring HTML or Markdown, it's Portable Text.

In the top right hand corner of the document editor, click the Inspect menu item. This will show you the JSON value of the current document, which the editor is writing.
In the content field you'll see an array of objects written in the standard of Portable Text. It's not intended to be human readable—it's a way of storing rich text and block content in a way that is serializable.
Rendering Portable Text
In the preview window to the left, you'll see content appear as you type. It does not have a direct 1:1 relationship with the presentation of content being authored in the Studio—it's not supposed to! Applications that consume Portable Text get to choose how and if each block is rendered.
As an example, your application may just render the body content. But it also may extract just blocks where the style
equals heading
in order to render a table of contents.
Querying Portable Text
Add a "link" to some text in the content field
Imagine you're tasked to get the URL from every link in every blog post.

With content stored as data, you can perform powerful queries for deeply nested content. From the top toolbar, open the Vision tool in the Studio and run the following GROQ query:
*[
_type == "post"
&& defined(content)
]
.content[count(markDefs) > 0]
.markDefs[_type == "link"].href
This query will look up:
- Every document in the dataset (represented by
*
) - Filtered to only those of the "post" type where the content field has a value
- Return the content field, filtered to only those with mark definitions
- Return the mark definitions, filtered to only those of the "link" type
- Filter those results to only return the "href" attribute
This should give you some idea of just how powerful it is to store your block content in a format that can be queried with such granularity! You might not perform this sort of query often, but when you do, it's a life saver.
Compare this to Markdown or HTML—hope you like Regex.
If you're new to GROQ, consider the Between GROQ and a Hard Place course on Sanity Learn.
How this works
Portable Text for React
Open nextjs-app/app/components/PortableText.tsx

When Portable Text content is queried into your application, you need a way to map over that data and render components.
The @portabletext/react library contains a component PortableText
which will handle a few default block types like headings and list, serializing them into the sorts of DOM elements you'd expect (<h1>
, <h2>
, <ul>
, etc.)
The CustomPortableText
component in this file contains that default component, along with a list of components
to add custom serialization to include functionality like clickable links on headings. As well as class names on each individual block.
Next steps
Some ideas of what you might do next.
- Commit this repository to your Git provider and deploy to Vercel to share these live previews with content creators
- Add or edit Sanity Studio schema types. The template comes with a few already created, try adding a new field to
post.ts
in the/studio
directory and see how the Studio updates imediately. - Explore GROQ,
queries.ts
in the/nextjs-app
directory contains all the queries that the Next.js app currently makes. You can test queries in the Studio using the Vision tool. - Take the Day One with Sanity Studio Course to get a from-the-ground-up understanding of how to create new Sanity projects.