Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change Site Theme
Sanity logo

Documentation

    • Overview
    • Platform introduction
    • Next.js quickstart
    • Nuxt.js quickstart
    • Astro quickstart
    • React Router quickstart
    • Studio quickstart
    • Build with AI
    • Content Lake
    • Functions
    • APIs and SDKs
    • Agent Actions
    • Visual Editing
    • Blueprints
    • Platform management
    • Dashboard
    • Studio
    • Canvas
    • Media Library
    • App SDK
    • Content Agent
    • HTTP API
    • CLI
    • Libraries
    • Specifications
    • Changelog
    • User guides
    • Developer guides
    • Courses and certifications
    • Join the community
    • Templates
Developer guides
Overview

  • Develop with AI

    Best practices

  • Query optimization

    Paginating with GROQ
    High performance GROQ

  • Roles and authentication

    Setting up Single Sign-On with SAML
    Third-Party Login (Deprecated)
    OAuth2
    Set up SSO authentication with SAML and Azure/Entra ID
    Set up SSO authentication with SAML and PingIdentity
    Set up SSO authentication with SAML and JumpCloud
    Reconcile users against internal systems
    Restrict Access to Specific Documents
    Setting up a Default Relay State for IdP Initiated - SAML Logins

  • Structured content

    Scalable navigation patterns
    An opinionated guide to Sanity Studio
    Browsing Content How You Want with Structure Builder
    Deciding on fields and relationships
    Create richer array item previews
    Dynamic folder structure using the currentUser and workflow states
    Create a time duration object field
    Level up Your Edit Modal with Next/Previous Navigation Buttons for Array Items
    Create a “coupon generator” string field input
    Managing redirects with Sanity
    Create a document form progress component
    Create an array input field with selectable templates
    Creating a Parent/Child Taxonomy
    Create interactive array items for featured elements
    Create a visual string selector field input
    Create a survey rating number field input
    How to use structured content for page building
    Create a recycling bin for deleted documents via Sanity Functions

  • Frontend integration

    Add live content to your application
    Forms with Sanity
    Vercel Integration
    Build your blog with Astro and Sanity
    How to implement front-end search with Sanity

  • Ecommerce

    Displaying Sanity content in Shopify
    Sanity Connect for Shopify
    Custom sync handlers for Sanity Connect

  • Integrating with other services

    A/B testing with Sanity and Growthbook
    Cookie consent integrations with Sanity
    Integrating external data sources with Sanity
    Klaviyo (email campaigns)
    Developing with Next.js on GitHub Codespaces

  • Adopting Sanity

    How to pitch Sanity.io to your team
    Convincing your clients to go with Sanity.io, rather than a traditional CMS
    Not-profit plan
    Agencies: Navigating the Spring 2025 Organization Changes
    How to generate massive amounts of demo content for Sanity
    How to implement Multi-tenancy with Sanity

  • GROQ

    GROQ-Powered Webhooks – Intro to Filters
    GROQ-Powered Webhooks – Intro to Projections

  • Portable Text

    Presenting Portable Text
    Add Inline blocks for the Portable Text Editor
    Beginners guide to Portable Text
    How to add custom YouTube blocks to Portable Text
    Converting Inline Styles to Sanity Block Decorators
    Add things to Portable Text
    Change the height of the PTE

  • Community and ecosystem

    Create your own Sanity template
    Community guides
    Community Code of Conduct
    Contribute to the ecosystem

  • Plugin development

    Migrating plugins to support Content Releases

On this page

Previous

GROQ-Powered Webhooks – Intro to Projections

Next

Add Inline blocks for the Portable Text Editor

Was this page helpful?

On this page

  • Serialization tooling
  • Portable Text to HTML
  • Portable Text to React
  • Portable Text to React Native
  • Portable Text to React PDF
  • Portable Text to Vue
  • Portable Text to Svelte
  • Portable Text to Astro
  • Portable Text to Hyperscript
  • Portable Text to Markdown
  • Portable Text in .NET
  • Portable Text in Python
  • Portable Text in PHP
  • Plain text serialization
  • Rendering Portable Text in React
  • Join references
  • Deserialization
Developer guidesLast updated January 9, 2026

Presenting Portable Text

Transform Portable Text to whatever you want

When you query your Sanity project’s API your rich text content is returned as Portable Text. If you are accustomed to traditional or other headless CMSes you are probably used to dealing with HTML or Markdown out of the box. Portable Text is designed to be used in pretty much any format or markup where you want to render rich text content.

