{"id":18471654,"url":"https://github.com/portabletext/svelte-portabletext","last_synced_at":"2025-04-12T18:47:08.111Z","repository":{"id":40293811,"uuid":"410934639","full_name":"portabletext/svelte-portabletext","owner":"portabletext","description":"Transform Portable Text content into Svelte components","archived":false,"fork":false,"pushed_at":"2025-03-26T06:12:13.000Z","size":1547,"stargazers_count":72,"open_issues_count":13,"forks_count":24,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-04T00:06:59.684Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"svelte-portabletext.sanity.build","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/portabletext.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-27T15:13:13.000Z","updated_at":"2025-03-29T09:51:24.000Z","dependencies_parsed_at":"2023-12-26T23:27:35.685Z","dependency_job_id":"2b799426-17ab-4ad8-bfd8-57d6391bff1d","html_url":"https://github.com/portabletext/svelte-portabletext","commit_stats":{"total_commits":103,"total_committers":3,"mean_commits":"34.333333333333336","dds":0.03883495145631066,"last_synced_commit":"5b4465e7aa7275c0e399a8cbaad3ecbe3ec47de7"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/portabletext%2Fsvelte-portabletext","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/portabletext%2Fsvelte-portabletext/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/portabletext%2Fsvelte-portabletext/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/portabletext%2Fsvelte-portabletext/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/portabletext","download_url":"https://codeload.github.com/portabletext/svelte-portabletext/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248617750,"owners_count":21134197,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T10:17:38.658Z","updated_at":"2025-04-12T18:47:08.081Z","avatar_url":"https://github.com/portabletext.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Portable Text renderer for Svelte\n\nRender [Portable Text](https://portabletext.org) block content with [Svelte](https://svelte.dev/) components.\n\n## Usage\n\n`npm i @portabletext/svelte -D`\n\n⚠️ Svelte 5.0.0 or higher is required.\n\n```svelte\n\u003cscript\u003e\n  import {PortableText} from '@portabletext/svelte'\n\u003c/script\u003e\n\n\u003cPortableText\n  value={[\n    // Portable Text array ...\n  ]}\n/\u003e\n```\n\nThis is enough to get you set-up with basic block content with formatting and text styles. When working with images, custom styles, blocks \u0026 marks, though, you'll need to customize your renderer with **components**:\n\n### Customizing rendering\n\nYou can use the `components` prop to determine how the renderer should process each block, mark or style type.\n\n```svelte\n\u003cPortableText\n  value={[\n    // Portable Text array ...\n  ]}\n  components={{\n    types: {\n      // block-level components\n      callout: Callout,\n      // inline-level components\n      userInfo: UserInfo\n    },\n    marks: {\n      absUrl: AbsoluteURL,\n      // Overwrite default mark renderers\n      strong: CustomStrong\n    },\n    block: {\n      normal: CustomParagraph,\n      blockquote: Quote,\n      // Re-using the same component across multiple styles\n      h1: CustomHeading,\n      h2: CustomHeading,\n      h3: CustomHeading,\n      // Custom user-defined style\n      textCenter: CentralizedText\n    },\n    list: {\n      // Swap only the list parts you need\n      bullet: UnorderedListWrapper,\n      // Custom user-defined list type\n      checklist: ChecklistWrapper\n    },\n    listItem: {\n      bullet: ListItem,\n      checklist: ChecklistItem\n    }\n  }}\n/\u003e\n```\n\nExample components from above:\n\n```svelte\n\u003c!-- UserInfo (custom block type) --\u003e\n\u003cscript lang=\"ts\"\u003e\n  import {session} from '$app/stores'\n  import type {CustomBlockComponentProps} from '@portabletext/svelte'\n\n  interface Props {\n    portableText: CustomBlockComponentProps\u003c{bold?: boolean}\u003e\n  }\n\n  // Property custom blocks receive from @portabletext/svelte when rendered\n  let {portableText}: Props = $props()\n\n  let userName = $derived($session?.user?.name || 'person')\n\u003c/script\u003e\n\n{#if portableText.value.bold}\n  \u003cstrong\u003e{userName}\u003c/strong\u003e\n{:else}\n  {userName}\n{/if}\n```\n\n```svelte\n\u003c!-- AbsoluteURL (custom mark) --\u003e\n\u003cscript lang=\"ts\"\u003e\n  import type {MarkComponentProps} from '@portabletext/svelte'\n  import {Snippet} from 'svelte'\n\n  interface Props {\n    portableText: MarkComponentProps\u003c{\n      url?: string\n      newWindow?: boolean\n    }\u003e\n    children: Snippet\n  }\n\n  // Property custom marks receive from @portabletext/svelte when rendered\n  let {portableText, children}: Props = $props()\n\n  // Remember to make your variables reactive with runes (or $: if using legacy syntax)\n  // so that they can reflect prop changes\n  // See: https://svelte.dev/docs/svelte/$derived\n  // Or if using legacy syntax: https://svelte.dev/docs/svelte/legacy-reactive-assignments\n  let {value} = $derived(portableText)\n  let newWindow = $derived(value.newWindow || false)\n\u003c/script\u003e\n\n{#if value.url}\n  \u003ca href={value.url} target={newWindow ? '_blank' : undefined}\u003e\n    \u003c!-- Marks receive children which you can render with Svelte's slots --\u003e\n    {@render children()}\n  \u003c/a\u003e\n{:else}\n  \u003c!-- If no valid URL, render only the contents of the mark --\u003e\n  {@render children()}\n{/if}\n```\n\n\u003e 📌 **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).\n\n```svelte\n\u003c!-- CustomHeading (blockStyle) --\u003e\n\u003cscript lang=\"ts\"\u003e\n  import type {BlockComponentProps} from '@portabletext/svelte'\n\n  interface Props {\n    portableText: BlockComponentProps\n    children: Snippet\n  }\n\n  let {portableText, children}: Props = $props()\n\n  let {indexInParent, global, value} = $derived(portableText)\n  let {ptBlocks} = $derived(global)\n  let {style} = $derived(value)\n\n  const headings = ['h1', 'h2', 'h3', 'h4', 'h5']\n\n  let precededByHeading = $derived(headings.includes(ptBlocks[indexInParent - 1]?.style))\n\n  let anchorId = $derived(`heading-${value._key}`)\n\u003c/script\u003e\n\n\u003c!-- If preceded by heading, have a higher margin top --\u003e\n\u003cdiv class=\"relative {precededByHeading ? 'mt-10' : 'mt-4'}\" id={anchorId}\u003e\n  \u003ca href=\"#{anchorId}\"\u003e\n    \u003cspan class=\"sr-only\"\u003eLink to this heading\u003c/span\u003e\n    🔗\n  \u003c/a\u003e\n  {#if style === 'h1'}\n    \u003ch1 class=\"text-4xl font-black\"\u003e\n      {@render children()}\n    \u003c/h1\u003e\n  {:else if style === 'h2'}\n    \u003ch2 class=\"text-3xl\"\u003e\n      {@render children()}\n    \u003c/h2\u003e\n  {:else if style === 'h3'}\n    \u003ch3 class=\"text-xl\"\u003e\n      {@render children()}\n    \u003c/h3\u003e\n  {:else}\n    \u003ch4 class=\"text-lg text-gray-600\"\u003e\n      {@render children()}\n    \u003c/h4\u003e\n  {/if}\n\u003c/div\u003e\n```\n\nThe component above is also an example of how you can access blocks surrounding the current one for rule-based design.\n\nFinally, you can pass **`context`** to your `\u003cPortableText\u003e` component to have custom external data exposed to all components. This is useful in scenarios such as:\n\n- Adding different styles to the same block depending on its placement\n- Loading in data from an external source/API\n- Running expensive calculations on your `value` only once\n\nHere'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:\n\n```svelte\n\u003c!-- Our page's content --\u003e\n\u003cscript\u003e\n  import Footnote from './Foonote.svelte'\n\n  let {value} = $props()\n\n  // Get all footnotes from markDefs in top-level value\n  let footnotes = $derived(value.reduce((notes, curBlock) =\u003e {\n    if (curBlock._type !== 'block' || !curBlock.markDefs?.length) {\n      return notes\n    }\n    return [...notes, ...curBlock.markDefs.filter((def) =\u003e def._type === 'footnote')]\n  }, []))\n\u003c/script\u003e\n\n\u003cPortableText\n  value={value}\n  components={{\n    marks: {\n      footnote: Footnote\n    }\n  }}\n  context={{\n    // Pass these footnotes inside the context\n    footnotes\n  }}\n/\u003e\n\n\u003c!-- And render them at the bottom --\u003e\n\u003col\u003e\n  {#each footnotes as note}\n    \u003cli id=\"note-{note._key}\"\u003e\n      \u003cPortableText\n        value={note.note}\n        components={{\n          marks: {\n            link: Link\n          }\n        }}\n      /\u003e\n      \u003ca href=\"#src-{note._key}\"\u003e👆\u003c/a\u003e\n    \u003c/li\u003e\n  {/each}\n\u003c/ol\u003e\n\n\n\u003c!-- Footnote.svelte --\u003e\n\u003cscript lang=\"ts\"\u003e\n  import type {MarkComponentProps} from '@portabletext/svelte'\n  import {Snippet} from 'svelte'\n\n  interface FootnoteProps {\n    _key: string\n    note: PortableTextBlocks\n  }\n\n  interface Props {\n    portableText: MarkComponentProps\u003c\n      FootnoteProps,\n      // Use the second argument to specify your context's type\n      { footnotes: FootnoteProps[] }\u003e\n    children: Snippet\n  }\n\n  let {portableText, children}: Props = $props()\n  let {footnotes} = $derived(portableText.global.context)\n\n  // From the context, let's figure out what's the position of this footnote\n  let number = $derived(footnotes.findIndex(note =\u003e note._key === portableText.value._key) + 1)\n\u003c/script\u003e\n\n\u003cspan id=\"src-{portableText.value._key}\"\u003e\n  {@render children()}\n  \u003csup\u003e\u003ca href={`#note-${portableText.value._key}`}\u003e{number}\u003c/a\u003e\u003c/sup\u003e\n\u003c/span\u003e\n```\n\n## Disabling warnings / handling unknown types\n\nWhen 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.\n\nTo 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:\n\n```jsx\nimport {PortableText} from '@portabletext/svelte'\n\n\u003cPortableText\n  value={[/* array of portable text blocks */]}\n  onMissingComponent={false}\n/\u003e\n\n// or, pass it a function:\n\u003cPortableText\n  value={[/* array of portable text blocks */]}\n  onMissingComponent={(message, options) =\u003e {\n    myErrorLogger.report(message, {\n      // eg `someUnknownType`\n      type: options.type,\n\n      // 'block' | 'mark' | 'blockStyle' | 'listStyle' | 'listItemStyle'\n      nodeType: options.nodeType\n    })\n  }}\n/\u003e\n```\n\n## Rendering Plain Text\n\nThis 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.\n\nFor instance, to render an OpenGraph meta description for a page:\n\n```svelte\n\u003cscript\u003e\n  import {toPlainText} from '@portabletext/svelte'\n\u003c/script\u003e\n\n\u003csvelte:head\u003e\n  \u003cmeta name=\"og:description\" value={toPlainText(myPortableTextData)} /\u003e\n\u003c/svelte:head\u003e\n```\n\n\u003c!-- ## TODO\n\n- [ ] If applicable, add support to `components.hardBreak`, similar to block-content-to-react\n- [ ] In example site, add examples with custom marks, block \u0026 block types\n  - Include a nested PT renderer example --\u003e\n\n## Credits\n\nBig 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) \u0026 [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!\n\n## License\n\nMIT-licensed. See LICENSE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fportabletext%2Fsvelte-portabletext","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fportabletext%2Fsvelte-portabletext","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fportabletext%2Fsvelte-portabletext/lists"}