Skip to content

RESOURCES / BLOG

Using Cloudinary Images in MDX With Next.js App Router

When you build a developer-focused site — whether it’s documentation, a personal blog, or a knowledge base — using MDX (Markdown with JSX) can streamline content creation and encourage community contributions. MDX is especially powerful in Next.js, thanks to the new App Router structure, which allows for file-based routing combined with React Server Components.

While MDX makes content writing more approachable for both developers and non-developers, many projects also need optimized image handling. This is where Cloudinary excels. Cloudinary provides developers with the ability to store, transform, and deliver optimal images (and videos) at scale. By integrating Cloudinary images directly in MDX files, you can simplify your workflow and provide end-users with fast-loading, high-quality visuals.

In this guide, we’ll walk through a step-by-step process of setting up MDX in Next.js, configuring your project to support Cloudinary images, and adding components so that images in your MDX files are optimized on the fly.

Before following along, you should have:

  1. Basic knowledge of Next.js (and the new App Router).
  2. A Next.js boilerplate project already set up, using the App Router (app/ directory).
  3. A free Cloudinary account.
  4. Familiarity with MDX or Markdown, including how MDX can incorporate React components.
  5. A working knowledge of JavaScript/TypeScript, Node.js, and installing `npm` packages.

You’ll also need:

  • Node.js (16 or higher is recommended).
  • An IDE or text editor (VS Code, WebStorm, etc.).
  • Some existing images or placeholders to test with.

If you don’t have these set up yet, take a moment to install Node, initialize a Next.js project, and create a Cloudinary account. Then, you’ll be ready to continue. Please see their respective linked documentation for reference.  

Throughout this post, we’ll assume you have a Next.js project with the following structure:

```
my-next-project/

  ├─ app/

  │   ├─ layout.js

  │   ├─ page.js

  │   └─ ...

  ├─ components/

  │   ├─ mdx-components.js

  │   ├─ ...

  ├─ lib/

  ├─ public/

  ├─ next.config.mjs

  ├─ package.json

  └─ ...

```Code language: JavaScript (javascript)

Within the app/ folder, you have layout components and route files that Next.js uses to render pages. We’ll add some new routes and MDX files to power a docs or blog section, along with Cloudinary-based images.

We’ll need to install packages to handle MDX in Next.js, plus a few helpful transformations and highlight tools (if you want them). In your terminal project root, run:

`npm install @next/mdx rehype-mdx-import-media rehype-pretty-code rehype-slug next-secure-headers shikiji-transformers json5`Code language: JavaScript (javascript)

Here’s what each package is used for:

  1. @next/mdx. Official MDX support for Next.js, letting you import and create .mdx files as pages.
  2. rehype-mdx-import-media. Allows MDX to handle media imports, like images or other files, within the MDX content.
  3. rehype-pretty-code. A plugin to format and highlight code blocks in your MDX content (useful for developer documentation).
  4. rehype-slug. Automatically adds IDs to headings in your MDX, so you can create anchor links to specific sections.
  5. next-secure-headers. A utility to help secure your Next.js app by automatically adding best-practice HTTP headers.
  6. shikiji-transformers. Additional transformations for code syntax highlighting.
  7. json5. For parsing or handling JSON with some extended syntax features (like comments).

If you do not plan to highlight code in your docs, you can remove the highlighting-related packages, but for many developer sites, they are quite handy.

Next.js supports an extended configuration file, which is called next.config.mjs in Next. Let’s look at a sample of how you might configure MDX. You’ll also set up your Next.js images object to allow Cloudinary images:

