This blog is powered by NextJS and Contentful, as explained in this post. I won't be covering how to start fetching data via Contentful as both NextJS and Contentful have articles on how to do this. I will however just cover the Static Site Generation (SSG) side of things, as well as how to keep the data fresh.


To fetch data from Contentful, I originally wrote this blog using getServerSideProps. This meant that I wasn't actually getting the full benefits of NextJS from an SEO perspective. Yes, this blog was being pre-rendered so looked good to a crawler, but page-speed wasn't as high as it could have been - I really should have been using getStaticProps, which would have meant that the blog was just statically generated blob of HTML!
There is a time and place for getServerSideProps though, e.g. if your page needs to know something about the current request at the time of render. E.g., if your page does something based off of a user's location, or if auth information needs to be considered for the rendering of the page, then getServerSideProps is the way to go.


I don't need to know anything about you before this page needs to be rendered, so I only need to be using getStaticProps. Utilising this made navigating through the blog lightning fast, but it did leave an issue: what happens if I update a post? We'll get on to how to solve that a little later.
Implementing getStaticProps is relatively easy. One needs to simply export the function as is shown in the contrived example below
function Blog ({articles, page, perPage}) { return <></>; }; export const getBlog = async (isProd: boolean) => { return { ... } }; export const getStaticProps = async () => { const { items, skip, limit } = await myMethodToGetContentfulArticles(isProd); return { props: { articles: items, page: Math.floor(skip / limit), perPage: limit }, }; }; export default Blog;
This works quite well for a page with a static URL (e.g. /blog). However, for my posts themselves, we need to also use getStaticPaths!


When dealing with dynamic routes, we need to tell NextJS which pages to actually generate, which is where getStaticPaths comes in. My blog posts follow the path /blog/read/[slug], so in [slug].tsx, I would also need to add something like the following
export const getStaticPaths = async () => { const { items } = await myMethodToGetContentfulArticles(); const paths = => { const { fields: article } = item; // Field in contentful for what my slug should be ... slug return { params: { slug: article.slug } }; }); return { paths, fallback: true }; };
There are three values for fallback, true, false and blocking. I chose true because I wanted NextJS to still render blog posts that hadn't been cached, as opposed to generating a 404 page.
There you have it - now your site is SSG rendered... only there's one issue. How do you deal with updated content?

On Demand Incremental Site Revalidation (ISR)

As of NextJS version 12.2.0, On Demand ISR is stable and was the way I chose to keep my content updated. I knew Contentful allows for webhooks, so I got cooking a solution. I navigated to /spaces/<space-id>/settings/webhooks and created a new webhook.
Contentful Settings
The webhook URL I chose was the one suggested, i.e. /api/revalidate.
Under Triggers, I opted to only fire webhooks for specific triggering events, just Publish and Unpublish for Assets as that's all that I needed.
I added some headers for auth and some extras so that I could determine the origin of the request.
I chose to just send as a regular application/json content type.
Most importantly for me, however, I chose to send a customised payload so that I only got what I needed:
{ "entryId": "{ /payload/sys/id }", "slug": "{ /payload/fields/slug }", "tags": "{ /payload/fields/articleTags }", "contentType": "{ /payload/sys/contentType/sys/id }" }
Revalidate endpoint
Create a new route in /pages/api/revalidate.ts, or whatever endpoint you decided to go for. The following, save for removing my personal code, should be enough to get your site revalidated when updating an entry on Contentful.


import { NextApiRequest, NextApiResponse } from "next"; type ContentfulWebhookBody = { entryId: string; tags: { ['en-US']: string[] }; slug: { ['en-US']: string }; contentType: string; }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const secret = ''; const origin = ''; // Don't forget to put NEXT_REVALIDATE_KEY in your production env file / manager if (secret !== process.env.NEXT_REVALIDATE_KEY) { return res.status(401).json({ message: 'Invalid token' }); } const body: ContentfulWebhookBody = req.body; const revalidatePaths: string[] = []; switch (origin) { case 'contentful': const topic = req.headers['X-Contentful-Topic']; // Change article to whatever your blog posts entry name is in Contentful if (body.contentType !== 'article') { return res.status(200).send({}); } if (topic === 'ContentManagement.Entry.unpublish') { // Contentful only sends the ID of a deleted entry, which I do not use. Simply regenerate the blog's landing page revalidatePaths.push(`/blog`); } else { const relativePathToArticle = methodToDetermineRelativePath(body); revalidatePaths.push(relativePathToArticle); revalidatePaths.push('/blog'); } break; default: return res.status(404).send({ error: `Unknown origin ${origin}` }); } try { for (const revalidatePath of revalidatePaths) { await res.revalidate(revalidatePath); } return res.status(200).json({ revalidated: true }); } catch (err) { return res.status(500).send({ error: 'Error revalidating' }); } }
Note that res.revalidate(revalidatePath) needs to be a completed relative URL, i.e. for me, passing /blog/read/[slug] wouldn't work!
And that it's!