Live updates and preview
Two route handlers connect your site to the Sonor dashboard. The webhook regenerates the pages a change touched as soon as you publish, so a case study goes live in seconds without a rebuild. The preview route lets the dashboard's Preview button open an unpublished case study on your real site. The setup CLI writes both.
The webhook: createRevalidateHandler
// app/api/seo-revalidate/route.ts
import { createRevalidateHandler } from '@sonordev/agency-site-kit/revalidate';
export const POST = createRevalidateHandler({ basePath: '/work' });Sonor calls https://<your-domain>/api/seo-revalidate, so mount it at exactly
that path. It covers every kind of content Sonor manages (managed copy, page
metadata, articles), not only case studies, so it's the only revalidation
route your site needs. Use it in place of site-kit's own
revalidate route, not beside it:
it's built on the same handler and adds the portfolio parts (your case study
index, detail pages by slug and the portfolio cache tag).
Options
| Option | Default | What it does |
|---|---|---|
secret | SONOR_API_KEY, read on every call | The key callers must present. A string, or a function that returns one, called on every request. |
basePath | '/work' | Where case studies live. Regenerated on every call, and each slug becomes <basePath>/<slug>. |
defaultTags | ['portfolio'] | Cache tags to revalidate when a call names none |
extraPaths | [] | More local paths to regenerate on every call, like '/feed.xml' or a home page that lists recent work |
basePath and every extraPaths entry must be a local path like /work.
Anything else throws a TypeError when the handler is created, so a typo
fails your build rather than a webhook call.
If your site also publishes Sonor articles, add the article index and its
feeds to extraPaths so they refresh with every publish:
export const POST = createRevalidateHandler({
basePath: '/work',
extraPaths: ['/insights', '/insights/rss.xml', '/insights/feed.xml'],
});What a call does
Sonor sends a POST with Authorization: Bearer <SONOR_API_KEY> and a JSON
body:
| Field | Meaning |
|---|---|
paths / path | Local paths to regenerate |
tags / tag | Cache tags to revalidate |
slug / slugs | Case studies to regenerate: each becomes <basePath>/<slug> |
revalidateAll | Regenerate the whole site from the root layout |
Every accepted call regenerates /sitemap.xml, /llms.txt, /llms-full.txt,
basePath and your extraPaths, plus the paths and slugs it names. A call
with revalidateAll, or one naming only tags that include seo, regenerates
the root layout (and so every page) instead of individual paths.
Then it revalidates the tags the call names, or defaultTags when it names
none. Tags use Next's 'max' profile: stale-while-revalidate. The next
visitor gets the cached page and triggers the rebuild, and everyone after
gets the new one.
Responses
| Status | When |
|---|---|
200 | { "revalidated": [...paths], "tags": [...tags] } |
401 | The bearer key is missing or wrong. The comparison is constant-time. |
413 | The body is over 16 KB |
400 | Any path, tag or slug is malformed (a path that isn't a plain local path, dot segments, a query or fragment, a [segment]), or there are more than 100 paths or tags. One bad value refuses the whole call. |
500 | A revalidation call failed inside Next |
A refused call regenerates nothing. With @sonordev/site-kit 7.1 or later
installed, { "ping": true } on its own regenerates nothing and answers
{ "ok": true, "ping": true, "version": "…" }, which Sonor uses to check the
route is installed.
Check it
curl -s -X POST https://youragency.com/api/seo-revalidate \
-H "Authorization: Bearer $SONOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"slug":"northwind-engineering"}'A 200 listing /work/northwind-engineering means it works. A 404 means
the route isn't deployed at that path, and a 401 means the key on your site
doesn't match your project's key.
Without the route nothing breaks: your case studies still update once their cached fetches expire, within an hour.
Draft preview: createPortfolioPreviewHandler
// app/api/portfolio-preview/route.ts
import { createPortfolioPreviewHandler } from '@sonordev/agency-site-kit/preview';
export const { GET } = createPortfolioPreviewHandler({ basePath: '/work' });It returns { GET }, so export it by destructuring. The one option,
basePath (default '/work'), must match where your case study pages live.
It must be a local path like /work: anything else throws a TypeError when
the handler is created, so a typo fails your build rather than a preview.
How it works
- In the Sonor dashboard, someone presses Preview on a case study. Sonor
issues a short-lived preview token and opens
/api/portfolio-preview?slug=<slug>&token=<token>on your site. - The route checks the token by asking Sonor for that draft with it. If the draft comes back, the token is good.
- It turns on Next's draft mode, stores the token in an
httpOnlycookie (sameSite: 'lax',secureon HTTPS, for one hour), and redirects to<basePath>/<slug>?preview=1. - On that page,
getPortfolioItemsees draft mode and the cookie, and fetches the unpublished version with the token, uncached.
Tokens last an hour, so the cookie does too.
Every redirect is a relative Location, so the browser stays on the address
it asked for. The route never builds a full URL from the request, because
behind some hosts (Netlify among them) that can name the deploy's own
address rather than your domain, and the preview cookies don't exist there.
The ?preview=1 is there because Netlify adds the incoming query to a
redirect that has none, which would put the token in the address bar.
Nothing reads it.
| Request | Response |
|---|---|
?slug=…&token=…, valid | A 307 to <basePath>/<slug>?preview=1, in draft mode |
?slug=…&token=…, invalid or expired | 401 { "error": "Invalid or expired preview token" } |
No slug or no token | 400 { "error": "Missing slug or token" } |
?exit=1 | Leaves draft mode, clears the cookie, and redirects (307) to basePath |
Draft mode only affects getPortfolioItem (and generatePortfolioMetadata,
which reads through it): lists, categories and the sitemap always show
published work. To add a way out of preview, link to
/api/portfolio-preview?exit=1.
Check it
curl -s -o /dev/null -D - "https://youragency.com/api/portfolio-preview?exit=1"A 307 with location: /work (your basePath, maybe followed by the query
you sent) means the route is deployed. A location that names another host
means the site is on a version of the kit before 0.11.1.