Valkyrian Labs logo

Rendering

Render Markdown fields and blocks with server-first components and scoped configuration.

Rendering

Markdown rendering is server-friendly and centralized in the plugin renderer. The same pipeline handles Markdown, GFM, Shiki code blocks, heading anchors, sanitized HTML, and registry-backed directives.

Render A Markdown Field

1import { MarkdownRenderer } from 'payload-markdown/server'2 3export function PostBody({ content }: { content?: string | null }) {4  return (5    <MarkdownRenderer6      collectionSlug="posts"7      markdown={content}8      scope="field"9    />10  )11}

Set collectionSlug when you want collection-scoped code, themes, or config to apply.

Renderer Props

Common props:

  • markdown: Markdown source string
  • collectionSlug: collection key used for config resolution
  • scope: field or blocks
  • variant: blog, docs, compact, or unstyled
  • size: lg, md, or sm
  • as: HTML tag for the rendered Markdown container, defaulting to article
  • className: class names for the Markdown element
  • wrapperClassName: class names for the outer wrapper
  • enableGutter: adds horizontal wrapper padding
  • mutedHeadings: slightly reduces heading contrast
  • colorScheme: auto (follow the site's dark mode), light, or dark; see Light And Dark
  • emptyFallback: returned for empty or whitespace-only Markdown
  • errorFallback: returned when compilation fails (the result has errors); warnings alone do not trigger it
  • code: local Shiki and code fence overrides
  • icons: local SVG icon pack overrides
  • themes: local directive theme overrides

Local Overrides

Direct component props are the final override layer:

1<MarkdownRenderer2  as="section"3  code={{4    fullBleed: true,5    lineNumbers: true,6  }}7  collectionSlug="posts"8  emptyFallback={<p>No content yet.</p>}9  markdown={post.content}10  scope="field"11  size="lg"12  themes={{13    card: {14      items: [15        {16          name: 'localFeature',17          classes: 'rounded-2xl border border-cyan-400/40 bg-cyan-950/30 p-5',18        },19      ],20    },21  }}22  variant="blog"23  wrapperClassName="max-w-4xl"24/>

Most applications should prefer plugin-level or collection-level config. Renderer-level overrides are useful for previews, one-off marketing pages, and admin-only rendering surfaces.

Render Blocks

1import { MarkdownBlockComponent } from 'payload-markdown/server'2 3export function MarkdownLayoutBlock({4  block,5  collectionSlug,6}: {7  block: {8    blockType: 'vlMdBlock'9    content: string10  }11  collectionSlug?: string12}) {13  return <MarkdownBlockComponent {...block} collectionSlug={collectionSlug} />14}

MarkdownBlockComponent resolves block scoped defaults with resolveMarkdownBlockDefaults(collectionSlug) and passes scope="blocks" to MarkdownRenderer.

Sanitized HTML And GFM

The render pipeline uses remark-gfm, rehype-raw, and rehype-sanitize. Markdown tables, task lists, strikethrough, and raw HTML are parsed, but unsupported or unsafe HTML is sanitized. Authored inline style attributes are stripped before final output. Raw HTML cannot use the renderer's internal data-* markers (such as data-vl-layout) to impersonate directives, and id/name values in raw HTML receive the standard user-content- prefix to prevent DOM clobbering. Heading, footnote, and tab ids generated by the renderer are not prefixed.

When warnings matter

Rendering reports warnings for malformed directives, invalid directive attributes, unknown themes, and renderer failures. Fatal failures are also listed in errors and logged on the server. MarkdownRenderer returns errorFallback only when compilation fails and errorFallback is provided; non-fatal warnings still render the HTML.

Headless Rendering

payload-markdown/render renders Markdown without React, Next.js or CSS imports, so it works in plain Node scripts, search indexers, feeds, emails and docs tooling:

1import { renderMarkdown } from 'payload-markdown/render'2 3const result = await renderMarkdown(markdown)4 5result.html // sanitized HTML, identical to MarkdownRenderer's inner HTML6result.headings // [{ depth, id, text }]: the heading ids emitted in html7result.text // plain text without directive markup or raw HTML8result.links // [{ url, kind: 'link' | 'image' | 'definition' | 'directive' }]9result.diagnostics // [{ message, severity, source, line?, column?, code? }]10result.warnings // diagnostic messages, as before11result.errors // fatal errors; html is a placeholder when non-empty

renderMarkdown(markdown, config) applies the same plugin defaults as MarkdownRenderer: pass scope, collectionSlug and, when a process builds several Payload configs, settings (a Payload config or instance). See Where Settings Live.

diagnostics carry a source (directive, theme, icon, code or render), a severity (error, warning or info) and, when the pipeline knows it, the 1-based line and column in the Markdown source. The same entry also exports compileMarkdown ({ html, warnings, errors }) and the heading anchor helpers extractHeadingAnchors, slugifyHeading and createHeadingSlugger.

Stylesheet

MarkdownRenderer imports its stylesheet for Next.js apps. Other consumers of the HTML, such as headless renders, can import the same file explicitly:

1import 'payload-markdown/styles.css'