A Beginner’s Guide to Routing in Next.js, with Examples

Kingsley Ubah
Share

There are many benefits of using Next.js, but one very useful feature is its file routing system. This architecture has significantly simplified the process of creating routes within a website. In this tutorial, we’ll learn how to set up a Next.js project and how the system of file routing in Next.js works.

We’ll also learn how to:

  • create static and dynamic pages
  • implement page transition with Link, as well as some of its props
  • use the useRouter() hook to obtain query param from the URL
  • nest routes dynamically

… and a lot more.

We’ll learn all of this by building a portfolio page.

Next.js Features

Next.js is a React-based web framework built on top on Node.js. Since it’s based on React, it also follows the component architectural design.

Next.js can be used to build static sites. These are sites with pages that get pre-rendered and served to the user at build time. In simple terms, the page is available even before the user requests it.

It also allows for the use of server-side rendering to generate dynamic web pages (pages that change every time a user makes a fresh request).

The architecture of routing in Next.js makes it very easy to create and link pages together. We can dynamically pass query parameters, and create dynamic pages without having to leave the pages folder.

Why use Next.js

The Next.js framework was first released on October 25, 2016. Since then, it’s gone on to become one of the most popular web frameworks, for a couple of reasons.

For one, Next.js is fundamentally React. This is great news for the huge community of developers who come from a React background. Developers can still make use of some React features like the component architecture, JSX and more.

Second is the ability to pre-render pages using Next. By default, Next.js generates all pages in advance, which will then be reused on every user request. Since the site is pre-generated, search engine crawlers can properly index the site for SEO.

As said, one very useful feature is the system of file routing in Next.js, which has significantly simplified the process of creating routes within a website. So you can basically create a bunch of .js files inside a single folder named pages, which Next.js will use for all your routes. Not only is it useful, but it’s very powerful as well.

The Project

The site will have two simple routes and two dynamic routes (we’ll get to the meaning of those later).

The Projects page will render a list of projects. Upon clicking on view more, we’ll be directed to a single project page.

The blog page will render a list of blog posts, and we can also view a single blog page by clicking Read more. The page will contain details about a specific post.

To demonstrate route nesting in Next, we’ll also create a nested /comments route for each blog post. For example, we can view the comments for the first post by visiting localhost:3000/blog/first-post/comments.

Here’s the live preview for the project:

You can get the code from its GitHub repository, run it on your machine and tweak it as you wish. You can delete my images and move yours to the /public folder. You only need to change the filenames in the markup.

Getting Started

To get started with Next, you need to have Node.js installed on your computer. The version of Node should not be lower than 12.x. You can check for the version by typing node -v on the command terminal.

If you don’t have Node.js installed, or have an older version, you can download the latest version from here.

After it’s downloaded, we’ll need to initialize our project. We can do this either automatically or manually. In this tutorial, we’ll be using the create-next-app code generator to automatically build a working framework for us.

Please navigate to the folder you want the project to live in and type in the following command:

cd your/path
npx create-next-app next-portfolio

Finally, run the following command:

npm run dev

You should see the following on your terminal window, if all went well.

Project all set

We can view the page on the web browser at http://localhost:3000.

Next project now live

The File-based Architecture of Routing in Next.js

When we ran that command, we created a folder named next-portfolio inside the current directory. Inside next-portfolio, you’ll find some important folders and files.

App directory structure

The folder we’ll be working in most frequently is pages. In Next, every .js file defined inside pages maps to a similarly named route:

  • pages/about.js will map to /about
  • pages/contact.js will map to /contact
  • pages/blog.js will map to /blog

Here’s a high-level representation of the pages folder inside a typical Next project:

my-site

└── pages

    └── api // API routes

    ├── _app.js // custom route (will **not** be used as a route)

    ├── index.js // index route (will be rendered at my-site.com)

    ├── about.js // predefined route (will be rendered at my-site.com/about)

    ├── contact.js // predefined route (will be rendered at my-site.com/contact)

    └── blog

        ├── index.js // index route for blog (will be rendered at my-site.com/blog) 

        ├── author.js // predefined route (will be rendered at my-site.com/blog/author)

        ├── [blog-id].js // handles dynamic route (will render dynamcially, based on the url parameter)

        └── [...slug].js // handles catch all route (will be rendered at all routes following my-site.com/blog)

Each React component will be bundled as a .js file, containing markup and logic for each page.

