Converting portable text to markdown or HTML in Sanity page builder

12 replies
Last updated: Aug 10, 2022
Heyy all, I am pretty new to Sanity (sorry in advance!) .. I am trying to figure a few things out 🙂 I'd like to create a pageBuilder (using the example provided in the docs) and within that use a media type that has an image with content that uses portableText. I did see a post that suggest this won't work because of nested arrays, but as I am very unfamiliar with how things work I was hoping someone might be able to either point me in the right direction if it is possible or confirm if it isn't! If it is indeed possible - has anyone any examples of how to convert the portableText from the content field to markdown? I am using 11ty. Thanks!
AI Update

Good news and some workarounds for you! You're right that Sanity has a limitation: arrays cannot contain other arrays due to a data store constraint. This means you can't directly nest a portable text field (which is an array of blocks) inside an array-based page builder.

However, there are a couple of solutions:

Instead of putting portable text directly in your page builder array, wrap it in an object type. Objects can contain arrays, so this works perfectly:

{
  name: 'pageBuilder',
  type: 'array',
  of: [
    {
      name: 'mediaWithContent',
      type: 'object',
      fields: [
        {
          name: 'image',
          type: 'image'
        },
        {
          name: 'content',
          type: 'array', // This is your portable text
          of: [{type: 'block'}]
        }
      ]
    }
  ]
}

This is the standard pattern and what most developers do. The page builder array contains objects, and those objects contain your portable text array. The limitation exists in the data store, but wrapping nested arrays in objects is the recommended workaround.

Solution 2: Reference a Separate Document

Create a separate document type that contains your image + portable text, then reference it from your page builder. This is useful if you want to reuse these content blocks across multiple pages.

Converting Portable Text to Markdown

For converting portable text to markdown in your 11ty setup, you'll want to use the official @sanity/block-content-to-markdown package:

npm install --save @sanity/block-content-to-markdown

Then in your 11ty templates or data files:

const toMarkdown = require('@sanity/block-content-to-markdown')

const markdown = toMarkdown(portableTextArray)

If you have custom blocks or marks in your portable text, you'll need to provide custom serializers to handle those:

const serializers = {
  types: {
    code: props => '```' + props.node.language + '\n' + props.node.code + '\n```'
  }
}

const markdown = toMarkdown(portableTextArray, {serializers})

The package documentation has good examples of handling custom serializers for different block types and marks.

The object-wrapping approach is definitely the way to go here - it's a well-established pattern in the Sanity community and works seamlessly with all the tooling. Welcome to Sanity, and hope this helps! 🎉

For a page builder, you typically have two approaches for your schema:• Use portable text only, and have your various modules and images as embeddable blocks within your portable text. So you have one field, and everything in it.
• Have a variety of modules as an an array of objects. Still one field, but it’s structured as an array of module objects.
Thank you for the quick reply! Excuse my examples below, a lot is from using examples provided in the docs and I am quickly trying to piece together a proof of concept.
I'd like to take the second approach as I think it will be little but more management for how I eventually going to try and implement the page builder.

I have a document type of something generic like 'mypage'


// mypage.js

export default {
  name: 'mypage',
  type: 'document',
  title: 'mypage',
  fields: [
    {
      name: 'name',
      type: 'string',
      title: 'Name'
    },
    {
      name: 'slug',
      type: 'slug',
      title: 'Slug',
      description: 'Some frontends will require a slug to be set to be able to show the person',
      options: {
        source: 'name',
        maxLength: 96
      }
    },
    {
      name: 'pageBuilder',
      type: 'array',
      title: 'Page builder',
      of: [
        { type: 'hero' },
        { type: 'textWithIllustration' },
        { type: 'media' },
      ]
    }
  ]
}
The the 'media' object is using some fields provided in from various examples including one for the rich text.


 // media.js

