Eslam Hefnawy Serverless Architect at Serverless, Inc. Co-creator of the Serverless Framework and the lead architect of Serverless Components.

Implementing SSR in Next.js: Dynamic routing and prefetching

12 min read 3461

Implementing SSR In Next.js: Dynamic Routing And Prefetching

Editor’s note: This post was updated on 16 June 2022 to ensure all information is current and to add sections covering multiple segments and imperative dynamic routing with Next.js.

Server-side rendering (SSR) has become a widely adopted technique to enhance the performance and SEO of web applications. And while static site generation (SSG) is considered simpler and faster, there are cases where server-side rendering is your only option.

Implementing server-side rendering on specific pages could be a challenging task, however. Next.js attempts to solve this problem by allowing you to choose between SSG and SSR for each page of your application.

This post will explore these and other concepts that make Next.js a powerful React framework by building a blog with Agility CMS, a CMS built for speed.

The goal of this post is to ensure that you understand the two types of preloading techniques Next.js offers, can effectively utilize the many inbuilt features that come with Next.js, and can create and set up an Agility CMS instance.

What we’ll cover:

Next.js: The React framework with built-in SSR

Next.js is a React framework that addresses common pain points developers face when building web applications. From ensuring all your code is bundled and minimized using a bundler like webpack, to implementing optimizations like code splitting to improve page performance, Next.js has all the tools you need.

If you’re a TypeScript user, you’ll be pleased to hear that all you need to do is create a tsconfig.json file to get started!

The development experience is great because you get to choose what you want to build your application with. For example, Next.js allows you to use a CSS-in-JS library, but it also ships with support for Sass and CSS Modules.

But what makes Next.js truly great is the fact that it pre-renders pages by default. This means every page will use static site generation by default, one of the two rendering options that make up the hybrid architecture of Next.js.

However, SSG isn’t always an ideal option because the markup is generated at build time. If your page contains content that is fetched from an external source and changes frequently, then those changes will not be reflected on your page until you build the page again. This is where SSR comes in!

Next.js allows you to fetch the dynamic content and then generate the markup appropriately at every request instead. Next.js does this by providing you the option of declaring one of two asynchronous functions in your page-type components called getServerSideProps() or getStaticProps().



Setting up Agility CMS

Agility CMS is a content management system built for speed with the same level of scalability as most cloud architectures. Setting up Agility CMS is pretty easy, as they have starting points for most popular React frameworks — Next.js included.

Your first step is creating an Agility CMS account. Once you’re done, you will have to configure some project settings.

Start by creating a project. Choose the “Blog with Next.js” option. After inputting your project name, you will be taken to your dashboard.

By this point, you should have a CMS with content ready to go. You can explore the content and pages sections to edit the content to your preferences, but what you really should look out for are the API keys.

To get the API keys, click on Settings in the left-hand side navigation bar, then select API Keys. You can see this option highlighted in yellow in the image below:

How To Get Your API Keys From Settings On Agility CMS Dashboard

On this page, you will find two important values: your project’s Globally Unique Identifier (GUID) and your defaultlive API Key. Save these for later — you’re going to need them!

Setting up the development environment for your Next.js project

To get started, you need to pull together a Next.js application. If you’ve ever created a React application, then you must be aware of Create React App. Next.js has its own analog called create-next-app.

Run the following command via a terminal:

npx create-next-app my-blog

Once it’s done installing all the dependencies, change into the my-blog directory by running cd my-blog.

To allow you to focus on the main topic of this post, you will use a component framework called MUI (previously called Material-UI). If you fancy writing your own styles, however, remember that you can make use of Next’s built-in CSS Modules and Sass support.

Additionally, you must install the Agility CMS SDK for fetching content from their API. Run the following command to install the SDK and Material-UI:

npm install @mui/material @emotion/react @emotion/styled @agility/content-fetch

Once that’s done, create a .env.local file in your project’s root and add in the following environment variables, which will expose the required credentials to connect to the Agility CMS API:

AGILITY_CMS_GUID=xxx
AGILITY_CMS_API_FETCH_KEY=xxx

Replace the xxx placeholders with the values for the GUID and defaultlive API key you found earlier. Remember, you should be able to find them under Getting Started when you click the API Keys button.

With that, we’re all set to write some code!

Creating your Next.js routes

Before you attempt to create your first Next.js page, you must understand that Next.js has a file system-based router, such that any file under a directory named pages will become a route of your application.

