# Course: Controlling cached content in Next.js
https://www.sanity.io/learn/course/controlling-cached-content-in-next-js

Creating a high performance web application for fast loading depends on caching. Learn how to implement a caching strategy you can understand, debug and depend on.

---

## Navigation

**Track:** [Work-ready Next.js](https://www.sanity.io/learn/track/work-ready-next-js) · [View as markdown](https://www.sanity.io/learn/track/work-ready-next-js.md)

## Contents

1. [Caching Fundamentals](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/introduction) · [markdown](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/introduction.md)
2. [Demystifying caching in development](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/debugging-caching-in-development) · [markdown](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/debugging-caching-in-development.md)
3. [Combining Sanity CDN with the Next.js Cache](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/combining-sanity-cdn-with-the-next-js-cache) · [markdown](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/combining-sanity-cdn-with-the-next-js-cache.md)
4. [Time-based cache revalidation](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/time-based-cache-revalidation) · [markdown](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/time-based-cache-revalidation.md)
5. [Path-based revalidation](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/path-based-revalidation) · [markdown](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/path-based-revalidation.md)
6. [Tag-based revalidation](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/tag-based-revalidation) · [markdown](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/tag-based-revalidation.md)
7. [Quiz to win cache prizes](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/conclusion) · [markdown](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/conclusion.md)

---

## Lesson 1: Caching Fundamentals
https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/introduction

Next.js has prioritized performance with its caching methods and expects you to configure them. Learn how to integrate the Next.js cache and Sanity CDN for high performance.

## Prefer live by default



You might not need this course.



The [Live Content API](https://www.sanity.io/learn/content-lake/live-content-api), and its simplified implementation with [`next-sanity`](https://www.sanity.io/learn/course/visual-editing-with-next-js/enhanced-visual-editing-with-react-loader), handles all aspects of fetching, rendering, caching and invalidating queries in a few lines of code. 



> [!WARNING]
> This course remains online to explain finer details of working with Sanity and the Next.js cache. And the code examples in it may not follow from the previous [Content-driven web application foundations](https://www.sanity.io/learn/course/content-driven-web-application-foundations) course. Our **strong recommendation** is to use live fetches by default.


> [!TIP]
> Skip to the [Integrated Visual Editing with Next.js](https://www.sanity.io/learn/course/visual-editing-with-next-js) course next.



## Welcome to caching



Caching is not unique to Next.js or Vercel; it's a common strategy across all programming and comes in many forms. For example, in-memory caching is one approach that stores data in the application's memory for quick access. 



When discussing caching for web applications, it typically refers to network requests. When a user makes a request from a web server, the response may be cached in their browser, so subsequent requests for the same page do not need to perform yet another round-trip for the same response.



Similarly, when a web server computes and returns a response, it may be cached on the server so that subsequent requests from many other clients can be fulfilled from its cache – faster than recomputing the same request.



This is where things get tricky. How long should your web server cache that response? If it's too long, your users may be frustrated by being served stale content. If it's too short, too many users may have to wait for the web server to compute responses – and your web server may use too many resources doing so.



## Next.js specific caching



In typical web applications, caching is handled by modifying the headers sent with a request.



> [!TIP]
> See the MDN documentation on [Cache-Control headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)



However, Next.js has framework-specific configuration options to scope and simplify setup. This course will primarily focus on these. The following resources may be valuable additional reading:



> [!TIP]
> [Next.js data fetching](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating) documentation


> [!TIP]
> [Next.js caching](https://nextjs.org/docs/app/building-your-application/caching) documentation



## Goals of this course



Once you have completed this course, you will:



- Understand why caching matters based on who is most impacted and how.

- Integrate requests for Sanity's CDN and API with the built-in Next.js cache, configured with sensible defaults.

- Observe the impact of – and debug changes to – cache configuration.

- Revalidate cached requests based on time, path, and tag.

- Setup [GROQ-powered webhooks](https://www.sanity.io/learn/content-lake/webhooks) to perform cache revalidation automatically when documents change.


## Who is impacted by caching?



There's no one-size-fits-all strategy for caching, so a developer team is responsible for fine-tuning their application's caches. Let's consider how different user groups are impacted by the types of caching that can be implemented.



### Content authors



In content-driven web applications, content authors typically want to see the effect of their changes happen immediately. The most reliable way to do this would be to remove all caching from the front end so that every response is freshly created. You could also retrieve content from Sanity's API instead of the CDN to ensure the freshest content is used.



However, this strategy also creates the slowest loading and most expensive operating web applications. Not ideal.



> [!NOTE]
> Content authors that would prefer to see fresh content before – or immediately after – publishing are better served by configuring Visual Editing – rather than modifying cache settings in production. Take the [Integrated Visual Editing with Next.js](https://www.sanity.io/learn/course/visual-editing-with-next-js) course to find out how.



### Business stakeholders



Stakeholders in your business would like to keep the running costs of your web application low and conversions high, so you might think the most aggressive caching strategy would suit them. The fewer requests directed to an API instead of a CDN, the better. The less bandwidth a web server spends computing and fulfilling requests, the better.



> [!TIP]
> See Cloudflare's documentation on how [website speed affects conversion rates](https://www.cloudflare.com/learning/performance/more/website-performance-conversion-rates)



However, overly aggressive caching is bound to frustrate your content authors and end-user groups.



### End users



The stakeholders mentioned above would also like to see improved conversions from end-users – who expect a mix of fast-loading pages and up-to-date, reliable content. For example, it's no good if a product page loads quickly but the stock level or price information is invalid.



> [!NOTE]
> Split requests for long-lived and dynamic parts of the same page. [Partial pre-rendering](https://nextjs.org/docs/app/api-reference/next-config-js/partial-prerendering) is one solution for the above problem.



As you can see, each group that is majorly impacted by your web application's cache brings a unique point of view. This makes knowing how caching works—and reacting to the changing realities of how your application is used—so important.



Now that you understand the problem space and who is impacted, it's best to equip yourself with the tools required to configure and debug your web application's caching configuration.



---

## Lesson 2: Demystifying caching in development
https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/debugging-caching-in-development

Set up Next.js so that as you make changes and navigate through the application, you can observe the impact of your cache configuration.

In recent years, the popularity of the "Jamstack" and Static Site Generators reduced the importance of caching when serving web applications. However, as the limitations of those approaches became more apparent, dynamic, server-rendered responses have once again become popular, spotlighting caching once again.



Next.js 14 not only provided aggressive caching for an application's `fetch` requests, but it also made it the default. 



This led to faster response times at the expense of increased developer frustration. Every `fetch` request was instantly cached, whether in development or production. Further, this cache is stored in a separate data layer from your site code, so redeploys did not reset the site's state like you may have expected in the Jamstack years.



Next.js 15 has reversed this decision, and caching is opt-in once again. This was likely a difficult decision because there are pitfalls either way. 



In this writer's opinion, this decision is not strictly *better*; it is just *different*. 



It's more important to understand what has been cached and when than whether a request was cached by default.



In short, you will want to specify the caching configuration and be able to observe its results.



## Logging fetch requests



Fortunately, a Next.js configuration setting logs [the full URL of any fetch request](https://nextjs.org/docs/app/api-reference/next-config-js/logging), along with information about whether it was a cache `HIT` or `MISS` – and why. 



> [!NOTE]
> A cache `HIT` occurs when the requested data is found in the cache, allowing it to be served quickly without fetching from the source. 


> [!NOTE]
> A cache `MISS` is the opposite, requiring it to be fetched from the source, which is slower than serving from the cache.



Sanity Client uses `fetch` under the hood, so once you have enabled this debugging mode below, every query you perform with it will appear in the console. 



- [ ] **Update** your `next.config.ts` with the following configuration


```typescript:next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  logging: {
    fetches: {
      fullUrl: true,
    },
  },
  // ...all other settings
};

export default nextConfig;
```

Now refresh any page that fetches data from Sanity – like the posts index or an individual post page – and you should see something like the following in your console:



```text
 GET /posts 200 in 39ms
 │ GET https://q1a918nb.apicdn.sanity.io/v2024-07-24/data/query/production?query=*%5B_type+%3D%3D+%22post%22+%26%26+defined%28slug.current%29%5D%5B0...12%5D%7B%0A++_id%2C+title%2C+slug%0A%7D&returnQuery=false 200 in 5ms (cache hit)
```

From this, you can observe:



- The `client.fetch()` request was for `apicdn.sanity.io` which means the request was performed with Sanity Client's `useCdn` set to `true`.

- As a **cache hit**, the response was fulfilled by the Next.js cache, so this request for `/posts` may not have been sent to Sanity's CDN. 


In the previous course – [Content-driven web application foundations](https://www.sanity.io/learn/course/content-driven-web-application-foundations) – these fetches were configured to update at most once every 60 seconds. 



- If a cache hit has already been served within 60 seconds, the response will be fast. 

- If that time has elapsed, the request will still be served stale, expired data – but in the background, the cache will be repopulated so that the next request receives fresh content. 

> [!TIP]
> This is similar to the [stale-while-revalidate](https://web.dev/articles/stale-while-revalidate) pattern of caching responses



## Purging the cache



Seeing what is cached is helpful, but it's even better to be able to completely reset the cache during development.



In the following lessons, you'll look at setting up surgical control for revalidating fetches based on time, path, and tag. This is the preferred option for your production web application. But sometimes, in development, you need a *hammer*.



- [ ] **Create** a new API route in your application:


```typescript:src/app/api/revalidate/all/route.ts
import { revalidatePath } from 'next/cache'

export async function GET() {
  if (process.env.NODE_ENV === 'development') {
    revalidatePath('/', 'layout')
    return Response.json({ message: 'Layout revalidated' })
  }
  return Response.json({
    message: 'This route is configured to only revalidate the layout in development',
  })
}
```

- [ ] **Visit** [http://localhost:3000/api/revalidate/all](http://localhost:3000/api/revalidate/all) and you should see the same message above in your browser.

- [ ] **Visit** [http://localhost:3000/posts](http://localhost:3000/posts) to check that it has worked.


You should see a different log in the terminal that finishes with `cache skip`:



```
GET /posts 200 in 893ms
 │ GET https://q1a918nb.apicdn.sanity.io/v2024-07-24/data/query/production?query=*%5B_type+%3D%3D+%22post%22+%26%26+defined%28slug.current%29%5D%5B0...12%5D%7B%0A++_id%2C+title%2C+slug%0A%7D&returnQuery=false 200 in 743ms (cache skip)
 │ │ Cache skipped reason: (cache-control: no-cache (hard refresh))
```

Refresh the page again, and the request should once again be a `cache hit`.



Now you can purge the entire Next.js cache on demand, and observe the caching behavior of every `fetch` request made in the application.



The two uses of `client.fetch` in your application currently have the same configuration. This presents an opportunity to make our code more DRY (don't repeat yourself) and set some sensible defaults.



In the next lesson, let's do this and better understand how Sanity and Next.js caching work together.



---

## Lesson 3: Combining Sanity CDN with the Next.js Cache
https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/combining-sanity-cdn-with-the-next-js-cache

Implement Sanity Client in a way that compliments and leverages the Next.js cache with sensible defaults.

Even if Next.js had no affordances for caching – or you use a framework with no in-built caching options – the Sanity CDN provides a performant way to query content. This lesson briefly summarizes how querying the correct endpoint in your Next.js application is situation-dependent.



> [!TIP]
> See the documentation on the [API CDN](https://www.sanity.io/learn/content-lake/api-cdn) for more details



## Querying Sanity's API or CDN



Your Sanity Client's `useCdn` setting determines whether your fetch request uses the CDN or the API. Depending on the context of your fetch, you may choose to query either one.



### Querying the Sanity API



Querying the API is slower but guaranteed to be fresh. Your project's plan will have a lower allocation of API requests, so you should factor that into your usage. For these reasons you should only query the API (`useCdn: false`) when responses are infrequent and fast responses are not required.



Examples include statically building pages ahead of time and performing incremental static revalidation or tag-based revalidation. 



### Querying the Sanity CDN



Querying the CDN is faster but not guaranteed to be fresh. The Sanity CDN's cache is flushed every time a publish mutation is performed on a dataset, so there may be a brief delay between the latest content being published and it being available from the CDN. Your project's plan will have a far higher allocation of CDN requests. You should query the CDN (`useCdn: true`) when responses are frequent and fast responses are desired.



Examples include all situations other than those outlined in the previous situation where the API is preferred. This makes `useCdn: true` a sensible default.



### Overriding `useCdn` per-request



Whatever your Sanity Client configuration, it can be overridden at the time of request using the `withConfig` method. In this section, you'll configure `generateStaticParams` to build individual post pages at build time, instead of at request time.



> [!TIP]
> Read more about [`generateStaticParams` on the Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-static-params)


- [ ] **Add** an exported `generateStaticParams` function to the dynamic route


```typescript:src/app/(frontend)/posts/[slug]/page.tsx
// update your imports
import { POST_QUERY, POSTS_SLUGS_QUERY } from "@/sanity/lib/queries";

// add this export
export async function generateStaticParams() {
  const slugs = await client
    .withConfig({useCdn: false})
    .fetch(POSTS_SLUGS_QUERY);

  return slugs
}
```

When you next deploy your Next.js application, every individual post page will be created ahead of time, with guaranteed fresh data (fresh at the time it was deployed) fetched direct from the Sanity API.



## Sanity Client and Next.js cache configuration



If you completed the previous lesson, you are relying on Sanity Live to perform caching and live updates. It is possible to manually set cache configuration options when performing fetches with the Sanity Client. 



### `sanityFetch` helper function



As detailed in the [next-sanity readme](https://github.com/sanity-io/next-sanity?tab=readme-ov-file#caching-and-revalidation), you may wish instead to create a helper function that wraps Sanity Client with caching configuration options.



This `next` key in the Sanity Client configuration takes the same configuration options found in the Next.js documentation for [controlling how fetches are cached](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#caching-data).



- [ ] **Update** your `client.ts` file to include an exported helper function, `sanityFetch`


```typescript:src/sanity/lib/client.ts
import {createClient, type QueryParams} from 'next-sanity'

// other imports, client export

// 👇 add this function
export async function sanityFetch<const QueryString extends string>({
  query,
  params = {},
  revalidate = 60, // default revalidation time in seconds
  tags = [],
}: {
  query: QueryString
  params?: QueryParams
  revalidate?: number | false
  tags?: string[]
}) {
  return client.fetch(query, params, {
    next: {
      revalidate: tags.length ? false : revalidate, // for simple, time-based revalidation
      tags, // for tag-based revalidation
    },
  })
}
```

The most important lines are highlighted above. By default this helper uses a 60 second revalidation—but will also accept tags for tag-based revalidation (covered in another lesson in this course).



- [ ] **Update** your `/posts` route to use our own `sanityFetch`


```typescript:src/app/(frontend)/posts/page.tsx
// update your imports
import { sanityFetch } from "@/sanity/lib/client";

// update your fetch
const posts = await sanityFetch({query: POSTS_QUERY});
```

- [ ] **Update** your individual post route to do the same


```typescript:src/app/(frontend)/posts/[slug]/page.tsx
// update your imports
import { client, sanityFetch } from '@/sanity/lib/client'

// update your fetch
const post = await sanityFetch({
  query: POST_QUERY,
  params,
})
```

> [!NOTE]
> This route still uses `client` directly in the `generateStaticParams` export. It's acceptable to use the Sanity Client when the cache settings of a request are not essential.



### What about `options.cache`?



Next.js will also accept options passed to the [Web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)'s cache, but incorrectly configuring both `next` and `cache` options can lead to errors. It's simplest to let `cache` fall back to its default setting and only focus on `next` in the following lessons.



> [!TIP]
> See Next.js [documentation on how it handles fetch](https://nextjs.org/docs/app/api-reference/functions/fetch#fetchurl-options).



### Onward!



With those changes made, you should see no change on your front end! However, you're in a much better place to implement time—and tag-based revalidation. Let's look a little more into time-based revalidation in the next lesson.



---

## Lesson 4: Time-based cache revalidation
https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/time-based-cache-revalidation

Time-based revalidation is simple to setup and predictable. It might be "enough" for your project.

> [!TIP]
> See the Next.js documentation about [time-based revalidation](https://nextjs.org/docs/app/building-your-application/caching#time-based-revalidation)



For every `fetch` made so far, you've only been implementing time-based revalidation. This has been primarily for convenience. It's simple to set up and invalidates itself.



The default setting of 60 seconds is okay. The type of content your application displays and the volume of traffic you receive will determine whether to modify that setting.



Content that changes often, like product pages, may benefit from a lower revalidation time because accuracy is more important than raw speed. Other content, like your terms and conditions pages, likely changes so infrequently that their cache time could safely be set to hours.



The blog post index and individual pages fall into the latter category. Let's update the requests on both routes to cache the responses for an hour.



- [ ] **Update** the fetch on your post-index route


```typescript:src/app/(frontend)/posts/page.tsx
const posts = await sanityFetch({
  query: POSTS_QUERY, 
  revalidate: 3600,
})
```

- [ ] **Update** the fetch on your post route


```typescript:src/app/(frontend)/posts/[slug]/page.tsx
const post = await sanityFetch({
  query: POST_QUERY,
  params,
  revalidate: 3600,
})
```

Instead of hitting Sanity's CDN at least once a minute, requests will be served by the Next.js cache for up to an hour, a significant performance and bandwidth improvement.



Your business stakeholders and end-users are happy. Your content authors could be less so.



## The "typo" problem



Imagine you're a content author who has just published a new post – or fixed a typo on a post – and want to ensure the site is fixed immediately. Right now, they may need to wait up to an hour for the world to see those changes.



You do have a route to clear the entire cache, but this can potentially impact the performance of every request to every page for every user. The *hammer *option isn't ideal for fixing a typo.



Fortunately, the passage of time isn't the only way to invalidate the cache. Next.js provides a way to invalidate a specific route on demand, and Sanity provides a way to run it automatically on content changes. 



Let's revalidate by path in the next lesson.



---

## Lesson 5: Path-based revalidation
https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/path-based-revalidation

Surgically revalidate individual post pages by their path when updates are made to their document in Sanity Studio.

> [!TIP]
> See the Next.js [documentation on `revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath).



Next.js provides a function `revalidatePath`, which will purge the cache for fetches that took place on a specific URL.



Implementing this feature is a massive win for your business stakeholder groups and content authors.



- If post routes are revalidated individually, you can safely give them a much longer time-based revalidation setting – perhaps even infinite – significantly reducing Sanity request volume and server bandwidth.

- Content authors can press "publish" on a document, which automatically revalidates the cache for just that page, and see their updates published instantly.


Currently, each post document in Sanity is rendered on a unique route in Next.js. Because of this 1:1 relationship between document and route, revalidating a cached post is straightforward.



The goal is to purge the cache for a post whenever that post is edited – fixing the "typo problem" highlighted in the previous lesson. Ideally, this should happen automatically, and [GROQ-powered webhooks](https://www.sanity.io/learn/content-lake/webhooks) make this possible.



## Why webhooks?



GROQ-powered webhooks allow you to automate side effects from the Content Lake based on any mutation to a document in a dataset.



While you could automate a function from the Studio to call one of Next.js' revalidate functions, triggering this from a webhook is much safer. It is guaranteed to be called from Sanity infrastructure – not in the browser because of a client interaction. It's also safer with automatic retries should the operation fail. 



So, for a little extra setup, you get much more reliability.



> [!TIP]
> See more about [GROQ-powered webhooks](https://www.sanity.io/learn/content-lake/webhooks) in the documentation.



## Create an API route to revalidate paths



The code below is for a new API route in your web application designed to be requested by a GROQ-powered webhook. It will:



1. Only handle a `POST` request.

2. Confirm that the request came from a Sanity GROQ-powered webhook.

3. Retrieve the `body` from the request.

4. Retrieve the `path` attribute from the request body and revalidate it.

- [ ] **Rename** your `.env` file to `.env.local`, as it will now contain secrets

- [ ] **Update** your `.env.local` file with a new secret to secure the route. It can be any random string


```:.env.local
SANITY_REVALIDATE_SECRET=<any-random-string>
```

You will also add this string to the GROQ-powered webhook you set up in this lesson.



- [ ] **Create** a new route to execute `revalidatePath`


```typescript:src/app/api/revalidate/path/route.ts
import { revalidatePath } from 'next/cache'
import { type NextRequest, NextResponse } from 'next/server'
import { parseBody } from 'next-sanity/webhook'

type WebhookPayload = { path?: string }

export async function POST(req: NextRequest) {
  try {
    if (!process.env.SANITY_REVALIDATE_SECRET) {
      return new Response(
        'Missing environment variable SANITY_REVALIDATE_SECRET',
        { status: 500 },
      )
    }

    const { isValidSignature, body } = await parseBody<WebhookPayload>(
      req,
      process.env.SANITY_REVALIDATE_SECRET,
    )

    if (!isValidSignature) {
      const message = 'Invalid signature'
      return new Response(JSON.stringify({ message, isValidSignature, body }), {
        status: 401,
      })
    } else if (!body?.path) {
      const message = 'Bad Request'
      return new Response(JSON.stringify({ message, body }), { status: 400 })
    }

    revalidatePath(body.path)
    const message = `Updated route: ${body.path}`
    return NextResponse.json({ body, message })
  } catch (err) {
    console.error(err)
    return new Response((err as Error).message, { status: 500 })
  }
}
```

This route confirms the value of the `SANITY_REVALIDATE_SECRET` environment variable and handles any invalid requests to the route.



Because this API route is only configured to handle requests with a `POST` method, visiting it in your browser (which performs a `GET` request) will not work. 



Let's configure the webhook to do that for us.



> [!NOTE]
> In this lesson, you'll only configure `revalidatePath` for a single static path. Still, it can also revalidate an entire dynamic path—like `/(frontend)/posts/[slug]`—see the Next.js [documentation on `revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) for more details.



## Create a webhook



A GROQ-powered webhook allows Content Lake to perform requests on your behalf whenever a mutation on a dataset is made. This is ideal for content operations like on-demand cache invalidation and will power a workflow where even a page set to be indefinitely cached can be purged and repopulated on demand.



### Remote access for your local environment



A tricky part of developing GROQ-powered webhooks is that even when making content changes in your Sanity Studio's local development environment, webhooks will fire remotely in the Content Lake – but the Content Lake cannot request API routes in your local development environment.



You'll need to share your local URL with the world. Several services can help you do this. Perhaps the simplest and best-known is Ngrok.



- [ ] **Create** a new [free account at Ngrok](https://ngrok.com/) if you do not already have one.

- [ ] Once logged in, complete any installation instructions for your machine

- [ ] **Run **the following command below to share your local Next.js remotely


```
ngrok http http://localhost:3000
```

Now, in the terminal, you should see many details about your account, version, etc. Along with a "Forwarding" URL which looks something like this:



```
https://8067-92-26-32-42.ngrok-free.app
```

Open that URL in your browser to see your local Next.js app. You can click links – and even open `/studio` (though you will need to add a CORS origin to interact with it).



Now, you have a remote URL of your local development environment for a GROQ-powered webhook to request.



### Create a new webhook



Fortunately, we have prepared a webhook template to add to your Project. It has most of the settings preconfigured. You'll just need to update a few that are unique to you:



- [ ] **Open** this [path revalidation webhook template](https://www.sanity.io/manage/webhooks/share?name=Path-based%20Revalidation%20Hook%20for%20Next.js&description=&url=https%3A%2F%2FYOUR-PRODUCTION-URL.TLD%2Fapi%2Frevalidate%2Fpath&on=create&on=update&on=delete&filter=_type%20in%20%5B%22post%22%5D&projection=%7B%0A%20%20%22path%22%3A%20select(%0A%20%20%20%20_type%20%3D%3D%20%22post%22%20%3D%3E%20%22%2Fposts%2F%22%20%2B%20slug.current%2C%0A%20%20%20%20slug.current%0A%20%20)%0A%7D&httpMethod=POST&apiVersion=v2021-03-25&includeDrafts=&headers=%7B%7D)

- [ ] **Update** the URL to use your ngrok URL

- [ ] **Click **"Apply Webhook" and select your project, apply to all datasets

- [ ] **Click** "Edit Webhook" on the next screen, scroll to the bottom, and add the same "Secret" you added to your `.env` file

- [ ] **Click **"Save"


You're now ready to publish changes in Sanity Studio to automatically revalidate a document's cached page in your web application.



> [!NOTE]
> The "Path" being revalidated is set within the "Projection" of the webhook using the [GROQ function](https://www.sanity.io/docs/groq-functions) `select()`. As your application grows, it could be extended to include other unique paths based on the document type or any other logic.



## Test it out



With all the machinery in place, you can test that what you've set up works.



- [ ] **Visit** an individual post page in your application, and check the terminal first to ensure it was a cache `HIT`.


If it is a cache `MISS`, reload the page, and you should see it cached for the following request.



- [ ] **Open** that post within your Sanity Studio at [http://localhost:3000/studio](http://localhost:3000/studio), change the `title` and publish it.


Almost instantly after publishing, in your terminal you should see a `POST` request which was automatically made from the GROQ-powered webhook:



```
 POST /api/revalidate/path 200 in 3540ms
```

- [ ] **Reload** the same individual post page again. You should see a cache `MISS` in the terminal and your updated title on the page.


If you reload the page again, it should be cached for the next request.



### Handling stale data



Are you still seeing stale data? Currently, GROQ-powered webhooks fire when the mutation** **is made via the Sanity API, but *before* the Sanity CDN is updated. Your request may have been made in the brief period in between, and the Next.js repopulated with stale data from the Sanity CDN.



If you encounter this situation, there are ways to mitigate it.



Within your API route to revalidate the path, the `parseBody` function from `next-sanity` takes a third argument to add a short delay before proceeding.



```typescript:src/app/api/revalidate/path/route.ts
const {isValidSignature, body} = await parseBody<WebhookPayload>(
  req,
  process.env.SANITY_REVALIDATE_SECRET,
  true
)
```

Alternatively, if you ensure Next.js caches **every** fetch, you could change the Sanity Client configuration to never use the CDN by setting `useCdn: false`.



> [!WARNING]
> Beware of the potential pitfalls of constantly querying the Sanity API directly. You don't want your application to go viral with uncached requests to the Sanity API.



## We can go deeper



What you have set up now is pretty great! Individual post pages have long-lived cache times and are revalidated automatically on demand when changes are published.



There are two new problems to solve:



1. The post index page **isn't** being revalidated when a post changes, so an update to the title does not appear.

2. The post index and individual post pages show `author` and `category` document values. If those documents are updated, we need to revalidate any component that renders those.


Fortunately, Next.js also offers "tag-based revalidation" for "update once, revalidate everywhere" content operations, and you'll set it up in the next lesson.



---

## Lesson 6: Tag-based revalidation
https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/tag-based-revalidation

Assign tags to queries to revalidate the cache of many paths by targeting any individual tag.

> [!TIP]
> See the Next.js [documentation about `revalidateTag`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag)



So far, the focus has been on revalidating individual post pages when a post changes. But with the current content model, more than just post-type document fields are queried—and there's more than one route where those responses are rendered.



A `post`-type document could contain many `category` references and a single `author` reference. Ideally, a content author editing **any** of these document types should impact **every** route where that content is rendered.



This is made possible with `revalidateTag`. 



> [!NOTE]
> Time-based and tag-based revalidation cannot be used together. This is why your `sanityFetch` function is configured to ignore the `revalidate` parameter if `tags` are provided.



In this lesson, you'll remove time-based revalidation from your existing queries instead of tag-based. You now know how to implement time and path-based revalidation in the future for instances where it is applicable.



## Tag your queries



When performing a fetch, `tags` can be an array of strings of any value. It's a standard convention to use the Sanity document types expected to be in the response.



For the posts index, add tags for the three document types that provide data for the response. If any documents of these types change, you'll want to revalidate any page that renders them.



- [ ] **Update** your fetch in the post-index route


```typescript:src/app/(frontend)/posts/page.tsx
const posts = await sanityFetch({
  query: POSTS_QUERY,
  tags: ['post', 'author', 'category'],
})
```

You can be more granular for an individual post page. You don't need to revalidate **every** post page because **one** has post changed. Thankfully, the dynamic route provides us with a unique identifier for this post—its slug—so you can use it for this page's cache tags.



- [ ] **Update** your fetch in the post route


```typescript:src/app/(frontend)/posts/[slug]/page.tsx
const post = await sanityFetch({
  query: POST_QUERY,
  params,
  tags: [`post:${params.slug}`, 'author', 'category'],
})
```

### Create an API route to revalidate tags



Once again, you will need to create an API route to accept a request from a GROQ-powered webhook and perform the revalidation.



- [ ] **Create** a new API route for `revalidateTag`


```typescript:src/app/api/revalidate/tag/route.ts
import { revalidateTag } from 'next/cache'
import { type NextRequest, NextResponse } from 'next/server'
import { parseBody } from 'next-sanity/webhook'

type WebhookPayload = {
  tags: string[]
}

export async function POST(req: NextRequest) {
  try {
    if (!process.env.SANITY_REVALIDATE_SECRET) {
      return new Response(
        'Missing environment variable SANITY_REVALIDATE_SECRET',
        { status: 500 },
      )
    }

    const { isValidSignature, body } = await parseBody<WebhookPayload>(
      req,
      process.env.SANITY_REVALIDATE_SECRET,
      true,
    )

    if (!isValidSignature) {
      const message = 'Invalid signature'
      return new Response(JSON.stringify({ message, isValidSignature, body }), {
        status: 401,
      })
    } else if (!Array.isArray(body?.tags) || !body.tags.length) {
      const message = 'Bad Request'
      return new Response(JSON.stringify({ message, body }), { status: 400 })
    }

    body.tags.forEach((tag) => {
      revalidateTag(tag)
    })

    return NextResponse.json({ body })
  } catch (err) {
    console.error(err)
    return new Response((err as Error).message, { status: 500 })
  }
}
```

> [!WARNING]
> If you wish to delay the revalidation due to the Sanity CDN, include the third argument in `parseBody`, highlighted above.



## Create a webhook



> [!NOTE]
> Instructions for how to test webhooks during local development are in the previous lesson: [Path-based revalidation](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/path-based-revalidation)



Once again, we have prepared a webhook template for your Project. It has most of the settings preconfigured. You'll just need to update a few that are unique to you:



- [ ] **Open** this [tag revalidation webhook template](https://www.sanity.io/manage/webhooks/share?name=Tag-based%20Revalidation%20Hook%20for%20Next.js%2015&description=&url=https%3A%2F%2FYOUR-PRODUCTION-URL.TLD%2Fapi%2Frevalidate%2Ftag&on=create&on=update&on=delete&filter=_type%20in%20%5B%22post%22%2C%20%22author%22%2C%20%22category%22%5D&projection=%7B%22tags%22%3A%20%5B_type%2C%20_type%20%2B%20%22%3A%22%20%2B%20slug.current%5D%7D&httpMethod=POST&apiVersion=v2021-03-25&includeDrafts=&headers=%7B%7D)

- [ ] **Update** the URL to use your ngrok URL

- [ ] **Click **"Apply Webhook" and select your project, apply to all datasets

- [ ] **Click** "Edit Webhook" on the next screen, scroll to the bottom, and add the same "Secret" you added to your `.env` file

- [ ] **Click **"Save"


You're now ready to automatically revalidate your posts index and individual post pages in your web application simply by changing a `post`, `author`, or `category` document in Sanity Studio.



> [!NOTE]
> According to the Next.js documentation, `revalidateTag` only invalidates the cache when the path is *next* visited. This means calling `revalidateTag` with a dynamic route segment will not immediately trigger many revalidations at once.



## Test it out



> [!WARNING]
> If you still have the path-based revalidation webhook enabled, disable it in Manage.



With all the machinery in place, you can test that what you've set up works.



- [ ] **Visit** the post index page at [http://localhost:3000/posts](http://localhost:3000/posts), and check the terminal first to ensure it was a cache `HIT`.


If it is a cache `MISS`, reload the page, and you should see it cached for the next request.



- [ ] **Open** any `category` or `author`-type document within your Sanity Studio at [http://localhost:3000/studio](http://localhost:3000/studio), change any field and publish.


Almost instantly after publishing, in your terminal you should see a `POST` request which was automatically made from the GROQ-powered webhook:



```
 POST /api/revalidate/tag 200 in 3540ms
```

- [ ] **Reload** the same individual post page again. 


If you reload the page again, it should be cached for the next request. You should see a cache `MISS` in the terminal, your updated title on the post-index and the individual post page.



> [!WARNING]
> Are you still seeing stale data? The previous lesson includes instructions on how to mitigate the time between a webhook firing and the CDN being repopulated: [Path-based revalidation](https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/path-based-revalidation)



With that, you're all cached up with somewhere to go. Let's review it in the final lesson.



---

## Lesson 7: Quiz to win cache prizes
https://www.sanity.io/learn/course/controlling-cached-content-in-next-js/conclusion

Let's review what you've learned about caching and balancing the content you have with the people it serves. 

With all these lessons completed, your application now only has one caching strategy: tag-based revalidation. It's a good one, but as your application grows, it may not be so one-sided.



Individual pages with slow-moving content and few connections may benefit from long revalidation times and path-based revalidation, as well as content such as terms and conditions, or help pages.



Connected content where many document types are joined together with references will continue to benefit from tag-based revalidation.



When to query the Sanity API or CDN is also situation dependent as well. You should be able to query the CDN most of the time. But in situations where requests are only made infrequently—or demand freshness—querying the API sparingly can still be useful.



To test what you've learned, let's take a brief quiz:



> **Question:** Caching impacts business users because
>
> 1. Time is money
> 2. It directly ties to bandwidth and server costs **[correct]**
> 3. Cache performance is their favorite KPI
> 4. Caching strategies impact tax liability

> **Question:** Caching impacts content authors because
>
> 1. It's their favorite thing to consider while authoring
> 2. They typically have strong opinions on Varnish
> 3. They'd like to see the impact of their published changes immediately **[correct]**
> 4. They expect manual control over caching configuration

> **Question:** Caching impacts end users because:
>
> 1. They have strong opinions on database caching
> 2. They prioritize fast and accurate content **[correct]**
> 3. They appreciate cache-control headers deeply
> 4. They're famously patient

> **Question:** Time-based revalidation is
>
> 1. Only available on paid plans
> 2. Impossible to set up
> 3. Simple and okay **[correct]**
> 4. Complex and not recommended

> **Question:** Path-based revalidation is
>
> 1. Able to revalidate dynamic paths
> 2. Able to revalidate the entire layout
> 3. Able to revalidate static paths
> 4. All the above **[correct]**

> **Question:** Tag-based revalidation is
>
> 1. Ideal for connected content **[correct]**
> 2. Ideal for single pages
> 3. Impossible
> 4. Required

> **Question:** GROQ-powered webhooks fire
>
> 1. When the Sanity CDN is refreshed
> 2. When manually triggered
> 3. When a mutation is performed on a dataset **[correct]**
> 4. When a page refreshes

---

## Related Resources

- [Track overview](https://www.sanity.io/learn/track/work-ready-next-js.md)
- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)
