Skip to content

Instant revalidation

ISR refreshes on a clock. To make a publish go live in seconds instead, pair createGhostwritr({ tags }) with createRevalidateHandler — a ready-made route handler that verifies Ghost Writr’s signed feed.updated webhook and busts your caches the moment the engine rebuilds your feed.

See Instant updates for the concept; this page is the Next.js wiring.

  1. Tag your feed fetches. Add tags to the client so the fetches it makes are revalidatable on demand. Keep revalidate as your fallback window.

    lib/ghostwritr.ts
    import "server-only";
    import { createGhostwritr } from "@ghostwritr/next/server";
    export const gw = createGhostwritr({
    siteId: process.env.GHOSTWRITR_SITE_ID!,
    revalidate: 3600,
    tags: ["ghostwritr"], // every feed fetch is tagged
    });
  2. Add the webhook route. createRevalidateHandler lives at the @ghostwritr/next/revalidate subpath (it imports next/cache, so it’s server-only). Export it as the POST of a route handler:

    app/api/revalidate/route.ts
    import { createRevalidateHandler } from "@ghostwritr/next/revalidate";
    export const POST = createRevalidateHandler({
    secret: process.env.GHOSTWRITR_FEED_SECRET!,
    tags: ["ghostwritr"], // MUST match the tags you set on createGhostwritr
    });
  3. Set the secret. GHOSTWRITR_FEED_SECRET is the shared secret from this site’s feed subscription — the engine signs the webhook body with it. It is distinct from your siteId: siteId is the public read capability; GHOSTWRITR_FEED_SECRET is the private signing key.

    .env.local
    GHOSTWRITR_SITE_ID=your-site-id
    GHOSTWRITR_FEED_SECRET=your-feed-secret
  4. Register the URL. Point your site’s feed subscription at the deployed route, e.g. https://yoursite.com/api/revalidate. On the next publish the engine POSTs a signed feed.updated body and your pages refresh in seconds.

The handler reads the raw request body once and verifies the X-GW-Signature HMAC-SHA256 over those exact bytes before it revalidates anything. A request with a missing, forged, or tampered signature is rejected with 401 and never touches the cache. Only after a valid signature does it call revalidateTag() for each tag and revalidatePath() for each path, then returns { "revalidated": true }.

Tags cover any page that fetched the feed through the tagged client. For pages rendered without a tagged fetch — a hand-built sitemap.xml, a route that lists slugs from a separate call — add their paths:

app/api/revalidate/route.ts
import { createRevalidateHandler } from "@ghostwritr/next/revalidate";
export const POST = createRevalidateHandler({
secret: process.env.GHOSTWRITR_FEED_SECRET!,
tags: ["ghostwritr", "blog"],
paths: ["/blog", "/sitemap.xml"],
});

paths defaults to []. Add your sitemap and any index pages so they refresh alongside the articles — see SEO metadata & sitemap.

If you’d rather build the handler yourself, verify the signature with the primitives from @ghostwritr/feed:

app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { FEED_SIGNATURE_HEADER, verifyFeedSignature } from "@ghostwritr/feed";
export async function POST(req: Request) {
const rawBody = await req.text();
const signature = req.headers.get(FEED_SIGNATURE_HEADER);
const ok = await verifyFeedSignature(process.env.GHOSTWRITR_FEED_SECRET!, rawBody, signature);
if (!ok) return new Response("invalid signature", { status: 401 });
revalidateTag("ghostwritr");
return Response.json({ revalidated: true });
}

Always verify over the raw body before parsing — the signature is over those exact bytes.