Static Site Generation with Next.js and TypeScript (Part V) - Build Time Access Tokens and Exporting Static HTML
Responses (0)
Disclaimer - Please read the fourth part of this blog post here before proceeding. It demonstrates how to statically generate pages with dynamic routes using the getStaticPath()
function. If you just want to jump straight into this tutorial, then clone the project repository and install the dependencies.
In the previous part of this tutorial series, we encountered a big problem: each getStaticProps()
and getStaticPath()
function required us to obtain an access token before being able to request any data from the Petfinder API. This meant that anytime we built the Next.js application for production, we had to obtain several access tokens for the Petfinder API:
One in
getStaticProps()
inpages/index.tsx
to retrieve a list of pet animal types.One in
getStaticProps()
inpages/types/[type].tsx
to retrieve a list of pet animal breeds and a list of recently adopted pets.One in
getStaticPath()
inpages/types/[type].tsx
to generate paths (/types/:type
).
If we were to add more statically generated pages to the Next.js application that depend on data from the Petfinder API, then we would continue to accumulate more access tokens that are scattered throughout the Next.js application.
Unfortunately, the Next.js's custom <App />
component does not support data fetching functions like getStaticProps()
and getStaticPath()
. This means we don't have the option of obtaining a single access token, fetching all of the necessary data (e.g., a list of pet animal types and lists of recently adopted pets) in the getStaticProps()
function of the custom <App />
component and passing the data as props to every page component at build time.
One way to make the access token globally available to all page components at build time is to inject it as an environment variable.
Below, I'm going to show you how to build a Next.js application with a single access token. We will obtain an access token from the Petfinder API via the cURL CLI tool, set it to an environment variable named PETFINDER_ACCESS_TOKEN
and execute the npm run build
command with this environment variable.
Then, I'm going to show you how to export the Next.js application to static HTML. This allows us to deploy and serve the Next.js application on fast, static hosting solutions like Cloudflare Pages and GitHub Pages, all without ever having to spin up a Node.js server.
Installation and Setup#
To get started, clone the project repository and install the dependencies.
If you're coming from the fourth part of this tutorial series, then you can continue from where the fourth part left off.
Within the project directory, create a Makefile
:
$ touch Makefile
With a Makefile, we can define rules that each run a set of commands. Rules are similar, purpose-wise, to npm scripts in package.json
files. Each rule consists of, at a minimum, a target and a command. The target is the name of the rule, and the command is the actual command to execute.
Inside of the Makefile
, add two rules: dev
and build
.
dev:
npm run dev
build:
npm run build
Note: Each indentation should be a tab that's four spaces wide. Otherwise, you may encounter the error *** missing separator. Stop.
.
Here, invoking the make
command with the dev
rule as the target (make dev
) runs npm run dev
, and invoking the make
command with the build
rule as the target (make build
) runs npm run build
.

The Makefile allows us to store the result of shell commands into variables.
For example, suppose we add the following line to the top of the Makefile
.
PETFINDER_ACCESS_TOKEN := $(shell echo "abcdef")
# ...
In the above example, we set the variable PETFINDER_ACCESS_TOKEN
to the output of the echo
command, which is the string "abcdef." The shell
function performs command expansion, which means taking a command as an argument, running the command and returning the command's output. Once the shell
function returns the command's output, we assign this output to the simply expanded variable PETFINDER_ACCESS_TOKEN
.
Anytime we reference a simply expanded variable, whose value is assigned with :=
, the variable gets evaluated once (at the time of assignment) and procedurally, much like what you would expect in a typical, imperative programming language like JavaScript. So if we were to reference the variable's value with $()
, then the value will just be the string "abcdef."
(Makefile
)
PETFINDER_ACCESS_TOKEN := $(shell echo "abcdef")
dev:
PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) npm run dev
build:
PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) npm run build
GNU make
comes with another "flavor" of variable, recursively expanded variable, which evaluates a variable's value completely different than what most developers are used to. It's out of the scope of this tutorial, but you can read more about them here.
If you print the value of the PETFINDER_ACCESS_TOKEN
environment variable in the <HomePage />
page component's getStaticProps()
function, then you will see the value "abcdef" logged in the terminal when you...
Run the
make dev
command and visithttp://localhost:3000
in the browser.Run the
make build
command.
(pages/index.tsx
)
// ...
const {
NEXT_PUBLIC_PETFINDER_API_URL,
NEXT_PUBLIC_PETFINDER_CLIENT_ID,
NEXT_PUBLIC_PETFINDER_CLIENT_SECRET,
PETFINDER_ACCESS_TOKEN,
} = process.env;
export const getStaticProps: GetStaticProps = async () => {
console.log({ PETFINDER_ACCESS_TOKEN });
// ...
};
// ...
Note: The PETFINDER_ACCESS_TOKEN
environment variable's name will not be prefixed with NEXT_PUBLIC_
.

