How to add dynamic routes to Next.js blog with Sanity.io content

How to add dynamic routes to Next.js blog with Sanity.io content

Summary: ↬ In this post, we look into how to fetch data from Sanity headless CMS and render dynamic pages using Next.js for a blog-style site.

Mar 19, 2021

8 min read

---


In the last post, I wrote a short guide on how to make a blog with Next.js and Sanity.io. We covered almost all the basics needed to build a blog with Next.js and connect it to the headless CMS Sanity.io. The last piece needed to complete the puzzle was, how to add dynamic pages using the content we fetch from Sanity.

Adding dynamic pages is easier than you may think. So, without further to do, let's get to work.

Table of Contents

Create a template for your routes

The first thing we need for our routes is a template. This template will be used to render all the pages that we need to look the same way. For example, let's say we are creating a blog, probably all our blog posts will have the same look (a header, a body content and a footer is a basic example). So, for our blog posts, we will create a template. Of course, the same example applies to any other content, a single product page for e-commerce, a podcast page, and so on.

To create the template, in your project folder navigate to your pages folder and create a file with the name [slug].js. Important, the name doesn't matter, what matters is that you put it inside square brackets, this is what will tell Next this is a template. Another common name you will see is to call the file [id].js. Basically, we name the file after something that will be unique for each page. Again, the name is not a rule, just a convention.

Note: the place where you put the template file will determine what the final route will look like. If you put your template inside a folder called blog in your pages folder, then the final route will be /blog/id. This is standard Next.js behavior and its file-based routing system.

Now, inside your template file is where you will create the model of how your page will look like. It will look something like this.

[slug].js
/*
  Your imports here
*/

import sanityClient from "@sanity/client"

// Sanity Client config for data fetch
export default sanityClient({
  projectId: "<YOUR_PROJECT_ID",
  dataset: "YOUR_PROJECT_DATASET",
  useCdn: true, // false if you want to ensure fresh data
})

const Post = ({ data }) => {
  return (
    // Your JSX here
  )
};
export default Post;

Remember that your template is like any other regular Next.js page, so you can use components here too. You can read more about dynamic routes and static generation here. Also, in this component, we are going to need our sanity client config. You can either create it here or in a separate file and then import it.

Fetching data from Sanity Studio

Once your template is ready or at least has the basics (you can create it using dummy data for the moment), we need to fetch the data from Sanity.

To fetch our data, we will make use of two Next.js functions. You can read more about Next.js data fetching here.

  • To fetch the content: getStaticProps
  • To create dynamic pages: getStaticPaths

Generate dynamic routes: getStaticPaths

First, we need to let Next know how many routes we want to render. For that, we use the getStaticPaths function to fetch something that will be unique for each newly created page. You could fetch for the id of each post but, that would not be very helpful from the User Experience point of view. So, with that in mind, we are going to fetch our slug.

[slug].js
export async function getStaticPaths() {
  const response = await sanityClient.fetch(postSlugsQuery)
  const paths = response.map((slug) => ({
    params: {slug}
  }))
  return {
    paths: paths,
    fallback: false,
  }
}

As you can see, the getStaticPaths is also asynchronous. Once it fetched the data we receive a response that we have to map into an array that contains all the slugs for each of our posts. Inside the array objects with the key name of params and a value for the slug. This response has to be returned as a prop called paths. Also for the moment, we add the fallback option as false, with this, any route or path that wasn't returned by getStaticPaths will result in a 404 page.

The postSlugsQuery prop is just a variable I created to manage my queries from outside this file in order to make the function a bit cleaner. In any case, here you should put your specific query. Also, note that I'm using GROQ, Sanity in house query language.

Your query should look something like this.

query.js
export const postSlugsQuery = `
  *[_type == "post" && defined(slug.current)][].slug.current
`;

This will fetch all the slugs inside the content of type post. This query will return all the slugs that are in your Sanity Studio and Next.Js will take care of generating one .html file for each slug.

Get static content: getStaticProps

Now that we have our dynamic paths, we need to fetch the data for each one of them. For this, we use the getStaticProps function from Next. We will have something like this.

[slug].js
...
export async function getStaticProps({ params }) {
  const allPosts = await sanityClient.fetch(
    postQuery,
    {
      slug: params.slug
    }
  )
  return {
    props: {
      data: {
        allPosts,
      },
    },
  }
}

Our function automatically takes on a special prop called context from which we can destructure using ES6 the params generated from the previews getStaticPaths. You can read more about context here.

Again we pass our sanity fetch function, this time using another parameter. First, a postQuery, note here that I removed the query from the function again just to make it look cleaner. Here let's say I'm looking to get the \_id, title, slug, image, published date, and body content from Sanity studio.

[slug].js
export const postQuery = `
  *[_type == "post" && slug.current == $slug] [0] | order(date desc, _createdAt desc) {
    _id,
    title,
    slug,
    mainImage {
      asset -> {
        _id,
        url
      }
    },
    publishedAt,
    body,
  }`;

The second parameter is the id for our single post, in this case, we use the slug. This will tell Next.js which specific content should be fetched for each .html page generated. The rest of the queries are just examples of what you can query for. Our function then returns data that can be passed as props to our [slug].js component.

Pass the fetched data to our template

Now that we have our data, we can pass it as a prop to our template component (see the template code example on step 1.

Note that in this example, I'm passing the response as a data prop just because I named it the same way inside the getStaticProps. If you name it differently you should use your variable name.

With your fetched data, you can finish your template.

If you have other parts of your site that require dynamic routes, you can follow the same steps here to create another template.

Incremental Static Regeneration

The last thing left is to deploy our site once it's ready. Of course, now we can create content from our sanity studio. The problem here is, because it is a Static Site, our studio updates won't show unless we rebuild our Next project which is of course, not ideal.

Luckily, Next got us cover. We can use Incremental Static Regeneration (ISR) which basically will rebuild our site after a certain amount of time and fetch new data if there is any. This process will be automatic and will guarantee to always have our site up to date.

To activate ISR, all we have to do is, on our template go to the getStaticProps function and add the revalidate option and the end of the return.

[slug].js
...
export async function getStaticProps({ params }) {
  const allPosts = await sanityClient.fetch(
    postQuery,
    {
      slug: params.slug
    }
  )
  return {
    props: {
      data: {
        allPosts,
      },
    },
    revalidate: 600,
  }
}

In this example, I'm telling Next.js to revalidate our data at most, every 600 seconds (10 minutes). The amount of time is up to you, it will depend on how up-to-date your content needs to be.

The other piece of the puzzle is, in our getStaticPaths change the fallback to true. Basically, this will make that our pages that are not generated at build time (meaning the new content) will not result in a 404 page.

Here we have a gotcha moment. If our page is not ready at build time, and we know it isn't because we have not created the content in the studio yet, then we will have a built error. Another possibility is that the page is requested and not built yet, this results in another error.

To avoid this we have to go into our template component and add something like this.

[slug].js
/*
  Your imports here
*/
import { useRouter } from "next/router"
import sanityClient from "@sanity/client"
// Sanity Client config for data fetch
export default sanityClient({
  projectId: "<YOUR_PROJECT_ID",
  dataset: "YOUR_PROJECT_DATASET",
  useCdn: true, // false if you want to ensure fresh data
})
const Post = ({ data }) => {
  const router = useRouter()
  if (router.isFallback) {
    return (
      <section>
        <h1>Loading...</h1>
      </section>
    )
  }
  return (
    // Your JSX here
  )
};

export default Post;

/*
  Your queries here
*/

This means that if our page is not ready yet or it's been built, then we will only render a Loading message. Of course, you can get more creative here and render something like a loading spinner or anything really. We're using the useRouter hook and the isFallback method provided by Next in order to know if our route is ready or not.

That's it!

That's it! everybody. Now we have all the basics we need to work with Next.js and Sanity.io together.