DEV Community

Cover image for How to Create Pages Dynamically in Gatsby Using MDX
Malik Gabroun
Malik Gabroun

Posted on • Updated on • Originally published at malikgabroun.com

How to Create Pages Dynamically in Gatsby Using MDX

Originally posted at malikgabroun.com

In this post, we will be looking into how to create pages programmatically using MDX in Gatsby.

To get up and running, we need to install a couple of plugins

npm i gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react
Enter fullscreen mode Exit fullscreen mode

Then we need to configure gatsby-mdx inside gatsby-config.js

plugins: [
  {
    resolve: 'gatsby-plugin-mdx',
    options: {
      defaultLayouts: {
        default: require.resolve('./src/components/Layout.js'),
      },
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

So first we need to resolve the plugin gatsby-plugin-mdx because we also want to pass in options object which defines what layout that we want to use in our MDX files.

Note: require.resolve give us the absolute path name.

As a result, any MDX files that we load will be loaded into the Layout.js template that we defined in the gatsby-config.

Now that we have installed the plugin, the plugin will look for mdx files in the pages or posts directory which we defined in gatsby-config.

So to get the post pages into gatsby, we are going to use another plugin gatsby-source-filesystem

npm i gatsby-source-filesystem
Enter fullscreen mode Exit fullscreen mode

to get them to the data layer so that we can access them.

The gatsby source file system is a way to use local files as part of the graphql data layer.

Once it gets installed, we need to update gatsby config to resolve source filesystem plugin

plugins: [
  {
    resolve: 'gatsby-plugin-mdx',
    options: {
      defaultLayouts: {
        default: require.resolve('./src/components/Layout.js'),
      },
    },
  },
  {
    resolve: 'gatsby-source-filesystem',
    options: {
      name: 'posts',
      path: `${__dirname}/content/posts`,
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

As a result, it will load anything that it finds in the path /content/posts as part of the data layer, and because we have gatsby MDX plugin installed it's going to look for MDX files and transform those into graphql nodes.

The whole reason for using MDX is because we want to add some sort of interactivity in the markup generated pages.

Now that we added configuration to look for files in the system and transform them to graphql nodes, we would need to generate those post files as pages programmatically using gatsby API createPages by configuring that in gatsby-node.js.

Gatsby in itself has a couple of available APIs that can be used to extend how gatsby works, inside of those you can export a function that is the same name as one of the hooks that gatsby looks for. As a result, gatsby will do those instructions at the build phase.

In this case, we want to create pages so we use exports.createPages and because we are going to load data we make the function async.

Gatsby will give us a couple of utility methods such as actions, graphql helper and reporter(which can be used in case you want to put something in the console, it's a gatsby internal kind of console.log)

exports.createPages = async ({ actions, graphql, reporter }) => {
  const result = await graphql(`
    query {
      allMdx {
        nodes {
          frontmatter {
            path
          }
        }
      }
    }
  `);

  if (result.errors) {
    reporter.panic('failed to create posts ', result.errors);
  }

  const pages = result.data.allMdx.nodes;

  pages.forEach(page => {
    actions.createPage({
      path: page.frontmatter.path,
      component: require.resolve('./src/templates/postTemplate.js'),
      context: {
        pathSlug: page.frontmatter.path,
      },
    });
  });
};
Enter fullscreen mode Exit fullscreen mode

In the createPage function we will use graphql helper to fetch the nodes from the data layer by passing a graphql query as you can see in the snippet above.

Then, we create the pages using actions.createPage as we loop through these pages that came back as an array to generate them programmatically as you can see in the screenshot below

allmdx-query

actions.createPage takes an options object as a parameter that has 3 properties: path, component and context.
Path is what we have defined in the mdx frontmatter.
Component takes in the path to the template you want to use for these pages. Below is a sample snippet used as page template.

import { graphql } from 'gatsby';
import { MDXRenderer } from 'gatsby-plugin-mdx';
import React from 'react';

import Layout from '../components/Layout';

export const query = graphql`
  query($pathSlug: String!) {
    mdx(frontmatter: { path: { eq: $pathSlug } }) {
      frontmatter {
        title
        path
      }
      body
    }
  }
`;

const Post = ({ data: { mdx: post } }) => {
  const { title } = post.frontmatter;
  const { body } = post;
  return (
    <div>
      <Layout>
        <h1>{title}</h1>
        <MDXRenderer>{body}</MDXRenderer>
      </Layout>
    </div>
  );
};

export default Post;
Enter fullscreen mode Exit fullscreen mode

Context takes in an object with pathSlug as its property which value is the page path.

Once we finish adding the above, now we can add interactivity to our MDX pages which would look like this

--------
path: '/blog/hello-world'
date: '2020/01/01'
title: 'Hello World'
summary: 'hello world post'
--------

import Counter from '../../../src/components/Counter';

Hello World

<Counter />
Enter fullscreen mode Exit fullscreen mode

mdxpage-example

Following the post and you can find a starter repo here that shows usage of mdx pages

Discussion (6)

Collapse
umbralimi profile image
Fraser Gorrie • Edited on

Doesn't your code create 2 versions of each MDX source page: the one that the plugin creates automatically using the filename-turned-slug and another with the frontmatter.path you use in your exports.createPages() function?

Collapse
malroun1 profile image
Malik Gabroun Author

which plugin you mean in this case that will create filename-turned-slug, gatsby-plugin-mdx? or gatsby-source-filesystem?
gatsby-plugin-mdx will let gatsby know that you are working with mdx files and gatsby-source-filesystem will load anything that it finds in the path defined in the option object (e.g./content/posts) as part of the data layer, and because we let gatsby know that we are going to work with MDX files it will look for any .mdx files and transform those into graphql nodes.

as for exports.createPages() its not creating new slug, it is rather fetching all pages as an array as you can see in the GraphQL playground screenshot

 const result = await graphql(`
    query {
      allMdx {
        nodes {
          frontmatter {
            path
          }
        }
      }
    }
  `);

then we creating the pages using createPage method as follow

  const pages = result.data.allMdx.nodes;

  pages.forEach(page => {
    actions.createPage({
      path: page.frontmatter.path,
      component: require.resolve('./src/templates/postTemplate.js'),
      context: {
        pathSlug: page.frontmatter.path,
      },
    });
  });
};

in the context object we mapping pathSlug to frontmatter.path as path keyword is a reserved word.
once we done this it will enable us in the template we using for the mdx file as defined in the createPage object to get the content of mdx file using the pathSlug argument we defined in gatsby-node as follow

export const query = graphql`
  query($pathSlug: String!) {
    mdx(frontmatter: { path: { eq: $pathSlug } }) {
      frontmatter {
        title
        path
      }
      body
    }
  }
`;

To conclude, neither does generate filename to slug, we using the frontmatter path defined in the mdx file. in case if you want to generate slug based on file path (e.g. /posts/post1) you can see the example written in the gatsby docs here

hopefully answer the questions

Collapse
umbralimi profile image
Fraser Gorrie

That was a very good explanation! Thank you!

I guess it's just me. I wrote a file to the /src/posts/ calledtest-me.md and my gatsby-config.js settings lead to the generation of the url /test-me so I assumed (naively, it seems) that gatsby-plugin-mdx did that by itself.

I had frontmatter in test-me.md that included the url I wanted to use, in this case path: "open-for-business". Then I wrote the createPages() function much as you have and when I checked, and clean-ed, both /test-me and /open-for-business had been created.

Anyway, that's just a bit of background on what I did and was trying to do. Your explanation will doubtless lead me to the point where I just have /open-for-business.

Again. Great explanation and thanks for responding so quickly!

Thread Thread
malroun1 profile image
Malik Gabroun Author

no worries.

perhaps, if you post your usage of mdx and sourcefile plugin in gatsby-config and createPage method in gatsby-node

also you can see my implementation in a starter repo here

Thread Thread
umbralimi profile image
Fraser Gorrie

I found the problem. I installed your starter from the article, confirmed that you only have 1 page created per mdx file and traced my issue to the inclusion of gatsby-plugin-page-creator that pointed at the same folder where I had the mdx files. Simple. I didn't understand the concept properly of the gatsby and mdx build process. You helped with that. Thanks

Thread Thread
malroun1 profile image
Malik Gabroun Author

great, glad I could help