Notice that the command (along with the value of environment variables passed to it), PETFINDER_ACCESS_TOKEN=abcdef npm run dev
gets logged to the terminal. To tell make
to suppress this echoing, you can prepend @
to lines that you want suppressed. For simple commands like echo
, you can suppress the echoing by prepending @
to the command itself, like so:
@echo "abcdef"
However, because the command we want suppressed begins with an environment variable, we wrap the entire command in @()
, like so:
(Makefile
)
# ...
dev:
@(PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) npm run dev)
build:
@(PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) npm run build)
When you re-run the make dev
command, PETFINDER_ACCESS_TOKEN=abcdef npm run dev
no longer gets logged to the terminal.

Obtaining an Access Token with cURL
#
To obtain an access token from the Petfinder API via cURL, you must send a request to the POST https://api.petfinder.com/v2/oauth2/token
endpoint with the grant type (grant_type
), client ID (client_id
) and client secret (client_secret
).
This data can be passed by specifying a single -d
option (short for --data
) as a concatenated string of key=value
pairs (delimited with an ampersand) or multiple -d
options, providing a key=value
pair for each one.
Here's what using the single -d
option looks like:
$ curl -d "grant_type=client_credentials&client_id=<NEXT_PUBLIC_PETFINDER_CLIENT_ID>&client_secret=<NEXT_PUBLIC_PETFINDER_CLIENT_SECRET>" <NEXT_PUBLIC_PETFINDER_API_URL>/oauth2/token
And here's what using the multiple -d
options looks like:
$ curl -d grant_type=client_credentials -d client_id=<NEXT_PUBLIC_PETFINDER_CLIENT_ID> -d client_secret=<NEXT_PUBLIC_PETFINDER_CLIENT_SECRET> <NEXT_PUBLIC_PETFINDER_API_URL>/oauth2/token
Here, we will use the single -d
option.
When you run the cURL command, you will see that the access token is returned in stringified JSON.

We can pluck the access token from this stringified JSON by piping the output of the cURL command (the stringified JSON) to a sed
command.
$ curl -d grant_type=client_credentials -d client_id=<NEXT_PUBLIC_PETFINDER_CLIENT_ID> -d client_secret=<NEXT_PUBLIC_PETFINDER_CLIENT_SECRET> <NEXT_PUBLIC_PETFINDER_API_URL>/oauth2/token | sed -E 's/.*"access_token":"?([^,"]*)"?.*/\1/'
On Unix-based machines, the sed
command performs many types of text processing tasks, from search to substitution. The -E
option (short for the --regexp-extended
option) tells the sed
command to find a substring based on an extended regular expression, which requires special characters to be escaped if you want to match for them as literal characters.
When you run the cURL command with the piped sed
command, you will see that only the access token is returned.

