Static Site Generation with Next.js and TypeScript (Part I) - Project Overview

Responses (14)

As advice: in order to speed up the build on local, you could add these params to each image on the enum/index.ts file: &h=320&w=320

BTW:

When creating the API key you must give a "real" app URL. (localhost:3000 is not working)

I made up a new one on Vercel. For example:

[whatever-you-want-as-app-url].vercel.app

Jennifer Tran2 months ago

Same for me - not able to get the API key from https://www.petfinder.com/developers/signup

It gives me a 403 forbidden error when I submit my application details.

Karl Krasnowsky3 months ago

Unable to obtain a petfinder api key via their signup system.

Clap
22|14|

Many of today's most popular web applications, such as G-Mail and Netflix, are single-page applications (SPAs). Single-page applications deliver highly engaging and exceptional user experiences by dynamically rendering content without fully reloading whole pages. However, because single-page applications generate content via client-side rendering, the content might not be completely rendered by the time a search engine (or bot) finishes crawling and indexing the page. When it reaches your application, a search engine will read the empty HTML shell (e.g., the HTML contains a <div id="root" /> in React) that most single-page applications start off with.

For a smaller, client-side rendered application with fewer and smaller assets and data requirements, the application might have all the content rendered just in time for a search engine to crawl and index it. On the other hand, for a larger, client-side rendered application with many and larger assets and data requirements, the application needs a lot more time to download (and parse) all of these assets and fetch data from multiple API endpoints before rendering the content to the HTML shell. By then, the search engine might have already processed the page, regardless of the content's rendering status, and moved on to the next page.

For sites that depend on being ranked at the top of a search engine's search results, such as news/media/blogging sites, the performance penalties and slower first contentful paint of client-side rendering may lower a site's ranking. This results in less traffic and business.

Such sites should not client-side render entire pages worth of content, especially when the content infrequently (i.e., due to corrections or redactions) or never changes. Instead, these sites should serve the content already pre-generated as plain HTML. A common strategy for pre-generating content is static site generation. This strategy involves generating the content in advance (at build time) so that it is part of the initial HTML document sent back to the user's browser when the user first lands on the site.

By exporting the application to static HTML, the content is created just once and reused on every request to the page. With the content made readily available in static HTML files, the client has much less work to perform. Similar to other static assets, these files can be cached and served by a CDN for quicker loading times.

Once the browser loads the page, the content gets hydrated and maintains the same level of interactivity as if it was client-side rendered.

Unlike Create React App, popular React frameworks like Gatsby and Next.js have first-class, built-in static site generation support for React applications. With the recent release of Next.js v12, Next.js applications build much faster with the new Rust compiler (compared to Babel, this compiler is 17x faster). Not only that, Next.js now lets you run code on incoming requests via middleware, and its APIs are compatible with React v18.

In this multi-part tutorial, I'm going to show you how to...

  • Create a Next.js application.

  • Generate an optimized version of the application (at build time) for production.

  • Deploy the application to Vercel.

We will be building a simple, statically generated application that uses the Petfinder API to display pets available for adoption and recently adopted. All of the site's content will be pre-rendered in advance with the exception of pets available for adoption, which the user can update on the client-side.

Home Page (/):

Listings for Pet Animal Type (/types/<type>):

Visit the live demo here: https://petfinder-nextjs.vercel.app/

Installation and Setup#

To get started, initialize the project by creating its directory and package.json file.

Note: If you want to skip these steps, then run the command npx [email protected] --ts to automatically scafford a Next.js project with TypeScript. Then, proceed to the next section of this tutorial.

Install the following dependencies and dev. dependencies:

Add a .prettierrc file with an empty configuration object to accept Prettier's default settings.

Add the following npm scripts to the package.json file:

Here's what each script does:

  • dev - Run the development environment (on port 3000 by default). With fast refresh, each time you edit the application's source code, the change instantly gets reflected in the running application, all while preserving the application's current state. This fast feedback loop keeps you productive and uninterrupted during development.

  • build - Build an optimized version of the application and generate the pre-rendered pages.

  • start - Spin up the application in production mode.

  • lint - Runs ESLint on the codebase.

At the root of the project directory, create an empty TypeScript configuration file (tsconfig.json).

By running next, Next.js automatically updates the empty tsconfig.json file with Next.js's default TypeScript configuration.

(tsconfig.json)

Note: If you come across the error message Error: > Couldn't find a `pages` directory. Please create one under the project root, then at the root of the project directory, create a new pages directory. Then, re-run the npm run dev command.

Note: Double-check the configuration, and make sure that the moduleResolution compiler option is not missing and set to node. Otherwise, you will encounter the TypeScript error Cannot find module 'next'.

Currently, this approach generates a TypeScript configuration that has the strict compiler option set to false. If you bootstrapped the project via the create-next-app CLI tool (--ts/--typescript option), then the strict compiler option will be set to true. Let's set the strict compiler option to true so that TypeScript enforces stricter rules for type-checking.

(tsconfig.json)

