On demand ISR with Next.JS and Sanity
Static sites are great, right? You get excellent performance and can make the most out of CDNs and caching. Frameworks like Next.JS have made it quite easy to create and deploy static content, and you can mix it up with SSR (server side rendering) when needed.
SSG (static site generation) has traditionally had a pretty big downside though - when you need to update something, you have to rebuild the whole thing all over again. With Next.JS though, we can mitigate that problem with ISR (Incremental Static Regenaration). Basically, ISR adds functionality to regenerate specific pages, and version 12.1 of Next brought us the highly requested on demand ISR.
This means a static Next site that fetches content from a CMS can now get updated on demand, (re)building only the parts that actually need to change.
So I decided to investigate how I could update a static site connected to Sanity CMS. For those of you who don't know, Sanity is an open-source CMS - it's flexible and pretty easy to get started with, and a great choice if you want to just host some content.
On demand revalidation
First, here's the example for ISR in the Next.JS docs:
The idea here is that you can send a request to a
api/revalidate endpoint, and then call the
revalidate method on the response object, passing in the path you want to update. Pretty simple right?
Sanity custom actions
So how do we call that
revalidate endpoint from Sanity?
However, you might need a little more flexibility and control - for instance you might not want to trigger rebuilds on every single update. Or maybe you need to send requests to different frontends at different times (like when you have a staging site and a production site).
In these cases, you can use custom Sanity actions. Basically, these let you add buttons to the studio editor.
You can find a good starter on adding actions in Sanity's docs here, so I'll skip to the action itself:
First, we check what the document type is - in this case I only want this action to apply to
"movie" documents. When the button is clicked, the
onHandle function runs, and we pass in the document slug to a
This sends the request with the updated path to our frontend's
revalidate endpoint. Success!
No, it's not so simple
In most cases one updated piece of content will require other changes elsewhere. Imagine a static site about movies - the homepage will show links to each movie's page, and on that page there's a list of the movie's cast, who also have their own page:
Fig.1: complex diagram clearly demonstrates how I could have been an architect
If we update Movie #1's name, we'll need to rebuild its page and the homepage, or else it would still show the old name. And if we update Person A's name, we need to also update the page of any movie that mentions them.
This will always depend on how your site is structured, but in this case we can deal with this logic within the action we just created:
We're simply passing the movie's path and the
/ homepage path. We then have to deal with multiple paths on revalidation endpoint:
So what about updates to Person pages? For that, we need a new action with a little more logic:
To know what movies mention the updated person, we need to query Sanity. And if you're wondering what on earth this is:
it's GROQ, Sanity's custom query language. I would probably just use GraphQL, but I thought it would be interesting to try out GROQ for this demo.
Anyway, the query returns an array of movies that refer to the person. So we just get those slugs and add the current person slug, and pass it all to
revalidatePath. And now we can update all the affected pages with just one click. This kind of logic will really be specific to each case - you should also change what paths you update based on the specifc data that was changed - an update to the person's bio might not require rebuilding pages that only contain their name.
Adding new data
Does this work when creating new pages? Yes, but there's an important detail you might miss. In the movie page scenario, I'm using
fallback is false, it won't work - the result of
getStaticPaths would be the same as at build time, when the new content didn't exist. Setting it to
true ensures it gets called again.
Some things to consider
With that, we have a much smarter way of updating static pages. Still, there are some things you should consider before you start ISRing everything:
- Serverless function limitations. There is usually some execution timeout on these (you can check out Vercel's here), so there's a limit to how many paths you can revalidate.
- Are you using a cached endpoint? For instance, Sanity provides a live endpoint and a
cdnversion. If you use the
cdnendpoint in your build, it might not get fresh data when revalidating.
- You will need to setup CORS for your endpoint to accept the requests once it's deployed. There's a guide on how to do that here.
- Don't do this in the
This was my first approach, and though it worked fine in local builds, when I deployed it to Vercel it would just fail. Basically you have to revalidate the paths sequentially, so
Promise.all is not an option.
On demand ISR is still a pretty recent feature, and I wouldn't be surprised if it changed somewhat in the near future. For now though, it looks like it could make static sites a lot easier to manage, and cut down on build times.
Next step: actually implementing it on this blog :)