Files
theater-ziefen-website/.agents/skills/portable-text-serialization/rules/vue.md
T
johannes.gasser 57af0b8386
Build and Deploy / build-and-deploy (push) Successful in 2m53s
add skills
2026-05-18 08:39:42 +02:00

3.0 KiB

title, description, tags
title description tags
Serialize Portable Text to Vue Render Portable Text in Vue 3 and Nuxt using @portabletext/vue
portable-text
vue
nuxt
serialization
rendering

Serialize Portable Text to Vue

Use @portabletext/vue to render PT in Vue 3 / Nuxt applications.

npm install @portabletext/vue

Basic Usage

<script setup lang="ts">
import {PortableText} from '@portabletext/vue'
import type {PortableTextBlock} from '@portabletext/types'

const props = defineProps<{value: PortableTextBlock[]}>()
</script>

<template>
  <PortableText :value="value" :components="components" />
</template>

Custom Components

Vue components can be defined as render functions, SFCs, or JSX.

Render Function Style (Concise)

import {h} from 'vue'
import type {PortableTextVueComponents} from '@portabletext/vue'

const components: PortableTextVueComponents = {
  types: {
    image: ({value}) => h('img', {src: urlFor(value).width(800).url(), alt: value.alt || ''}),
    code: ({value}) => h('pre', {'data-language': value.language}, h('code', value.code)),
  },

  marks: {
    link: ({value}, {slots}) => {
      const rel = !value?.href?.startsWith('/') ? 'noreferrer noopener' : undefined
      return h('a', {href: value?.href, rel}, slots.default?.())
    },
    highlight: (_, {slots}) => h('span', {class: 'bg-yellow-200'}, slots.default?.()),
  },

  block: {
    h1: (_, {slots}) => h('h1', {class: 'text-4xl font-bold'}, slots.default?.()),
    h2: (_, {slots}) => h('h2', {class: 'text-3xl font-semibold'}, slots.default?.()),
    blockquote: (_, {slots}) => h('blockquote', {class: 'border-l-4 pl-4 italic'}, slots.default?.()),
  },

  list: {
    bullet: (_, {slots}) => h('ul', {class: 'list-disc ml-6'}, slots.default?.()),
    number: (_, {slots}) => h('ol', {class: 'list-decimal ml-6'}, slots.default?.()),
  },
}

SFC Style (For Complex Components)

<!-- ImageBlock.vue -->
<script setup lang="ts">
import type {PortableTextComponentProps} from '@portabletext/vue'

const props = defineProps<PortableTextComponentProps<{
  asset: {_ref: string}
  alt?: string
  caption?: string
}>>()
</script>

<template>
  <figure>
    <img :src="urlFor(value).width(800).url()" :alt="value.alt || ''" />
    <figcaption v-if="value.caption">{{ value.caption }}</figcaption>
  </figure>
</template>

Then register:

import ImageBlock from './ImageBlock.vue'

const components = {
  types: {
    image: ImageBlock,
  },
}

Props Pattern

Custom components receive:

Prop Description
value The block/mark data
index Position in parent array
isInline Whether this is an inline element
renderNode Internal renderer (rarely needed)

Children are passed via slots (slots.default?.()), not props.

Plain Text Extraction

import {toPlainText} from '@portabletext/vue'

const text = toPlainText(blocks)

Reference