Join us and panelists from Shopify, Figma, Loom, and Fnatic for the next Sanity.io Open House. Dec 8th.

PortableText to Svelte

Official(made by Sanity team)

By Henrique Doro

Render Portable Text block content with Svelte components.

Portable Text renderer for Svelte

Render Portable Text block content with Svelte components.

Usage

npm i @portabletext/svelte -D

<script>
  import PortableText from '@portabletext/svelte'
</script>

<PortableText
  blocks={[
    // Portable Text array ...
  ]}
/>

This is enough to get you set-up with basic block content with formatting and text styles. When working with images, custom styles, blocks & marks, though, you'll need to customize your renderer with serializers:

Customizing rendering

You can use the serializers prop to determine how the renderer should process each block, mark or style type.

<PortableText
  blocks={[
    // Portable Text array ...
  ]}
  serializers={{
    types: {
      // block-level components
      callout: Callout,
      // inline-level components
      userInfo: UserInfo
    },
    marks: {
      absUrl: AbsoluteURL,
      // Overwrite default mark renderers
      strong: CustomStrong
    },
    blockStyles: {
      normal: CustomParagraph,
      blockquote: Quote,
      // Re-using the same component across multiple styles
      h1: CustomHeading,
      h2: CustomHeading,
      h3: CustomHeading,
      // Swap only the list parts you need
      list_bullet: UnorderedListWrapper,
      list_number: OrderedListWrapper,
      listItem_bullet: ListItem,
      listItem_number: ListItem,
      // Custom user-defined style
      textCenter: CentralizedText
    }
  }}
/>

Example components from above:

<!-- UserInfo (block type) -->
<script lang="ts">
  import {session} from '$app/stores'
  import type {BlockProps} from '@portabletext/svelte'

  // Property custom blocks receive from @portabletext/svelte when redered
  export let portableText: BlockProps<{bold?: boolean}>

  $: userName = $session?.user?.name || 'person'
</script>

{#if portableText.block.bold}
  <strong>{userName}</strong>
{:else}
  {userName}
{/if}
<!-- AbsoluteURL (custom mark) -->
<script lang="ts">
  export let mark: {url?: string; newWindow?: boolean} = {}
  import type {MarkProps} from '@portabletext/svelte'

  // Property custom marks receive from @portabletext/svelte when redered
  export let portableText: MarkProps<{
    url?: string
    newWindow?: boolean
  }>

  // Remember to make your variables reactive so that they can reflect prop changes
  // See: https://svelte.dev/docs#3_$_marks_a_statement_as_reactive
  $: mark = portableText.mark
  $: newWindow = mark.newWindow || false
</script>

{#if mark.url}
  <a href={mark.url} target={newWindow ? '_blank' : undefined}><slot /></a>
{:else}
  <slot />
{/if}

πŸ“Œ To keep in mind: Svelte's SSR mode seems to have issues with whitespace, where it does strip unnecessary space between components. Due to this, marks (formatting, links, etc.) some times are rendered incorrectly.

<!-- CustomHeading (blockStyle) -->
<script lang="ts">
  import type {BlockProps} from '@portabletext/svelte'

  export let portableText: BlockProps

  $: index = portableText.index
  $: blocks = portableText.blocks
  $: block = portableText.block

  $: style = block.style
  $: precededByHeading = ['h1', 'h2', 'h3', 'h4', 'h5'].includes(blocks[index - 1]?.style)

  $: anchorId = `heading-${block._key}`
</script>

<!-- If preceded by heading, have a higher margin top -->
<div class="relative {precededByHeading ? 'mt-10' : 'mt-4'}" id={anchorId}>
  <a href="#{anchorId}">
    <span class="sr-only">Link to this heading</span>
    πŸ”—
  </a>
  {#if style === 'h1'}
    <h1 class="text-4xl font-black"><slot /></h1>
  {:else if style === 'h2'}
    <h2 class="text-3xl"><slot /></h2>
  {:else if style === 'h3'}
    <h2 class="text-xl"><slot /></h2>
  {:else}
    <h4 class="text-lg text-gray-600"><slot /></h4>
  {/if}
</div>

The component above is also an example of how you can access blocks surrounding the current one for rule-based design.

License

MIT-licensed. See LICENSE.

Install command

npm i @portabletext/svelte -D

Contributor

Henrique Doro

Sanity user & community member turned employee 😊 (Applications Engineer)

Henrique is located at Brazil
Visit Henrique Doro's profile

Categorized in

Other plugins by author

AWS S3 media browser

Allows uploading, referencing and deleting video and audio files to S3 directly from your Sanity studio.

Henrique Doro
Go to AWS S3 media browser