{
  "$type": "site.standard.document",
  "canonicalUrl": "https://johnnyreilly.com/posts/docusaurus-3-how-to-migrate-rehype-plugins",
  "description": "Learn how to migrate rehype plugins to Docusaurus 3.",
  "path": "/posts/docusaurus-3-how-to-migrate-rehype-plugins",
  "publishedAt": "2023-10-09T00:00:00.000Z",
  "site": "at://did:plc:yy3apqjlms24kso7ahn7lbmb/site.standard.publication/3mova7c4nho2b",
  "tags": [
    "docusaurus"
  ],
  "textContent": "Docusaurus v3 is on the way. One of the big changes that is coming with Docusaurus 3 is MDX 3. My blog has been built with Docusaurus 2 and I have a number of rehype plugins that I use to improve the experience of the blog. These include:\n\n- a plugin to improve Core Web Vitals with fetchpriority / lazy loading\n- a plugin to serving Docusaurus images with Cloudinary\n\nI wanted to migrate these plugins to Docusaurus 3. This post is about how I did that - and if you've got a rehype plugin it could probably provide some guidance on the changes you'd need to make.\n\n\n\nWhat needs to change?\n\nThe Docusaurus team put out a blog post on preparing for the Docusaurus 3 migration. Part of that post mentions MDX plugins:\n\n> All the official packages (Unified, Remark, Rehype...) in the MDX ecosystem are now ES Modules only and do not support CommonJS anymore.\n\nThis affects how you write your plugins. It also has a bearing on how you import your plugins, given that the Docusaurus configuration file itself is still CommonJS. The post adds:\n\n> If you created custom Remark or Rehype plugins, you may need to refactor those, or eventually rewrite them completely, due to how the new AST is structured.\n\nThis turned out to be the case for me. I had to rewrite my plugins completely. I'll go through each of them in turn.\n\nMigrating the fetchpriority plugin\n\nThe fetchpriority plugin is a rehype plugin that I wrote to improve the Core Web Vitals of my blog. It does this by making the first image on a page eager loaded with fetchpriority=\"high\" and lazy loading all other images. The Docusaurus 2 / MDX 1 code looked like this:\n\nThe new plugin looks like this:\n\nWhat's different? Well, a number of things; let's go through them.\n\nCommonJS to ES Module\n\nYou'll note the old plugin has the name image-fetch-priority-rehype-plugin.js and the new plugin has the name image-fetch-priority-rehype-plugin.mjs. This is because the new plugin is an ES Module and the old plugin is CommonJS.\n\nFurther to that, the old plugin used module.exports = imageFetchPriorityRehypePlugin to expose functionality and the new plugin uses export default imageFetchPriorityRehypePlugin.\n\nDifferent AST\n\nThe abstract syntax tree (AST) is different. MDX 1 and MDX 3 make different ASTs and we must migrate to the new one. Interestingly, it seems to be slightly simpler in some ways. MDX 1 surfaced both element / img nodes and jsx nodes. By contrast, MDX 3 appears to surface just mdxJsxTextElement which are similar to MDX 1's jsx nodes, but come with their own AST representation of expression based attributes in the data property.\n\nThe logic of the new plugin is similar to the old plugin, but the code is different to cater for the different AST.\n\nAnd that's it - we have a new fetchpriority plugin that works with Docusaurus 3 and MDX 3!\n\nMigrating the cloudinary plugin\n\nFirstly, let's remind ourselves what the cloudinary plugin does. It takes an image URL and transforms it into a Cloudinary URL. So like this:\n\nAnd at runtime, Cloudinary's Fetch mechanism will handle transforming the image into a format that is optimised for the browser that is requesting it.\n\nIt turns out that the fetchpriority plugin is a much more straightforward migration than the cloudinary plugin. And the reason for that is related to the aforementioned AST changes. Let's start with the old plugin:\n\nThe old plugin had two kinds of nodes it had to deal with, element and jsx. The new plugin will have to deal with just one kind of node, mdxJsxTextElement. (Just the same as with the fetchpriority plugin.)\n\nNow you may have noticed that the JSX node in the old plugin has a slightly more complex src attribute:\n\nThat src attribute is a JavaScript expression. It's not a string. It's a JavaScript expression that will be evaluated later by webpack, and will return the path to the image in the final (webpack-based) Docusaurus build.\n\nSo transformation into a Cloudinary URL for JSX nodes is a little tougher. In the MDX 1 plugin, we needed to wrap the require expression in backticks and prefix it with https://res.cloudinary.com/${cloudName}/image/fetch/${baseUrl} where ${baseUrl} is the base URL of our website. We also need to prefix the expression with a $ to indicate that it's a JavaScript expression. Tough to read but it works.\n\nRereading that paragraph, I realise it's hard to understand. Perhaps easier to see it in action. Here's what we want our plugin to do to the JSX node above:\n\nIt turns out it's even tougher doing this with MDX 3 as compared to MDX 1. This is because MDX 3's AST includes all kinds of metadata around the mdxJsxAttributeValueExpression:\n\nThe data object above is a full on AST representation of the require expression. And to make a plugin that works with MDX 3, we need to use that AST representation to build up the new src attribute. This involves some string manipulation and some AST traversal. It's not pretty but it works.\n\nHere's the new plugin:\n\nMuch is happening here. Let's go through it.\n\nCommonJS to ES Module\n\nThis amounts to the same changes as the fetchpriority plugin. The old plugin has the name cloudinary-rehype-plugin.js and the new plugin has the name cloudinary-rehype-plugin.mjs. This is because the new plugin is an ES Module and the old plugin is CommonJS. Related to this, the old plugin used module.exports = imageCloudinaryRehypePlugin to expose functionality and the new plugin uses export default imageCloudinaryRehypePlugin.\n\nDifferent AST\n\nWe're dealing with a different AST and just need to tackle the mdxJsxTextElement which are similar to MDX 1's jsx nodes, but come with their own AST representation of expression based attributes in the data property.\n\nThe hardest part of this (and it is hard / confusing) is dealing with the require expression in the src attribute. What we do is:\n\n1. Convert the mdxJsxTextElement to back to markdown - this is the full img element in its AST form\n2. Use a regex to find the require expression in the src attribute of the markdown\n3. Transform the require expression to a Cloudinary URL using the same mechanism as with the MDX 1 plugin\n4. Convert the markdown back to an mdxJsxTextElement using a technique adapted from mdast-util-mdx-jsx\n5. Replace the src attribute with the new src attribute including the updated require expression AST in the mdxJsxAttributeValueExpression attributes data property.\n\nIf you were to compare the MDX 1 plugin with the MDX 3 plugin, 2 and 3 from the above points are the same. Points 1, 4 and 5 are new.\n\nWith this in place we have a new plugin that works with Docusaurus 3 and MDX 3!\n\nrehype-cloudinary-docusaurus@2\n\nYou may recall that I published an npm package named rehype-cloudinary-docusaurus which packages up the plugin to make it easy for people to use. I've updated that package to use the new plugin and it is available now. You can see the pull request here. The new version is 3.0.0.",
  "title": "Docusaurus 3: how to migrate rehype plugins"
}