Abigail Young

Abigail Young

Add KaTeX and Markdown to Next.js Blog Posts

PRs, READMEs, Bear notes — I write Markdown so much that I catch myself writing it outside MD-friendly editors. More science and math in my life have meant a new desire to throw some LaTeX into my Markdown. Possible?

My goal was to add support to this site specifically, which is a Next.js app. There weren't as many general examples online as I'd hoped, and definitely none using Next.js.

After lots of confusion, tinkering, and persistence, I found my perfect combination of Remark plugins and KaTeX for processing. An example:

i=1ni3=(i=1ni)2\sum_{i=1}^n i^3 = \left(\sum_{i=1}^n i\right)^2

Look at that! Beautiful! Unrelated, this is my current favorite equation, found while reading this LaTeX Intro guide.

"It says that the sum of the first n cubes of integers is equal to the sum of the first n integers squared!"

DIY with UnifiedJS

If you want to add web-friendly LaTeX processing to your own Markdown-powered website, here are the libraries I used:

  • UnifiedJS - An ecosystem that encompasses all the other packages/plugins. Think of it as the container that holds all your tools.

  • Remark - A Markdown processor with dozens of plugins. It's required to use the following plugins that do the heavy lifting.

  • Remark Math - Remark plugin that parses $ and $$ markers as math nodes.

  • Remark HTML KaTeX - Remark plugin that transforms those math nodes with KaTeX.

  • KaTeX - Install as a package in order to import css. This is essential!

Important: Remark v12.0.0 is required to work with Remark Math. If it's not working, check your version.

Next.js Example

I've modified a Next.js starter blog to support KaTeX in Markdown (which is what I'm using on this site). You can check out the repo for full source code and documentation.

The main changes are in the js lib/posts.tsx file, where the text processing happens.

import matter from 'gray-matter';
import html from 'remark-html';
import markdown from 'remark-parse';
import math from 'remark-math';
import htmlKatex from 'remark-html-katex';
import unified from 'unified';

export async function getPostData(id: string) {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents);

  // Use remark to convert markdown into HTML string
  const processedContent = await unified()
    .use(markdown)
    .use(math)
    .use(htmlKatex)
    .use(html)
    .process(matterResult.content);
  const contentHtml = processedContent.toString();

  // Combine the data with the id and contentHtml
  return {
    id,
    contentHtml,
    ...(matterResult.data as { date: string; title: string }),
  };
}

Additional Reading

I had to read a lot of READMEs for various packages, old and new, most of which I wish had better documentation. The end result seems so simple now, but it was a journey.

One blog post that got me over the finish line was this piece about MDX and Gatsby by Nicky Meuleman. Some differences in tech stack, but he had great explanations and callouts. If you're going down this path, I recommend checking it out.