On-Demand ISR For Astro on Vercel

In this post we will go over how to set on-demand incremental static regeneration for your Astro website when deploying to Vercel.

Overview

Here is what we will go over in this blog post:

  1. What is ISR and what is on-demand ISR
  2. How to set up on-demand ISR for Astro on Vercel
  3. How to on-demand invalidate a cached static page
  4. If you wanna know how to deploy your Astro project to Vercel you can go over the steps in my other post in the deploying our blog to Vercel section.

What is ISR?

ISR or incremental static regenration is a flow that allows you to update static content like blog posts or GET API endpoints without having to rebuild or redeploy your entire website in order to serve updated static content from your cache, usually from a CDN.

What is On-Demand ISR?

For ISR you’d usually set TTL (time to live) like 1 hour or 1 day, after which the content would be automatically invalidated and the subsequent request would trigger a rebuild of the page and caching of the fresh build, serving directly from cache for subsequent requests.

With on-demand ISR you get additional control over your cache in the sense that now you can on-demand invalidate it and trigger a fresh build even if the TTL hasn’t been reached yet.

What Use Cases is On-Demand ISR Good For?

  • Content that doesn’t change frequently but when it does it is crutial to reflect the changes as quickly as possible
  • Content that is accessed frequently and is expensive to render, either due to computing power, length of time, or due to external API price

In these cases and more you’d wanna have more control over the freshness of your content.

Note: Vercel’s ISR default TTL is unlimited as far as I’m aware but it would be deleted in the event of the content not being accessed for 31 days.

Initialize Astro Project

In this guide I’ll use the Astro with tailwind template as well as React (though we won’t need either of those in this guide) and I will also initialize git to push the project to Github so I can easily deploy it to Vercel, you can do the same with this single command:

npx create-astro@latest my-isr-site  --template with-tailwindcss --install --add react --git

Adding The Vercel Adapter to Astro

To start setting up ISR we first need to add the Vercel adapter to our Astro project by running the following command and agreeing to the prompted changes to our Astro config file:

npx astro add vercel

The Vercel adapter would be added to our config file with no custom configuration, let’s add the following ISR settings to it:

  • bypassToken: A 32+ char long string which will be our secret for authorizing cache invalidations (so only we will be able to invalidate the cache on demand)
  • exclude: We will exclude all routes starting with /api to not cache any of our API endpoints

Here’s how our astro.config.mjs file should look like after these changes:

// astro.config.mjs
// @ts-check
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";

import vercel from "@astrojs/vercel";

// https://astro.build/config
export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
  /*
   * Setting our website to be server rendered
   * Vercel will cache every page by default due to the adapter ISR settings
   */
  output: "server",
  adapter: vercel({
    isr: {
      // secret token that will be used later for invalidating routes
      bypassToken: "1234567890123456789012345678901234567890",
      // excluding our API from ever being cached
      exclude: [/^\/api\/.+/],
    },
  }),
});

Note: The bypassToken should be generated at build time and never be exposed to the client, even not for authenticated users. It should never leave the server.

Adding a Dynamic Page

Let’s add the following to our home page src/pages/index.astro, let’s only render the timestamp in which the server has rendered the page, note that in local development the page would not be cached so on every refresh you’s see new timestamp.

---
// src/pages/index.astro
import "../styles/global.css";

const renderedAt = new Date();
---

<html lang="en">
  <head> ... </head>

  <body>
    <div class="grid place-items-center h-screen content-center">
      <p>Rendered At Millis: {renderedAt.getTime()}</p>
      <p>Rendered At ISO: {renderedAt.toISOString()}</p>
    </div>
  </body>
</html>

If we push our changes to Vercel and go to the home page of our app (the Vercel build URL is constructed like so: <project-name>.vercel.app) we will see the timestamp the page was rendered at, we can refresh the page and see that we get the same value, meaning we get the cached version of the page.

We can also take a look at the netwrok tab and see the response header of x-vercel-cache having a value of HIT, meaning it is confirming that we get the page from cache.

On-Demand Invalidation

Now all we have left to do is to invalidate the homepage’s cache on-demand via an API request so the page will be rebuilt and the renderedAt value will be updated.

To invalidate a route’s cache in Vercel all we need to do is send a GET or HEAD request to that route with a header of key x-prerender-revalidate and value of our bypassToken that we provided to the Vercel adapter in our Astro config file.

To do so without exposing the bypassToken to the client let’s do the following:

  • Let’s create a new src/pages/api/invalidate.ts file and create a POST API endpoint
  • In the API route handler we will make a simple fetch request with a method of HEAD and add a header of x-prerender-revalidate with our bypassToken
  • After the fetch request we will check the response headers for the X-Vercel-Cache header and make sure it has a value of REVALIDATED, only if we get that header with that value the route was actually invalidated and rebuilt

This is how our src/pages/api/invalidate.ts file should look like:

// src/pages/api/invalidate.ts
import type { APIRoute } from "astro";

export const POST: APIRoute = async ({ url, request }) => {
  // Note: you should put this endpoint behind auth or other similar protection
  const body = await request.json();
  // we get the route we want to invalidate from the request body
  const route = body.route ?? "/";

  // we send head request to the route we want to invalidate
  const res = await fetch(`https://${url.host}${route}`, {
    method: "HEAD",
    headers: {
      // we add the bypass token we provided the Vercel adapter
      // Note: do NOT hard code the bypass token
      "x-prerender-revalidate": "1234567890123456789012345678901234567890",
    },
  });

  // checking the response header to make sure the route was revalidated
  const wasInvalidated = res.headers.get("X-Vercel-Cache") === "REVALIDATED";

  return new Response(JSON.stringify({ wasInvalidated }));
};
  • After deploying our changes to Vercel we can navigate to our homepage and see that due to the new deployment the renderedAt value was already changes, if we wait a few seconds and refresh again we will see that the value remains the same (we can see the exact time it was rendered, to the millisecond so no need to wait too long to make sure we get the cached version)
  • Now we can test our invalidation endpoint by making a POST request to it with our desired route to invalidate, like so:
curl --location 'https://<project-name>.vercel.app/api/invalidate' \
--header 'Content-Type: application/json' \
--data '{
    "route": "/"
}'

We can see that we get from our endpoint a response confirming the route was invalidates:

{"wasInvalidated":true}

And that is it we have set up on-demand ISR for our Astro website on Vercel! 🎉🚀

If you liked this post and wanna see more follow me on X and 📩 subscribe for free to my newsletter to get notified about future posts.


Next Post
Getting Started With Astro's Content Collection API