Change the URLs of posts in Gatsby

Last update: 12 May, 2020
Table of contents

If you want to create an MDX blog in Gatsby, you have to do 3 things:

  • Install 3 packages: gatsby-plugin-mdx, @mdx-js/mdx, and @mdx-js/react.
  • Add the gatsby-plugin-mdx to your plugins array inside gatsby-config.js.
  • Create some MDX files inside src/pages.

And now you have an MDX blog. If you create a src/pages/post-1.mdx file, Gatsby will create a page for that file and will be available at:

http://example.com/post-1/

But sometimes, you want your posts to be under a /posts/ URL. For example:

http://example.com/posts/post-1/

One way to do this is to create the MDX pages yourself with the createPages API. But creating the pages this way requires 3 extra steps:

  • You have to implement the createPages API in your gatsby-node.js to create the pages and maybe the onCreateNode to add extra fields to your nodes—for example, a slug in the Mdx GraphQL type.
  • You have to install and configure the gatsby-source-filesystem plugin to look for MDX files. This is important because otherwise, your MDX files won’t be available to GraphQL queries; and you want that for the first step (to create the pages with the createPages API).
  • Finally, you have to create a post template to use it as a layout for your post pages.

But that’s only one way to create custom URLs for your posts; let’s now see what alternatives you have.

See also Working with data in Gatsby.

Folder solution

If you want to change only the URL of your posts, you can place your MDX files inside folders. For example, if you want a bunch of notes under the /notes/ URL, create a notes folder, and place your notes inside that folder. Consider the following file structure:

src
|__ pages
|____ index.js
|____ page-2.js
|____ notes
|______ note-1.mdx
|______ note-2.mdx

This solution has some limitations though. For example, you can’t pass context data to your page or use a custom template for the posts. If you want to have those options, you can use the onCreatePage API.

onCreatePage API

You can use the createPages API to programmatically create pages. But in addition to the createPages, there is another Node API: the onCreatePage. This API is called, as you may have guessed, when a page is created. But an important detail here is that is not called for those created by the createPages.

So the plan is to use the onCreatePage to delete the created pages from src/pages and then create new pages with the updated path. You don’t want to delete all the pages, though, only the MDX pages that you’ll use as posts. To identify them, you can add a frontmatter field called type with a value of post at the top of the file. For example:

---
type: post
---

# Post 1

Content

onCreatePage example

To see this in action, create a new project with the hello-world starter:

mkdir change-post-url
cd change-post-url
gatsby new . https://github.com/gatsbyjs/gatsby-starter-hello-world.git

Install gatsby-plugin-mdx and the other dependencies:

yarn add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react

Configure the plugin in your gatsby-config.js:

gatsby-config.js
module.exports = {
  plugins: ["gatsby-plugin-mdx"],
};

Then, create an MDX file inside src/pages that will be used as a regular page:

src/pages/page-2.mdx
# Page 2

This is the second page in MDX.

import { Link } from "gatsby"

<Link to="/">Go back to homepage</Link>

And another MDX file that you’ll use as a post:

src/pages/post-1.mdx
---
type: post
---

# Post 1

This is my first post.

After you do all the above, you end up with 3 pages:

  • /
  • /page-2/
  • /post-1/

What’s left now is to change the URL of /post-1/ page to /posts/post-1/. Implement the onCreatePage API in your gatsby-node.js:

gatsby-node.js
exports.onCreatePage = ({
  page,
  actions: { createPage, deletePage },
}) => {
  const frontmatter = page.context.frontmatter;
  if (frontmatter && frontmatter.type === "post") {
    deletePage(page);
    createPage({
      ...page,
      path: `/posts${page.path}`,
    });
  }
};

If the page has a frontmatter object in the context, then we’re dealing with an MDX page. To check if it’s also a post, you look inside that frontmatter object for a type field with a value of "post". If all the previous are true, you delete the page, and create a new one with the updated path:

Notice that I change only the path in the createPage method. You can also pass some context or a template (via the component field) if you want. Check the createPage method for more details.

After implementing the onCreatePage API, you end up with the following pages:

  • /
  • /page-2/
  • /posts/post-1/

You can do that for any data you display on your site; it doesn’t have to be a post. If you’re building a hotel website, that data could be rooms or special offers.

Get the MDX files with GraphQL

You may want to create a page that lists all the posts. To do that, you want to get your MDX pages with a GraphQL query. gatsby-source-filesystem can help you with that. First, install the plugin:

yarn add gatsby-source-filesystem

And configure it in your gatsby-config.js to look for files in your src/pages:

gatsby-config.js
module.exports = {
  plugins: [
    "gatsby-plugin-mdx",
    {
      resolve: "gatsby-source-filesystem",
      options: {
        name: "mdx",
        path: `${__dirname}/src/pages`,
      },
    },
  ],
};

List all posts example

If you want to show only the posts (type: post), you can write a GraphQL query like the following:

`
AllPostsQuery {
  allMdx(filter: { frontmatter: { type: { eq: "post" } } }) {
    nodes {
      id
      timeToRead
      excerpt(pruneLength: 200)
    }
  }
}
`;

The following snippet shows how to display the posts in the index page:

src/pages/index.js
import React from "react";
import { graphql, Link } from "gatsby";



import slugify from "slugify";

export default ({
  
  data: {
    allMdx: { nodes: posts },
  },
}) =>
  console.log(posts) || (
    <main>
      <h1>Hello world!</h1>
      <p>In the next section, you can find all the posts.</p>
      <h2>Posts</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link
              to={`/posts/${slugify(post.headings[0].value, {
                lower: true,
              })}/`}
            >
              <h3>{post.headings[0].value}</h3>
            </Link>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </main>
  );

export const query = graphql`
  {
    allMdx(filter: { frontmatter: { type: { eq: "post" } } }) {
      nodes {
        id
        timeToRead
        excerpt(pruneLength: 200)
        headings(depth: h1) {
          value
          depth
        }
      }
    }
  }
`;

There are better ways to get the slug of the page; don’t consider what I did above a good practice. I use slugify to transform the <h1> that I get from GraphQL—assuming that you should only have one <h1> per page. A better way would be to add the slug yourself as a frontmatter field in the post. You can also do it programmatically on built time with onCreateNode, and make it available in your mdx GraphQL queries as a fields property—this is a common practice but adds more complexity.

Use the page creator plugin

Another thing you may want to do is to move your MDX posts from the src/pages directory to a posts directory. You can use the gatsby-plugin-page-creator to create pages from a different folder. First, install the plugin:

yarn add gatsby-plugin-page-creator

Then, configure it in your gatsby-config.js to create pages from the files in the posts folder (also change the folder for the gatsby-source-filesystem):

gatsby-config.js
module.exports = {
  plugins: [
    "gatsby-plugin-mdx",
    {
      resolve: "gatsby-source-filesystem",
      options: {
        name: "posts",
        path: `${__dirname}/posts`,
      },
    },
    {
      resolve: `gatsby-plugin-page-creator`,
      options: {
        path: `${__dirname}/posts`,
      },
    },
  ],
};

If you move the post-1.mdx file from src/pages to posts, everything should work as before.

Other things to read

Popular

Previous/Next