This is a contribution from Leigh Halliday. Leigh recently created a memory project using Hygraph and Next.js, covering Static Site Generation & API Routes using a headless CMS.
In his video, see how to use getStaticPaths
and getStaticProps
alongside Hygraph, 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 project
Hygraph 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 Hygraph 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 Hygraph through the exposed mutations.
Source code can be found on GitHub.
#Our Data Model
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" }) {idslugtitledatedescriptionimage {url}memories(last: 10) {namestory}}}
#Dynamic Static Site Generation
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 Hygraph, 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].tsxconst client = new GraphQLClient(process.env.NEXT_PUBLIC_HYGRAPH_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 Event
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].tsximport { 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 }) {idslugtitledatedescriptionimage {url}}}`;const data: { event: IEvent | null } = await client.request(query, { slug });// Handle event slugs which don't exist in our CMSif (!data.event) {return {notFound: true,};}// Convert the Markdown into a compiled source used by MDXconst source = await serialize(data.event.description);// Provide Props to the Page Componentreturn {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 Hygraph 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 Hygraph 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 Event
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].tsximport { 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 Data
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].tsximport useSWR from "swr";const fetchMemories = async (id: string) => {const query = gql`query Memories($id: ID!) {memories(where: { event: { id: $id } }) {idnamestory}}`;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 Form
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 Hygraph 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:
- User enters data into form.
- Form data is submitted to
/api/memories/add
. - This serverless function receives the form data and prepares variables for the mutation.
- The form data is then sent to Hygraph 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/addconst 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}><inputtype="text"requiredname="name"placeholder="Your name"value={name}onChange={(event) => setName(event.target.value)}/><textareaname="story"value={story}placeholder="Your story"onChange={(event) => setStory(event.target.value)}/><button type="submit">Add</button></form>);}
#API Routes and Mutations
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 Hygraph.
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 Hygraph 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: $namestory: $storyevent: { connect: { id: $eventId } }}) {id}}`;const client = new GraphQLClient(process.env.NEXT_PUBLIC_HYGRAPH_URL, {headers: {Authorization: `Bearer ${process.env.HYGRAPH_MUTATION_TOKEN}`,},});await client.request(mutation, variables);res.status(200).json({ success: true });};
After the Memory is created in Hygraph, 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.
#Conclusion
By combining powerful technologies such as Hygraph, 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 Hygraph via a GraphQL Mutation.
Blog Author