```

// next.config.mjs

import { env } from 'node:process';

import createMDX from '@next/mdx';

import { transformerNotationDiff } from '@shikijs/transformers';

import { createSecureHeaders } from 'next-secure-headers';

import rehypeMdxImportMedia from 'rehype-mdx-import-media';

import { rehypePrettyCode } from 'rehype-pretty-code';

import rehypeSlug from 'rehype-slug';

/**

 * @type {import('next').NextConfig}

 */

const nextConfig = {

  reactStrictMode: true,

  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],

  images: {

    remotePatterns: [

      {

        protocol: 'https',

        hostname: 'res.cloudinary.com',

        pathname: '/**',

      },

    ],

  },

  webpack: (config, { isServer }) => {

    // Example: if you have custom server plugins, you can push them here

    if (isServer) {

      // Potential server-only code or plugins

    }

    return config;

  },

  async headers() {

    // Using next-secure-headers to add recommended security headers

    return [

      {

        source: '/(.*)',

        headers: createSecureHeaders({

          xFrameOptions: 'SAMEORIGIN',

          xContentTypeOptions: 'nosniff',

          // ... other header settings

        }),

      },

    ];

  },

};

const withMDX = createMDX({

  extension: /\.mdx?$/,

  options: {

    rehypePlugins: [

      rehypeMdxImportMedia,

      rehypeSlug,

      [

        rehypePrettyCode,

        {

          transformers: [transformerNotationDiff()],

          theme: 'github-dark-dimmed',

          defaultLang: 'plaintext',

        },

      ],

    ],

    // add any remark plugins here if needed

  },

});

export default withMDX(nextConfig);

```Code language: PHP (php)

In the code block above, here are what the key sections are for:

  • pageExtensions. Allows Next.js to process .mdx files as pages.
  • images object. We explicitly list Cloudinary as a remote pattern, so Next.js knows to optimize and host images from res.cloudinary.com. Adjust the hostname or pathname as needed.
  • withMDX. The @next/mdx wrapper adds MDX-specific configuration. We specify the file extensions and pass rehype plugins for extra functionality, like syntax highlighting with rehypePrettyCode.

Before you begin embedding images in .mdx files, make sure you have:

  1. A Cloudinary account where your images or folders reside.
  2. A Cloud name, API key, and API secret (if you plan to do programmatic uploads). For the simplest usage (just referencing existing images by URL), you only need your cloud name.

You can store your keys in environment variables. To do so, create a .env.local file in your Next.js root then add these keys and values:

CLOUDINARY_CLOUD_NAME=your_cloud_name

CLOUDINARY_API_KEY=123456789012345

CLOUDINARY_API_SECRET=abcdefg-hijklmnop

