Dynamic Routing in Next.js


How I finally made sense of Next.js' getStaticProps and getStaticPaths functions to statically generate pages for a blog

Click here for a quick summary.

Background

I recently started buidling a blog for my partner. Based on the features we've decided to include, I've identified this project as a great opportunity to use Next.js for the first time. Even though the the blog has quite a bit of progress to go before it's finished, it's already been a good learning experience.

What's Next.js?

Next.js is a React application framework (or distro, if you prefer) which was released in 2016 and sits nicely in the Jamstack world. It provides abstractions for building websites with React* that can use pre-rendered with SSG, SSR, or a mix of the two. Like React itself, Next.js is unopinionated about styling or state management.

The momentum behind Next.js on a community-building level is super impressive. For instance, Vercel, the company which owns and primarily develops Next.js, has worked with the Google Chrome team on image optimization and has hired the author of webpack. Suffice to say, Next.js is a tool worth keeping an eye out for—and in my opinion, one worth learning.

I’ve been following developers who make/use Next.js on Twitter for about a year. It’s very cool to see that the framework can create sophisticated sites while maintaining an excellent developer experience with features like fast refresh and automatic code splitting. In addition to its production use in sites for big names including GitHub, Nike, and Square Enix; here are some Next.js sites that stand out to me:

Each Next.js page is a React component which lives in its own file in the project's pages directory. For example, an about page might look like this:

// pages/about.js

export default function About() {
return (
<main>
<h1>About me</h1>
<p>The inner machinations of my mind are an enigma.</p>
</main>
);
}

This concept of pages as components allows you to create new pages with little effort while leveraging React's flexibility and scalability. It also allows you to automatically generate pages with dynamic routes.

What's dynamic routing?

Dynamic routing refers to generating routes (URLs) to serve individual pages based on data which is subject to change.

Let's say we're making a blog using any old static site generator. The following data for our blog posts lives on a CMS which is queried by our static site generator at build time:

[
{
Id: 0,
Slug: "my-first-post",
Title: "My First Post",
Text: "People talk loud when they wanna act smart, right?"
},
{
Id: 1,
Slug: "another-post",
Title: "Another Post",
Text: "It's not just a boulder... It's a rock!"
}
]

After our blog is built, we want to be able to access pages for each of these posts based on their slugs, i.e., https://our.blog/post/my-first-post and https://our.blog/post/another-post. We also want to do this without having to manually write a file like post/my-first-post/index.html—a process which would get very tedious with dozens or hundreds of posts. This is where static site generators shine!

Let's say we add a new post:

[
...
{
Id: 2,
Slug: "the-newest-post",
Title: "The Newest Post",
Text: "Can't have dirty garbage!"
}
]

With dynamic routing, we want to automatically create a new page, https://our.blog/post/the-newest-post, when this post is added.

How does Next.js handle dynamic routing?

In Next.js, generating blog post pages based on JSON data is done by creating a page with a special filename, and implementing two functions: getStaticProps and getStaticPaths. These functions are the secret sauce behind how Next.js statically generates a bunch of pages.

I first tried to learn about them by reading Next.js' docs. I didn't really understand what I read, because I didn't have any live code samples to examine and tinker with.* I thought, maybe I can just make and break stuff until I see how these functions work together.

Let's make a basic blog

I got started on this project by following along with Chris Sev's tutorial on how to make a blog with Strapi and Next.js. Over the course of an hour, Chris demonstrates how to set up a database and API endpoints to store blog posts with the Strapi CMS, and how to retrieve and render those blog posts with Next.js.

For this toy example, let's make the following assumptions:

  1. The body of each blog post is just a single paragraph. It's fairly straightforward to store post content as Markdown and transform the Markdown into HTML, but that's outside of the scope of this blog post.
  2. I'm modeling the example API endpoints/responses off of the ones provided by Strapi.
  3. Posts are served at the post/ subdirectory.

After following along with Chris' tutorial, I had a homepage plus a page for each post, like this:

Let's see how those blog post pages are implemented.*

Blog post page implementation

As I stated earlier, dynamically rendering blog posts is done by implementing the getStaticProps and getStaticPaths functions in a page file with a special name. If you'll bear with me, I'll work backwards and wait to give definitions for these functions until we see how they interact with each other.

This is more or less what the code for my blog post page looked like after completing that tutorial:

// post/[slug].js

export default function Post({ post }) {
return (
<article>
<h1>{post.Title}</h1>
<p>{post.Text}</p>
</article>
);
}

export async function getStaticPaths() {
const res = await fetch(`${process.env.BLOG_API_ENDPOINT}/posts`);
const posts = await res.json();

const paths = posts.map((post) => ({
params: { slug: post.Slug },
}));

return {
paths,
fallback: false,
};
}

export async function getStaticProps({ params }) {
const { slug } = params;

const res = await fetch(`${process.env.BLOG_API_ENDPOINT}/posts?Slug=${slug}`);
const data = await res.json();
const post = data[0];

return {
props: { post },
};
}

There's a lot going on here, so let's break it down:

Page component definition

export default function Post({ post }) {
return (
<article>
<h1>{post.Title}</h1>
<p>{post.Text}</p>
</article>
);
}

Like the About component above, Post is a function which returns a page. The text content of this page is determined by the post property of the object that Post receives as a prop.

Where does that prop come from?

getStaticProps

export async function getStaticProps({ params }) {
const { slug } = params;

const res = await fetch(`${process.env.BLOG_API_ENDPOINT}/posts?Slug=${slug}`);
const data = await res.json();
const post = data[0];

return {
props: { post },
};
}