To get started, then, you should create all of your routes. Under the pages directory, you will find an index.js file. This file will contain the page component that will be served at /, or the default/main route of your application.


More great articles from LogRocket:


Additionally, you need a route to serve all of your blog pages. So, create a directory called blog and a file named [...slug].js. You will get back to this concept in the next topic. Your project structure should look like this:

Next.js And Agility CMS Project Structure

Next up, you should initialize the Agility CMS client. To encourage reusability, you will make a separate file called agility.js under a new directory in the project root called lib.

// lib/agility.js
import agility from "@agility/content-fetch";

const api = agility.getApi({
  guid: process.env.AGILITY_CMS_GUID,
  apiKey: process.env.AGILITY_CMS_API_FETCH_KEY
});

export default api;

Make sure you have entered the credentials of your Agility CMS project in the .env.local file.

Creating your first Next.js page

Now let’s head over to the home/index page of your application and use getServerSideProps() to call the Agility CMS API to get the content for the homepage:

// pages/index.js
import api from "../lib/agility";

/* 
* IndexPage content collapsed for readability
*/

export async function getServerSideProps() {
  const page = await api.getPage({
    pageID: 2,
    languageCode: "en-us"
  });

  return {
    props: {
      meta: { title: page.title, desc: page.seo.metaDescription },
      data: page.zones.MainContentZone.reduce((acc, curr) => {
        acc[curr.module] = curr.item;
        return acc;
      }, {})
    }
  };
}

getPage() is an asynchronous method provided by the Agility CMS SDK to fetch entire pages in one go, as long as you provide a pageID. If your content is in multiple locales, then you must also pass in the correct languageCode.

The value for the pageID property can be found under the specific page’s settings. For example, to find the pageID for a page named home, you would navigate to Pages in the Agility CMS dashboard, then select the home page, then click Properties in the rightmost panel.

How To Get Your PageID From The Agility CMS Dashboard's Properties Tab

Once a response has been received from getPage(), the data can be transformed in such a way that your component can consume it conveniently. In the code block, for example, the data property will always contain all of the values from the MainContentZone array.

MainContentZone is an array of objects called modules, which have names. It is more convenient to access the content via these module names rather than having to use their respective indices (i.e., positions in the array).

With the data you now have, you can construct your page in a way that suits your design, like so:

import Head from "next/head";
import { useRouter } from "next/router";

import AppBar from '@mui/material/AppBar';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';

import api from "../lib/agility";

const HomePage = ({ meta, data }) => {
  const router = useRouter();
  const pageContent = data?.TextBlockWithImage?.fields;

  return (
    <>
      <Head>
        <title>{meta.title}</title>
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
        <meta name="description" content={meta.desc} />
      </Head>
      {
        pageContent && <Container maxWidth="lg" style={{ marginTop: "30px" }}>
          <Grid container spacing={3} alignItems="center">
            <Grid item xs={6}>
              <Typography variant="h2">
                {pageContent.title}
              </Typography>
              <Typography variant="h3">
                {pageContent.tagline}
              </Typography>
              <Typography variant="body1">{pageContent.content}</Typography>
              <Button
                variant="contained"
                color="primary"
                onClick={() => router.push(pageContent.primaryButton.href)}
                role="link"
              >
                {pageContent.primaryButton.text}
              </Button>
            </Grid>
            <Grid item xs={6}>
              <img
                src={pageContent.image.url}
                alt={pageContent.image.label}
                height="360px"
                width="100%"
              />
            </Grid>
          </Grid>
        </Container>
      }
    </>
  );
};

// getServerSideProps() has been collapsed for readability.

In the code above, you split the data you received into two properties: meta and data. The meta property consists of all the SEO-related data for the page, which you can set in the SEO category of your page in Agility CMS.

If you’ve added in meta tags in your application, then you may be familiar with React Helmet. Next.js has its own library for customizing the <head> tag: next/head. All of a page’s meta tags can be put inside the Head component that next/head provides.

Adding a global navigation bar to your Next.js App component

When using plain React, you become intimately familiar with the app.js file, which acts as the entry point to your entire component hierarchy. Next.js projects also come with such a file: _app.js. This file is pre-generated for you.

If you wanted to wrap the entire application in a component, perform some initialization operation, or include some global styles, then the _app.js file is the place to do it. This will be the custom App component for the application.

Let’s add in a navigation bar to your App component:

import '../styles/globals.css'

import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';

