Skip to content

Feed-freshness webhook

React Router SSR reads the current feed on every request, so you only need a webhook when you cache the feed at a CDN. Register a resource route (a route with an action and no UI), point your site’s feed-freshness subscription at it, verify the signature, and purge your cache. Then new articles appear in seconds — see Instant updates.

When a site’s static feed rebuilds, the engine POSTs a feed.updated body to your registered URL with an X-GW-Signature header — a hex HMAC-SHA256 of the raw request body, keyed by your shared secret. You verify it with verifyFeedSignature against the same raw bytes, then purge.

app/routes/webhooks.gw-feed.ts
import {
verifyFeedSignature,
FEED_SIGNATURE_HEADER,
type FeedUpdatedPayload,
} from "@ghostwritr/react-router";
import type { Route } from "./+types/webhooks.gw-feed";
export async function action({ request }: Route.ActionArgs) {
if (request.method !== "POST") {
throw new Response("Method not allowed", { status: 405 });
}
// 1. Read the RAW body — HMAC is over these exact bytes.
const rawBody = await request.text();
const signature = request.headers.get(FEED_SIGNATURE_HEADER);
// 2. Verify before trusting anything (constant-time, returns boolean).
const ok = await verifyFeedSignature(process.env.GHOSTWRITR_FEED_SECRET!, rawBody, signature);
if (!ok) {
throw new Response("Bad signature", { status: 401 });
}
// 3. Now it's safe to parse.
const payload = JSON.parse(rawBody) as FeedUpdatedPayload;
// payload.event === "feed.updated", payload.siteId, payload.buildId
// 4. Purge your CDN cache for this site's feed / blog routes.
await purgeCdn(payload.siteId);
return Response.json({ ok: true });
}

FEED_SIGNATURE_HEADER is the constant "X-GW-Signature". verifyFeedSignature(secret, rawBody, signature) returns false for a missing or mismatched signature (constant-time), and true only on a genuine match. It uses Web Crypto, so it runs identically in Node 20+ and on edge runtimes.

  1. Pick a shared secret and store it as GHOSTWRITR_FEED_SECRET in your environment. This is the HMAC key both sides use.

  2. Deploy the resource route at a stable, public path (e.g. /webhooks/gw-feed).

  3. Subscribe your site’s feed to that URL via the Ghost Writr API:

    PUT /v1/sites/{siteId}/feed-subscription

    with your webhook URL and the shared secret. The engine then signs every feed.updated POST with it.

FeedUpdatedPayload is small and stable:

interface FeedUpdatedPayload {
event: "feed.updated";
siteId: string;
buildId: string;
}

You generally only need siteId to scope your purge. The buildId identifies the new immutable feed snapshot the next request will read.