Next.js with GraphCMS: Static Site Generation & API Routes

In this example we will integrate GraphCMS with Next.js, taking advantage of SSG for an incredibly performant experience, client-side data loading using SWR.

Leigh Halliday
Leigh Halliday
Working with Next.js and GraphCMS

This is a contribution from Leigh Halliday. Leigh recently created a memory project using GraphCMS and Next.js, covering Static Site Generation & API Routes using a Headless CMS.

In his video, see how to use getStaticPaths and getStaticProps alongside GraphCMS, additionally creating an API Route which will create new data inside of the CMS using GraphQL Mutations.

Fork the repo and build your own example as you follow along!

About the projectAnchor

GraphCMS is headless CMS which exposes your data through a GraphQL API. Immediately I thought of it as read-only, querying data that had been entered through their interface, but that's not the case. You can also perform mutations on your data, allowing users to create content through your own application, while still controlling publishing and approval flows yourself.

In this example we will integrate GraphCMS with Next.js, taking advantage of Static Site Generation (SSG) for an incredibly performant experience, client-side data loading using SWR for rapidly changing data, and API routes which securely create content within GraphCMS through the exposed mutations.

Source code can be found on GitHub.

Our Data ModelAnchor

The example we will be working with today contains two models: Events and Memories. An Event represents a historical event (Obama becoming the first African American president, England winning the World Cup in 1966), where each of these events can have many Memories from people telling their story.

If I were to query the data through GraphQL it may look like:

query Event {
event(where: { slug: "england-world-cup-1966" }) {
id
slug
title
date
description
image {
url
}
memories(last: 10) {
name
story
}
}
}

Dynamic Static Site GenerationAnchor

Static Site Generation isn't typically paired with the word dynamic, but that is exactly what this is. We must first export a function called getStaticPaths which will allow us to tell Next.js which Events to pre-render during build-time.

The goal of this function is to query the Events from GraphCMS, returning them as the paths to be used later inside of getStaticProps which we'll cover later. I have used graphql-request to perform the GraphQL query, just asking for each event's slug.

The URL (endpoint) is being stored inside of an ENV variable, which locally lives inside .env.local and is not committed to the repository. Wherever you end up deploying your application will provide you a place to enter these ENV variables into that environment.

// pages/events/[slug].tsx
const client = new GraphQLClient(process.env.NEXT_PUBLIC_GRAPHCMS_URL);
export const getStaticPaths: GetStaticPaths = async () => {
const query = gql`
query Events {
events {
slug
}
}
`;
const data = await client.request(query);
return {
paths: data.events.map((event) => ({ params: { slug: event.slug } })),
fallback: "blocking",
};
};

The fallback: "blocking" attribute signifies to Next.js that if a request is made to a path NOT returned from this function, it should be generated on the fly in a blocking manner and then cached for future requests. This would allow you to only pre-render your most popular pages, handling the remaining ones in an on-demand basis.

Statically Generate Each EventAnchor

If getStaticPaths has returned 10 events to generate pages for, this function will be called once for each. getStaticProps receives the params returned from getStaticPaths, and has the job of loading the data which will then be passed as props to the actual Page Component.

// pages/events/[slug].tsx
import { serialize } from "next-mdx-remote/serialize";
export const getStaticProps: GetStaticProps = async ({ params }) => {
const slug = params.slug as string;
const query = gql`
query Event($slug: String!) {
event(where: { slug: $slug }) {
id
slug
title
date
description
image {
url
}
}
}
`;
const data: { event: IEvent | null } = await client.request(query, { slug });
// Handle event slugs which don't exist in our CMS
if (!data.event) {
return {
notFound: true,
};
}
// Convert the Markdown into a compiled source used by MDX
const source = await serialize(data.event.description);
// Provide Props to the Page Component
return {
props: { event: { ...data.event, source } },
revalidate: 60 * 60, // Cache response for 1 hour (60 seconds * 60 minutes)
};
};

There are a few special things happening in this code. The first is that the GraphQL query we are sending to GraphCMS contains a variable: await client.request(query, { slug });. The slug is passed in so that we can find the matching Event.

The second special thing is that our description field doesn't contain just regular text, it is actually Markdown which we will compile into a source that can be used by MDX to render it as HTML (or any custom component you pass to the MDX renderer). Normally MDX exists locally as a static file, but in this case we are using next-mdx-remote that allows us to use MDX content with getStaticProps inside of Next.js. We'll see how to render this later.

Lastly, I have defined a TypeScript interface called IEvent which is used to let our code know what to expect back from GraphCMS and to be passed into our Page Component.

interface IEvent {
id: string;
slug: string;
title: string;
date: string;
image: {
url: string;
};
description: string;
source: { compiledSource: string };
}

Rendering The EventAnchor

