Images with MDX-Bundler

29th April 2021

mdx-bundler 3.4.0 has just been released! This new version improves on an awesome feature of 3.3.0 which allows you to set the current working directory of the build. With this you can require modules directly and co-locate your images within the same directory as the post!

This cuts down on your overheads and means that mdx-bundler works alot more like the way mdx does in Gatsby.


From my last post I had a getComponents function that ran through the given directory to find any .ts or .tsx files and read them into an object that mdx-bundler would understand. This function is now redundant!

Instead of giving getComponents the posts directory I can now set cwd in the options for bundleMDX to the posts directory and thats it! Relative imports are resolved by esbuild like they would in any other file.

You can see from this commit how much I was able to remove from this site now that cwd exists.


Images can be linked to in the same way and with a couple of extra bits of config they can be made to work extremely well!

Before we can get esbuild to handle the images it needs to know that there are files to import. Enter remark-mdx-images which takes the images in your MDX and turns them into imports, like you would in your .tsx files.

1import image from './image.jpg'
2;<img src={image} />
Language js

So now esbuild knows about the images we need to do something with them, right now its just going to complain that it doesn't have a loader for the .jpg file type.

To start with the dataurl loader will do the job, it inlines all the images as dataurl:image/jpg.... Not the most effecient solution but it does work out of the box and I used it as a proof of concept.

1const {code} = await bundleMDX(source, {
2 cwd: '/posts/directory/on/disk',
3 xdmOptions: options => {
4 options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkMdxImages]
6 return options
7 },
8 esbuildOptions: options => {
9 options.loader = {
10 ...options.loader,
11 '.jpg': 'dataurl'
12 }
14 return options
15 }
Language js#9-12

This just works, but as I said does inline all the images which will result in some pretty big bundles. The real aim was always the file loader which copies files from the source to the destination.

As the file loader is copying files around it needs to know more details about the resulting environment. At a minimum, where should the files be outputted to and what url is needed to request them.

1const {code} = await bundleMDX(source, {
2 cwd: '/posts/directory/on/disk',
3 xdmOptions: options => {
4 options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkMdxImages]
6 return options
7 },
8 esbuildOptions: options => {
9 options.outdir = '/public/directory/on/disk/img'
10 options.loader = {
11 ...options.loader,
12 '.jpg': 'file'
13 }
14 options.publicPath = '/img/'
15 options.write = true
17 return options
18 }
Language js#9-15

outdir is where on disk to write to, publicPath is the url to that folder and most importantly write tells esbuild that it should output the files into the outdir.

The directories are specific to your site but here I supply a different folder for each post/page etc... to give an example:

ContentPost DirectoryoutdirpublicPath
This Post<root>/_content/posts/2021-04-29-images-with-mdx-bundler<root>/public/img/posts/2021/04/images-with-mdx-bundler/img/posts/2021/04/images-with-mdx-bundler
About Page<root>/_content/pages/<root>/public/img/pages/about/img/pages/about

This prevents images from later builds overwriting earlier ones. You have to remember that each call to bundleMDX is isolated so esbuild can't tell if the image it wants to overwrite is from an another build or from the last time your site was built.

This has been a big improvement to this site, getting very close now to the experiance you get from gatsby-plugin-mdx but in an entirely platform agnostic way!

MDX-Bundler is going from strength to strength, I'm really happy with the new features and it's only getting better!