Originally published at parvejshah.com/blog/rendering-katex-formulas-nextjs-server-components by Parvej Shah.
MathPro Academy teaches JSC, SSC, and HSC mathematics to secondary students across Bangladesh. Formula rendering — quadratic formulas, trigonometric identities, the full range of secondary-school algebra — is not a cosmetic detail here; it's most of what the product actually shows a student.
What Build-Time Rendering Assumes
Pre-rendering LaTeX to HTML at build time (or on the server per-request, ahead of sending anything to the browser) works cleanly when content is fixed and known in advance. MathPro's content isn't: instructors edit course material continuously through the admin panel, and — because the platform migrated its editor from plain text to Lexical in December 2025 — the database holds a permanent mix of both formats side by side, not a one-time migration that finished and left a single clean shape behind.
An earlier version of this rendering path leaned toward doing more of that work ahead of time. It didn't hold up against that mix: a renderer that assumes one content shape breaks the moment it meets the other.
What Actually Ships
The real renderer, SafeHtmlRenderer, runs entirely client-side and does two jobs in sequence:
- Format detection. It checks whether a record contains HTML tags. If not, it's legacy plain text — escaped and wrapped for consistent display. If it does, it's Lexical-authored HTML, sanitized through a fixed allowlist of tags and attributes before it ever touches the DOM.
-
LaTeX rendering. After the sanitized content mounts, a
useEffectwalks the container's text nodes looking for$...$(inline) and$$...$$(block) spans, and replaces each one with KaTeX-rendered markup — the same delimiter-matching logic the admin editor's own LaTeX plugin uses, so what an instructor sees while writing matches what a student sees while reading.
// Runs after mount, not on the server — the content it walks
// isn't known until the sanitized HTML is actually in the DOM.
useEffect(() => {
if (processedContent?.type !== "html") return;
renderLatexInElement(containerRef.current);
}, [processedContent]);
This is a real trade-off, not a free win: KaTeX ships to the browser, and rendering happens after mount rather than being baked into the HTML response. What it buys back is correctness across every record in the database, old and new, without a migration project or a format the renderer has to assume in advance.
The Sanitization Boundary Matters More Than the Renderer
The more interesting design decision isn't the KaTeX call — it's what happens before it. New content is run through a fixed allowlist of tags and attributes (DOMPurify) before dangerouslySetInnerHTML ever sees it. KaTeX's own markup is deliberately not pushed through that same sanitizer — its output relies on precise inline styles, MathML, and SVG that would need a much larger, riskier allowlist to preserve faithfully. Instead, KaTeX renders into the DOM directly, after the surrounding content has already been sanitized. Getting that boundary right — sanitize the untrusted parts, trust the library you control — is the part of this that would have been easy to get wrong.
What We'd Tell the Next Team Doing This
If your content has one shape and it's fixed at build time, server-side pre-rendering is the better call — no argument. But "server-side" isn't automatically the more sophisticated choice. When the actual constraint is "the data has two generations mixed together and keeps changing," a runtime renderer that normalizes format first and renders second is the simpler, more honest design — even though it costs a client-side bundle and a post-mount pass that a cleaner dataset wouldn't need.
Parvej Shah is a Lead Full-Stack Web Developer & Platform Architect based in Dhaka, Bangladesh. Explore full architecture case studies and production code at parvejshah.com.
Top comments (0)