The public folder

Next.js provides a public folder where you can store static assets like images, custom scripts and fonts, and refer to them from your components/code.

We’ll be using the following images in various pages on our portfolio site:

  • A personal photo. This will be used on the home page (index.js).
  • Four social media icons. This will be used on the contact page (contact.js).

Custom pages

You may have noticed the page _app.js in your pages folder. This page is a custom page. Custom pages are not used as routes by Next.js, and they’re prefixed with an underscore (_).

Next.js uses the _app.js to initialize the web page. This component initializes the app and passes down the pageProps prop, which is the data needed by all nested components in our website.

Being the root component, we can define a layout that we want to persist across all pages.

We can also use a global stylesheet that applies to all elements, like in the following example:

//next-portfolio/pages/_app.js

import Layout from '../components/Layout'
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  ); 
}

export default MyApp

Index routes

Whenever we navigate to index routes (aka home pages) such as my-site.com, my-site.com/blog, or my-site.com/projects, Next.js will read any files from inside that directory named index.js.

So in essence, pages/index.js returns the markup for the home page, which is displayed at localhost:3000. pages/blog/index.js returns the markup for the blog home page, which is at localhost:3000/blog.

In your code editor, please go to the index page and delete all of the file content. The following markup is used for testing purpose:

// next-portfolio/pages/index.js

import Image from 'next/image'
import Link from 'next/link'

export default function Home() {
  return (
    <div className="container">
      <h1>Hello World</h1>
    </div>  
    )
}

Note: move into next-portfolio/styles in your text editor, and delete Home.module.css, as we won’t be needing it at all.

Now save the file changes and open http://localhost:3000. The changes in the index file will reflect on the index route.

Hello world

So much more will go into the index.js file. The top section of the home page will hold navigation links. However, it is more intuitive to build other pages before the home page, so we can link to them properly.

For that reason, we’ll need to first create some of the other pages before building our home page.

Static routes

First, we’ll be creating two static routes for our portfolio site. These routes render static data: they don’t use the query parameter from the URL for rendering data.

The two static routes we’ll be creating are about.js and contact.js. These files will be for the /about and /contact routes respectively.

To do so, navigate into next-portfolio/pages and create a new file named about.js. The markup for the About page will go inside it:

// next-portfolio/pages/About.js

export default function About() {
    return (
        <div className="container">
            <h1> About me </h1>
            <p> My name is Kingsley Ubah. I'm a 22-year-old web developer from Nigeria. I'm particularly interested in technical writing. When I'm not coding or writing, I read my favorite books and play some cool video games. I'm a huge fan of good movies and football. Also, don't play with my food!</p>
            <p>I'm skilled in front-end web development. I'm equally good at the back end. Technologies I work well with include React, Node.js, Vue, Next, Gatsby, OAuth, MongoDB, MySQL and many others. </p>
            <p>I could keep going on and on about my life but I can guarantee that you'll be bored in the end. So I'll just end it right here.</p>
        </div>
    )
}

Note: of course, you can customize the content to your own set of skills if you like!

Now please save the file changes, go over to next-portfolio/styles/globals.css and type in the following styles:

@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300&display=swap');

html,
body {
  padding: 0;
  margin: 0;
  font-family: "lato", sans-serif;
  font-size: 20px;
  background-color: #D7E5f0;  
}

* {
  box-sizing: border-box;
}

h1 {
  font-size: 60px;
}

.logo {
  font-weight: 600;
  font-size: 30px;
}

p {
  font-size: 20px;
  font-weight: 600;
  line-height: 1.2;
}

a {
  text-decoration: none;
  color: black;
}

.container {
  margin: 0 auto;
  max-width: 1200px;
}

Note: once again, go to town if you’d like a different set of styles.

Save the changes. On your web browser, please navigate to http://localhost:3000/about.

The current About page

Finally, for static pages, please create a contact.js file inside of pages and create the Contact component, like so:

// next-portfolio/pages/Contact.js

import Image from 'next/image'

export default function Contact() {
    return (
        <div className="container">
            <h1> Contact me </h1>
            <p> I'd love to hear from you. Want to reach out, you can contact me on the 
                following profiles</p>
            <ul className="contact">
                <div className="link">
                    <li>
                        <Image src='/facebook.png' height={20} width={20} /> 
                        <a href='https://facebook.com/UbahTheBuilder'> Like me on Facebook</a>
                      </li>
                </div>
                <div className="link">
                    <li>
                        <Image src='/twitter.png' height={20} width={20} /> 
                        <a href='https://twitter.com/UbahTheBuilder'> Follow me on Twitter</a>
                    </li>
                </div>
                <div className="link">
                    <li>
                        <Image src='/linkedin.png' height={20} width={20} /> 
                        <a href='https://linkedin.com/UbahTheBuilder'> Connect with me on LinkedIn</a>
                    </li>
                </div>
                <div className="link">
                    <li>
                        <Image src='/whatsapp.png' height={20} width={20} /> 
                        <a href='https://whatsapp.com/UbahTheBuilder'> Chat with me on Whatsapp</a>
                      </li>
                </div>
            </ul>

            <form>
                <input type="text" placeholder="your name" /> 
                <br />
                <input type="email" placeholder="your email address"  /> 
                <br />
                <input type="text" placeholder="subject" /> 
                <br />
                <textarea id="message" rows="15" cols="65" placeholder="your message"></textarea> 
                <br />
                <input type="submit" value="Reach out" />
            </form>
        </div>
    )
}

From this component, we return a page containing the social media links, as well as a contact form.

For the links, you’ll notice that we imported and used the Image component provided by next/image.

The Image component helps to create better optimized and responsive images which scale along with the browser window’s size.

To style it better, feel free to copy the following styles and paste them into the global stylesheet:

/* next-portfolio/styles/globals.css */

/* CONTACT PAGE */
.link {
  width: 500px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 5px 0;
  font-size: 17px;
}

input {
  height: 50px;
  width: 500px;
  margin: 10px 0;
  font-size: 17px;
  padding-left: 3px;
}

input[type=submit] {
  background-color: blue;
  color: white;
  border: none;
}

Please save file changes and navigate to http://localhost:3000/contact.

Our current Contact page

Client-side route transitions

Building pages is one process. A user also has to be able to navigate between those pages. With two of the four pages already implemented, let’s now finish up the home page. First, we go to the index file and modify the Home component, like so:

// pages/index.js`

import Image from 'next/image'
import Link from 'next/link'

export default function Home() {
  return (
    <div className="container">
      <div className="navbar">
        <div className="logo">Pragmatic Developer</div>
        <ul>
          <li>
            <Link href="/about">
              <a>About me</a>
            </Link>
          </li>
          <li>
            <Link href="/contact">
              <a>Contact me</a>
            </Link>
          </li>
          <li>
            <Link href="/blog">
              <a>Blog</a>
            </Link>
          </li>
          <li>
            <Link href="/projects">
              <a>Projects</a>
            </Link>
          </li>
        </ul>
      </div>
      <div className="profile">
        <Image src="/me.png" height={200} width={200} alt="My profile image" />        
          <div className="intro">
            <h1>Hi, I'm Kingsley</h1>
            <p>I am a web developer and technical writer</p>
        </div>
      </div>
    </div>
  )
}

If you’ve ever implemented client-side routing in of a React application, you might be familiar with React’s Link component from React Router.

Next.js also provides us with a similar component, which we imported from next/link.

The <Link> component is used to implement page transitions within a Next app. The biggest feature of this component is that it allows you to pass query parameters to the useRouter, which is what you use to render content on dynamic routes.

Inside of the JSX markup, we register the component and pass in valid href attributes specifying the pages we want to link to from the navigation menu.

The component can also take in a couple of properties, some of which are shown in the following sections.

as

Sometimes you might want to use a custom URL, probably to make the URL more readable and semantic.

For this, you can pass along the as property to Link, like so:

<ul>
    <li>
        <Link href="/about" as="/king">
          <a>About me</a>
        </Link>
    </li>
    <li>
        <Link href="/contact">
            <a>Contact me</a>
        </Link>
    </li>
    <li>
        <Link href="/blog">
          <a>Blog</a>
        </Link>
    </li>
    <li>
      <Link href="/projects">
          <a>Projects</a>
      </Link>
    </li>
</ul>

Image of as

preFetch

I did mention that Next.js as a framework allows us to pre-render pages. This property enables us to pre-fetch the resources needed to render the About page in the background:

<Link href="/about" prefetch>
  <a>About me</a>
</Link>

Now save the file. Feel free to use the following styles in your global stylesheet:

/* next-portfolio/styles/globals.css */