function MyApp({ Component, pageProps }) {
  return (
    <>
      <AppBar
        style={{ marginBottom: "50px" }}
        color="transparent"
        position="static"
      >
        <Toolbar>
          <Typography variant="h6">NextJS Blog</Typography>
        </Toolbar>
      </AppBar>
      <Component {...pageProps} />
    </>
  )
}

export default MyApp

<Component /> is the page component that will inevitably be loaded into the App component as a child. Ideally, the App component is where you would add a ThemeProvider or a Redux <Provider> for your application.

Here is what you should see once the page is reloaded:

Your Next.js Blog Frontend Populated With Agility CMS Content, Including Text And A Button On The Left And An Image On The Right

Understanding dynamic routing in Next.js

Next.js has a very powerful router that has been carefully built with a variety of use cases in mind. If you remember earlier, you created a file named [...slug].js. This file utilizes the dynamic routing features of Next.js, which address multiple use cases:

/page/[page-id].js will match with routes like /page/1 or /page/2, but not /page/1/2
/page/[...slug].js will match with routes like /page/1/2, but not /page/
/page/[[...slug]].js will match with routes like /page/1/2and /page/

Something to keep in mind is that a query parameter with the same name as a particular route parameter will be overwritten by the route parameter. For example, if you had a route /page/[id], then in the case of /page/123?id=456, the ID will be 123, not 456.

Now that you have an understanding of Next.js dynamic routes, let’s work on the blog route:

import ErrorPage from "next/error";

import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';

import api from "../../lib/agility";

const BlogPostPage = ({ page }) => {

  if (!page) {
    return <ErrorPage statusCode={404} />;
  }
  return (
    <Container maxWidth="lg">
      <Typography variant="h2">{page.fields.title}</Typography>
      <Typography variant="subtitle1">
        Written by: {page.fields.authorName} on{" "}
        {new Date(page.fields.date).toUTCString()}
      </Typography>
      <img
        style={{ maxWidth: "inherit", margin: "20px 0" }}
        src={page.fields.image.url}
        alt={page.fields.image.label}
      />
      <div dangerouslySetInnerHTML={{ __html: page.fields.content }} />
    </Container>
  );
};

export async function getServerSideProps(ctx) {
  const posts = await api.getContentList({
    referenceName: "posts",
    languageCode: "en-us"
  });

  const page = posts.items.find((post) => {
    return post.fields.slug === ctx.params.slug.join("/");
  });

  return {
    props: {
      page: page || null
    }
  };
}

export default BlogPostPage;

BlogPostPage follows all the same concepts and principles as HomePage except that here, you make use of the context object (abbreviated as ctx).

ctx contains a few properties that you can make use of, including the params property. params contains all the route parameters and their values.

Additionally, you are making use of a new method from the Agility CMS SDK called getContentList(). This method allows you to fetch a list of data by its reference name.

If you navigate to Content in the Agility CMS dashboard, then select Blog Posts, you will see a list of posts that have already been created, as shown below:

You Should Be Able To See Posts You Have Already Created Under The Content Tab In The Agility CMS Dashboard

With the data that you receive, you can use the array higher-order method to find a post that has the slug equivalent to the router parameter you declared as slug. If you find it, the page details are passed; otherwise, null is passed as a prop.

Next.js doesn’t allow you to pass undefined as a prop. If the page prop is falsy, then the BlogPostPage will render the inbuilt ErrorPage instead!

Handling a common use case for dynamic routing in Next.js

Quite often, you want to show a list of featured posts or some kind of content in a situation where the route parameter is not defined at all — i.e., /blog/.

You could use the optional catch-all syntax of dynamic routing ([[...slug]].js). However, you would then have to check whether the slug route parameter is undefined and conditionally render something instead.

A better approach would be to declare an index.js file under that route so that when the route parameter is not defined, it will point to this file instead. This happens because named, or static, routes have a higher priority than a dynamic route.

For example, if you have declared a dynamic route like /blog/[id].js, but also a static route like /blog/featured, then Next.js will favor the file that handles the static route over the one that handles the dynamic route.

Create a file called index.js under the blog directory:

import Link from "next/link";

import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';

import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';

import api from "../../lib/agility";

