DEV Community

Cover image for How to Generate Dynamic Routes with NextJS and Sanity.
Zachery Morgan
Zachery Morgan

Posted on

How to Generate Dynamic Routes with NextJS and Sanity.

Like in my last lesson, there will be a reference image of my file structure and file content so you can compare at the end.

Adding links to each post

Inside of your /pages/index.js file, we need to add links to each post that will take us to that specific post's page.

Start by adding NextJS's Link feature.



import Link from "next/link";


Enter fullscreen mode Exit fullscreen mode

Now inside of our postsElements variable, wrap the entire element inside of a Link tag.



const postsElements = posts.map((post, index) => (
    <Link key={index} href={`/posts/[slug]`} as={`/posts/${post.slug}`}>
      <div>
        <p>{new Date(post.publishedAt).toDateString().slice(4)}</p>
        <h3>{post.title}</h3>
      </div>
    </Link>
  ));


Enter fullscreen mode Exit fullscreen mode

A few things are happening here.

  1. We moved our key={index} from our div to our Link because it is now our outer most element.

  2. There is an href attribute which you should be familiar with. This tells the browser where to go to when the element is clicked. I have it taking us to a /posts/[slug] file that we will create soon.

  3. There is also an as attribute. This is a cosmetic attribute that turns our url into one that displays the slug value we added to each post. This will be used later to search for specific posts.

That's all the code needed to allow us to link to a post's page when clicked. Now it's time to add that page in our project.


Displaying a single post

It is time for us to create the file I just mentioned.
Add a folder named posts in the pages folder, then add a file named [slug].js to that new folder. Your pages folder should now look like this.

Pages folder

The reason we add the brackets around our file name is so that we can reference it later.

This is called a Dynamic Route and will be referenced to multiple times in this lesson so I advise you to check out the docs for it. NextJS Dynamic Routes

Inside of our /pages/posts/[slug].js/ file we just created, we are going to add our parts one at a time.

Post component



export default function Post() {
  return;
}


Enter fullscreen mode Exit fullscreen mode

Standard React component. We will add the content of this component in a little bit.

getStaticProps



export async function getStaticProps({ params }) {
  const query = `*[_type == "post" && slug.current == $slug] {
    _id,
    title,
    publishedAt,
    'slug': slug.current,
    body
  }`;

  const options = { slug: params.slug };

  const post = await client.fetch(query, options);

  return {
    props: { post: post[0] },
  };
}


Enter fullscreen mode Exit fullscreen mode

This is very similar to the getStaticProps we added in our /pages/index.js file in the last lesson, but with a few additions.

  • We add our { params } as an argument.

    • To explain this a little, getStaticProps has access to an argument called context. Within this context argument you have a few pieces of information to pull from. We use destructuring to pull out the params value.
    • Inside of this params object is the url associated with this file. This is why we added the brackets to [slug].js.


[_type == "post" && slug.current == $slug]


Enter fullscreen mode Exit fullscreen mode
  • Same query as before, except this time we remove our order option since we are only fetching one post, and instead we add && slug.current == $slug
    • This will fetch an entry that has a type of post AND has a slug.current (the url we provided in our post) that matches the current pages url (more on this in a bit).


const options = { slug: params.slug }


Enter fullscreen mode Exit fullscreen mode
  • We assign the current url to an object.
    • Above I mentioned how we are using params to get our current url and naming it our slug. This is where that comes into play.
    • The unique url for this post is inside of params.slug. We assign that value to the key slug within an options object variable.


const post = await client.fetch(query, options)


Enter fullscreen mode Exit fullscreen mode
  • Here we simply call our fetch like before, but now we add our options object we created.
    • This all works together because our options object has our url stored inside of it.
    • Our query matches our post's slug.current with this url we saved in options to fetch us the post for the page we are on.


return {
    props: { post: post[0] },
  };


Enter fullscreen mode Exit fullscreen mode
  • Finally we return our post. But since this fetched us an array, even though we only received 1 post, we make it easier on ourselves in our props by assigning the first (and only) object in our array to post so we can import it.

getStaticPaths

By far the hardest section to explain. Honestly I don't think I can give this function justice in text, so I'm going to give the basics and provide links that will explain what's happening in greater detail.

I mentioned Dynamic Routes earlier when we created [slug].js. From the NextJS getStaticProps Documentation it explains that...

If a page has Dynamic Routes and uses getStaticProps, it needs to define a list of paths to be statically generated.

Well, we have Dynamic Routes, and we used getStaticProps, so we must need this. The idea of this is NextJS will now pre-render every path that you specify inside of getStaticPaths.



export async function getStaticPaths() {
  const query = `*[_type == "post"]{ 'slug': slug.current }`;

  const posts = await client.fetch(query);

  const paths =
    posts?.map((post) => ({
      params: {
        slug: post.slug,
      },
    })) || [];


  return {
    paths,
    fallback: false,
  };
}


Enter fullscreen mode Exit fullscreen mode

To see examples similar to this one you can review NextJS API-reference for getStaticPaths.

You might notice this seems very familiar to our getStaticProps. We need to create our query that fetches every post but only returns the slug.current value (the url we gave it), then give that query to our client.fetch.

But what is that paths variable? It looks more complicated than it is.

  • First it checks if there are any posts in the first place, that's why we add the ? to posts. If there isn't then we return an empty array with || [].
    • Essentially meaning posts.map if there are posts, if there aren't then return [].
  • Now inside of our map. For each post we create...
    • { params: { slug: post.slug } }
    • This is fetching every single post's url and assigning it as a param for getStaticPaths to render at build time.
  • Finally we return our paths and a fallback: false
    • Having fallback as false just means that other routes will 404 if you go to one that we didn't render in our map.

Rendering the post

We are now going to use the data from the post we fetched inside of our Post component we created.



export default function Post({ post }) {
  return (
    <div>
      <h1>{post.title}</h1>
      <span>{new Date(post.publishedAt).toDateString()}</span>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

Similar code to what we used in our homepage to display each post, but this time we are including the day of the week in the date.

You might have noticed we aren't returning the body of our post, that is because this requires a little more effort to render (and to style) so I will be including that in the next short lesson.


Wrapping up

If you go to your homepage and click one of your posts it will take you to that post's url displaying the data of that specific post.

In the next lesson I will be showing you how to...

  • Style a webpage with Styled-Components
  • Style Rich Text components

We will also be including a back button to navigate back to our homepage from a post.


References

File Structure

Lesson 5 File Structure

pages/posts/[slug].js



import client from "../../lib/sanity";

export default function Post({ post }) {
  return (
    <div>
      <h1>{post.title}</h1>
      <span>{new Date(post.publishedAt).toDateString()}</span>
    </div>
  );
}

export async function getStaticProps({ params }) {
  const query = `*[_type == "post" && slug.current == $slug] {
    _id,
    title,
    publishedAt,
    'slug': slug.current,
    body
  }`;

  const options = { slug: params.slug };

  const post = await client.fetch(query, options);

  return {
    props: { post: post[0] },
  };
}

export async function getStaticPaths() {
  const query = `*[_type == "post"]{ 'slug': slug.current }`;

  const posts = await client.fetch(query);

  const paths =
    posts?.map((post) => ({
      params: {
        slug: post.slug,
      },
    })) || [];

  return {
    paths,
    fallback: false,
  };
}


Enter fullscreen mode Exit fullscreen mode

Top comments (0)