Next.js Static Site Generation Practical Example

igorasilveira

Igor Silveira

Posted on June 15, 2022

Next.js Static Site Generation Practical Example

This article is part of an entire Next.js series of articles that I am putting together to help you become a Next.js pro and start building blazing fast React apps.

šŸ’”Ā If you donā€™t want to miss out on any of the tutorials, signup for my newsletter by clicking here or head over to DailyDev.io for more.

On this issue, we will be learning about how Next.js enables high-performant websites by pre-rendering every page by default instead of having it all done by client-side JavaScript, like regular React apps usually do.

šŸ’” You can find the source code for this project here.

So letā€™s get started!

Pre-requisites

  • Node ā‰„ 12
  • React Basics

Quick Recap

Up to this point, we have been talking about the concept of pages, how to represent them within our Next.js project, and how to make them either static or dynamic so that Next.js would know how to render and match specific URLs to their corresponding React components.

We then fired up our development server by running npm run dev and waited for a browser window to pop up with our app running at http://localhost:3000. Great! šŸ‘

But one thing we have not done is dive deeper into how Next.js is assembling those pages and serving them back to us when we visit some URL. And better yet, how the production build of our app differs from the development environment we are running locally. And this is really where Next.js shines.

Pre-rendering

ā€œWhat is pre-rendering?ā€ you might ask. Pre-rendering is the act of taking a page in the application and generating the plain HTML for it beforehand, instead of letting the client-side handle the bulk of the work. The HTML is then also shipped with minimal JavaScript code that will run in the client and that is necessary to make that page fully interactive.

Pre-Rendering Example

This process helps solve two of the main downsides normally associated with React apps and general Single Page Applications (SPAs):

  • shockingly low Search Engine Optimization (SEO) capabilities, since all pages and transitions are handled by the client through JavaScript code and, therefore, not crawlable by Search Engines
  • heavy loads for the clients as they have to download and run the entire application on the browser which quickly presented problems as the applications became larger and more interactive

How Next.js Handles Pre-Rendering

Next.js will pre-render every page, by default. And it can happen in two different ways, the difference is when it generates the HTML for a page:

  • Static Generation: The HTML is generated at build time and is reused on every request for that page.
  • Server-side Rendering (for another article): The HTML for a page is generated on each request.

Both of these options will offer the benefits we discussed in the previous section but they can be used for different use cases upon different needs and you can even develop hybrid approaches within the same application by statically generating most pages and server-side rendering others.

The best and most performant choice for serving a web application is by statically generating all the applicationā€™s pages since they can be easily cached in a Content Delivery Network (CDN) and boost performance by serving them closest to the requesting client. However, in some cases, Server-side Rendering might be the only option.

For now, letā€™s take a look at how you can achieve Static Generation within or dog app.

Static Generation

Using Static Generation, the HTML for a page is generated at build time when we run the next build command. That generated HTML is then served and reused whenever the page is requested.

There are two ways to statically generate pages, with or without data from external sources.

Static Generation without data

This is the most basic use case for a Next.js page, as it is the default behavior of the framework.

A simple component exported from a file in the pages folder that does not need to fetch any external data before being pre-rendered generates a single HTML file during the build time.

An example would be the individual dog pages we created in our first tutorial on Next.js Basic Routing:

const Doggo: NextPage = () => {
  return (
    <div>
      <main>
        <h1>
          This is a Doggo.
        </h1>

        <Image alt="This is a doggo" src='google.com' width={520} height={520}/>
        <p style={{color: "#0070f3"}}><Link href="/">Back Home</Link></p>
      </main>
    </div>
  )
}

export default Doggo;
Enter fullscreen mode Exit fullscreen mode

Static Generation with Data

Then there is Static Generation dependent on fetching external data for pre-rendering. You can imagine two different use cases for needing to fetch external data for rendering pages:

  1. Your page content depends on external data.
  2. Your page paths (existing routes) depend on external data.

Scenario 1

We can think of an example within our doggo app where our page content will depend on external data. We made our page dynamic in the last tutorial, so all dogs are rendered by the same React Component. But all dogs have different information to be rendered on the page, therefore, the pages for each dog have distinct content.

Letā€™s assume the following snippet of our updated dog page:

// Need to get a dog from the API
const Doggo: NextPage = ({ dog }) => {
  return (
    <div>
        <h1>This is a {dog.name}.</h1>

        <Image
          alt="This is a doggo"
          src={dog.imageURL}
          width={520}
          height={520}
        />

        <p>{dog.description}</p>
    </div>
  );
};

export default Doggo;
Enter fullscreen mode Exit fullscreen mode

To render each dog page with the correct data, we need to provide that specific dog data to our React Component.

To do this in Next.js, we will export an async function with a specific name, getStaticProps within the same page where the React Component representing the page is exported. This function will be called at build time when pre-rendering the page, and you can pass the necessary fetched data to the pageā€™s props.

const Doggo: NextPage = ({ dog }) => {
  ...
};

// This function gets called at build time
export const getStaticProps: GetStaticProps = async () => {
  // Call an external API endpoint to get a dog
  const res = await fetch("https://.../dogs/a-doggo");
  const dog = await res.json();

  // By returning { props: { dog } }, the Doggo component
  // will receive `dog` as a prop at build time
  return {
    props: {
      dog,
    },
  };
}

export default Doggo;
Enter fullscreen mode Exit fullscreen mode

Scenario 2

Last time, we created a dynamic page within our app that enabled dynamic routes. With that, our app started responding to all requests for pages under /dogs/:id. But instead of only exposing routes for existing dog ids, our application is matching every id, so it will never return a 404 - Not Found under that route.

In a real-world scenario, this does not make much sense. We would only want to render and serve pages for specific and individual resources that exist within our database.

So our page paths depend on external data and should be pre-rendered. Similar to before, Next.js allows you to declare a specific function within your page componentā€™s file, whose sole purpose is to return a list of paths that this dynamic page should be rendered on, getStaticPaths. This function also gets called at build time.

// This function gets called at build time
export const getStaticPaths: GetStaticPaths = async () => {
  // Call an external API endpoint to get dogs
  const res = await fetch("https://.../dogs");
  const dogs = await res.json();

  // Get the paths we want to pre-render based on dogs
  const paths = dogs.map((dog: any) => ({
    params: { id: dog.id },
  }));

  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { paths, fallback: false };
}

export default Doggo;
Enter fullscreen mode Exit fullscreen mode

Now getStaticPaths and getStaticProps can work together to pre-render all pages for existing dogs, based on a single dynamic React Component.

Updating our Dog App

Now it is time to see this in action and power up our previously created dynamic page so that it can reach its full potential.

Creating a Dog Interface

Since we are using TypeScript to ensure type safety and easy development, we should make use of it and create an interface to represent our dog and facilitate its usage through the app.

Letā€™s create a new /definitions folder to store our definitions files and create a dogs.d.ts file with the following content, and now we have a simple representation of our dog object.

interface Dog {
    id: number;
    name: string;
    description: string;
}
Enter fullscreen mode Exit fullscreen mode

Creating our Dog Database

For simplicity, we will be creating a small in-memory structure to store our dogs and their information, so that Next.js can then access them and pre-rendered all the individual pages.

Letā€™s create a /db folder where we can store all our in-memory structures of data for ease of access. Inside we will create a dogs.ts file and populate it with some structure data of some dogs using our previously created interface.

export const dogs: Dog[] = [
    {
        id: 1,
        name: 'Fido',
        description: 'A friendly dog',
    },
    {
        id: 2,
        name: 'Rex',
        description: 'A big dog',
    },
    {
        id: 3,
        name: 'Spot',
        description: 'A small dog',
    }
]
Enter fullscreen mode Exit fullscreen mode

Updating our Dog Page Component

We will make some updates to our page component in order for it to become 100% dynamic, namely:

  • Remove the use of the Next.js Router: Next.js will be giving us all the necessary information through the component props.
  • Create the getStaticPaths function to generate a list of string-based paths that represent only our available dogs.
  • Create the getStaticProps function to fetch the respective dog based on the information received in the params.
  • Update our page content to use the dog information present on the dog prop is it now receiving from getStaticProps.

By the end, our React Component should look something like this:

import type { GetStaticPaths, GetStaticProps, NextPage } from "next";
import Link from "next/link";

import { dogs as dogsDB } from "../../db/dogs";

const Doggo: NextPage<{ dog: Dog }> = ({ dog }) => {
  return (
    <div>
      <main>
        <h1>This is {dog.name}.</h1>
        <p>{dog.description}</p>

        <p style={{ color: "#0070f3" }}>
          <Link href="/dogs">Back to Dogs</Link>
        </p>
      </main>
    </div>
  );
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  if (!params || !params.id) {
    return { props: {} };
  }

  const dog = dogsDB.find((dog) => dog.id === parseInt(params.id as string));

  return {
    props: {
      dog,
    },
  };
};

export const getStaticPaths: GetStaticPaths = async () => {
  const dogs = dogsDB;
  const paths = dogs.map((dog: Dog) => ({
    params: { id: dog.id.toString() },
  }));

  return { paths, fallback: false };
};

export default Doggo;
Enter fullscreen mode Exit fullscreen mode

Final Touch: Update Dogs Index Page

Just to end this on a high note, letā€™s update our dogsā€™ index.tsx page so that it will list all existing dogs and link to their individual pages.

The same principles apply here, but since it is only a single non-dynamic page, we only use getStaticProps and pass the dog list as props to the page so that it can render the list.

import type { GetStaticProps, NextPage } from "next";
import Head from "next/head";
import Link from "next/link";

import { dogs as dogsDB } from "../../db/dogs";

const Doggo: NextPage<{ dogs: Dog[] }> = ({ dogs }) => {
  return (
    <div>
      <Head>
        <title>Our Doggos</title>
      </Head>

      <main>
        <h1>Check out our doggos.</h1>

        <ul style={{ color: "#0070f3" }}>
          {dogs.map((dog) => (
            <li key={dog.id}>
              <Link href={`/dogs/${dog.id}`}>{dog.name}</Link>
            </li>
          ))}
        </ul>

        <p style={{ color: "#0070f3" }}>
          <Link href="/">Back Home</Link>
        </p>
      </main>
    </div>
  );
};

export const getStaticProps: GetStaticProps = async () => {
  const dogs = dogsDB;

  return {
    props: {
      dogs,
    },
  };
};

export default Doggo;
Enter fullscreen mode Exit fullscreen mode

Final Result

By the end, your app should look something like this. Pretty neat! šŸ˜Ž

End Result

Final Remarks

To experience the full power and speed of Static Generation, donā€™t forget to run the build command (npm run build) followed by serving (npm run start) the generated files. This is how the pages would be served in a production environment and cached by some CDN.

Running the project in npm run dev mode will always build all pages on each request.

NextJS Build Process

Notice how Next.js detected which pages were static and dependent on external data, generating exactly the routes defined by our in-memory database.

If you run into any trouble feel free to reach out to me on Twitter, my DMs are always open.

Next Steps: Keep an eye out for my following Next.js tutorials where we will go over much more in Next.js territory! If you donā€™t want to miss out on any of the tutorials, signup for my newsletter by clicking here.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
igorasilveira
Igor Silveira

Posted on June 15, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related