const BlogPage = ({ posts }) => {
  console.log(posts)

  return (
    <Container maxWidth="lg">
      <Typography variant="h2" gutterBottom>
        Featured Blog Posts
      </Typography>
      <Grid container spacing={2}>
        {posts && posts.map((post) => (
          <Grid item xs={4}key={post.slug}>
            <Card>
              <CardActionArea>
                <CardMedia
                  style={{ height: "240px" }}
                  image={post.image.url}
                  title={post.image.label}
                />
                <CardContent>
                  <Typography gutterBottom variant="h5" component="h2">
                    {post.title}
                  </Typography>
                  <Typography
                    variant="body2"
                    color="textSecondary"
                    component="p"
                  >
                    Written on{" "}
                    {new Date(post.date).toUTCString()}
                  </Typography>
                </CardContent>
              </CardActionArea>
              <CardActions>
                <Link href="/blog/[...slug]" as={`/blog/${post.slug}`} prefetch>
                  <Button role="link" size="small" color="primary">
                    Read
                  </Button>
                </Link>
              </CardActions>
            </Card>
          </Grid>
        ))}
      </Grid>
    </Container>
  );
};

export default BlogPage;

export async function getServerSideProps() {
  const posts = await api.getContentList({
    referenceName: "posts",
    languageCode: "en-us",
    take: 3
  });

  return {
    props: {
      posts: posts.items.map((post) => {
        const { title, slug, image, date } = post.fields;
        return { title, slug, image, date };
      })
    }
  };
}

This page is more or less identical to the BlogPostPage in terms of the concepts at work, but in such a setting, it could be beneficial to prefetch pages. Next.js allows you to fetch pages ahead of time if you know that the user will visit those pages.

To do so, you should use the <Link> component provided to you via next/link. Since you are making use of dynamic routes, you should use the as prop that will contain the value of the route parameter.

Imperative dynamic routing with Next.js

The <Link/> component is extremely handy when it comes to Next.js routing. However, it is important to note that Next.js offers a useRouter hook to navigate in your app. Instead of the <Link/> component, you could also have done the following:

import { useRouter } from 'next/router'

const BlogPage = ({ posts }) => {
  const router = useRouter()

  return (
   {...}
    <Button role="link" size="small" color="primary" onClick={() => router.push(`/blog/${post.slug}`)}>
                  Read
    </Button>
  {...}
  )
}

You can also programmatically prefetch a page in Next.js by tapping into the Next.js router and using the prefetch method:

import { useRouter} from "next/router";
import { useEffect } from "react";
// If you want to prefetch the contact page 
// while in the home page, you can do so like this

const HomePage = () => {
 const router = useRouter();
 useEffect(() => {
  router.prefetch('/contact');
 }, []);
 // ...
}

Next.js dynamic routing: Multiple segments

By now, you have discovered dynamic routing in Next.js with one segment such […slug]. However, this begs the question: Can you have multiple segments with Next.js dynamic routing? Thankfully, the answer is yes!

Your blog posts in Agility CMS are currently linked to a particular category of your blog. You can see those categories in Content > Blog Categories.

Let’s say you wanted to have both a category and a slug in your URL. Use the example below and follow along with these next steps:

blog/travel-guide/virtual-tours-ways-to-travel-from-home

First, inside your blog folder, create a [category] folder. Like your […slug].js, this will allow you to have multiple routes for all your categories.

Next, create a […slug].js file. Inside this file, you can then grab your category and slug params from the Next.js ctx object:

export async function getServerSideProps(ctx) {
  const { params } = ctx
  const { slug, category } = params

  //Grab your page

  return {
    props: {
      page: page || null
    }
  };
}

For the URL /blog/travel-guide/virtual-tours-ways-to-travel-from-home, your params would then be:

params: {
  category: 'travel-guide',
  slug: [ 'virtual-tours-ways-to-travel-from-home' ]
}

SSR vs. SSG in Next.js: Making the best choice

While SSR in Next.js is the best choice when the content on your page changes frequently, SSG in Next.js would be a better pick if the content is static.

That said, you don’t have to jump into server-side rendering all in one go if a majority of your page is static. A better approach would be to use client-side data fetching by using a custom hook made by Next.js for data fetching, like useSWR or a library like React Query.

Conclusion

While the application you built is far from being production-ready, you should now have a pretty solid understanding of how Next.js works and how Agility CMS could help with your content management needs.

If you want to delve deeper into Next.js, make sure to check out the step-by-step tutorial by Next.js. If you want to take a peek at how a more production-ready Agility CMS and Next.js combo would look, check out this example repository.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Eslam Hefnawy Serverless Architect at Serverless, Inc. Co-creator of the Serverless Framework and the lead architect of Serverless Components.

Leave a Reply