Issues with svelte-portabletext: custom components work but standard text fails
I can help you work through these related issues with @portabletext/svelte! Let me break down what's happening and how to fix it.
Issue 1 & 2: Component Configuration Structure
The main problem is that you have block and listItem incorrectly placed inside the types object. In @portabletext/svelte, the component configuration works like this:
typesis for custom block types (yourcta,image,vimeo, etc.)marksis for inline annotations (like yourlink)block,list, andlistItemshould be top-level properties in thecomponentsobject, not nested undertypes
Here's the corrected structure:
<PortableText
value={post.body}
components={{
types: {
// Only custom types here
cta: Cta,
code: Code,
image: ImageBlock,
authorReference: AuthorBlock,
button: Button,
vimeo: Vimeo,
embedHTML: EmbedHTML,
},
marks: {
link: Link,
},
block: {
// Optional: custom renderers for different block styles
normal: Block,
h1: Block,
h2: Block,
// etc.
},
list: {
// Optional: custom list container components
bullet: YourBulletListComponent,
number: YourNumberListComponent,
},
listItem: {
bullet: ListItem,
number: ListItem,
// Note: no "normal" - that's a block style, not a list item style
}
}}
/>About the "Unknown list item style 'normal'" error: This happens because you had listItem nested under types with a normal property. List items don't have a "normal" style - that's a block style for regular paragraphs. The Portable Text specification shows that listItem is a property on blocks (like "listItem": "bullet") to indicate the block is part of a list, not a separate style type. Once you move listItem to the top level of your components configuration and remove normal from it, this error will disappear.
The simplest solution: If you don't need custom styling for blocks and lists, just remove those configurations entirely and let the library use its defaults:
<PortableText
value={post.body}
components={{
types: {
cta: Cta,
code: Code,
image: ImageBlock,
authorReference: AuthorBlock,
button: Button,
vimeo: Vimeo,
embedHTML: EmbedHTML,
},
marks: {
link: Link,
}
// That's it! Default rendering for blocks and lists
}}
/>Why Paragraphs with Links Are Disappearing
If you keep your custom Block component, make sure it properly renders its children. Your Block component must accept and render the text/children passed to it:
<!-- block.svelte --> <script> export let portableText; export let value; </script> <p> <slot /> </p>
The <slot /> is critical - without it, any inline content (including text with your Link mark component) won't render. This is why paragraphs with links are disappearing.
My recommendation: Unless you absolutely need custom block styling, remove the Block component configuration entirely and let @portabletext/svelte handle it with its defaults. This is the most reliable approach.
Issue 3: SvelteKit Hydration Problem
This is a separate SvelteKit issue unrelated to Portable Text. The symptom - the wrapper appearing on client-side navigation but disappearing on direct page load - indicates a hydration mismatch between server-rendered HTML and client-side expectations.
Check your src/app.html file. The standard SvelteKit structure should look like:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>If you've manually added <div id="svelte">, it can cause hydration conflicts. Your options:
- Use
display: contentson any wrapper (as shown above) - this makes the div transparent to layout - Remove the wrapper entirely and use
%sveltekit.body%directly in the body tag - Move your wrapper to a layout component:
<!-- src/routes/+layout.svelte --> <div id="svelte"> <slot /> </div>
This ensures the wrapper is part of your Svelte component tree and will hydrate consistently.
Summary
- Move
blockandlistItemout oftypesto be top-level properties incomponents - Remove
normalfromlistItem(or better yet, remove custom block/listItem configs entirely) - Ensure your Block component has
<slot />or remove it completely - Fix your app.html by using
display: contents, removing custom wrappers, or moving them to+layout.svelte
These changes should resolve all three issues! The first two fixes will solve your Portable Text rendering problems, and the third will fix the SvelteKit hydration issue.
Show original thread16 replies
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.