Juji Blog

Working With Contentful's Webhook and NextJs's fetch

#cache#contentful#nextJs#webhook
| updated at

I created this NextJs blog, using Contentful as CMS. I found things that i should have remembered, but i forgot. This is a note to remind myself How NextJs and Contentful work with each other. Also, there's a little bit about Algolia.

NextJS and Fetch caching

NextJs caches request. We know this. Now, how do we use this to our advantage? One, is to revalidate requests every some period of time. The second one, is to use request tag and webhook. Let's see how they can help us.

Revalidate Each Request

One way, is to revalidate all request with a sane period of time. Say 15 minutes? To do that, we can configure the fetch function. For example:

1function getPost(slug:string){
2
3 return await fetch(
4 `... your URL`,
5 { next: { revalidate: 900 } }
6 )
7
8}

That should do it. It will be enough for most blog. But what if i have more time to do this? Ideally, i want each cache to be revalidated after the content have been updated, that should remove unnecessary calls to Contentful. That's where tag comes in

Adding Tag To Each Request and Using Webhook

Instead of using time to revalidate our request, we can use tag(s) for each request. and revalidate them when things are updated.

1function getPost(slug:string){
2
3 return await fetch(
4 `... your URL`,
5 { next: { tags: [`post/${slug}`] } }
6 )
7
8}

Next, in our webhook, we can revalidate the tag. So the in next call, it will actually make the request.

1// somewhere in your API
2
3revalidateTag(`post/${slug}`)

I think this is a better option, NextJs will cache all our request, and we can remove the cache whenever we need it. This is called On Demand Revalidation. The request will hit Contentful's server after we revalidate the tag.

Now, to handle the webhook from Contentful, we need to create the handler on NextJs. Checkout this example POST handler in NextJs:

1import {
2 getPostById,
3 getDraftById
4} from '@/lib/content/contentful/fetch'
5import { revalidateTag } from 'next/cache';
6
7import { WEBHOOK_SECRET } from '@/lib/constants';
8
9export async function POST(request: Request) {
10
11 const topic = request.headers.get('X-Contentful-Topic')
12 // topic === 'ContentManagement.Entry.publish'
13 // topic === 'ContentManagement.Entry.unpublish'
14 // topic === 'ContentManagement.Entry.auto_save'
15
16 const secret = request.headers.get('X-JUJI-WEBHOOK')
17 if(secret !== WEBHOOK_SECRET) return Response.error()
18
19 const data = await request.json();
20
21 let slug = ''
22 let tags:any[]|null = null
23
24 // request content when it's not available
25 if(!data.fields?.slug || !data.metadata?.tags){
26
27 // get full data from post || draft
28 const [ post, draft ] = await Promise.all([
29 getPostById(data.sys.id),
30 getDraftById(data.sys.id),
31 ])
32
33 // draft will always have the content,
34 // but post sometimes not
35 slug = post.fields?.slug || draft.fields?.slug
36 tags = post.metadata?.tags || draft.metadata?.tags
37
38 }else{
39
40 slug = data.fields?.slug['en-US']
41 tags = data.metadata?.tags
42
43 }
44
45 if(
46 topic === 'ContentManagement.Entry.unpublish' ||
47 topic === 'ContentManagement.Entry.publish'
48 ){
49
50 // revalidate the pages
51 revalidateTag(`post/${slug}`)
52 revalidateTag(`home`)
53
54 // and the tag
55 tags && tags.forEach((tag:any) => {
56 revalidateTag(`tag/${tag.sys.id}`)
57 });
58
59 }
60
61 if(
62 topic === 'ContentManagement.Entry.auto_save'
63 ){
64 revalidateTag(`draft/${slug}`)
65 revalidateTag(`draft`)
66 }
67
68 return Response.json({ ok: true })
69
70}

Sometimes, Contentful only sends us partial data: not containing fields and metadata keys. I'm only seeing this when i update the post status to unpublish. But i'll get the data from Post and Draft that way, so i can sleep easy.

The request getPostById and getDraftById should not be cached. We can use { cache: 'no-store' } for this.

Don't forget to use a secret. We don't want anyone to be able to do this to our blog.

Contentful's Webhook to NextJS

Setting up Webhook in Contentful is easy. Go to "webhook" in the settings menu:

Contentful Webhook Setting

Setup the name, URL, and Configure the triggers to only send events for Autosave, Publish and Unpublish on the Entry row. Like this:

Select specific triggering events

In the Header section, set the secret header however you like:

Set secret header

That's it.

Contentful's Webhook to Algolia

In the webhook to algolia, only send the data you want the user to search. Sending the whole data will cause errror from Algolia, saying the data is too big. Checkout this example payload:

1{
2 "metadata": "{ /payload/metadata }",
3 "fields": {
4 "title": "{ /payload/fields/title }",
5 "slug": "{ /payload/fields/slug }",
6 "description": "{ /payload/fields/description }",
7 "excerpt": "{ /payload/fields/excerpt }",
8 "updatedAt": "{ /payload/fields/updatedAt }"
9 },
10 "sys": {
11 "createdAt": "{ /payload/sys/createdAt }"
12 }
13}

To bad we can't have the metadata field to only show the tag content. Set it up in the payload section:

payload section

In case you're wondering, why is it dark? I'm using Firefox, with the UltimaDark extension.