npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@payload-bites/astro-richtext-renderer

v2.1.1

Published

render Payload CMS Lexical richtext content in Astro

Readme

Astro richtext renderer

Render Payload CMS Lexical richtext content in Astro using native Astro components.

Installation

pnpm add @payload-bites/astro-richtext-renderer

Usage

Import the RichText component and pass your Lexical editor state to the data prop:

---
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'

import { RichText } from '@payload-bites/astro-richtext-renderer'

interface Props {
  data: SerializedEditorState
}

const { data } = Astro.props
---

<article>
  <RichText data={data} />
</article>

Props

| Prop | Required | Description | | ------------ | -------- | ------------------------------------------------------------------------------------------ | | data | Required | Serialized editor state to render. | | converters | Optional | Custom Astro component converters to override or extend the defaults. | | config | Optional | Configuration options (see below). | | class | Optional | CSS class for the container element. Defaults to "payload-richtext". |

Config Options

Pass configuration via the config prop:

| Option | Type | Description | | ------------------ | ---------------------- | -------------------------------------------------------------------------- | | disableContainer | boolean | If true, removes the container div wrapper. | | disableIndent | boolean \| string[] | Disable indentation globally or for specific node types. | | disableTextAlign | boolean \| string[] | Disable text alignment globally or for specific node types. | | internalDocToHref| function | Convert internal document links to href strings (see Internal Links). |

Astro Converters

The RichText component uses Astro components as converters. Built-in converters handle common Lexical nodes (paragraphs, headings, lists, links, etc.). You can add or override converters for custom blocks, custom nodes, or any modifications you need.

[!IMPORTANT] When fetching data, ensure your depth setting is high enough to fully populate Lexical nodes such as uploads. Converters require fully populated data to work correctly.

Internal Links

By default, Payload doesn't know how to convert internal links to URLs. To handle internal links, pass the internalDocToHref function in the config prop:

---
import type { SerializedLinkNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'

import { RichText } from '@payload-bites/astro-richtext-renderer'

interface Props {
  data: SerializedEditorState
}

const { data } = Astro.props

const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
  const { relationTo, value } = linkNode.fields.doc!

  if (typeof value !== 'object') {
    throw new Error('Expected value to be an object')
  }

  const slug = value.slug

  switch (relationTo) {
    case 'posts':
      return `/posts/${slug}`
    case 'categories':
      return `/category/${slug}`
    case 'pages':
      return `/${slug}`
    default:
      return `/${relationTo}/${slug}`
  }
}
---

<RichText data={data} config={{ internalDocToHref }} />

Lexical Blocks

If your rich text includes custom Blocks or Inline Blocks, you must supply custom Astro component converters that match each block's slug. Create an Astro component for each block type and pass them via the converters prop.

First, create your block component:

---
interface Props {
  node: {
    fields: {
      id: string
      blockType: 'myNumberBlock'
      number: number
    }
  }
}

const { node } = Astro.props
---

<div class="number-block">{node.fields.number}</div>

Then pass them to the RichText component:

---
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'

import { RichText, type AstroConvertersFunction } from '@payload-bites/astro-richtext-renderer'

import NumberBlock from '@/components/blocks/NumberBlock.astro'

interface Props {
  data: SerializedEditorState
}

const { data } = Astro.props

const astroConverters: AstroConvertersFunction = ({ defaultConverters }) => ({
  ...defaultConverters,
  blocks: {
    myNumberBlock: NumberBlock,
  },
});
---

<RichText data={data} converters={astroConverters} />

Overriding Converters

You can override any of the default converters by passing your custom Astro component, keyed to the node type.

Each converter component receives these props via ConverterProps<T>:

| Prop | Description | | ------------ | -------------------------------------------------------- | | node | The Lexical node data | | converters | All available converters (for rendering children) | | config | User configuration | | style | Computed inline styles (textAlign, paddingInlineStart) | | RenderNode | Helper component for rendering child nodes |

Example - overriding the upload node converter:

---
import type { SerializedUploadNode } from '@payloadcms/richtext-lexical'
import type { AstroConverterProps } from '@payload-bites/astro-richtext-renderer'

type Props = AstroConverterProps<SerializedUploadNode>

const { node, style } = Astro.props

const upload = node.value

if (typeof upload !== 'object') {
  console.warn('Upload not populated')
}

const doc = typeof upload === 'object' ? upload : null
const url = doc?.url
const alt = doc?.alt || ''
---

{url && <img src={url} alt={alt} style={style} loading="lazy" decoding="async" />}

Then use it:

---
import { RichText, AstroConvertersFunction } from '@payload-bites/astro-richtext-renderer'

import CustomUpload from '@/components/converters/CustomUpload.astro'

const data = /* your Lexical editor state */

const astroConverters: AstroConvertersFunction = ({ defaultConverters }) => ({
  ...defaultConverters,
  upload: CustomUpload,
});
---

<RichText data={data} converters={astroConverters} />

Media Uploads

When using Payload local media storage, make sure you have set serverURL in your payload.config.ts to ensure the media collection returns absolute URLs for your Astro website to use.

Rendering Child Nodes

For converters that need to render nested content (like links, lists, or custom blocks with rich text fields), use the RenderNode component passed as a prop:

---
import type { AstroConverterProps } from '@payload-bites/astro-richtext-renderer'
import type { SerializedLexicalNode } from '@payloadcms/richtext-lexical/lexical'

interface BlockNode extends SerializedLexicalNode {
  fields: {
    id: string
    blockType: 'richTextBlock'
    title: string
  }
  children: SerializedLexicalNode[]
}

type Props = AstroConverterProps<BlockNode>

const { node, converters, config, RenderNode } = Astro.props

const children = node.children || []
---

<div class="rich-text-block">
  <h3>{node.fields.title}</h3>
  {children.map((child) => (
    <RenderNode node={child} converters={converters} config={config} />
  ))}
</div>