Pilcrow for Next.js

A build-time rehype plugin that drops Pilcrow typesetting into a Next.js MDX pipeline.

Build-time only

Next.js bundles MDX at compile time. Running headless Chromium per request inside a serverless or edge runtime is not viable, so this plugin runs at next build time only. The pt-line spans are baked into the compiled MDX output. Nothing typeset-related runs at the reader, and nothing runs at the request edge. If you need request-time typesetting, this is the wrong tool.

What it does

When next build compiles an .mdx file, the plugin intercepts the rehype pass, serialises the post body to HTML, runs typeset() over it, and parses the result back into the HAST tree before Next.js's MDX bundler turns it into JavaScript. Drop caps, hyphenation, and per-line spans are handled by pilcrow-typeset. A single Chromium instance opens on first invocation per build process and is reused across files, so you pay the browser-launch cost once.

Install

npm install pilcrow-nextjs pilcrow-typeset @next/mdx @mdx-js/loader @mdx-js/react
npx playwright install chromium

Playwright is a peer dependency. The Chromium download is roughly 170 MB on first install. The package is published on npm.

next.config.mjs

import createMDX from '@next/mdx';
import pilcrowNext from 'pilcrow-nextjs';

const withMDX = createMDX({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [],
    rehypePlugins: [[pilcrowNext, {}]],
  },
});

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
};

export default withMDX(nextConfig);

The plugin is the last rehype step you want before the MDX code generator runs, because the typesetter measures against the final rendered HTML structure.

Options

rehypePlugins: [[pilcrowNext, {
  fontShorthand: '18px ui-serif',
  maxWidth: 720,
  lineHeight: 30,
  dropCap: true,
}]]

The empty-string and zero defaults are deliberate. Leaving them at the defaults tells the renderer to read font, column width, and line height from your CSS at typeset time, so your stylesheet stays the source of truth for measurement geometry. The full options reference lives on the API page.

Common pitfalls

The build host has to be able to install and launch Playwright Chromium. Vercel, Netlify, Cloudflare Pages, GitHub Actions, and GitLab CI are confirmed working. Locked-down build images that block browser sandboxing or omit the Chromium binary will not work; if your CI runs inside a stripped container, check first.

Pure-Markdown .mdx files typeset normally. Files that contain JSX components or {expression} nodes hold MDX-AST shapes that cannot survive the HTML round-trip the plugin performs. When the plugin sees one it logs a warning to stderr naming the file and returns the tree unchanged for that file. If you want the body content typeset, move the JSX out into a layout or a surrounding page so the body itself stays plain Markdown.

The plugin assumes the MDX file is the post body. If your route renders the MDX inside a wrapper that adds the prose column CSS, that wrapper has to be loaded by the typesetter's measurement page in the same shape as the production page; otherwise the line breaks will be measured against the wrong column width. The default behaviour reads from the CSS already on the page, so the most common cause of this is a stylesheet that loads after the typesetter measures.

Reference fixture

A working end-to-end fixture lives in the repository at packages/pilcrow-nextjs/test/fixture. It is the same project the plugin's tests run against, so it stays in sync with the current released version.