export default {
	name: 'media',
	type: 'object',
	title: 'Media',
	fields: [
    {
			name: 'content',
			type: 'array',
			title: 'Content',
			of: [
				{
					type: 'block',
					title: 'Block',
					// Styles let you set what your user can mark up blocks with. These
					// corrensponds with HTML tags, but you can set any title or value
					// you want and decide how you want to deal with it where you want to
					// use your content.
					styles: [
						{ title: 'Normal', value: 'normal' },
						{ title: 'H1', value: 'h1' },
						{ title: 'H2', value: 'h2' },
						{ title: 'H3', value: 'h3' },
						{ title: 'H4', value: 'h4' },
						{ title: 'Quote', value: 'blockquote' }
					],
					lists: [{ title: 'Bullet', value: 'bullet' }, { title: 'Number', value: 'number' }],
					// Marks let you mark up inline text in the block editor.
					marks: {
						// Decorators usually describe a single property – e.g. a typographic
						// preference or highlighting by editors.
						decorators: [{ title: 'Strong', value: 'strong' }, { title: 'Emphasis', value: 'em' }],
						// Annotations can be any object structure – e.g. a link or a footnote.
						annotations: [
							{
								name: 'link',
								type: 'object',
								title: 'URL',
								fields: [
									{
										title: 'URL',
										name: 'href',
										type: 'url'
									}
								]
							}
						]
					},
					//of: [{ type: 'authorReference' }]
				},
				// You can add additional types here. Note that you can't use
				// primitive types such as 'string' and 'number' in the same array
				// as a block type.
				{
					type: 'mainImage',
					options: { hotspot: true }
				}
			]
		},
		{
			name: 'imageSize',
			type: 'string',
			title: 'Image Size',
			options: {
				list: [
					{ title: 'Small', value: 'small' },
					{ title: 'Large', value: 'large' }
				], // <-- predefined values
				layout: 'radio', // <-- defaults to 'dropdown'
			}
		},
		{
			name: 'imagePosition',
			type: 'string',
			title: 'Image Position',
			options: {
				list: [
					{ title: 'Start', value: 'start' },
					{ title: 'End', value: 'end' }
				], // <-- predefined values
				layout: 'radio', // <-- defaults to 'dropdown'
			}
		},
	],
}
I think that's correct, but I am getting stuck on how to then convert my 'content' to markdown. Obviously for some of the 'mypages' they may not have the media type within their page builder. And the ones that do I have been trying to get to work using
@sanity/block-content-to-markdown
without much success. But I wasn't able to find much in the way of documentation to help
To Markdown? You can’t do that.
Markdown is a standardized text format. It will not support your custom blocks/modules.
The
@sanity/block-content-to-markdown
is used to transform text content from a portable text field (array of blocks) into Markdown content. But this is not what you are doing here. First because you’re not using portable text (you have your own array of objects, not an array of blocks), and also because you have your own custom objects; it’s not really text that can be converted to Markdown.
I’m also curious why you would want to convert things to Markdown?
ah ok! that makes a lot more sense - something I had missed. Thank you for explaining! so I guess I wanted a way that editors could perhaps add their image but tap into rich text for the content, apply some headings, perhaps make text bold or add a link. That's then why I was trying to use markdown with
@sanity/block-content-to-markdown
as that was used in the example starter project for sanity with 11ty. I think I have just over complicated and confused myself!
Sorry, just one more question if I may 😬 Is it possible to convert to html instead? Using just regular portable text.
Thank you! I have been testing that out and it works great! I have an example working where portable text is used for a 'pageBody' field on my document type and I can see the html rendered as expected. The only thing I can't figure out, and perhaps this is incredibly obvious (😬 ) but how I would then use
toHTML
for portable text fields within my page builder modules. I have spent a couple of hours researching but can't seem to find any examples that cover that instance. No worries at all if you it's too much to ask. Really appreciate the help already!
You would have to pass the
components
option to map the type of objects that can appear in your portable text to an actual chunk of HTML. See that example: https://github.com/portabletext/to-html#customizing-components 🙂
ah perfect! thank you! I did experiment quickly with a similar example, but wasn't sure it was what I needed/wasn't sure it was the right rabbit hole! Again, much appreciated 🙂 I am going to explore this further

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?