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
- Setting up Agility CMS
- Setting up the development environment for your Next.js project
- Creating your Next.js routes
- Creating your first Next.js page
- Adding a global navigation bar to your Next.js
App
component - Understanding dynamic routing in Next.js
- Handling a common use case for dynamic routing in Next.js
- Imperative dynamic routing with Next.js
- Next.js dynamic routing: Multiple segments
- SSR vs. SSG in Next.js: Making the best choice
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:
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.
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 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.
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:
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:
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.