If all you plan to do is embed images by static URL (e.g., https://res.cloudinary.com/<cloud_name>/image/upload/...), you don’t need the API key or secret. However, including them can help if you want to use server-side transformations, automated image fetching, or advanced features.

With your Next.js App Router, you can place MDX files directly in subfolders under app/ to define routes, or store them in a dedicated app/docs/ folder. For example:

Inside page.mdx, you can add your content in Markdown/MDX format. Let’s illustrate how to embed a Cloudinary image:

```markdow/mdx file

---

title: 'Introduction'

date: '2025-01-24'

---

# Introduction to Our Docs

Welcome! This is a test **MDX** file for verifying our Cloudinary image embedding.

Below is an example image, served directly from Cloudinary:

![A Cloudinary Example](https://res.cloudinary.com/<cloud_name>/image/upload/v1670000000/sample.jpg)

You can also embed inlined images or dynamic transformations using query parameters, for example:

![A Transformed Example](https://res.cloudinary.com/<cloud_name>/image/upload/w_500,h_300,c_fill/v1670000000/sample.jpg)

```Code language: PHP (php)

Here, we are using standard Markdown syntax for images (![Alt Text](URL)), though you could also import them as modules or use a custom React component. As soon as you add these .mdx files and run npm run dev (or your build command), Next.js will treat them like any other page in the App Router structure.

The Next.js 13 App Router organizes pages via nested folders inside app/. Each folder can contain a page.js, page.tsx, or page.mdx (as we’ve just done), which exports the UI for that route.

  • Layout Files. layout.js or layout.tsx can define shared layouts or wrappers for each segment of the route.
  • page.js or `.mdx**. The actual content or React components that define the UI for that path.

If your docs hierarchy grows, you might nest subfolders like so:

Each directory with a page.mdx automatically becomes a route:

  • /docs/quickstart/
  • /docs/introduction/

This approach is extremely powerful for a developer relations or documentation site, as you can keep all your MDX files in one place without complicated routing logic.

One of the powerful features of MDX is the ability to override default HTML elements with React components. For instance, you can globally replace <img> with Next.js’s native <Image> component to get automatic optimization, lazy loading, and more.

Create a file called mdx-components.js (or .tsx) in your components/ folder:

```markdow/mdx file

---

title: 'Introduction'

date: '2025-01-24'

---

# Introduction to Our Docs

Welcome! This is a test **MDX** file for verifying our Cloudinary image embedding.

Below is an example image, served directly from Cloudinary:

![A Cloudinary Example](https://res.cloudinary.com/<cloud_name>/image/upload/v1670000000/sample.jpg)

You can also embed inlined images or dynamic transformations using query parameters, for example:

![A Transformed Example](https://res.cloudinary.com/<cloud_name>/image/upload/w_500,h_300,c_fill/v1670000000/sample.jpg)

```Code language: PHP (php)

In your root layout or a dedicated provider file, import this useMDXComponents function to let Next.js know how to interpret elements in your MDX:

```
// app/layout.js (or wherever you manage MDX context)

import './globals.css'; // example CSS import

import { useMDXComponents } from '@/components/mdx-components';

import { MDXProvider } from '@mdx-js/react';

export default function RootLayout({ children }) {

  const mdxComponents = useMDXComponents({});

  return (

    <html lang="en">

      <body>

        <MDXProvider components={mdxComponents}>

          {children}

        </MDXProvider>

      </body>

    </html>

  );

}

```Code language: PHP (php)

Now, any <img> tags you place in .mdx files become Next.js <Image> components automatically, allowing you to serve Cloudinary images with built-in optimizations.

With the configurations in place, run this command in your terminal:

npm run dev

Visit one of your MDX-defined routes, such as /docs/introduction/.

Observe how the page loads your Cloudinary-hosted images. If you open your browser’s network tab, you’ll see that images load from res.cloudinary.com, potentially with additional query parameters if you set transformations.

If you want to see how everything runs in production mode:

```

npm run build

npm run start

```Code language: JavaScript (javascript)

Next.js will compile your MDX files, ensuring the pages function the same, but with production optimizations. Cloudinary images will be optimized, sized, and delivered in next-gen formats when possible (e.g., WebP) to browsers that support them.

One of Cloudinary’s biggest draws is the ability to transform images on the fly. For instance, specifying w_300,h_200,c_fill (width=300, height=200, crop=fill) will automatically resize and crop. If you want to use these transformations in your MDX images, simply append them to the URL:

`![Transformed Example](<a href="https://res.cloudinary.com/">https://res.cloudinary.com/</a><cloud_name>/image/upload/w_300,h_200,c_fill/v1670000000/sample.jpg)`Code language: HTML, XML (xml)

Or if you want advanced usage, you can build a custom server-side route in Next.js to sign transformation requests using your Cloudinary API credentials. However, for many docs or blog scenarios, simply building the transformations into the URL is sufficient.

  • Custom route for images. If you need more fine-tuned control over transformations, you could create an API endpoint (e.g., app/api/images/route.js) to fetch images from Cloudinary with secret transformations or dynamic logic based on user input.
  • Cloudinary plugins. The Cloudinary npm package allows you to manage resources, handle upload widgets, or serve advanced transformations in React. You can incorporate these into your Next.js server or client components as needed.
  • MDX metadata. Notice that at the top of your .mdx file, you can define metadata. Next.js also provides a metadata export that can help define page titles, meta tags, or open graph tags. Explore how you might unify MDX front matter with Next.js metadata for consistent SEO across your documentation site.

Integrating Cloudinary with MDX in Next.js offers a powerful setup for developer-focused sites. You benefit from:

  1. Streamlined content creation. MDX lets you write rich, Markdown-style documentation with embedded React components and easy collaboration.
  2. Optimized image delivery. Cloudinary automatically serves the best formats and sizes, speeding up load times and improving UX.
  3. Scalable file structure. The Next.js App Router organizes your docs or blog routes so that each .mdx file becomes a page. You don’t need a complicated custom routing system.

When combining these tools, you have a flexible environment that can handle both text-based content and high-quality, performant images — perfect for hosting everything from code snippets and design assets to large blog images or developer guides. By overriding default <img> elements with Next.js <Image>, you ensure that every Cloudinary-served image can be transformed or optimized on demand.

Sign up for a free Cloudinary account today.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free