You render Portable Text by serializing the arrays that contain your content into the format you need it. There is tooling for generic markup and programming languages and for popular frameworks, that makes it easier to serialize Portable Text and lets you decide how custom content types should be handled.

Serialization tooling

We have helpers for different languages and platforms.

Protip

You may notice some mentions of block text, including in the tool names. This was the nomenclature we used before open sourcing and publishing the specification for Portable Text. You can explore the specification on www.portabletext.org.

Portable Text to HTML

https://github.com/portabletext/to-html

Portable Text to React

https://github.com/portabletext/react-portabletext

Portable Text to React Native

https://github.com/portabletext/react-native-portabletext

Portable Text to React PDF

https://github.com/portabletext/react-pdf-portabletext

Portable Text to Vue

https://github.com/portabletext/vue-portabletext

Portable Text to Svelte

https://github.com/portabletext/svelte-portabletext/

Portable Text to Astro

https://github.com/theisel/astro-portabletext

Portable Text to Hyperscript

https://github.com/sanity-io/block-content-to-hyperscript

Portable Text to Markdown

https://github.com/sanity-io/block-content-to-markdown

Portable Text in .NET

https://github.com/oslofjord/sanity-linq#9-rendering-block-content

Portable Text in Python

https://github.com/otovo/python-portabletext-html

Portable Text in PHP

https://github.com/sanity-io/sanity-php#rendering-block-content

Need to serialize to something not listed here in a language we don't cover? Create an issue on the repo for Portable Text, or join us on Slack and let us know.

Plain text serialization

Serializing Portable Text to plain text can be useful when you need it previews or similar. It also helps demystify what goes into serializing Portable Text. Here's a function written in JavaScript that takes a Portable Text array as an argument, and returns it as paragraphs in plain text:

function toPlainText(blocks = []) {
  return blocks
    // loop through each block
    .map(block => {
      // if it's not a text block with children, 
      // return nothing
      if (block._type !== 'block' || !block.children) {
        return ''
      }
      // loop through the children spans, and join the
      // text strings
      return block.children.map(child => child.text).join('')
    })
    // join the paragraphs leaving split by two linebreaks
    .join('\n\n')
}

Rendering Portable Text in React

A common use case is to render rich text content from Sanity in the popular web framework React. We have made tooling that deals with the defaults out of the box, and lets you add components for controlling how custom content type should be rendered in the front end. Let's look at an example of how to set it up:

import React from 'react'
import * as ReactDOM from 'react-dom'
import {PortableText} from '@portabletext/react'
import {createClient} from '@sanity/client'

const client = sanityClient({
  projectId: '<your project id>',
  dataset: '<your dataset>',
  apiVersion: '2022-05-05',
  useCdn: true
})

const components = {
  types: {
    code: (props) => {
      const {language, code} = props.value
      return (
        <pre data-language={language}>
          <code>{code}</code>
        </pre>
      )
    }
  }
}

client.fetch('*[_type == "article"][0]')
  .then(article => {
    ReactDOM.render(
      <PortableText value={article.body} components={components} />,
      document.getElementById('root')
    )
  })

Additional details are available in our documentation or on the @portabletext/react README.

Portable Text to React

In 2022, the @sanity/block-content-to-react package was deprecated in favour of @portabletext/react. The example above uses components and values from the new package instead of serializers and blocks, but we offer a migration guide and will continue to answer questions about @sanity/block-content-to-react in the Slack community.

Join references

If you have references to other documents such as internal links, files or images in your Portable Text, you typically want to join the data from those documents into your Portable Text. Here's how:

Say you have a Portable Text with internal links to other articles using mark annotations like so:

