Here's a quick snippet that allows for On-Demand ISR (incremental static regeneration) in NextJS and Ghost CMS (headless). This gets triggered as a webhook anywhere in your api/
folder.
You'd also set a webhook secret in your environment variables to protect it from misuse. There's a couple of lines specific to this site, but the file should mostly remain the same after adjusting it to your use case.
It detects the revalidation content type – posts or pages – and revalidates a path with a given mapping.
// /pages/api/ghost/revalidate.ts import { NextApiRequest, NextApiResponse } from "next"; import get from "lodash/fp/get"; import { REVALIDATE_KEY } from "../../../config"; import { postMessage } from "../../../lib/discord"; import isError from "lodash/isError"; import reduce from "lodash/reduce"; interface RevalidateMap { [type: string]: (slug: string) => string; } const PATH_MAP: RevalidateMap = { post: (slug) => `/posts/${slug}`, page: (slug) => `/${slug}`, }; interface SlugItem { type: "post" | "page"; slug: string; } function collectSlugs(req: NextApiRequest): SlugItem[] { return reduce( Object.keys(req.body), (result: SlugItem[], value) => { const currentSlug = get(`${value}.current.slug`, req.body); const previousSlug = get(`${value}.previous.slug`, req.body); if (previousSlug === undefined && currentSlug) { return [ ...result, { type: value as SlugItem["type"], slug: currentSlug }, ]; } else if (currentSlug === undefined && previousSlug) { return [ ...result, { type: value as SlugItem["type"], slug: previousSlug }, ]; } else { return [ ...result, { type: value as SlugItem["type"], slug: currentSlug }, { type: value as SlugItem["type"], slug: previousSlug }, ]; } }, [] ); } export default async (req: NextApiRequest, res: NextApiResponse) => { // Check for secret to confirm this is a valid request if (req.query.secret !== REVALIDATE_KEY) { return res.status(401).json({ message: "Invalid token" }); } try { const collected = collectSlugs(req); // Revalidate post lists. await res.revalidate("/"); await res.revalidate("/posts"); // Revalidate individual pages. await Promise.all( collected.map(async (item) => { const path = PATH_MAP[item.type](item.slug); await res.revalidate(path); await postMessage(`✅ Revalidated w/ ISR: \`${path}\``); }) ); return res.json({ revalidated: true }); } catch (err) { // If there was an error, Next.js will continue // to show the last successfully generated page // eslint-disable-next-line no-console console.error(err); let message = "Unknown Error"; if (isError(err)) message = err.message; postMessage(`⚠️ Failed to on-demand revalidate. \`${message}\``); return res.status(500).send("Error revalidating"); } };
After adding this endpoint to your NextJS site, you'd add the URL to Ghost webhooks on any related events (page/post updated, published, etc).
Your pages should now update incrementally without a full build process. This works great on Vercel!