@libretimes/markdown
Markdown rendering pipeline for academic publishing: KaTeX, syntax highlighting (shiki), and LaTeX-style statement blocks, assembled as a set of remark/rehype plugins on top of unified.
Install
npm install @libretimes/markdownPeer dependencies: react (>=19) and unified (>=10).
Import the stylesheet once in your app:
import "@libretimes/markdown/styles.css";
import "katex/dist/katex.min.css";Rendering
The pipeline is asynchronous: the default rehype set includes
rehype-pretty-code, whose shiki highlighter is async. So rendering can't go
through react-markdown (it evaluates the tree with runSync). There are two
entry points.
MarkdownRenderer (Server Component)
An async React Server Component. Use it on server-rendered pages, such as publication pages.
import { MarkdownRenderer } from "@libretimes/markdown";
export default function Page({ body }: { body: string }) {
return <MarkdownRenderer content={body} language="en" />;
}| Prop | Type | Default |
|---|---|---|
content |
string |
required |
language |
Language (en/ru/fr/de/zh) |
"en" |
className |
string |
"prose dark:prose-invert max-w-none" |
The default className expects Tailwind Typography (prose). Override it for
bare-element styling.
renderMarkdown (anywhere)
The async core: markdown in, sanitized HTML out. Use it when you can't render an async server component -- for example a client-side live preview, where you call it inside an effect and inject the result.
"use client";
import { useEffect, useState } from "react";
import { renderMarkdown } from "@libretimes/markdown";
function Preview({ body }: { body: string }) {
const [html, setHtml] = useState("");
useEffect(() => {
let cancelled = false;
renderMarkdown(body, { language: "en" }).then((out) => {
if (!cancelled) setHtml(out);
});
return () => { cancelled = true; };
}, [body]);
return (
<div
className="prose dark:prose-invert max-w-none"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}The output is sanitized by rehype-sanitize inside the pipeline, so it's safe
to inject.
Plugin access
For a custom pipeline, the building blocks are exported individually:
getMarkdownPlugins, getRemarkPlugins, getRehypePlugins, sanitizeSchema,
the individual plugins, and the statement registry/labels. See src/index.ts.
Design
Why rendering is async, why it returns HTML instead of a React tree, and when to change that: docs/rendering.md.