Build Blazing Fast Websites with Next.js ISR and Headless CMS
Aditya Kadam Published on: December 29, 2023In the fast-paced world of web development, speed is essential. Users expect lightning-fast page loads, and search engines favour quick-loading sites. With Next.js Incremental Static Regeneration (ISR) you can significantly boost your website's performance.
It's also important to have a smooth content management workflow. A headless CMS is a great option, providing flexibility to use any front-end tech stack. In this article, we'll use Garchi CMS, an easy-to-use SaaS headless CMS.
Let's get started 🚀
1. Garchi CMS setup
Garchi CMS separates content from presentation, ideal for Next.js ISR since we can fetch content via API.
To get started we will need a Garchi CMS account and the API key.
Once the account is created successfully, we get a default space with its id just after the # (we will need it for the api call). Select the default space and then on the top right corner you should see the avatar. Click on that then Subscription and API and at the bottom there should be an option to generate an API token.
In my case, I have already created an API token so that's why in the image you see the text Revoke and Create New Token.
We will put this token inside our Next.js .env file in the next step.
2. Setting up the Next.js project
Garchi CMS has its starter kits. So I will use the Next.js one.
npx @adiranids/garchi-starter-kit -k next
Let's install all the packages that are predefined in the package.json of the starter kit.
npm i
Let's paste the API key generated in step 1 in .env file.
GARCHI_API_URL=https://garchi.co.uk/api/v2
GARCHI_API_KEY=YourApiKey
3. Creating components and connecting them with Garchi CMS.
To have a smooth experience, it is best to keep the component file name inside the project the same as the one created on Garchi CMS. It helps in dynamic import. Any component created on Garchi CMS must exist in the project. More info can be found here
For demonstration, I will show one component example but you can create as many as you want :) All our components will be inside the components/garchi folder.
Most of the components I have created are server components.
Brief.jsx
import { Prose } from '../Prose'
export default async function Brief({ title, content, ...props }) {
return (
<div className="lg:order-first lg:row-span-2">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
{title}
</h1>
<div className="mt-6 space-y-7 text-base text-zinc-600 dark:text-zinc-400">
<Prose markdown={content} />
</div>
</div>
)
}
Prose.jsx
import clsx from 'clsx'
import { cleanHTML } from '@/lib/domcontent'
export function Prose({ markdown, className, ...props }) {
return (
<div dangerouslySetInnerHTML={{ __html: cleanHTML(markdown)}} className={clsx(className, 'prose dark:prose-invert')} {...props} />
)
}
Garchi CMS passes HTML string in the api response so it is essential to sanitise it to avoid XSS attack. For that purpose I have created a utility function inside lib/domcontent.js
import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';
export function cleanHTML(html) {
const window = new JSDOM('').window;
const purify = DOMPurify(window);
return purify.sanitize(html);
}
To make it work we need to install jsdom and dompurify
npm i jsdom dompurify
Adding components in Garchi CMS
All the components that are used on Garchi CMS should be there in the code base.
I will add a Brief component as a section template which I can then use anywhere on my page on Garchi CMS. I have also added same props to it as I have the one in Brief.jsx.
I can then go to the Pages section in Garchi CMS and edit the page to add the Brief component.
4. Server-side functions to fetch data from Garchi CMS.
Choosing the right data fetching strategy is key for web page performance. For frequently updated, dynamic content, Server Side Rendering (SSR) or Static Site Generation (SSG) with incremental updates works well. SSG alone can be used for mostly static pages.
Incremental Static Regeneration (ISR) strikes a balance - it allows fresh content on pages that don't need full SSR. Next.js generates static pages at build time, then automatically updates them incrementally as content changes. This means fast performance without rebuilding the entire site."
The key points are:
- SSR/ISR for dynamic content
- SSG for static content
- ISR balances fresh content without full rebuild
How ISR works in general
-
Create Static Pages Once: When someone visits a page for the first time, a static version of that page is generated and saved.
-
Reuse and Update as Needed: This static page is then reused for other visitors, making it super fast to load. This page can also be updated in the background if the content changes, without you having to rebuild the whole site.
-
Automatic Refreshing: After a set amount of time (which you decide), the server checks if the page needs updating with new content and refreshes the static page if needed.
How to use ISR in Next.js (app router)
With the pages router, we could use getStaticProps with revalidate and getStaticPaths functions (only for dynamic routes) to achieve ISR.
With Next.js app router, there are some strategies we could use for ISR.
Strategy 1: Time-based revalidation
On any page.jsx/tsx file if we add
//value in seconds
export const revalidate = 3600
Next.js will automatically revalidate that page (look for updated content) after a specified interval (in this case 3600 seconds i.e. 1 hour).
Another approach is passing next.revalidate option in the fetch request.
fetch('https://garchi.co.uk/api/items', {
headers: yourHeaders,
next: { revalidate: 3600 }
})
Strategy 2: On-demand revalidation
This is more useful when you want to revalidate a page based on user action or certain system actions or based on certain logic.
You can use this feature inside a server action or route handler using the revalidatePath function (on-demand by path) or the revalidateTag function (on-demand by tag).
As I am using time-based revalidation I won't go in-depth for on-demand revalidation. But you can read more over here
Now we have the basic idea of how ISR works in Next.js app
router, let's see the server functions of our code.
As I am using the starter kit, there are base utility functions provided in utils/garchi.ts. It looks something like below
import { GarchiAsset, GarchiPage } from "@/types/garchi"
class GarchiHelper {
private baseHeaders : {[x:string] : any }
private GARCHI_URL: string
constructor() {
this.baseHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${process.env.GARCHI_API_KEY}`
}
this.GARCHI_URL = process.env.GARCHI_API_URL as string
}
async garchiPostRequest(endpoint: string, payload: any){
const response = await fetch(`${this.GARCHI_URL}/${endpoint}`, {
method: 'POST',
headers: this.baseHeaders,
body: JSON.stringify(payload)
})
return await response.json()
}
async garchiGetRequest(endpoint: string){
const response = await fetch(`${this.GARCHI_URL}/${endpoint}`, {
method: 'GET',
headers: this.baseHeaders,
})
return await response.json()
}
async getGarchiPage(spaceUID: string, mode: "draft" | "live" = "draft" ,pageSlug: string = "/") :
Promise<GarchiPage>
{
const response = await this.garchiPostRequest("page", {
space_uid: spaceUID,
mode,
slug: pageSlug
})
return response
}
async getGarchiAsset(spaceUID: string, assetName: string) : Promise<GarchiAsset>
{
const response = await this.garchiGetRequest(`space/assets/${assetName}?space_uid=${spaceUID}`)
return response
}
}
export default new GarchiHelper()
We need to get the page content using Garch CMS API. For that, there is the getGarchiPage function which needs a slug, space uid and mode. This function calls the http POST request helper function garchiPostRequest. All we have to do is modify this garchiPostRequest function and pass next.revalidate option
//prev code
async garchiPostRequest(endpoint: string, payload: any){
const response = await fetch(`${this.GARCHI_URL}/${endpoint}`, {
method: 'POST',
headers: this.baseHeaders,
body: JSON.stringify(payload),
next: {
revalidate: 15
}
})
return await response.json()
}
// after code
In the above function, I am making a fetch POST request and having it revalidated every 15 seconds.
4. Bringing it all together
We now have the helper functions and some components to fetch content and render our page.
In the starter kit, there are two main components in the components/garchi folder. Page.tsx and GarchiComponent.tsx
The purpose of these two components is that once we register all our components in Garchi CMS, we can create pages using these components with all the flexibility we want and we don't have to touch the code. The Page component can be used to render any route and GarchiComponent can be used to render any registered component in any order (sequence) dynamically.
Finally, we need a dynamic route segment to capture all routes for dynamic routing. So within app folder we can create [[...slug]]/page.tsx
The double square brackets around ...slug make it possible to catch all the routes including the index/home route.
/app/[[...slug]]/page.tsx
import Page from "@/components/garchi"
type Props = { params: { slug: string[] } }
export default function DynamicPage({ params }: Props) {
return <Page slug={params?.slug?.join("/")} />
}
There you have it - a blazing-fast site with headless CMS and ISR!