Portable Text
Global helper
This module defines a global <SanityContent> component that can turn portable text into HTML. It is a lightweight functional component without an instance.
As of v2, <SanityContent> uses @portabletext/vue for rendering portable text. This means features and properties available to @portabletext/vue also work with <SanityContent>. Please refer to their Usage guide for advanced configuration options.
<SanityContent> v2 components. Refer to the following upgrade guide:- To reflect
@portabletext/vue's props,blocks→valueandserializers→componentsattribute name changes have been made. The property types remain the same. - Custom components now receive their data nested within a
props.valueobject. When defining components, you need to extract your props from this structure using object spreading:{...props.value}. This applies to all component types (blocks, marks, styles).
Example
<template>
<SanityContent :value="content" />
</template>
Image handling
The <SanityContent> component automatically handles Sanity images using the <SanityImage> component, which will use <NuxtImg> if @nuxt/image is installed.
The default image component supports:
- Asset ID: Extracted from the image block and passed to
<SanityImage> - Hotspot: Converted to focal point coordinates (
fp-x,fp-y) for proper cropping - Crop: Converted to a
rectparameter using the image dimensions from the asset ID
Custom fields like caption and attribution are not rendered by the default component. If you need to display captions or other custom data, provide a custom image component as shown below.
Example with custom components
<template>
<SanityContent :value="content" :components="components" />
</template>
<script setup>
import { defineAsyncComponent, h, resolveComponent } from 'vue'
import CustomBlockComponent from '~/components/CustomBlockComponent.vue'
const components = {
types: {
// This is how to access a component registered by `@nuxt/components`
lazyRegisteredComponent: props => h(resolveComponent('LazyCustomSerializer'), {
...props.value,
}),
// A directly imported component
importedComponent: props => h(CustomBlockComponent, {
...props.value,
}),
// Example of a more complex async component
dynamicComponent: props => h(defineAsyncComponent({
loadingComponent: () => 'Loading...',
loader: () => import('~/other/component.vue'),
}), {
...props.value,
}),
// You can override the default image component if needed
image: props => h('CustomImageComponent', {
...props.value,
}),
// Example of handling caption and attribution in a custom component
imageWithCaption: props => {
const { asset, caption, attribution, crop, hotspot } = props.value
return h('div', { class: 'custom-image-wrapper' }, [
h('img', { src: `https://cdn.sanity.io/images/.../${asset._ref}` }),
caption && h('p', { class: 'caption' }, caption),
attribution && h('p', { class: 'attribution' }, attribution)
])
},
},
marks: {
// Custom marks handling
internalLink: props => h('a', { href: props.value.href }, props.text)
}
}
</script>
Image Block Structure
The automatic image handling works with the standard Sanity portable text image block structure:
{
"_type": "image",
"asset": {
"_type": "reference",
"_ref": "image-61991cfbe9182124c18ee1829c07910faadd100e-2048x1366-png"
},
"caption": "This is the caption (ignored by default component)",
"attribution": "Public domain (ignored by default component)",
"crop": {
"top": 0.028131868131868132,
"bottom": 0.15003663003663004,
"left": 0.01875,
"right": 0.009375000000000022
},
"hotspot": {
"x": 0.812500000000001,
"y": 0.27963369963369955,
"height": 0.3248351648351647,
"width": 0.28124999999999994
}
}
The component automatically:
- Extracts the
_reffrom the asset object and passes it asassetIdto<SanityImage> - Converts
hotspot.xandhotspot.ytofp-xandfp-yfocal point parameters - Calculates the
rectparameter from thecropobject using the original image dimensions (parsed from the asset ID)
Caption and attribution fields are ignored by the default component. Use a custom image component if you need to render these.
Disabling Default Image Handling
If you want to handle images yourself or disable the automatic image handling entirely, you can use the disableDefaultImageComponent prop:
<template>
<!-- Disable automatic image handling -->
<SanityContent
:value="content"
disable-default-image-component
/>
<!-- Or provide your own image component -->
<SanityContent
:value="content"
disable-default-image-component
:components="{
types: {
image: props => h('MyCustomImage', {
assetId: props.value.asset._ref,
caption: props.value.caption
})
}
}"
/>
</template>
When disableDefaultImageComponent is set to true, the component will not automatically handle image blocks. If you don't provide your own image component in the components.types.image prop, PortableText will show a warning about the missing component.
<MySanityContent>) which wraps SanityContent with your default components. By creating ~/components/MySanityContent.vue you should be able to use this everywhere in your app without importing it.Advanced Props
The SanityContent component accepts all props from @portabletext/vue:
<template>
<SanityContent
:value="content"
:components="components"
:onMissingComponent="handleMissingComponent"
:listNestingMode="'html'"
/>
</template>
<script setup>
const handleMissingComponent = (message, options) => {
console.warn(`Missing component: ${options.type} (${options.nodeType})`)
}
</script>
TypeScript Support
All types from @portabletext/vue and @portabletext/types are re-exported from @nuxtjs/sanity:
<script setup lang="ts">
import type { PortableTextComponents } from '@nuxtjs/sanity/runtime/types'
import { defineAsyncComponent, h } from 'vue'
import CustomBlockComponent from '~/components/CustomBlockComponent.vue'
const components: PortableTextComponents = {
types: {
customBlock: props => h(CustomBlockComponent, {
...props.value,
}),
},
marks: {
link: props => h('a', {
href: props.value.href,
target: '_blank'
}, props.text)
}
}
</script>
<template>
<SanityContent :value="content" :components="components" />
</template>