Creating Dynamic Tag Pages With NextJS Nested Routes
Tags: nextjs, react, typescript
Published: 2021-09-08 19:42:02

---
title: Creating Dynamic Tag Pages With NextJS Nested Routes
published: true
date: 2021-09-05 00:00:00 UTC
tags: nextjs, react, typescript
description: How to automatically create individual tag pages for a NextJS blog
canonical_url: https://bionicjulia.com/blog/creating-dynamic-tag-page-nextjs-nested-routes
---

If you've seen my blog, you'll notice that I tag all my blog posts with relevant tags, like "react-native" or "typescript". Up to now, those tags never really served a purpose, apart from to communicate to readers, roughly speaking, what the blog post pertains to.

After completing and deploying a [search function for my blog](https://bionicjulia.com/blog/add-search-functionality-nextjs-markdown-blog-part-1), I finally decided to build out functionality for my blog post tags. In terms of user design, I figured something like this would serve as a good starting point:

- Each tag on a blog post should be clickable.
- Upon clicking the tag, the user should be able to see a list of other blog posts tagged with the same tag.
- The user should be able to navigate directly to a blog post from that view.

I initially toyed with the idea of having popup results, but decided that the simplest and most intuitive design for a user would be to implement tag pages that would be accessible via a URL. Each tag page would then list all the blog posts with that particular tag. One key requirement I wanted was for the generation of these tag pages to be dynamic - I already have quite a number of tags in use, but also foresee myself adding new tags in the future and wanted to make it easy to maintain going forward.

Here are the steps I took to implement this. The TL;DR is that this was achieved with NextJS's concept of dynamic and nested routes.

### Step 1: Define nested routes for your tag pages

In my NextJS blog, I wanted my tag page URLs to be located at something like `https://bionicjulia.com/blog/tags/react-native`. My blog is located at `https://bionicjulia.com/blog`. Routes in NextJS are defined in the `pages` folder, so to do this, create a folder called `tags` within `pages` > `blog`. Within the new `tags` folder, create a new file called `[tag].tsx`. The square brackets tells Next that this is a template, to be replaced with a dynamic name.

### Step 2: Create the template for the tag page

To define what the layout of each of the tag pages will look like, open the newly create `[tag].tsx` file and create a component for rendering the HTML you want (I called mine `BlogTag`). I wanted my layout to pretty much mirror that of my `/blog` page, so I just needed to reuse my `BlogPosts` component. (Please refer to my [previous blog post](https://bionicjulia.com/blog/setting-up-nextjs-markdown-blog-with-typescript) on setting up a markdown blog on NextJS if you're not familiar with how to set this up.)

Part of the setup involves defining `getStaticProps` (to get the individual props required for each tag, to pass into the `BlogTag` component) and `getStaticPaths` (since this is a dynamic route, we need to let NextJS know what the valid paths will be when it does its build).

```jsx
import BlogPosts from '@/components/BlogPosts'
import SiteContainer from '@/components/SiteContainer'
import { getAllPostsWithFrontMatter, getTags } from '@/lib/utils'
import { Params } from 'next/dist/next-server/server/router'
import React from 'react'
import { BlogTagProps } from 'types'

export default function BlogTag({ posts, title, description, tag }: BlogTagProps) {
  return (
    <SiteContainer title={title} description={description}>
      <div className="mb-6">
        <section>
          <h3>✍🏼 Blog posts tagged "{tag}"</h3>
          <BlogPosts posts={posts} />
        </section>
      </div>
    </SiteContainer>
  )
}

export async function getStaticProps({ params }: Params) {
  const posts = await getAllPostsWithFrontMatter('blog', params.tag)

  return {
    props: {
      posts,
      title: `Blog Posts - ${params.tag}`,
      description: `Posts on software engineering for tag ${params.tag}`,
      tag: params.tag,
    },
  }
}

export async function getStaticPaths() {
  const tags = await getTags('blog')

  const paths = tags.map((tag: string) => ({
    params: {
      tag,
    },
  }))

  return {
    paths,
    fallback: false,
  }
}
```

For `getStaticProps` , I have a utility method `getAllPostsWithFrontMatter` which I already use for listing all my posts at `/blog`. I amended this to allow for a `tag` string to be passed into the method, so that the blog posts returned from the method would be filtered to only include ones that have been relevantly tagged.

```jsx
export async function getAllPostsWithFrontMatter(dataType: string, filterByTag: string | null = null) {
  const files = fs.readdirSync(path.join(root, 'data', dataType))

  return files.reduce((allPosts, postSlug) => {
    const source = fs.readFileSync(path.join(root, 'data', dataType, postSlug), 'utf8')
    const { data } = matter(source)

    if (filterByTag) {
      if (data.tags.includes(filterByTag)) {
        return [
          {
            frontMatter: data,
            slug: postSlug.replace('.md', ''),
          },
          ...allPosts,
        ]
      } else {
        return allPosts
      }
    }

    return [
      {
        frontMatter: data,
        slug: postSlug.replace('.md', ''),
      },
      ...allPosts,
    ]
  }, [])
}
```

For `getStaticPaths`, I use another utility method called `getTags` which in turn calls `collateTags`. The `collateTags` method basically:

- Gets all the files in the relevant directory (`blog`).
- Maps through each file to "read" the contents and get the tags from the frontmatter.
- Create a `Set` and add all the tags from each post to the set (using a set means that only unique tags can be added to it).
- The return value is an array (which is created from the set).

Note that the reason I have an intermediate `getTags` method is to make it easier for when I expand this tags functionality to other parts of my website like `/books`.

```jsx
async function collateTags(dataType: string) {
  const files = fs.readdirSync(path.join(root, 'data', dataType))
  let allTags = new Set<string>() // to ensure only unique tags are added

  files.map((postSlug) => {
    const source = fs.readFileSync(path.join(root, 'data', dataType, postSlug), 'utf8')
    const { data } = matter(source)

    data.tags.forEach((tag: string) => allTags.add(tag))
  })

  return Array.from(allTags)
}

export async function getTags(dataType: string) {
  const tags: TagOptions = {
    blog: await collateTags('blog'),
		// books: await collateTags('books'),
  }
  return tags[dataType]
}
```

Aside: If you're interested in how I defined `TagOptions`:

```jsx
export type TagOptions = {
  [key: string]: string[],
}
```

### Step 3: Amend HTML to include links to tag pages

As mentioned in step 1, `BlogPosts` is the component I use to render the list of relevant blog posts. I amended it to map through a post's tags and render a `Link` to `/blog/tags/${tag}`. (Note that I use `CustomLink` which is a custom component I created, but for all intents and purposes, it's the NextJS `Link` component.)

```jsx
import { BlogPostsProps } from 'types'
import CustomLink from './CustomLink'

const BlogPosts = ({ posts }: BlogPostsProps) => {
  return (
    <div>
      // ... blah blah
      <ul className="pl-0">
        {posts &&
          posts.map((post) => {
            return (
              <article key={post.slug} className="my-6 post-title">
                // ... blah blah
                <p className="my-0">
                  {post.frontMatter.tags.map((tag) => (
                    <CustomLink key={tag} href={`/blog/tags/${tag}`}>
                      #{tag}{' '}
                    </CustomLink>
                  ))}
                </p>
              </article>
            )
          })}
      </ul>
    </div>
  )
}

export default BlogPosts
```

And that's pretty much it! I also show tags on each individual blog post's page, so on a similar note, I also amended the layout here to include `Links` to the tag pages.

Any comments or improvement points? I'd love to hear from you on [Twitter](https://twitter.com/bionicjulia) or [Instagram](https://instagram.com/bionicjulia).