Colocated images with MDX and Next.js

I recently migrated this site to Next.js from Gatsby. I wish I had a good reason for this, but I don't.

The Gatsby site used MDX and I wanted to carry that over. Little did I know what a pain in the arse it is to get MDX working just so on Next.js.

I needed a setup that supported:

  • frontmatter
  • inlined components
  • colocated images
  • presentation templates

Next.js has @next/mdx which supports inlined components, and I guess colocated images through imports, but not frontmatter. Also, presentation templates need to be imported inline which is ugly, IMO. I could live with the image imports, but having to import the same components into every MDX file that I needed was not optimal. It's nice that you can keep your mdx files in pages/ though.

So onward to next-mdx-remote. This supports frontmatter using grey-matter, and you can pass common inline components through the template which you can set up through Next.JS dynamic routes. Ace. But no colocated images. In fact, images can only be served through public/. I attempted to dynamically require or import them. I tried a variation on this solution to load the images, but kept getting runtime errors. Alas, etc.

Resolving that I didn't have the time to faff around with this just now (procrastination task), my solution was to use copy-webpack-plugin and have webpack copy the images to public/. I don't like it, but it works, and I can figure out if there's a nicer way of doing this later.

webpack: (config) => {
        new CopyPlugin({
            patterns: [
                    from: path.join(__dirname, 'path/**/*.jpg'),
                    to: path.join(__dirname, 'public/[path][name][ext]'),

Update the from to point to where your colocated images will be, and the to will match the directory structure in public/. You also need to use path.join or whatever you want to use to have the full path. Relative paths won't work here.

You'll now be able to use markdown image syntax to reference the image:

\![Image description](colocated-image.jpg)

This will generate an <img /> tag, which you can replace in your template by passing an img component to MDXRemote. You can replace it with next/image or a custom image component. path is a variable set to the path to the mdx file.

const components = (path) => ({
    img: (props) => {
        return <Image {...props} src={`/assets/${path}/${props.src}`} fill lazy />;

return (
    <MDXRemote {...mdxSource} components={components(path)} />

See also

Published November 08, 2022