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.

Outcomes

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
  • Can create and set up an Agility CMS instance

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

Next.js is a React framework that addresses common pain points that many developers face when building web applications. Whether it’s ensuring all your code is bundled and minimized using a bundler like webpack or 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 truly 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, which means 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 it up is pretty easy, as they have starting points for most popular React frameworks (Next.js included). You can start by creating an account. Once you’re done, you will have to set up some project settings.

By the time you’re done creating a project, you will have a CMS with content ready to go. You can explore the dashboard and pages sections to edit the content to your preferences, but what you really should look out for are the API keys:

Highlighting Location of the API Keys Button in Agility CMS

When you click on the API Keys button, a modal will pop up. From there, click on Show API Key(s). Save these keys for later — you’re gonna need them!

Setting up the development environment

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 stick to the main topic of this post, we will use a component framework called Material-UI. If you fancy writing your own styles, however, remember that you can make use of Next’s inbuilt CSS Modules and Sass support. Additionally, we 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 @material-ui/core @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

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 our first page

Before we attempt to create our 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, we should create all of our routes. Under the pages directory, we’ll find an index.js file. The index.js file will contain the page component that will be served at /, or the default/main route of our application.

Additionally, we need a route to serve all of our blog pages. So, let’s create a directory called blog and a file named [...slug].js. We will get back to this concept in the next topic. Our project structure should look like this:

Next.js + Agility CMS Project Structure

Next up, we should initialize the Agility CMS client. To encourage reusability, we 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. Now let’s head over to the home/index page of our 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 that is provided by the Agility CMS SDK to fetch entire pages in one go, as long as we provide a pageID. If our content is in multiple locales, then we must also pass in the correct languageCode.

The value for the pageID property can be found under the specific page settings. For example, if it is a page named home, then you would navigate to pages -> home -> settings in the Agility CMS dashboard.

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 that are called modules. These modules have names, and it is 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 we now have, we can construct our page in a way that suits our design:

import Head from "next/head";
import { useRouter } from "next/router";
import {
  AppBar,
  Toolbar,
  Typography,
  Button,
  Grid,
  Container
} from "@material-ui/core";
import api from "../lib/agility";

const HomePage = ({ meta, data }) => {
  const router = useRouter();
  const hero = data.Hero.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>
      <Container maxWidth="lg" style={{ marginTop: "30px" }}>
        <Grid container spacing={3} alignItems="center">
          <Grid item xs={6}>
            <Typography variant="h2">
              {hero.title} <strong>{hero.subTitle}</strong>
            </Typography>
            <Typography variant="body1">{hero.announcement}</Typography>
            <Button
              variant="contained"
              color="primary"
              onClick={() => router.push(hero.primaryCTA.href)}
              role="link"
            >
              {hero.primaryCTA.text}
            </Button>
          </Grid>
          <Grid item xs={6}>
            <img
              src={hero.backgroundImage.url}
              alt={hero.backgroundImage.label}
              height="360px"
              width="100%"
            />
          </Grid>
        </Grid>
      </Container>
    </>
  );
};

export default HomePage;

// getServerSideProps() has been collapsed for readability.

We split the data we received into two properties: meta and data. The meta property consists of all the SEO-related data for the page, which we can set in the SEO category of our 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

When using plain React, you become intimately familiar with the app.js file, which acts as the entry point to your entire component hierarchy. If you take a look at our project directory, you’ll notice that it doesn’t have an app.js. This is where we’d usually set up our routing logic, but since Next.js handles it for us, the app.js file is never generated.

If we wanted to wrap the entire application in a component, perform some initialization operation, or include some global styles, then we must create a file named _app.js. This will be the custom App component for the application. Let’s add in a navigation bar to our newly created App component:

import "../styles/global.css";
import { AppBar, Toolbar, Typography } from "@material-ui/core";

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

export default App;

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

One more thing to note is that Next.js does not allow you to add global styles in page components — if you do, Next.js will throw an error asking you to do so. So let’s fix that! Create a directory called styles and a stylesheet in it named global.css, then paste in the following styles:

body {
  margin: 0;
  box-sizing: border-box;
}

This will get rid of the default margin applied to the <body> tag, but ideally, you’d want to add some kind of style reset here, like normalize.css.

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, we 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/2 and /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 we 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 dynamic routes, let’s work on the blog route:

import api from "../../lib/agility";
import ErrorPage from "next/error";
import { Container, Typography } from "@material-ui/core";

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, we make use of the context object (abbreviated as ctx), which contains a few properties that we can make use of. params happens to be one such property. params contains all the route parameters and their values.

Additionally, we are making use of a new method from the Agility CMS SDK called getContentList(). This method allows us to fetch a list of data by its reference name. If you navigate to pages -> blog -> Posts Listing -> Edit Content, then we’ll see a list of posts that have already been created.

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

Next.js doesn’t allow us 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

Quite often, we 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/. We can use the optional catch-all syntax of dynamic routing ([[...slug]].js), but then we would 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/static routes have a higher priority than a dynamic route.

For example, if we’ve 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.

So let’s create a file called index.js under the blog directory:

import {
  Card,
  Container,
  CardContent,
  CardActionArea,
  Typography,
  CardActions,
  Button,
  CardMedia,
  Grid
} from "@material-ui/core";
import Link from "next/link";
import api from "../../lib/agility";

const BlogPage = ({ posts }) => {
  return (
    <Container maxWidth="lg">
      <Typography variant="h2" gutterBottom="10px">
        Featured Blog Posts
      </Typography>
      <Grid container spacing={2}>
        {posts.map((post) => (
          <Grid item xs={4}>
            <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 by: {post.authorName} 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, authorName, image, date } = post.fields;
        return { title, slug, authorName, 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, we should use the <Link> component provided to us via next/link. Since we’re making use of dynamic routes, we should use the as prop that will contain the value of the route parameter.

You could also programmatically prefetch a page by tapping into the Next.js router and using the prefetch method. To tap into the Next.js router, you have to use the useRouter() hook.

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');
 }, []);
 // ...
}

SSR vs. SSG: Making the best choice

While server-side rendering is the best choice when the content on your page frequently changes, static-site generation will 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.

You can read more about how to make the decision between SSR and SSG on the Vercel blog.

Conclusion

While the application we 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 Next.js produced. 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.