getStaticProps returns an object with a props key, whose value becomes the prop object for the Post component.

In this case, getStaticProps makes an async API request to grab a post with a particular slug. The slug we want is determined by the params property of the object that getStaticProps receives as an argument.

Likewise, that argument object containing params has to come from somewhere too.

getStaticPaths

export async function getStaticPaths() {
const res = await fetch(`${process.env.BLOG_API_ENDPOINT}/posts`);
const posts = await res.json();

const paths = posts.map((post) => ({
params: { slug: post.Slug },
}));

return {
paths,
fallback: false,
};
}

That somewhere is getStaticPaths, which returns an object with two properties:

  1. paths - An array of objects which each have a params key
  2. fallback - A boolean that indicates whether a fallback page has been defined. Since it's set to false here, trying to navigate to any page that's not included in paths (for example, https://our.blog/post/blargen-fezibble-nohip) will result in a 404 response.

Note that in this example, the shape and size of paths correspond to the list of all blog posts that our API gives us.

Filename

// post/[slug].js

Last but not least, it's crucial that:

  1. The location of this file corresponds to what we want the page's URL to look like, i.e., it's in the correct directory in our project. Since we want to serve posts from the post/ subdirectory of the built site, this file must be placed in a project folder that's also called post/.
  2. The file's name is the page's dynamic route parameter (in this case, slug, which is defined in each object in the paths array of getStaticPaths), wrapped in square brackets.

Adding categories

Now we're getting to why I decided to write this blog post.

I encountered a fun learning opportunity in the blog I'm making because it's really going to be several blogs in one, separated by category. These categories are kind of like tags commonly seen in blogs, except that each post belongs to exactly one category.

In Strapi, this is done by creating a new collection for categories, then setting up a one-to-many mapping between categories and posts.

With our three blog posts, let's say one category contains two posts, and the other category contains the remaining post. So this is what the category collection looks like:

[
{
Id: "a",
Slug: "alpha",
Name: "Alpha",
posts: [
{
Id: 0,
...
},
{
Id: 1,
...
},
]
},
{
Id: "g",
Slug: "gold",
Name: "Gold",
posts: [
{
Id: 2,
...
},
]
}
]

Now, when we fetch blog posts, the data we receive looks like this:

[
{
Id: 0,
Slug: "my-first-post",
Title: "My First Post",
Text: "People talk loud when they wanna act smart, right?"
category: {
Id: "a",
...
}
},
{
Id: 1,
Slug: "another-post",
Title: "Another Post",
Text: "It's not just a boulder... It's a rock!",
category: {
Id: "a",
...
}
},
{
Id: 2,
Slug: "the-newest-post",
Title: "The Newest Post",
Text: "Can't have dirty garbage!",
category: {
Id: "g",
...
}
}
]

Category page implementation

This is where the docs and tutorials ended. Seeing as there's no page in the Next.js docs about adding category pages for blog posts, I winged creating a new file to dynamically generate category pages, by duplicating and modifying post/[slug].js.

Writing the new page for categories wasn't too hard, but naming it was. Initially I had posts served from the root directory, like https://our.blog/my-first-post. I wanted to serve categories from the root directory instead, like https://our.blog/gold. This caused a conflict, since the slug property for both posts and categories is called slug—you can't have two files in the same directory named [slug].js! This is why I moved posts to the post/ subdirectory. Plus I figured that having /post/ in a post's URL would make that URL easier to read when shared.

Also, Next.js will tell you if the statically generated page's filename isn't right. If we change the filename to [wumbo].js, we get this error in the browser:

Server Error
Error: A required parameter (wumbo) was not provided as a string in getStaticPaths for /[wumbo]

The bracketed filename determines what property Next.js looks for when statically generating pages. That is, this file's pages will fail to build if the objects in the paths array returned by getStaticPaths do not contain the bracketed word.

Next.js' error messages made these errors clear and quick to diagnose. After working around them, I was pleasantly surprised to arrive at this implementation of category pages in what seemed like no time at all:

Seeing that this thing I hacked together just worked put a huge grin on my face. It was like magic.

Summary

After writing the above implementation, I ended up with this understanding of Next.js' SSG functions:

What does getStaticProps do?

getStaticProps defines the props that a statically generated page will receive.

What does getStaticPaths do?

getStaticPaths defines which and how many pages will be statically generated at build time.

I was delighted to reread Next.js' docs at this point and find that what they said about getStaticProps and getStaticPaths actually made sense to me, because it matched my own newly acquired comprehension.

The big picture

This is a rather specific example of dynamic routing. And React-based static site generation is arguably overkill for a simple blog. But I think I can use lessons from this to apply to other situations. Maybe you can too!

At first, I didn't quite get how Next.js could fit into ecommerce sites. After figuring out this whole category pages thing, it clicked and I thought, of course Next.js can do ecommerce. Easily. For example, you can can define a route, product/[id].js, to generate a page for every single product in your catalog database. It's amazing to me that process can be orchestrated in JavaScript.

So Next.js’ secret sauce is not dynamic routing itself, but great API design. I've heard that one of Next.js' best features is how its conventions allow developers who are unfamiliar with it to quickly become productive with it.

Also, this was a learning moment that made me feel knowledgeable and capable about JavaScript and React. As an early-career developer with plenty of impostor syndrome, that's kind of a big deal. It reminded me of why I enjoy programming in the first place!