Skip to content
Postkit

Renderers

Since version 0.3.0

Email. Send the post as a newsletter without rewriting it.

Your posts already have callouts, figures and footnotes. @postkit/email turns the same file into an email that looks right in Gmail, Outlook and Apple Mail, so the newsletter is never a second draft.

Render a post

renderEmail takes an authored post and returns email-safe HTML, a plain-text version and any warnings:

ts
// scripts/send-issue.ts
import { renderEmail } from '@postkit/email';

const { html, text, warnings } = await renderEmail(post.body, {
  format: 'mdx',
  baseUrl: 'https://example.com',
  definitions: postkit.definitions, // your createPostkit() set
});

await newsletter.send({ subject: post.title, html, text });

Hand html and text to any provider. Postkit never sends anything itself.

Options

OptionDescription
format'mdx', 'markdown' or 'document' (a @postkit/core document).
themeOptional Chakra Email theme. Defaults to the light Postkit email theme.
baseUrlResolves relative links and images, such as 'https://example.com'.
componentsOptional overrides, keyed like your MDX map: components by name, prose by element.
wrapperOptional layout component for the header, footer and unsubscribe link.
definitionsThe components the post may use. Defaults to every built-in.
urlThe post on your site, where link fallbacks point.
audiencesThe audiences this email is for, for AudienceBoundary.
newsletterUrlWhere a NewsletterSignup would link.
prettyIndents the HTML, for review.

format defaults to 'markdown' for a string and 'document' for an object. It resolves to { html, text, warnings }. To use only part of a post, as a digest does, call parseEmailSource first, pick the nodes you want, and pass the result with format: 'document'.

How it works

renderEmail parses the post with Postkit's Remark preset and directive validation, as your site does, then @postkit/core's safe MDX parser. Invalid props fail as they do on the site; unknown components, MDX code (import, {expressions}) and dangling footnotes become warnings instead. It maps each component to its email view, or to the component's declared fallback, and renders the result with Chakra Email's render(). The plain-text part comes from the same document, so it makes the same decisions as the HTML.

It doesn't depend on a framework. Email is rendered in scripts, scheduled jobs and queue workers as often as in a page, so renderEmail lives in @postkit/email, not in the framework adapters.

Email always renders in the light theme. Email clients apply their own dark mode.

What's supported

ComponentsStatusIn the inbox
Callout · Aside · CallToActionSupportedTable layout, inline styles, your tones
Figure · Audio · VideoSupportedImage with caption; media as a linked poster
KeyTakeaway · PullQuote · StepsSupportedBoxed summary, ruled quote, numbered rows
Stat · ProductCard · LinkPreviewSupportedLinkPreview as a static card, never an embed
Footnote · FootnotesSupportedNumbered notes at the end, no links
Everything elseFallbackDeclared per component: text, link or omit

The status on each component page is computed from what @postkit/email exports, so it can't drift out of date. Footnotes always render as end notes with superscript numbers and no links, because in-message anchors are unreliable across clients. The plain-text part marks them [1].

Fallbacks

Interactive components can't run in an inbox. Each one declares what to do instead, so a newsletter never shows a broken widget:

FallbackWhat readers seeUsed by
textThe content as plain, readable text, code and linksAppearsOn, AudienceBoundary, AuthorCard, CardGrid, Carousel, Chart, CodeBlock, CodeGroup, Comparison, Device, Diff, Disclosure, FileCard, FileTree, Gallery, RelatedContent, SeriesNavigation, SponsorBlock, Tabs, Terminal
linkA short line linking to it on the webPoll (to your post), SocialPost (to the post itself)
omitNothing; the block is skippedShareActions, NewsletterSignup

Your own components declare one the same way, with email: { fallback: 'text' } in definePostkitComponent. A component of your own with no email view and no fallback renders as text. When a fallback applies, or a component is unknown, renderEmail adds an entry to warnings, with a code such as email-fallback or unknown-component, a message, and the component and position when there is one. It never throws on content your site renders.

To give your own component an email view, or replace a built-in one, pass it in components. A component with a view is never a fallback.

Views and the provider

renderEmail is built from parts you can use directly. The package exports:

  • Views: Aside, Audio, Callout, CallToAction, Figure, Footnote, Footnotes, KeyTakeaway, LinkPreview, ProductCard, PullQuote, Stat, Steps and Video, and postkitEmailViews, the map of all of them that the manifest's support.email is computed from.
  • PostkitEmailProvider, which applies postkitEmailTheme (light, with literal tone.* and tint.* colours) and accepts Chakra Email theme overrides on top, such as your own tones.
  • createPostkitEmailMdxComponents() and postkitEmailMdxComponents, the MDX map of the views, for email builds that compile trusted MDX.
  • parseEmailSource, the parsing step of renderEmail.
tsx
import { render } from '@chakra-email/core';
import { Callout, CallToAction, PostkitEmailProvider } from '@postkit/email';

const html = await render(
  <PostkitEmailProvider>
    <Callout title="Before you publish" tone="warning">
      Review the destination and audience.
    </Callout>
    <CallToAction
      title="Read the field guide"
      primaryLabel="Read now"
      primaryHref="https://example.com/guide"
    />
  </PostkitEmailProvider>,
);

Audio and Video never emit native media elements, because major email clients don't support them consistently.

Edit this page on GitHub

Postkit

Richer posts, written once, right everywhere.

© 2026 Postkit. MIT licensed.
LicensingPrivacy