Setting the Access Token to an Environment Variable#
Within the Makefile
, let's set the PETFINDER_ACCESS_TOKEN
variable to the cURL command with the piped sed
command, like so:
PETFINDER_ACCESS_TOKEN := $(shell curl -d grant_type=client_credentials -d client_id=$(NEXT_PUBLIC_PETFINDER_CLIENT_ID) -d client_secret=$(NEXT_PUBLIC_PETFINDER_CLIENT_SECRET) $(NEXT_PUBLIC_PETFINDER_API_URL)/oauth2/token | sed -E 's/.*"access_token":"?([^,"]*)"?.*/\1/')
dev:
@(PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) npm run dev)
build:
@(PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) npm run build)
To pull the NEXT_PUBLIC_PETFINDER_CLIENT_ID
, NEXT_PUBLIC_PETFINDER_CLIENT_SECRET
and NEXT_PUBLIC_PETFINDER_API_URL
environment variables from the .env
file, we can use the include
directive to pause reading from the current Makefile
and read from the .env
file before resuming.
include .env
Then, with the export
directive, we can export the environment variables that were read from the .env
file.
include .env
export
(Makefile
)
include .env
export
PETFINDER_ACCESS_TOKEN := $(shell curl -d grant_type=client_credentials -d client_id=$(NEXT_PUBLIC_PETFINDER_CLIENT_ID) -d client_secret=$(NEXT_PUBLIC_PETFINDER_CLIENT_SECRET) $(NEXT_PUBLIC_PETFINDER_API_URL)/oauth2/token | sed -E 's/.*"access_token":"?([^,"]*)"?.*/\1/')
dev:
@(PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) npm run dev)
build:
@(PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) npm run build)
When you re-run the make dev
command and visit http://localhost:3000/
in a browser, you will find that the access token is immediately available to the <HomePage />
page component's getStaticProps()
function.