Note: Setting strict to true automatically enables the following seven type-checking compiler options: noImplicitAny, noImplicitThis, alwaysStrict, strictBindCallApply, strictNullChecks, strictFunctionTypes and strictPropertyInitialization.

Additionally, this command auto-generates a next-env.d.ts file at the root of the project directory. This file guarantees Next.js types are loaded by the TypeScript compiler.

(next-env.d.ts)

To further configure Next.js, create a next.config.js file at the root of the project directory. This file allows you to override some of Next.js's default configurations, such as the project's base Webpack configurations and mapping between incoming request paths and destination paths.

For now, let's just opt-in to React's Strict Mode to spot out any potential problems, such as legacy API usage and unsafe lifecycles, in the application during development.

(next.config.js)

Similar to the tsconfig.json file, running next lint automatically installs the eslint and eslint-config-next development dependencies. Plus, it creates a new .eslintrc.json file with Next.js's default ESLint configuration.

Note: When asked "How would you like to configure ESLint?" by the CLI, select the "Strict" option.

(eslintrc.json)

This application will be styled with utility CSS rules from the Tailwind CSS framework. If you are not concerned with how the application is styled, then you don't have to set up Tailwind CSS for the Next.js application and can proceed to the next section. Otherwise, follow the directions here to properly integrate in Tailwind CSS.

Registering for a Petfinder Account and Setting Up Environment Variables#

To register for a Petfinder account, visit the Petfinder for Developers and click on "Sign Up" in the navigation bar.

Follow the registration directions. Upon creating an account, go to https://www.petfinder.com/developers/ and click on the "Get an API Key" button. The form will prompt you for two pieces of information: "Application Name" and "Application URL" (at the minimum). For "Application Name," you can enter anything as the application name (e.g., "find-a-cute-pet"). For "Application URL," you can enter https://<application_name>.vercel.app since the application will be deployed on Vercel by the end of this tutorial series. To see if an https://<application_name>.vercel.app URL is available, visit the URL in a browser. If Vercel returns a 404: NOT_FOUND page with the message <application_name>.vercel.app might be available. Click here to learn how to assign it to a project., then the application name is likely available and can be used for completing the form.

