Helpers

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.

This render change introduces breaking changes for <SanityContent> v2 components. Refer to the following upgrade guide:
  • To reflect @portabletext/vue's props, blocksvalue and serializerscomponents attribute name changes have been made. The property types remain the same.
  • Custom components now receive their data nested within a props.value object. 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 rect parameter 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:

  1. Extracts the _ref from the asset object and passes it as assetId to <SanityImage>
  2. Converts hotspot.x and hotspot.y to fp-x and fp-y focal point parameters
  3. Calculates the rect parameter from the crop object 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.

If you want to use the same components in multiple places, consider creating your own component (e.g. <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>

Other resources