markdownmdxjsremarkjs

Replacing paragraph nodes with HTML nodes


Question

How can I replace a "paragraph" node to an "html" node for MDX?

Background

Replacing every "twitter.com" links to expanded Twitter embed HTML within an MDX content.

Reproducible CodeSandbox: https://codesandbox.io/s/dazzling-curran-2bcwe?file=/src/index.mjs:2024-3439

Issue

I have an MDX content, which contains a list of twitter links (e.g. https://twitter.com/BrendanEich/status/1151317825908166656), and replacing paragraph node causes following error

../node_modules/esbuild/lib/main.js:869:27: error: [plugin: esbuild-xdm] Cannot read property 'line' of undefined at failureErrorWithLog (/sandbox/node_modules/esbuild/lib/main.js:1449:15) at /sandbox/node_modules/esbuild/lib/main.js:1131:28 at runOnEndCallbacks (/sandbox/node_modules/esbuild/lib/main.js:921:63) at buildResponseToResult (/sandbox/node_modules/esbuild/lib/main.js:1129:7) at /sandbox/node_modules/esbuild/lib/main.js:1236:14 at /sandbox/node_modules/esbuild/lib/main.js:609:9 at handleIncomingPacket (/sandbox/node_modules/esbuild/lib/main.js:706:9) at Socket.readFromStdout (/sandbox/node_modules/esbuild/lib/main.js:576:7) at Socket.emit (events.js:315:20) at addChunk (internal/streams/readable.js:309:12)

Code in question

const remarkTwitter = (options) => {
  return transformer;

  async function transformer(tree) {

    // gather Twitter links
    visit(tree, "paragraph", (node) => {
      if (isTwitterLink(node)) {
        const tweetLink = node.children[0].value;
        tweetNodeList.push([node, tweetLink]);
      }
    });

    // Build Tweet embed HTML
    // and replace the current node
    for (let i = 0; i < tweetNodeList.length; i++) {
      const twitterNode = tweetNodeList[i];
      const node = twitterNode[0];
      const tweetLink = twitterNode[1];

      try {
        const embedData = await getEmbeddedTweet(tweetLink, options);

        node.type = "html";
        node.value = embedData.html;

        return node;
      } catch (err) {
        console.error(err);
      }
    }
  }
};


Solution

  • Instead of my custom plugin, I used @remark-embedder/core plugin

    Here is the part of the code that uses the plugin.

    import remarkEmbedder from "@remark-embedder/core";
    // other imports...
    
    const getMDXOptions = (year: number, slug: string) => {
      const cwd = `${getBlogRoot(year, slug)}`;
      const imagesUrl = `/images/blog/${year}/${slug}`;
    
      return {
        cwd,
        xdmOptions: (options: any) => {
          options.format = "mdx";
          options.allowDangerousRemoteMdx = true;
          options.rehypePlugins = [twitterWidgetScriptPlugin];
          options.remarkPlugins = [
            remarkMdxImages,
            [
              remarkEmbedder,
              {
                transformers: [
                  githubGistTransformer,
                  codeSandboxTransformer,
                  [oembedTransformer, oembedConfig],
                ],
                handleHTML,
              },
            ],
          ];
    
          return options;
        },
        esbuildOptions: (options: BuildOptions) => {
          options.outdir = path.join(process.cwd(), "public", imagesUrl);
          options.loader = {
            ...options.loader,
            ".png": "file",
            ".jpg": "file",
            ".gif": "file",
            ".svg": "file",
          };
          options.publicPath = imagesUrl;
          options.write = true;
    
          return options;
        },
      };
    };