Making MDX AMP-Compatible with a Custom Loader in Next.js
TL;DR
Markdown and MDX files use tags like <img> by default. Furthermore, they can't handle mathematical formula rendering or code syntax highlighting under AMP. So by creating a custom MDX loader, we can make them AMP-compatible.
For details on custom loaders, the MDX official documentation is a good resource.
AMP Compatibility Using JSX
The MDX format supports JSX. And there are AMP components for JSX. So to make things AMP-compatible, we just need to transform each default tag (like img) into its corresponding AMP component (like <amp-img ... />).
Basics
In the AST, JSX syntax is represented as follows:
So when we find a node containing a certain tag, we just need to transform it into a JSX node with the corresponding AMP component embedded in its value.
Mathematical Formulas
The AMP tag for rendering formulas is <amp-mathml>. For inline formulas, <amp-mathml inline> makes it inline.
Using remark-math, portions enclosed in $$ are converted to math and those enclosed in $ to inlineMath. So we transform math to <amp-mathml> and inlineMath to <amp-mathml inline>.
img
Formula conversion was straightforward since it's just a simple transformation. However, the AMP component corresponding to the img tag is <amp-img />, which requires width and height attributes. One workaround is to resize via CSS (reference), but you need to fix either width or height, and the fixed dimension pulls the layout, sometimes creating unnatural whitespace above and below images on mobile.
This time, since MDX parsing happens on the server side anyway, I decided to use a Node module to get the actual image size and set the dimensions properly.
The image-size package makes it easy to get dimensions. Getting sizes from URLs is a bit tricky because you can't use async processing. If you do, the width and height only become available after parsing is complete. I don't fully understand the details, but I solved it by using sync-request, a module that makes synchronous requests.
Note
However, sync-request is apparently deprecated (reference), so use it at your own risk. The issue is that the client side can become crash-prone, but since it only runs during builds, I'd like to think it's not a problem. It might become an issue if you start using dynamic import.
Syntax Highlighting
This simply does the tokenization (normally done by prismjs) on the custom loader side. Register the languages you want to use with refractor.register. For this, I used the code from amdx as-is. I've thoroughly studied this repository. Thank you very much.