Now we can remove all instances of fetching an access token within getStaticProps()
and getStaticPath()
functions from the Next.js application, and pass the PETFINDER_ACCESS_TOKEN
environment variable to Authorization
header of any request that's sent to the Petfinder API.
Also, you can now remove the console.log({ PETFINDER_ACCESS_TOKEN })
line from the <HomePage />
page component's getStaticProps()
function.
(pages/index.tsx
)
// ...
const { NEXT_PUBLIC_PETFINDER_API_URL, PETFINDER_ACCESS_TOKEN } = process.env;
export const getStaticProps: GetStaticProps = async () => {
let types: AnimalType[] = [];
try {
({ types } = await (
await fetch(`${NEXT_PUBLIC_PETFINDER_API_URL}/types`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${PETFINDER_ACCESS_TOKEN}`,
},
})
).json());
if (types.length > 0) {
types = await Promise.all(
types.map(async (type) => {
const { blurhash, img } = await getPlaiceholder(
ANIMAL_TYPES[type.name].image.url
);
return {
...type,
id: (type._links.self.href.match(/\/types\/([\w-]+)$/) || "")[1],
blurhash,
img: {
...img,
objectPosition:
ANIMAL_TYPES[type.name].image.styles?.objectPosition ||
"center",
},
};
})
);
}
} catch (err) {
console.error(err);
}
return {
props: {
types,
},
};
};
// ...
(pages/types/[type].tsx
)
// ...
const { NEXT_PUBLIC_PETFINDER_API_URL, PETFINDER_ACCESS_TOKEN } = process.env;
// ...
export const getStaticProps: GetStaticProps<
PageProps,
StaticPathParams
> = async ({ params }) => {
let adoptedAnimals: Animal[] = [],
breeds: AnimalTypeBreed[] = [],
type!: AnimalType;
let { type: typeParam } = params as StaticPathParams;
try {
({ type } = await (
await fetch(`${NEXT_PUBLIC_PETFINDER_API_URL}/types/${typeParam}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${PETFINDER_ACCESS_TOKEN}`,
},
})
).json());
({ breeds } = await (
await fetch(
`${NEXT_PUBLIC_PETFINDER_API_URL}/types/${typeParam}/breeds`,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${PETFINDER_ACCESS_TOKEN}`,
},
}
)
).json());
({ animals: adoptedAnimals } = await (
await fetch(
`${NEXT_PUBLIC_PETFINDER_API_URL}/animals?type=${typeParam}&status=adopted&limit=5`,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${PETFINDER_ACCESS_TOKEN}`,
},
}
)
).json());
} catch (err) {
console.error(err);
}
return {
props: {
type: {
...(type || {}),
id: typeParam,
breeds,
},
adoptedAnimals,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
let paths: TypePath[] = [];
try {
const { types }: AnimalTypesResponse = await (
await fetch(`${NEXT_PUBLIC_PETFINDER_API_URL}/types`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${PETFINDER_ACCESS_TOKEN}`,
},
})
).json();
if (types.length > 0) {
paths = types.map((type) => ({
params: {
type: (type._links.self.href.match(/\/types\/([\w-]+)$/) || "")[1],
},
}));
}
} catch (err) {
console.error(err);
}
return {
paths,
fallback: false, // Return a 404 page for a non-existent type.
};
};
// ...
By passing the access token as an environment variable to the Next.js application, the Next.js application now makes ten fewer requests.
Like with any solution, this approach does come with a caveat. Since an access token from the Petfinder API expires one hour from the time it was issued, a caveat of this approach is that you will have to reset the development server every hour to refresh the access token.
Exporting the Next.js Application to Static HTML#
To export the Next.js application to static HTML, we must add an export
npm script to the package.json
file that:
Builds an optimized, production-ready version of the Next.js application (
next build
).Using the build, generates an HTML file for each page of the Next.js application and outputs it to an
out
directory (next export
).
(package.json
)
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"lint": "next lint",
"export": "npm run build && next export"
}
}
Add a new rule named export_html
to the Makefile
that runs the export
npm script with the PETFINDER_ACCESS_TOKEN
environment variable:
(Makefile
)
# ...
export_html:
@(PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) npm run export)
Note: Remember, export
is already a GNU make
directive. Therefore, you cannot name the rule export
.
When you run the make export_html
command, you will find that the Next.js application could not be exported as static HTML because it makes use of the image optimization feature, which requires a Node.js server.

To resolve this problem, we need to set experimental.images.unoptimized
to true
in the Next.js configuration to disable the image optimization feature. Specifically, we only want to disable this feature when the NEXT_EXPORT
environment variable is present. The NEXT_EXPORT
environment variable will only be set when exporting the Next.js application to static HTML.
(Makefile
)
# ...
export_html:
@(PETFINDER_ACCESS_TOKEN=$(PETFINDER_ACCESS_TOKEN) NEXT_EXPORT=1 npm run export)
(next.config.js
)
const { withPlaiceholder } = require("@plaiceholder/next");
const { NEXT_EXPORT } = process.env;
/**
* @type {import('next').NextConfig}
*/
const config = {
reactStrictMode: true,
experimental: {
externalDir: true,
images: { allowFutureImage: true, unoptimized: !!NEXT_EXPORT },
},
images: {
domains: [
"images.unsplash.com",
"via.placeholder.com",
"photos.petfinder.com",
"dl5zpyw5k3jeb.cloudfront.net",
],
},
};
module.exports = withPlaiceholder(config);
When you re-run the make export_html
command, the Next.js application will be exported to static HTML.

Inside the project directory, you will find the exported HTML in an out
directory:

To test the static pages, you can spin up a standalone HTTP server that serves the contents of the out
directory:
$ npx http-server out

If you visit http://localhost:8080/types/horse
in a browser and disable JavaScript, then you will see that this page has already been pre-rendered at build time.
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 dive into building interactive, client-side features for the Next.js application.
If you want to learn more advanced techniques with TypeScript, React and Next.js, then check out our Fullstack React with TypeScript Masterclass:
