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.
How it works
Section titled “How it works”When a site’s static feed rebuilds, the engine POSTs a feed.updated body to your registered URL with three headers — Webhook-Id, Webhook-Timestamp, and Webhook-Signature (a v1=<hex> HMAC-SHA256 over the id, timestamp, and raw request body, keyed by your shared secret). You verify it with verifyWebhook against the same raw bytes, then purge.
The resource route
Section titled “The resource route”import { verifyWebhook, 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 (plus the id + timestamp). const rawBody = await request.text();
// 2. Verify before trusting anything. verifyWebhook reads Webhook-Id / Webhook-Timestamp / // Webhook-Signature from the headers itself (constant-time, replay-safe, returns boolean). const ok = await verifyWebhook(process.env.GHOSTWRITR_FEED_SECRET!, rawBody, request.headers); 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 });}verifyWebhook(secret, rawBody, headers) reads the Webhook-Id / Webhook-Timestamp / Webhook-Signature headers itself; it returns false for a missing, stale-timestamp, or mismatched delivery (constant-time), and true only on a genuine, in-window match. It uses Web Crypto, so it runs identically in Node 20+ and on edge runtimes.
Wire it up
Section titled “Wire it up”-
Pick a shared secret and store it as
GHOSTWRITR_FEED_SECRETin your environment. This is the HMAC key both sides use. -
Deploy the resource route at a stable, public path (e.g.
/webhooks/gw-feed). -
Subscribe your site’s feed to that URL via the Ghost Writr API:
PUT /v1/sites/{siteId}/feed-subscriptionwith your webhook URL and the shared secret. The engine then signs every
feed.updatedPOST with it.
The payload
Section titled “The payload”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.