// portableText.js
export default {
  name: 'portableText',
  type: 'array',
  title: 'Content',
  of: [
    {
      type: 'block',
      marks: {
        annotations: [
          {
            name: 'internalLink',
            type: 'object',
            title: 'Internal link',
            fields: [
              {
                name: 'reference',
                type: 'reference',
                title: 'Reference',
                to: [
                  { type: 'article' },
                  // other types you may want to link to
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}

In order to get the slug of the linked article you need to use GROQ to query your document and use the join syntax to fetch the referenced article like so:

*[_type == "post"]{
  ...,
  body[]{
    ...,
    markDefs[]{
      ...,
      _type == "internalLink" => {
        "slug": @.reference->slug
      }
    }
  }
}

Read the full guide on including links in Portable Text.


Deserialization

If you need to convert existing markup to Portable Text you can use the JavaScript library Sanity Block Tools. You can use it both in a browser and in a node.js environment. It also lets you make custom rules to deserialize parts of your HTML into custom content types etc.

Complete example of deserialization of HTML into Portable Text blocks in a browser environment:

import Schema from '@sanity/schema'
import blockTools from '@portabletext/block-tools'


// Start with compiling a schema we can work against
const defaultSchema = Schema.compile({
  name: 'myBlog',
  types: [
    {
      type: 'object',
      name: 'blogPost',
      fields: [
        {
          title: 'Title',
          type: 'string',
          name: 'title'
        },
        {
          title: 'Body',
          name: 'body',
          type: 'array',
          of: [{type: 'block'}]
        }
      ]
    }
  ]
})

// The compiled schema type for the content type that holds the block array
const blockContentType = defaultSchema.get('blogPost')
  .fields.find(field => field.name === 'body').type


// Convert HTML to block array
const blocks = blockTools.htmlToBlocks(
  '<html><body><h1>Hello world!</h1><body></html>',
  blockContentType
)
// Outputs
//
//  {
//    _type: 'block',
//    style: 'h1'
//    children: [
//      {
//        _type: 'span'
//        text: 'Hello world!'
//      }
//    ]
//  }


// Get the feature-set of a blockContentType
const features = blockTools.getBlockContentFeatures(blockContentType)

function toPlainText(blocks = []) {
  return blocks
    // loop through each block
    .map(block => {
      // if it's not a text block with children, 
      // return nothing
      if (block._type !== 'block' || !block.children) {
        return ''
      }
      // loop through the children spans, and join the
      // text strings
      return block.children.map(child => child.text).join('')
    })
    // join the paragraphs leaving split by two linebreaks
    .join('\n\n')
}
import React from 'react'
import * as ReactDOM from 'react-dom'
import {PortableText} from '@portabletext/react'
import {createClient} from '@sanity/client'

const client = sanityClient({
  projectId: '<your project id>',
  dataset: '<your dataset>',
  apiVersion: '2022-05-05',
  useCdn: true
})

const components = {
  types: {
    code: (props) => {
      const {language, code} = props.value
      return (
        <pre data-language={language}>
          <code>{code}</code>
        </pre>
      )
    }
  }
}

client.fetch('*[_type == "article"][0]')
  .then(article => {
    ReactDOM.render(
      <PortableText value={article.body} components={components} />,
      document.getElementById('root')
    )
  })
// portableText.js
export default {
  name: 'portableText',
  type: 'array',
  title: 'Content',
  of: [
    {
      type: 'block',
      marks: {
        annotations: [
          {
            name: 'internalLink',
            type: 'object',
            title: 'Internal link',
            fields: [
              {
                name: 'reference',
                type: 'reference',
                title: 'Reference',
                to: [
                  { type: 'article' },
                  // other types you may want to link to
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}
*[_type == "post"]{
  ...,
  body[]{
    ...,
    markDefs[]{
      ...,
      _type == "internalLink" => {
        "slug": @.reference->slug
      }
    }
  }
}
import Schema from '@sanity/schema'
import blockTools from '@portabletext/block-tools'


// Start with compiling a schema we can work against
const defaultSchema = Schema.compile({
  name: 'myBlog',
  types: [
    {
      type: 'object',
      name: 'blogPost',
      fields: [
        {
          title: 'Title',
          type: 'string',
          name: 'title'
        },
        {
          title: 'Body',
          name: 'body',
          type: 'array',
          of: [{type: 'block'}]
        }
      ]
    }
  ]
})

// The compiled schema type for the content type that holds the block array
const blockContentType = defaultSchema.get('blogPost')
  .fields.find(field => field.name === 'body').type


// Convert HTML to block array
const blocks = blockTools.htmlToBlocks(
  '<html><body><h1>Hello world!</h1><body></html>',
  blockContentType
)
// Outputs
//
//  {
//    _type: 'block',
//    style: 'h1'
//    children: [
//      {
//        _type: 'span'
//        text: 'Hello world!'
//      }
//    ]
//  }


// Get the feature-set of a blockContentType
const features = blockTools.getBlockContentFeatures(blockContentType)