You can find the API key (passed as the client ID in the request payload to the POST https://api.petfinder.com/v2/oauth2/token endpoint) and secret (passed as the client secret in the request payload to the POST https://api.petfinder.com/v2/oauth2/token endpoint) under your account's developer settings.

Here, you can track your API usage. Each account comes with a limit of 1000 daily requests and 50 requests per second.

At the root of the project directory, create a .env file with the following environment variables:

  • NEXT_PUBLIC_PETFINDER_API_URL - The Petfinder API's base URL (https://api.petfinder.com/v2).

  • NEXT_PUBLIC_PETFINDER_CLIENT_ID - Your Petfinder account's API key.

  • NEXT_PUBLIC_PETFINDER_CLIENT_SECRET - Your Petfinder account's secret.

  • NEXT_PUBLIC_URL - The URL of the application. For development, the development server spins up on http://localhost:3000. If deployed to Vercel, then the URL would be https://<project-name>.vercel.app.

(.env)

Replace the Xs with your account's unique client ID and secret.

Creating the Application's Home Page#

The home page features a grid of cards.

Each card represents a pet animal type catalogued by the Petfinder API:

  • Dog

  • Cat

  • Rabbit

  • Small & Fury (e.g., Hamsters and Gerbils)

  • Horse

  • Birds

  • Scales, Fins & Other (e.g., Goldfish and Lizards)

  • Barnyard (e.g. Pigs)

These cards lead to pages that contain listings of pets recently adopted and available for adoption, along with a list of breeds associated with the pet animal type (e.g., Shiba Inu and Golden Retriever for dogs).

Suppose you have to build this page with client-side rendering only. To fetch the types of animal pet from the Petfinder API, you must:

  1. Receive an access token from the Petfinder API (POST https://api.petfinder.com/v2/oauth2/token).

  2. Send a request to the endpoint GET https://api.petfinder.com/v2/types to get a list of types from the Petfinder API. All Petfinder API endpoints (except for POST https://api.petfinder.com/v2/oauth2/token) require an "Authorization" header set to a valid access token.

Initially, upon visiting the page, the user would be presented with a loader as the client...

  1. Sends a request to the API for an access token.

  2. Waits for a response (with the access token) from the API.

  3. Sends a request to the API for a list of types.

  4. Waits for a response (with the list of types) from the API.

Having to wait on the API to process these two requests before any content is shown on the page only adds to a user's frustrations. Wait times may even be worst if the API happens to be experiencing downtime or dealing with lots of traffic.

You could store the access token in a cookie to avoid sending a request for a new access token each time the user loads the page. Still, you are left with sending a request for a list of types each time the user loads the page.

Note: For stronger security (i.e., mitigate cross-site scripting by protecting the cookie from malicious JavaScript code), you would need a proxy backend system that interacts with the Petfinder API and sets an HttpOnly cookie with the access token on the client's browser after obtaining the token from the API. More on this later.

This page serves as a perfect example for using static site generation over client-side rendering. The types returned from the API will very rarely change, so fetching the same data for each user is repetitive and unnecessary. Rather, just fetch this data once from the API, build the page using this data and serve up the content immediately. This way, the user does not have to wait on any outstanding requests to the API (since no requests will be sent) and can instantly engage with the content.

With Next.js, we will leverage the getStaticProps function, which runs at build time on the server-side. Inside this function, we fetch data from the API and pass the data to the page component as props so that Next.js pre-renders the page at build time using the data returned by getStaticProps.

Note: In development mode (npm run dev), getStaticProps gets invoked on every request.

Previously, we created a pages directory. This directory contains all of the Next.js application's page components. Next.js's file-system based router maps page components to routes. For example, pages/index.tsx maps to /, pages/types/index.tsx maps to /types and pages/types/[type.tsx] maps to types/:type (:type is a URL parameter).

Now let's create four more directories:

  • components - Houses the application's React components.

  • helpers - Houses reusable methods that are specific to the application.

  • enums - Houses enumerations.

  • shared - Houses shared types, interfaces, etc.

The Petfinder API documentation provides example responses for each of its endpoint. With these responses, we can define interfaces for the responses from the following endpoints:

  • GET /animals - Returns a list of pet animals.

  • GET /types - Returns a list of pet animal types (e.g., dog, cat, etc.).

  • GET /types/:type/breeds - Returns a list of breeds for a pet animal type (e.g., for dog, Labrador Retriever, Rottweiler, etc.).

Create an interfaces directory within the shared directory. Inside of the interfaces directory, create a petfinder.interface.ts file.

(shared/interfaces/petfinder.interface.ts)

Note: This tutorial skips over endpoints related to organizations.

Inside of the pages directory, create an index.tsx file, which corresponds to the home page at /.

To build out the home page, we must first define the <HomePage /> page component's structure.

(pages/index.tsx)

Then, create the <TypeCardsGrid /> component, which renders a grid of cards (each represents a pet animal type). This component places the cards in a...

  • 4x2 grid layout for large screen sizes (width >= 1024px).

  • 3x3 grid layout for medium screen sizes (width >= 768px).

  • 2x4 grid layout for small screen sizes (width >= 640px).

  • A single column for mobile screen sizes (width < 640px).

(components/TypeCardsGrid.tsx)

Then, create the <TypeCard /> component, which renders a card that represents a pet animal type. This component shows a generic picture of the pet animal type and a link to browse listings (recently adopted and available for adoption) of pet animals of this specific type.

Note: The types returned from the Petfinder API do not have an id property, which serves as both a unique identifier and a URL slug (e.g., the ID of type "Small & Furry" is "small-furry"). In the next section, we will use the type name to create an ID.

(components/TypeCard.tsx)

Since the Petfinder API does not include an image for each pet animal type, we can define an enumeration ANIMAL_TYPES that supplements the data returned from the API with an Unsplash stock image for each pet animal type. To account for the images' different dimensions in the <AnimalCard /> component, we display the image as a background cover image of a <div /> and position the image such that the animal in the image appears in the center of a 10rem x 10rem circular mask.

(.enums/index.ts)

For users to browse the pet animals returned from the Petfinder API for a specific pet animal type, they can click on a card's "Browse Listings" link. The link is wrapped with a Next.js <Link /> component (from next/link) to enable client-side navigation to the listings page. This kind of behavior can be found in single-page applications. When a user clicks on a card's "Browse Listings" link, the browser will render the listings page without having to reload the entire page.

The <Link /> component wraps around an <a /> element, and the href gets passed to the component instead of the <a /> element. When built, the generated markup ends up being just the <a /> element, but having an href attribute and having the same behavior as a <Link /> component.

To apply TailwindCSS styles throughout the entire application, we need to override the default <App /> component that Next.js uses to initialize pages. Next.js wraps every page component with the <App /> component. This is also useful for other reasons like...

  • Maintaining shared state across pages.

  • Enforcing a consistent layout across pages.

  • Injecting additional data into pages.

To override the default <App /> component, create an _app.tsx file within the pages directory. Inside of this file, import the styles/globals.css file that contains TailwindCSS directives, and define an <App /> component, like so:

(pages/_app.tsx)

The <App /> component takes two props:

  • Component - The current route's page component.

  • pageProps - An initial set of props that comes from one of Next.js's data-fetching methods (e.g., getStaticProps, getServerSideProps, etc.). If no such data-fetching method is defined within this file, then pageProps is an empty object. All of the props in pageProps gets directly passed to the page component.

To verify that the homepage works and that the TailwindCSS styles have been applied correctly, let's run the application:

Within a browser, visit localhost:3000.

Currently, the <TypeCardsGrid /> is not rendered since we have not yet fetched any pet animal types from the Petfinder API.

Next Steps#

If you find yourself stuck at any point during this tutorial, then feel free to check out the project's repository for this part of the tutorial here.

Proceed to the next part of this tutorial series to learn how to fetch this data from the Petfinder API with the getStaticProps function.

If you want to learn more advanced techniques with TypeScript, React and Next.js, then check out our Fullstack React with TypeScript Masterclass:

Sources#


Clap
22|14