Caching with Next.js and Agility

When working with Next.js and Agility, you should be using the Agility Next.js SDK, found here. That SDK goes hand in hand with the Agility Fetch SDK, which can be found here. A great reference example for all of the concepts is the Agility Next.js Starter Site - all the code is here on GitHub.  

The starter has been updated for the Next.js app router and includes advanced object and path caching.

Terminology Note

In Agility a Layout is the object that represents a Page on your website or Digital Solution, so you may see these terms used interchangeably.

Getting Content From Agility

Getting content from Agility is done through the 2 SDKs mentioned above. The Agility Next.js SDK handles getting the initial Layout's route data.  

The first place to look is the /app/[...slug]/page.tsx file. You'll notice that this is a React Server Component, which is rendered asynchronously on the server, so you can do any data access in the render method that you need. We are doing a call to getAgilityPage here and passing in the params object that we get from Next.js. Taking a look at that method's implementation, we are calling getAgilityPageProps and sending contentLinkDepth: 0, signalling that we ONLY want the Layout data and NOT the Component data.  

This is a big decision and will determine a lot about how your Components will be implemented. That decision should primarily be determined by where you're going to host the site, and if it supports on-demand re-validation. 

If the hosting provider supports on-demand validation, you don't have to wait for the re-validation timeout to expire before live Layout routes are refreshed, meaning your Content team will see much faster updates from Agility to the site.

The next 2 sections will deal with the cases for both types of hosting capabilities.

Hosting Environments that Support On-Demand Re-validation

If you are using an environment (like Vercel and others soon) that supports on-demand re-validation, we recommend using contentLinkDepth: 0 when accessing Layout objects. This means that you will only receive a contentID in the props for the Components on that Layout, however, it means that you will be able to control the caching for that Component content independently from the Layout itself. When Content is changed in Agility, you will be able to instantly invalidate it and see much faster updates in your site.

The object level caching methods that go hand in hand with on-demand re-validation are in the `/lib/cms/` folder of the starter.

Caching Content Objects

Here's the getContentItem method, which is a wrapper around the base-sdk method of the same name.

export const getContentItem = async <T>(params: ContentItemRequestParams) => {

	const agilitySDK = getAgilitySDK()

	agilitySDK.config.fetchConfig = {
		next: {
			tags: [`agility-content-${params.contentID}-${params.languageCode || params.locale}`],
			revalidate: cacheConfig.cacheDuration,
		},
	}

	return await agilitySDK.getContentItem(params) as ContentItem<T>

}

You can see from the code in there that we are adding specific tags to our call to the fetch API with a special next  property. The methods for Content Items, Lists, Layouts and Sitemaps have been similarly wrapped in this project so that we can automatically clear the cache using Agility's webhooks. 

Invalidating Content Objects

Let's take a look at the code we're using the invalidate these cache objects. It's in the /app/api/revalidate/route.tsx file.

export async function POST(req: NextRequest, res: NextResponse) {

	//parse the body
	const data = await req.json() as IRevalidateRequest

	//only process publish events
	if (data.state === "Published") {

		//revalidate the correct tags based on what changed
		if (data.referenceName) {
			//content item change
			const itemTag = `agility-content-${data.referenceName}-${data.languageCode}`
			const listTag = `agility-content-${data.contentID}-${data.languageCode}`
			revalidateTag(itemTag)
			revalidateTag(listTag)
			console.log("Revalidating content tags:", itemTag, listTag)
		} else if (data.pageID !== undefined && data.pageID > 0) {
			//page change
			const pageTag = `agility-page-${data.pageID}-${data.languageCode}`
			revalidateTag(pageTag)


			//also revalidate the sitemaps
			const sitemapTagFlat = `agility-sitemap-flat-${data.languageCode}`
			const sitemapTagNested = `agility-sitemap-nested-${data.languageCode}`
			revalidateTag(sitemapTagFlat)
			revalidateTag(sitemapTagNested)

			console.log("Revalidating page and sitemap tags:", pageTag, sitemapTagFlat, sitemapTagNested)
		}
	}

	return new Response(`OK`, {
		status: 200
	})
}

The code above is taking a web-hook request from Agility and determining if it's for published content.  If so, we then determine if it was a Layout or a Content item (or Component) that was updated. 

For Content, we invalidate 2 tags - one for the item itself and one for the whole list that the Content might be part of. 

For Layouts, we invalidate the actual item itself as well as the Sitemaps.

Route Level Caching

On top of this object-level caching, we also have route (or path segment) level caching, meaning that Next.js will only rebuild the route every x seconds. This rebuild is a much faster operation if the route is built from cached objects.  

When we invalidate a cached object, Next.js automatically invalidates the routes that used that object in the first place.  

In the case where your hosting environment supports on-demand re-validation, you can set the path re-validation timeout to a larger value.

See the page.tsx file exports for the extra exports that we found we had to set in addition to the re-validate value.

export const revalidate = cacheConfig.pathRevalidateDuration
export const runtime = "nodejs"
export const dynamic = "force-static"

The 2 env vars that important to note here are:

  • AGILITY_PATH_REVALIDATE_DURATION
    • This value specifies the number of seconds before a path is re-validated.
  • AGILITY_FETCH_CACHE_DURATION
    • This value specifies the number of seconds before an object is removed from cache.

What It All Means

Here's what this means for and end user.  When we cache a Layout and its Components separately (by using contentLinkDepth: 0 in our getAgilityPageProps calls) we can cache the Components for a Layout independently from the Layout itself.

The editor experience becomes so much better with on-demand re-validation!

Hosting Environments that Support On-Demand Re-validation

If you're hosting your site on an environment that doesn't support on-demand re-validation, you can still provide an excellent experience for your content team while providing a high-performing site, with the caveat that editors will have to wait for the path re-validation timeout to elapse before they can changes to live content.

The key value here is the AGILITY_PATH_REVALIDATE_DURATION env var, which is exported from the page.tsx file.

export const revalidate = cacheConfig.pathRevalidateDuration