It is now quite simple to render the event inside of our page level component. It receives the event data as a prop, and then we can embed it into the function's response. Here we are using the MDXRemote component to render the compiled Markdown.

// pages/events/[slug].tsx
import { MDXRemote } from "next-mdx-remote";
export default function Event({ event }: { event: IEvent }) {
return (
<main>
<h1>{event.title}</h1>
<h2>{event.date}</h2>
<img src={event.image.url} alt={event.title} />
<div>
<MDXRemote {...event.source} />
</div>
<Memories eventId={event.id} />
<NewMemory eventId={event.id} />
</main>
);
}

We haven't touched on Memories and NewMemory yet, but those are coming next!

Client Side Querying DataAnchor

We'll be using SWR to load the event's memories on the client after the initial page load. The useSWR hook typically receives two parameters: The key, which in our case is the eventId, and then a fetcher function which will load the data.

Thankfully, graphql-requst works both client-side and server-side, so we can reuse the same client variable from before to perform the GraphQL Query.

// pages/events/[slug].tsx
import useSWR from "swr";
const fetchMemories = async (id: string) => {
const query = gql`
query Memories($id: ID!) {
memories(where: { event: { id: $id } }) {
id
name
story
}
}
`;
return client.request(query, { id });
};

Once we have the data back from useSWR, after checking to make sure data exists (it won't when there is either an error or is still loading the memories), we can render them out as blockquotes.

function Memories({ eventId }: { eventId: string }) {
const { data } = useSWR(eventId, fetchMemories);
if (!data) return null;
return (
<div>
<h2>Memories</h2>
{data.memories.map((memory) => (
<blockquote key={memory.id}>
{memory.story} - {memory.name}
</blockquote>
))}
</div>
);
}

Submit New Memory FormAnchor

We've loaded memories, but how do new memories actually get created? It all starts by rendering a form that will collect a name and story. When the user submits the form we will send this data to a serverless function defined at /api/memories/add within our Next.js app.

The reason we are doing it this way (rather than calling the mutation directly) is that mutating data inside of GraphCMS requires additional permissions, and a special API Token which we don't want to expose publicly to our users. The flow of the data looks like this:

  1. User enters data into form.
  2. Form data is submitted to /api/memories/add.
  3. This serverless function receives the form data and prepares variables for the mutation.
  4. The form data is then sent to GraphCMS via a GraphQL mutation.
function NewMemory({ eventId }) {
const [name, setName] = useState("");
const [story, setStory] = useState("");
// Send the form data to our API route /api/memories/add
const onSubmit = async (event) => {
event.preventDefault();
await fetch(`/api/memories/add`, {
method: "POST",
body: JSON.stringify({ name, story, eventId }),
headers: {
"Content-Type": "application/json",
},
});
setName("");
setStory("");
};
return (
<form onSubmit={onSubmit}>
<input
type="text"
required
name="name"
placeholder="Your name"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<textarea
name="story"
value={story}
placeholder="Your story"
onChange={(event) => setStory(event.target.value)}
/>
<button type="submit">Add</button>
</form>
);
}

API Routes and MutationsAnchor

Within our serverless function, we can receive the data sent from the form and use this data as the variables within the mutation that will be called to create a new memory within GraphCMS.

Something important to keep in mind is that special permissions are required to perform a mutation. You will have to create a Permanent Auth Token within your GraphCMS settings and ensure that it has read, update, and read versions permission on the Event model, and read, create, and read versions on the Memory model.

export default async (req: NextApiRequest, res: NextApiResponse) => {
const variables: { eventId: string; name: string; story: string } = req.body;
const mutation = gql`
mutation CreateMemory($eventId: ID!, $name: String!, $story: String!) {
createMemory(
data: {
name: $name
story: $story
event: { connect: { id: $eventId } }
}
) {
id
}
}
`;
const client = new GraphQLClient(process.env.NEXT_PUBLIC_GRAPHCMS_URL, {
headers: {
Authorization: `Bearer ${process.env.GRAPHCMS_MUTATION_TOKEN}`,
},
});
await client.request(mutation, variables);
res.status(200).json({ success: true });
};

After the Memory is created in GraphCMS, it will not be immediately available for users to see on your website. This is because it has been created in a draft state, and won't be seen until it has been published by an admin.

ConclusionAnchor

By combining powerful technologies such as GraphCMS, GraphQL, Markdown and Next.js, we can create an amazing user experience quickly and performantly. This example showed how to generate Event pages using dynamic Static Site Generation in Next.js, loading additional data client-side with SWR, and finally how to use API routes in Next.js to create data in GraphCMS via a GraphQL Mutation.

It's Easy To Get Started

GraphCMS plans are flexibly suited to accommodate your growth. Get started for free, or request a demo to discuss larger projects with more complex needs