/* HOME PAGE */
.navbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.navbar ul {
  display: flex;
}

.profile {
  display: flex;
  max-width: 900px;
  margin: 180px auto;
}

li {
  list-style-type: none;
}

.navbar a {
  text-decoration: none;
  color: rgb(59, 58, 58);
  margin: 0 25px;
  transition: 0.2s;
}

.navbar a:hover {
  background-color: blue;
  color: white;
  padding: 8px 8px;
  border-radius: 6px;
}

.intro {
  margin: 0 90px;
}

.contact a {
  margin: 0 15px;
}

Save the styles to the file and navigate to http://localhost:3000 on your web browser.

The current home page

Upon clicking on Contact on the navigation menu, you’ll observe that we can now move from the home page to the Contact page.

Dynamic Routes

In Next.js, dynamic routes are special routes that render content dynamically, depending on the query id from the URL.

Dynamic routes are handled by special files, defined with the [param].js convention. param is obtained from the query object.

So instead of defining different files for different routes, as in:

  • blog/first-post.js for /blog/first-post
  • blog/second-post.js for /blog/second-post
  • blog/third-post.js for /blog/third-post

… we can define a single dynamic page to handle any dynamic route in /blog:

  • blog/[blog-id].js

Whenever any of the above URL is navigated to, such as in the following:

<li><Link href="/blog/1"><a>Visit my first post</a></Link></li>

// 1 is the blog-id which will get sent to the dynamic component 

… inside the dynamic component, we can access the query ID (that is, 1, 2 ,3, first-post, and so on) from the URL.

We do so by importing and calling the useRouter() hook. Then we deconstruct the param value from the router object and decide what to render based on that.

So if you navigate to blog/1 from a home page, the :id of 1 can be obtained like so:

import {useRouter} from 'next/router'

export default function Blog() {
    const router = useRouter();
    const {id} = router.query;

return (
        <div className="container">
            <h1> You are now reading article {id} </h1> // You are now reading article 1
        </div>
    )
    
 }

You can also use query strings instead of full URL paths:

<li><Link href="/blog?title=my-first-post"><a>Visit my first post</a></Link></li>

Note: typically, you’d query a database using the query ID and then retrieve a matching data record that will be displayed on the dynamic page. In this tutorial, I’ll be using mock JSON data to keep everything simpler.

Creating the Projects page

The first dynamic page will be for projects.

Inside pages, create a new folder named projects. Then in the new folder, create a file named index.js.

This file will return what gets displayed when we view http://localhost:3000/projects on a web browser. In other words, that will be the home page for /projects.

We also need some mock JSON data for projects. Inside pages, create a file named projects.json. Then create an array of your own projects, like so:

// next-portfolio/pages/projects.json

[
    {
        "id": 1,
        "cover": "https://uploads.sitepoint.com/wp-content/uploads/2021/10/1633599028SkilllzLanding.png",
        "title": "Skilllz",
        "slug": "projects/first-project",
        "excerpt": "A Sleek Purple Landing Page For an online training platform. Learn some important CSS concepts by building a landing page"
    },
    {
        "id": 2,
        "title": "Movie Generator App",
        "cover": "https://uploads.sitepoint.com/wp-content/uploads/2021/10/1633599458moviegenerator.png",
        "slug": "projects/second-project",
        "excerpt": "Learn how to build CRUD applications with React and HarperDB. This in depth tutorials covers a lot about API integartion"
    },
    {
        "id": 3,
        "title": "Hacker News Clone",
        "cover": "https://uploads.sitepoint.com/wp-content/uploads/2021/10/1633599423hackernewsclone.png",
        "slug":  "projects/third-project",
        "excerpt": "Have you always wanted to clone a web page? Build a Hacker News Clone using React and HarperDB. Get started with it now"
    }
]

The JSON contains the projects data that we want to display at http://localhost:3000/projects.

After that, we’ll bring this data into the markup, like so:

// next-portfolio/pages/projects/index.js

import Portfolios  from '../projects.json'
import Link from 'next/link'

export default function Projects() {
    return (
        <div className="container">
            <h1> My Projects </h1>
            <div className="projects">
                    {Portfolios.map(portfolio => {
                        return(
                            <div className="project" key={portfolio.id}>
                                <img src={portfolio.cover} alt="project image" />
                                <h2>{portfolio.title}</h2>
                                <p>{portfolio.excerpt}</p>
                                <Link href={portfolio.slug}><a>View More</a></Link>
                            </div>
                        )}
                    )}
            </div>
        </div>
    )
}

First thing we did was import the data. Then we mapped each project into the JSX template using the JavaScript map() function.

We also need to make it more presentable, so feel free to use the following styles:

// next-portfolio/styles/globals.css

/* PROJECTS */
.projects {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}

.project img {
  height: 100px;
  width: 200px;
}

.project a {
  color: white;
  background-color: black;
  padding: 10px 10px;
  border-radius: 6px;
}

.project {
  max-width: 500px;
  background-color: blue;
  border-radius: 6px;
  color: white;
  padding: 30px 30px;
  margin: 30px 0;
}

To view the page on the browser, navigate to http://localhost:3000/projects.

List of projects

Single project page

Now, we need to implement the dynamic route for displaying a single project. So if we navigate to http://localhost:3000/projects/1, the first project will be displayed.

Inside projects folder in pages, create a new file named [project].js.

This file will render the dynamic page a for single project, such as on projects/1, projects/2 and so on.

Inside the file, we define the template that will be used for a single project page, like so:

// next-portfolio/pages/projects/[project].js

import {useRouter} from 'next/router'

export default function Project() {
    const router = useRouter();
    const {project} = router.query;
    
        
    return (
        <div className="container">
            <div>
                   <h1>This is the {project}</h1> 
                   <p>Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
                   <p>Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
                   <p>Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
            </div>
        </div>
    )
}

Note: from the router object, we grab the query ID from the query object. Typically, you’d use that key to make an API query for a matching record. That way, you can also display an error message for cases where a matching project isn’t found on the database.

Since we don’t have an API to query for a project, we instead display the URL slug along with some static lorem ipsum text. The slug identifies what page is rendered.

The following image shows how the page can change based on the URL.

Single project page

Nesting Routes

Let’s consider a blog, for example. When the user navigates to my-site.com/blog, a list of blog posts is displayed.

When the user navigates to my-site/blog/first-post, the first blog post is displayed. And when they navigate to my-site/blog/first-post/comments, there will be all of the comments relating to the first post. This is called route nesting.

In Next.js, you can also nest dynamic routes. Each child route can access the query :id of the parent. That way, my-site.com/blog/first-post/comments will be different from, say, my-site.com/blog/second-post/comments, because you can check the post :id from the URL or query object, using useRouter().

In fact, we’ll be doing something similar with our blog page. Each blog post will have its own set of comments. In other words, we’ll be nesting a dynamic page named [comments].js inside another dynamic page, named [blog].js.

Creating the blog home page

Before getting into route nesting, we’ll create the blog home page first.

To do so, cd into next-portfolio/pages and create a folder named blog. Inside of the new folder, create a file named index.js.

This file will return what gets displayed at http://localhost:3000/blog. In other words, it’s the home page for that route.

Next, we create data for blog posts:

// next-portfolio/pages/posts.json

[
    {
        "id": 1,
        "cover": "https://uploads.sitepoint.com/wp-content/uploads/2021/10/1633515082detectcanva.png",
        "title": "How to detect the operating system in React and Render accordingly",
        "slug": "blog/first-post",
        "excerpt": "Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        "body": "Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
    },
    {
        "id": 2,
        "title": "Learn all about the JavaScript reduce method",
        "cover": "https://uploads.sitepoint.com/wp-content/uploads/2021/10/1633515150jsreduce.png",
        "slug": "blog/second-post",
        "excerpt": "Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        "body": "Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
    },
    {
        "id": 3,
        "title": "Understanding React props",
        "cover": "https://uploads.sitepoint.com/wp-content/uploads/2021/10/1633515109react-props-2.png",
        "slug":  "blog/third-post",
        "excerpt": "Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        "body": "Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem Ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
    }
]

The JSON array contains blog posts that we’ll render on our blog page. Normally such data should be obtained from an API, and not stored in a JSON object.

Next, import and use it in the markup, like so:

// next-portfolio/pages/blog/index.js

import Posts from '../posts.json'
import Link from 'next/link'

export default function Blogs() {
    return (
        <div className="container">
            <h1> Latest Posts </h1>
            <div className="posts">
                    {Posts.map(post => {
                        return(
                            <div className="post" key={post.id}>
                                <img src={post.cover} />
                                <h2>{post.title}</h2>
                                <p>{post.excerpt}</p>
                                <Link href={post.slug}>
                                  <a>Read Post</a>
                                </Link>
                            </div>
                        )}
                    )}
            </div>
        </div>
    )
}

To make the page look better, here are some styles:

// next-portfolio/styles/globals.css

/* BLOG PAGE */
.posts {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 50px;
  max-width: 1200px;
  margin: 0 auto;
}

.post-container {
  margin: 15px auto;
  max-width: 900px;
}

.post-container img {
  width: 100%;
}

.post img {
  height: 300px;
  width: 500px;
}

.posts a {
  background-color: black;
  color: #D7E5f0;
  padding: 10px 10px;
  cursor: pointer;
  margin: 30px 0;
  border-radius: 6px;
}

.post {
  background-color: white;
  margin: 30px 0;
  padding: 30px 30px;
  border-radius: 6px;
}

Now please navigate to http://localhost:3000/blog on your web browser.

Blog page

Displaying a single post and nested comments

In this section, we’ll be doing two things:

  • creating a page for a single blog post
  • creating a dynamic nested route for showing comments

To do so, go into pages/blog and create a new folder named [blog]. Inside the folder, create two files, [index].js and [comments].js

my-site

└── pages

    ├── index.js // index route (will be rendered at my-site.com)

    └── blog

        ├── index.js // list of blog post (my-site.com/blog) 

        └── [blog] 
                  
            ├── [index].js // (eg: my-site.com/blog/first-post)
               
            ├── [comments].js // (eg: my-site.com/blog/first-post/comments) 

Navigate to [index].js and type in the following code:

import {useRouter} from 'next/router'
import Link from 'next/link'
import Posts from '../../posts.json'

export default function Blog() {
    const router = useRouter();
    const {blog} = router.query;
    const fullPath = blog+"/comments";
        
    if (blog === "first-post") {
    return (
        <div className="post-container">
            <div>
                    <img src={Posts[0].cover} alt="post image" />    
                   <h1> {Posts[0].title}</h1>
                   <p>{Posts[0].body}</p>
                   <p>{Posts[0].body}</p>
                   <p>{Posts[0].body}</p>
                   <hr />
                   <div className="comments">
                        <h3>Comments</h3>
                        <h5>Marina Costa</h5>
                        <p>Absolutely spot on! Thanks for sharing, Kingsley!</p>
                        <Link href={fullPath}>
                          <a>Read all comments for this article</a>
                        </Link>
                   
                   </div>
            </div>
        </div>
    )
    } else if (blog === "second-post") {
        return (
        <div className="post-container">
            <div>
                    <img src={Posts[1].cover} alt="post image"/> 
                    <h1> {Posts[1].title}</h1>
                   <p>{Posts[1].body}</p>
                   <p>{Posts[1].body}</p>
                   <p>{Posts[1].body}</p>
                   <hr />
                   <div className="comments">
                        <h3>Comments</h3>
                        <p>Marina Costa</p>
                        <p>Absolutely spot on! Thanks for sharing, Kingsley!</p>
                        <Link href={fullPath}>
                          <a>Read all comments for this article</a>
                        </Link>
                   
                   </div>
            </div>
        </div>
        )
    } else {
        return (
        <div className="post-container">
            <div>
                    <img src={Posts[2].cover} alt="post image"/> 
                    
                   <h1> {Posts[2].title}</h1>
                   <p>{Posts[2].body}</p>
                   <p>{Posts[2].body}</p>
                   <p>{Posts[2].body}</p>
                   <hr />
                   <div className="comments">
                        <h3>Comments</h3>
                        <h5>Marina Costa</h5>
                        <p>Absolutely spot on! Thanks for sharing, Kingsley!</p>
                        <Link href={fullPath}>
                          <a>Read all comments for this article</a>
                        </Link>
                   
                   </div>
            </div>
        </div>
        )}
}

Please note that, in a real project, you won’t need an if conditional statement to render based on the post :id. That’s because you’d typically have all the posts stored in a database. Then, you’d query the API for only the post which matches the query ID.

Code for that would look similar to this:

import Link from 'next/link'

export default function Blog( {post} ) {
    
    return (
        <div className="post-container">
            <div>
                   <img src={posts.cover} alt="post image" />    
                   <h1> {post.title}</h1>
                   <p>{post.body}</p>
                   <hr />
                   <div className="comments">
                        <h3>Comments</h3>
                        <h5>{post.commenter}</h5>
                        <p>{post.featured_comment}</p>
                        <Link href={post.fullPath}>
                          <a>Read all comments for this article</a>
                        </Link>
                   </div>
            </div>
        </div>
    )}
}

export async const getStaticProps = ({ params }) => {
  const res = await fetch(`https://your-api.com/posts/${params.title}`);
  const post = await res.json();
    return {
      props: { post },
    };
}

Observe how we eliminated the need for useRouter(). This is because getStaticProps() automatically takes in the query ID from the param object, which is part of the context object. A post object that matches that title is then retrieved from the API and passed as props into the Blog component.

Now that we’ve established the correct way of fetching external data, it’s time to view what a single post page would look like: http://localhost:3000/blog/first-post.

First blog post

Nested route from comments

Do you still remember the [comments].js file which we created earlier? Next.js will treat this page as a nested page:

//next-portfolio/pages/blog/[blog]/[comments].js

import {useRouter} from 'next/router'

export default function Comments() {
    const router = useRouter();
    const {blog} = router.query;
    
    return (
        <div className="container">
            <div>
                    <h2> You are now reading the comments from the {blog} </h2>
                    <div className="comments">
                        <h3>Comments</h3>
                        <hr />
                        <h5>Marina Costa</h5>
                        <p>Absolutely spot on! Thanks for sharing, Kingsley!</p>
                        <hr />
                        <h5>Marina Costa</h5>
                        <p>Absolutely spot on! Thanks for sharing, Kingsley!</p>
                        <hr />
                        <h5>Marina Costa</h5>
                        <p>Absolutely spot on! Thanks for sharing, Kingsley!</p>
                        <hr />
                        <h5>Marina Costa</h5>
                        <p>Absolutely spot on! Thanks for sharing, Kingsley!</p>
                        <hr />
                        <h5>Marina Costa</h5>
                        <p>Absolutely spot on! Thanks for sharing, Kingsley!</p>
                        <hr />
                        <h5>Marina Costa</h5>
                        <p>Absolutely spot on! Thanks for sharing, Kingsley!</p>                    
                    </div>
            </div>
        </div>
    )
}

This is what you’d typically do in a real-life project:

export default function Comments( {comments} ) {
        
    return (
        <div className="container">
            <div>
                    <h2> You are now reading the comments from the {blog} </h2>
                    <div className="comments">
                        {comments.map(comment => {
                        return(
                            <div className="comment" key={comment.id}>
                                <h5>{comment.name}</h5>
                                <p>{comment.body}</p>
                                <hr />
                            </div>
                        )}
                    )}              
                    </div>
            </div>
        </div>
    )
}

export async const getStaticProps = ({ params }) => {
  const res = await fetch(`https://jsonplaceholder.typicode.com/blog-comments/${params.title}`);
  const comments = await res.json();
    return {
      props: { comments },
    };
}

Comments from first post

Wrapping Up

Page routing in Next.js is one of the most important concepts to know about in Next. It’s also the most powerful feature because you can structure your website however you want and pass data between routes by nesting them.

In this tutorial, we learned much about the implementation of page routing in Next.js by building a simple portfolio website. I hope you’ve found it useful. If you have any feedback, hit me up on Twitter.

FAQs about Routing in Next.js

How does routing work in Next.js?

Next.js provides automatic code splitting and an intuitive file-based routing system. Files inside the “pages” directory become individual routes, making it easy to create a new page by simply adding a new file.

Can I create pages with dynamic routes in Next.js?

Yes, Next.js supports dynamic routes using brackets [] in the filename. For example, [id].js would match any route with a dynamic segment, and you can access the value of the segment using the useRouter hook or getServerSideProps.

How does client-side routing differ from server-side routing in Next.js?

Next.js supports both client-side and server-side routing. Client-side routing is handled by the browser, while server-side routing involves fetching the page content from the server. Next.js provides APIs like getServerSideProps for server-side rendering (SSR) and getStaticProps for static site generation (SSG).

Can I nest routes in Next.js?

Next.js provides the Link component and the useRouter hook for client-side navigation. Additionally, for server-side navigation, you can use functions like Router.push or Router.replace.

Can I implement authentication with Next.js routing?

Next.js provides various options for implementing authentication, including protecting routes with conditional rendering, using server-side authentication methods, or integrating third-party authentication libraries.