Raşit

Originally published on nomacms.com. I work on NomaCMS.

Static and ISR sites are fast because they cache content. That same cache is why your homepage still shows yesterday’s headline after you hit Publish in the CMS.

You do not need a full rebuild every time. Point a webhook at a small API route, verify the signature, then call on-demand revalidation (revalidateTag / revalidatePath in Next.js, or a rebuild hook on Astro/Cloudflare). This guide walks through that pattern with NomaCMS webhooks.

Why webhooks beat “redeploy everything”

Approach What happens Best for
Full rebuild / redeploy Every page regenerates Rare big releases
Time-based ISR (revalidate: 60) Site catches up after N seconds Soft freshness
Webhook + on-demand revalidation Cache clears when CMS events fire Editors who expect “publish means live”

NomaCMS POSTs JSON to your HTTPS URL when content (or auth) events match a webhook you configure. Your app decides what to invalidate. That keeps CDN and framework caches honest without babysitting deploys.

What you will set up

  1. A webhook in the NomaCMS dashboard for publish and update events
  2. A Next.js Route Handler that checks X-Webhook-Signature
  3. revalidateTag (or revalidatePath) so pages refresh
  4. Delivery logs so you can debug failed calls

You need a NomaCMS project (app.nomacms.com) and a publicly reachable HTTPS URL for the handler (local tunnels work for testing).

1. Create the webhook in NomaCMS

  1. Open your project in app.nomacms.com.
  2. Go to Project Settings → Webhooks.
  3. Add a webhook with a clear name, for example Next.js revalidate.
  4. Set URL to your handler, such as https://your-site.com/api/revalidate.
  5. Set a long random secret (you will verify it in code). Minimum length is 6 characters if you set one.
  6. Subscribe to at least:
    • content.published
    • content.updated
    • Optionally content.unpublished and content.deleted
  7. Set sources to both cms and api if you want dashboard edits and API edits to trigger deliveries.
  8. Leave payload on if you want the entry body in the JSON (useful for tagging by collection or slug). For content.deleted, deliveries include ids but not a full content_entry.
  9. Save with the webhook enabled.

You can create the same hook with the API or @nomacms/js-sdk (webhooks.create) using a personal access token with the admin ability. See Create a Webhook.

URL rules: HTTPS is required in normal setups. The platform rejects private, loopback, and credential-in-URL targets. That protects against SSRF-style misconfiguration.

2. What each delivery looks like

When an event matches, NomaCMS POSTs JSON to your URL. At send time the body includes at least:

  • event (for example content.published)
  • project_uuid
  • timestamp (ISO 8601)
  • delivery_id (unique per attempt)

If you set a secret, the request includes header X-Webhook-Signature: hex-encoded HMAC-SHA256 of the exact JSON body using that secret.

Keep your handler fast. Return 2xx quickly. Do heavy work after you acknowledge the request if your framework allows it. Timeouts are short (about 10 seconds on the sender).

3. Next.js: revalidate on webhook

Tag your CMS fetches so you can clear them by name.

// Example: fetch with a tag in a Server Component
const posts = await fetch("https://app.nomacms.com/api/...", {
  next: { tags: ["cms-posts"] },
})

Enter fullscreen mode Exit fullscreen mode

Or, if you use @nomacms/js-sdk inside a Server Component, wrap data loading so the result participates in Next.js caching and use the same tag strategy your app already uses. The Next.js guide notes revalidate, revalidatePath, and revalidateTag for CMS updates: Next.js guide.

Create a Route Handler:

// app/api/revalidate/route.ts
import { createHmac, timingSafeEqual } from "crypto"
import { revalidateTag } from "next/cache"
import { NextRequest, NextResponse } from "next/server"

export const runtime = "nodejs"

function verifySignature(rawBody: string, signature: string | null, secret: string) {
  if (!signature) return false
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex")
  try {
    return timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
  } catch {
    return false
  }
}

export async function POST(req: NextRequest) {
  const secret = process.env.NOMA_WEBHOOK_SECRET
  if (!secret) {
    return NextResponse.json({ error: "Missing NOMA_WEBHOOK_SECRET" }, { status: 500 })
  }

  const rawBody = await req.text()
  const signature = req.headers.get("x-webhook-signature")

  if (!verifySignature(rawBody, signature, secret)) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 })
  }

  const body = JSON.parse(rawBody) as { event?: string }

  // Map events to tags you use when fetching content
  if (
    body.event === "content.published" ||
    body.event === "content.updated" ||
    body.event === "content.unpublished" ||
    body.event === "content.deleted"
  ) {
    revalidateTag("cms-posts")
  }

  return NextResponse.json({ revalidated: true, event: body.event ?? null })
}

Enter fullscreen mode Exit fullscreen mode

Store NOMA_WEBHOOK_SECRET in your host env (same value as the webhook secret in NomaCMS). Do not put it in client bundles.

Prefer revalidateTag when many pages share one content set. Use revalidatePath('/blog') when you know exact routes. Avoid invalidating the entire site unless you must.

4. Astro and other static hosts

The idea is the same: webhook hits an endpoint, endpoint triggers a rebuild or cache purge.

  • Astro on a host with on-demand regeneration or deploy hooks: call your host’s rebuild API from the webhook handler. See the Astro guide note on rebuilding when CMS content changes.
  • Cloudflare / Netlify / similar: many platforms expose a “deploy hook” URL. Point the NomaCMS webhook at that URL, or at a thin proxy that checks the signature first.

Signature checking still matters even if the target is a deploy hook, if you can put a small verifier in front.

5. Retries and delivery logs

From the sender:

  • Failed network calls and 5xx (and 429) are retried (job tries 3, backoff 30 seconds).
  • Most 4xx responses are not retried (your bug, not a blip). Fix the handler, then publish again or wait for the next edit.

In the dashboard (or via GET /api/webhooks/{uuid}/logs / client.webhooks.logs), open delivery logs for the webhook. You will see the event name, URL, HTTP status (or blocked / error), and truncated request/response bodies. Use that when your Route Handler returns 401 or 500.

Docs: Webhook Logs.

6. Practical checklist

  • [ ] Webhook URL is public HTTPS
  • [ ] Secret set and verified with HMAC-SHA256 of the raw body
  • [ ] Events include the publish path you care about (content.published, and usually content.updated)
  • [ ] Sources include cms (and api if you write content via API)
  • [ ] Fetch layer uses tags or paths you actually revalidate
  • [ ] Handler returns 2xx quickly
  • [ ] You confirmed a 200 (or other 2xx) status in webhook logs after a test publish

Links

Questions welcome in the comments.