Incremental Static Regeneration (ISR) allows you to update static pages after they've been generated without rebuilding the entire site. While Astro doesn't have built-in ISR like Next.js, you can implement it on Vercel using the Build Output API.
When using ISR with Vercel, you should know that 1:
Here's how to implement ISR with Astro on Vercel:
Install the Vercel adapter for Astro:
npm install @astrojs/vercel
vercel-build-output.js
script to your project root.package.json
build script to run the custom script after Astro build.astro.config.mjs
to use the Vercel adapter.Deploy to Vercel:
vercel deploy
You can implement on-demand revalidation by creating an API endpoint 2:
Please make sure to add the following environment variables to your project:
I implemented things a little different. This is what I have in my astro.config,js:
/// <reference path="./src/env.d.ts" /> import { defineConfig } from "astro/config"; import tailwind from "@astrojs/tailwind"; import vercel from "@astrojs/vercel/serverless"; import preact from "@astrojs/preact"; import { readFileSync } from "fs"; import { resolve } from "path"; import dotenvx from "@dotenvx/dotenvx"; import exit from "exit"; import sentry from "@sentry/astro"; import { extraErrorDataIntegration, thirdPartyErrorFilterIntegration } from "@sentry/browser"; import sitemap from "@astrojs/sitemap"; import { logger } from "./src/utils/logger"; import sitemapShim from "./integrations/sitemap-shim";
dotenvx.config({
path: [".env.local"]
});
const modulesPath = resolve(process.cwd(), node_modules
, @coak
);
const redirectFilePath = resolve(modulesPath, redirects.json
);
/** sync load the redirects file because no async API available for defineConfig */
const getRedirectsFromJsonFileSync = () => {
try {
const fileContents = readFileSync(redirectFilePath, {
encoding: "utf8",
flag: "r"
});
return JSON.parse(fileContents).reduce((obj, acc) => ({
...obj,
...acc
}), {});
} catch (error) {
logger(
[fatal] Error while loading redirects from file:\n ${error.message}
,
);
exit(1);
}
};
/** https://astro.build/config / export default defineConfig({ site: "https://growtherapy.com", integrations: [ tailwind(), preact(), sitemap(), sitemapShim(), /* Put the Sentry vite plugin after all other plugins / sentry({ bundleSizeOptimizations: { excludeDebugStatements: true, /* Initial config does not have tracing or performance monitoring enabled / excludePerformanceMonitoring: true, }, dsn: import.meta.env.MODE === "production" ? process.env.PUBLIC_SENTRY_DSN : "", enabled: { client: true, server: true }, integrations: [ extraErrorDataIntegration(), thirdPartyErrorFilterIntegration({ filterKeys: ['8caa002b-ec80-4518-ac5d-f13e8b845d9a'], behaviour: 'drop-error-if-contains-third-party-frames', }) ], sourceMapsUploadOptions: { project: "grow-therapy", /* Enables readable stack traces in your builds */ authToken: process.env.PUBLIC_SENTRY_AUTH_TOKEN, }, }), ], output: "server", adapter: vercel({ isr: true, }), image: { domains: [], }, redirects: { ...getRedirectsFromJsonFileSync(), }, });
This is what I have in revalidate.ts: import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => { let body; try { body = await request.json(); } catch (e) { return new Response(JSON.stringify({ message: 'Invalid JSON body' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); }
const { path, secret } = body;
// Validate the secret if (secret !== import.meta.env.REVALIDATION_SECRET) { return new Response(JSON.stringify({ message: 'Invalid token' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); }
if (!path) { return new Response(JSON.stringify({ message: 'Path is required' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); }
try {
// Simply fetch the page with cache-busting headers
// This will cause Vercel to generate a fresh version
const revalidateResponse = await fetch(https://${import.meta.env.VERCEL_URL}${path}
, {
method: 'GET',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
},
});
if (!revalidateResponse.ok) {
throw new Error(`Failed to revalidate path: ${path}`);
}
return new Response(JSON.stringify({
revalidated: true,
path: path,
now: Date.now()
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (err) { console.error('Error revalidating:', err); return new Response(JSON.stringify({ message: 'Error revalidating' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } };
import PageItemLayout from "@layouts/PageItemLayout/index.astro"; import { getData, type Params } from "@domain/pages/[slug]"; import type { PageProps } from "@domain/pages/types";
const { slug } = Astro.params as Params;
const { data, error } = await getData(slug); if (error) return Astro.redirect("/404/");
const { globalSettings, page, payors, specialties, states, } = data as PageProps['data'];
<PageItemLayout globalSettings={globalSettings} page={page} insurance={payors} slugForPageConfigs={page.slug} specialties={specialties} states={states} />
Your implementation is using Vercel's built-in ISR support with the serverless adapter, which is a great approach. Let me explain how your setup works and suggest some potential improvements.
You're using:
@astrojs/vercel/serverless
adapter with isr: true
output: "server"
)s-maxage=86400, stale-while-revalidate=300
This is a valid approach for implementing ISR on Vercel. The isr: true
flag in the Vercel adapter enables Incremental Static Regeneration for your Astro project.
Your revalidation endpoint is attempting to trigger a refresh by fetching the page with cache-busting headers. However, there's a more efficient way to handle on-demand revalidation with Vercel's ISR.
Instead of using cache-busting headers, you can use Vercel's specific revalidation header 1:
// src/pages/api/revalidate.ts
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => {
let body;
try {
body = await request.json();
} catch (e) {
return new Response(JSON.stringify({ message: 'Invalid JSON body' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const { path, secret } = body;
// Validate the secret
if (secret !== import.meta.env.REVALIDATION_SECRET) {
return new Response(JSON.stringify({ message: 'Invalid token' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
if (!path) {
return new Response(JSON.stringify({ message: 'Path is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
try {
// Use Vercel's x-prerender-revalidate header for on-demand revalidation
const revalidateResponse = await fetch(`https://${import.meta.env.VERCEL_URL}${path}`, {
method: 'GET',
headers: {
'x-prerender-revalidate': import.meta.env.REVALIDATION_SECRET
},
});
if (!revalidateResponse.ok) {
throw new Error(`Failed to revalidate path: ${path}`);
}
return new Response(JSON.stringify({
revalidated: true,
path: path,
now: Date.now()
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (err) {
console.error('Error revalidating:', err);
return new Response(JSON.stringify({ message: 'Error revalidating' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};
Your current cache settings are good, but you might want to consider different values based on your specific needs:
Astro.response.headers.set(
'Cache-Control',
's-maxage=3600, stale-while-revalidate=86400'
);
This would:
This can be better for frequently updated content while still maintaining good performance.
You might want different cache times for different types of content:
// For frequently updated content
Astro.response.headers.set(
'Cache-Control',
's-maxage=900, stale-while-revalidate=3600'
);
// For relatively stable content
Astro.response.headers.set(
'Cache-Control',
's-maxage=86400, stale-while-revalidate=604800'
);
To test if your ISR implementation is working correctly:
If the changes appear without redeploying, your ISR implementation is working correctly.
Your implementation is using both ISR and Cache-Control headers, which work together:
isr: true
) enables Vercel's infrastructure to cache and revalidate pagesThis combination gives you fine-grained control over caching behavior 2.
Would you like me to explain any specific part of your implementation in more detail?