Change the URLs of posts in Gatsby
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 insidegatsby-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 yourgatsby-node.js
to create the pages and maybe theonCreateNode
to add extra fields to your nodes—for example, a slug in theMdx
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 thecreatePages
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
:
module.exports = {
plugins: ["gatsby-plugin-mdx"],
};
Then, create an MDX file inside src/pages
that will be used as a regular page:
# 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:
---
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
:
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:
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
:
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:
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
):
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
- Reveal animations on scroll with react-spring
- Gatsby background image example
- Extremely fast loading with Gatsby and self-hosted fonts