docs
    agency-site-kit: Live updates and preview
    v0.11.1.md

    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

    OptionDefaultWhat it does
    secretSONOR_API_KEY, read on every callThe 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:

    FieldMeaning
    paths / pathLocal paths to regenerate
    tags / tagCache tags to revalidate
    slug / slugsCase studies to regenerate: each becomes <basePath>/<slug>
    revalidateAllRegenerate 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

    StatusWhen
    200{ "revalidated": [...paths], "tags": [...tags] }
    401The bearer key is missing or wrong. The comparison is constant-time.
    413The body is over 16 KB
    400Any 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.
    500A 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

    1. 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.
    2. The route checks the token by asking Sonor for that draft with it. If the draft comes back, the token is good.
    3. It turns on Next's draft mode, stores the token in an httpOnly cookie (sameSite: 'lax', secure on HTTPS, for one hour), and redirects to <basePath>/<slug>?preview=1.
    4. On that page, getPortfolioItem sees 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.

    RequestResponse
    ?slug=…&token=…, validA 307 to <basePath>/<slug>?preview=1, in draft mode
    ?slug=…&token=…, invalid or expired401 { "error": "Invalid or expired preview token" }
    No slug or no token400 { "error": "Missing slug or token" }
    ?exit=1Leaves 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.