https://github.com/portabletext/svelte-portabletext
Transform Portable Text content into Svelte components
https://github.com/portabletext/svelte-portabletext
Last synced: over 1 year ago
JSON representation
Transform Portable Text content into Svelte components
- Host: GitHub
- URL: https://github.com/portabletext/svelte-portabletext
- Owner: portabletext
- License: mit
- Created: 2021-09-27T15:13:13.000Z (almost 5 years ago)
- Default Branch: main
- Last Pushed: 2025-03-26T06:12:13.000Z (over 1 year ago)
- Last Synced: 2025-04-04T00:06:59.684Z (over 1 year ago)
- Language: TypeScript
- Homepage: svelte-portabletext.sanity.build
- Size: 1.48 MB
- Stars: 72
- Watchers: 3
- Forks: 24
- Open Issues: 13
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# Portable Text renderer for Svelte
Render [Portable Text](https://portabletext.org) block content with [Svelte](https://svelte.dev/) components.
## Usage
`npm i @portabletext/svelte -D`
⚠️ Svelte 5.0.0 or higher is required.
```svelte
import {PortableText} from '@portabletext/svelte'
```
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 **components**:
### Customizing rendering
You can use the `components` prop to determine how the renderer should process each block, mark or style type.
```svelte
```
Example components from above:
```svelte
import {session} from '$app/stores'
import type {CustomBlockComponentProps} from '@portabletext/svelte'
interface Props {
portableText: CustomBlockComponentProps<{bold?: boolean}>
}
// Property custom blocks receive from @portabletext/svelte when rendered
let {portableText}: Props = $props()
let userName = $derived($session?.user?.name || 'person')
{#if portableText.value.bold}
{userName}
{:else}
{userName}
{/if}
```
```svelte
import type {MarkComponentProps} from '@portabletext/svelte'
import {Snippet} from 'svelte'
interface Props {
portableText: MarkComponentProps<{
url?: string
newWindow?: boolean
}>
children: Snippet
}
// Property custom marks receive from @portabletext/svelte when rendered
let {portableText, children}: Props = $props()
// Remember to make your variables reactive with runes (or $: if using legacy syntax)
// so that they can reflect prop changes
// See: https://svelte.dev/docs/svelte/$derived
// Or if using legacy syntax: https://svelte.dev/docs/svelte/legacy-reactive-assignments
let {value} = $derived(portableText)
let newWindow = $derived(value.newWindow || false)
{#if value.url}
{@render children()}
{:else}
{@render children()}
{/if}
```
> 📌 **To keep in mind**: Svelte's SSR mode seems to have issues with whitespace (see [#3168](https://github.com/sveltejs/svelte/issues/3168)), where it does strip unnecessary space between components. Due to this, marks (formatting, links, etc.) some times are rendered incorrectly. We're tracking this in [#1](https://github.com/portabletext/svelte-portabletext/issues/1).
```svelte
import type {BlockComponentProps} from '@portabletext/svelte'
interface Props {
portableText: BlockComponentProps
children: Snippet
}
let {portableText, children}: Props = $props()
let {indexInParent, global, value} = $derived(portableText)
let {ptBlocks} = $derived(global)
let {style} = $derived(value)
const headings = ['h1', 'h2', 'h3', 'h4', 'h5']
let precededByHeading = $derived(headings.includes(ptBlocks[indexInParent - 1]?.style))
let anchorId = $derived(`heading-${value._key}`)
Link to this heading
🔗
{#if style === 'h1'}
{@render children()}
{:else if style === 'h2'}
{@render children()}
{:else if style === 'h3'}
{@render children()}
{:else}
{@render children()}
{/if}
```
The component above is also an example of how you can access blocks surrounding the current one for rule-based design.
Finally, you can pass **`context`** to your `` component to have custom external data exposed to all components. This is useful in scenarios such as:
- Adding different styles to the same block depending on its placement
- Loading in data from an external source/API
- Running expensive calculations on your `value` only once
Here's a complete example with a `footnote` annotation, where editors focus on writing its contents, and the front-end smartly position it and define its number:
```svelte
import Footnote from './Foonote.svelte'
let {value} = $props()
// Get all footnotes from markDefs in top-level value
let footnotes = $derived(value.reduce((notes, curBlock) => {
if (curBlock._type !== 'block' || !curBlock.markDefs?.length) {
return notes
}
return [...notes, ...curBlock.markDefs.filter((def) => def._type === 'footnote')]
}, []))
{#each footnotes as note}
-
👆
{/each}
import type {MarkComponentProps} from '@portabletext/svelte'
import {Snippet} from 'svelte'
interface FootnoteProps {
_key: string
note: PortableTextBlocks
}
interface Props {
portableText: MarkComponentProps<
FootnoteProps,
// Use the second argument to specify your context's type
{ footnotes: FootnoteProps[] }>
children: Snippet
}
let {portableText, children}: Props = $props()
let {footnotes} = $derived(portableText.global.context)
// From the context, let's figure out what's the position of this footnote
let number = $derived(footnotes.findIndex(note => note._key === portableText.value._key) + 1)
{@render children()}
{number}
```
## Disabling warnings / handling unknown types
When the library encounters a block, mark, list or list item with a type that is not known (eg it has no corresponding component in the `components` property), it will by default print a console warning.
To disable this behavior, you can either pass `false` to the `onMissingComponent` property, or give it a custom function you want to use to report the error. For instance:
```jsx
import {PortableText} from '@portabletext/svelte'
// or, pass it a function:
{
myErrorLogger.report(message, {
// eg `someUnknownType`
type: options.type,
// 'block' | 'mark' | 'blockStyle' | 'listStyle' | 'listItemStyle'
nodeType: options.nodeType
})
}}
/>
```
## Rendering Plain Text
This module also exports a function (`toPlainText()`) that will render one or more Portable Text blocks as plain text. This is helpful in cases where formatted text is not supported, or you need to process the raw text value.
For instance, to render an OpenGraph meta description for a page:
```svelte
import {toPlainText} from '@portabletext/svelte'
```
## Credits
Big thanks to [Emma Agering](https://github.com/emagining), [Jacob Størdahl](https://github.com/stordahl), [Ollie Taylor](https://github.com/OllieJT), [Rune](https://github.com/runeh), [Stephane Vanraes](https://github.com/stephane-vanraes) & [Toby Milner-Gulland](https://github.com/tmgulland) (alphabetical order) for working on their custom renderers while we didn't have an official one. You've helped many ship Svelte + Sanity projects!
## License
MIT-licensed. See LICENSE.