In a recent project, we learned about Next.js’ data rendering methods the hard way — through slow page loads, routing nightmares, and revalidation migraines. After battling through the pitfalls of SSR, SSG, ISR and ODR, we devised a custom solution that gave us the best of all: pre-rendered HTML, fast page loads, dynamic data, no backend changes. In this post, we share our hard-earned knowledge so you can skip the pain and get to the gain.
Next.js offers a couple of ways to handle having content managed separately from the website code (using a CMS):
- Server-Side Rendering (SSR)
- Static Site Generation (SSG)
- Incremental Static Regeneration (ISR)
- On-Demand Revalidation (ODR)
Server-Side Rendering (SSR) is when the Next.js server fetches the content for the page being requested at the moment when someone is visiting the page. This means the page will take longer to display, there will be more load on the CMS server, but people will always see the latest content. (There can be caching mechanisms to help with this).
Static Site Generation (SSG) moves all the content fetching to the build stage. The Next.js server fetches all the content necessary for all the pages, builds the website and stores everything. When someone visits the website the Next.js server simply returns what it stored. This means pages are fast, there isn’t a lot of load on the CMS server (only during builds), but people will see the content as it was the last time the website was built.
Incremental Static Regeneration (ISR) tries to solve the issues of SSG by having an option to refresh the content after the website is built. When it is enabled, the Next.js server still returns what it stored, but in the background it refreshes and updates the content. The downside is only the next person will see the updated content (since the person that triggered the content refresh still sees the outdated content).
On-Demand Revalidation (ODR) improves on ISR by providing an API to trigger content updates. So instead of relying on people visiting the page, we can ping the Next.js server when a piece of content is updated (in the CMS for example). The downside of this approach is that it requires serverless functions (instead of a simple static server) and requires configuring the CMS to trigger those endpoints.
Having this understanding of the options, here’s our problem description:
We want to update the content of the website, up to once a day, without having to configure a lot of things in the CMS, and we don’t want to rely on visitors, since they might not visit the specific pages that are updated.
If at this point you are thinking we just need a cron, you are right. At least half right. Because the second issue is we don’t want to be making unnecessary deployments (for billing reasons), so we need a way to know if the content was updated and only deploy if yes.
Our approach turned out to be quite simple, even if getting there was quite complex. Using a cron in Github Actions, we build the website once a day and create a hash with the files where the CMS content is stored: the .json
files inside .next/server/pages
(we need to ignore any .nft.json
files as those are for Node File Tracing).
By creating a hash with the file contents we can compare it with the hash from the previous day and only trigger a deployment if they are different. We can store the hash in a file in the public
folder, so that if there is a manual deployment throughout the day, we can still have a hash to compare to.
We could optimise this approach by hashing the CMS content directly and skipping the website build, but that requires maintaining a list of all the content being used. The method we ended up with is hopefully zero maintenance as it relies on the same build process that is used to deploy the website.
How would you solve this problem? Let us know on twitter @whitesmithco
The Non-Technical Founders survival guide
How to spot, avoid & recover from 7 start-up scuppering traps.
The Non-Technical Founders survival guide: How to spot, avoid & recover from 7 start-up scuppering traps.