Incremental Static On-Demand Revalidation
Luke Kumar-Jan 05, 2024
A brief summary of NextJS features (using the Pages routing system):
NextJS lets you render React Pages on the Server (Server-side rendering)
This means that any assets (Images, fonts, data from APIs) will be loaded on the Next server, and delivered to the client's browser as a fully loaded page. This is better for SEO than loading Data within the page's main react component, as search engine crawlers will find more meaningful content when they load your page (not just the initial react javascript that must execute to render HTML).
You can either do this rendering on-demand every time that a page is loaded, or you can pre-build a page so that all of its assets are ready, and the html can be immediately sent upon request. The latter strategy makes sense for pages that don't frequently change. It's the best configuration for SEO, as search engine crawlers will come across a fully ready HTML page. It's also ideal for visitors to your website, as page load time is fastest this way.
With Next, you can decide how each individual page will be rendered: with assets fetched on-demand upon request by a visitor to your site, or with all assets pre-fetched, and the page fully built and cached in server memory when the website code is compiled for deployment.
When you compile a NextJS project for production, all of the static pages are pre-built and cached in memory. They are compiled at build time.
Two server-side rendering strategies
1. Fetch props for page when the project is built:
// [page].jsx
export const getStaticProps = async ({ params }) => {
try {
const slug = params.slug.toString();
const resource = await api.getResource(slug);
return { props: { resource }};
} catch (e) {
throw e;
}
};
const page = ({ resource }) => {
return <>{resource.text}</>
}
The getStaticProps constant gives our NextJS server instructions on how to retrieve the data for this page. It will fetch the resources and compile the page when the project is built, so that the page is ready to serve whenever a client requests it.
2. Fetch props for page when the page is requested:
// [page].jsx
export const getServerSideProps= async ({ params }) => {
try {
const slug = params.slug.toString();
const resource = await api.getResource(slug);
return { props: { resource }};
} catch (e) {
throw e;
}
};
const page = ({ resource }) => {
return <>{resource.text}</>
}
The only difference between this block of code and the previous one is the constant name - here, it's getServerSideProps; this tells your NextJS server to fetch any resources for this page upon request by a client, then render the page with these resources, and then serve it to the client.
The fastest page load times (and best SEO optimization) will come with option #1; but option #2 guarantees the most up-to-date data will be served to the client, because the data is re-fetched every time the page is requested.
So, which strategy you choose will depend on the function of your page.
Strategies for this site
Basically, this site uses these strategies as follows:
Client-facing blog posts and quizzes use the first strategy; since these posts are the main focus of this section, it makes sense to prioritize the user experience with optimal load times.
Editing blog posts and quizzes as admin uses the second strategy; for myself as admin, I don't care about waiting an extra half-second-to-few-seconds for load time. Also, when editing a post, it's important that it be the latest version.
Updating edited posts
If we never edit the client-facing blog posts, then we don't need to take any additional steps; the pages that are built when the project is compiled will always be the same. However, if we update posts, we need these updates to be reflected in the pages cached on the server. To achieve this, we can combine our compile-time rendering with Incremental Static Regeneration. This is a strategy where the server rebuilds pages to reflect updates, either at regular intervals or on demand .
Here's a page that will be re-built when a client requests it, if the request happens more than 60 seconds after the last time it was built:
export const getStaticProps = async ({ params }) => {
try {
const slug = params.slug.toString();
const resource = await api.getResource(slug);
return {
props: { resource },
revalidate: 60
};
} catch (e) {
throw e;
}
};
The revalidate property in our return object tells our server that this page should be rebuilt if a request is made 60 seconds after the previous rebuild. So, if client A requests the page Monday morning, and the next client, B, requests it Monday afternoon, it will be rebuilt when client B requests it.
On-Demand revalidation
This is more complex. We make use of NextJS's api routes, which represent server endpoints on our NextJS server. These endpoints export a handler function which takes a request and response object. The response objects on these functions have access to a next method, revalidate; when you call this method, and pass it a route on your website, it tells your server to look up that route's getStaticProps method, and use it to rebuild the page.
So, for the blog posts on this site, the process is: =>1. A post is edited in the browser =>2. The post is updated in the database =>3. We send a request to our NextJS server, telling it to revalidate the page for that post =>4. The server runs the method getStaticProps that this page uses, and rebuilds the page with the newest data
Here are those steps with relevant code:
1. A post is edited in the browser
Here's how we send the edited post to the server:
const response = await api.post(`/blog/edit/${blogID}`, postData);
2. The post is updated in the database
// express server
const updateResponse = await BlogModel.findByIdAndUpdate(blogId, postData);
3. We send a request to our NextJS server, telling it to revalidate the page for that post
// express server
await axios.get(`${env.WEBSITE_URL}/api/revalidate-post/${slug}?secret=${env.POST_REVALIDATION_KEY}`);
4a. The NextJS server receives this request, and after verifying that it's legitimate, calls the code to revalidate
// pages/api/revalidate/[blog].ts
await res.revalidate(`/blog/${slug}`);
4b. The server runs the method getStaticProps that this page uses, and rebuilds the page with the newest data
// pages/blog/[slug].tsx
export const getStaticProps = async ({ params }) => {
try {
const blog = await fetchBlog(params.slug);
return { props: { blog }};
} catch (e) {
throw e;
}
};
And that's it; that last strategy is pretty complex, but if you can understand it, you'll have a pretty solid grasp of the Next pages routing system.
For more reading, check out the NextJS docs on ISR: https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration