Integrating a site with Sonor — agent guide
You installed
@sonordev/site-kit. This file tells a coding agent how to wire a Next.js site to Sonor correctly and prove it works — no web access needed. Editing site-kit itself? SeeCONTRIBUTING.md.
For the full machine-readable contract (modules, env, blessed patterns, failure modes, exit codes) run:
npx sonor-setup manifest --jsonEverything below is also in that manifest. This file is the human-skimmable version.
North star
Take a bare Next.js repo → a fully wired, verified-green Sonor site. The
signal that you are done is verify exiting 0 — not "the command ran."
Canonical sequence
npx sonor-setup manifest --json # discover the package
npx sonor-setup init --api-key "$SONOR_API_KEY" --yes --json # wire layout + .env.local + postbuild
npx sonor-setup scaffold --yes --json # sitemap, robots, llms, proxy
npx sonor-setup mcp --yes --json # agent tools: MCP endpoint, card, relay (optional)
# Existing 2.x-6.x site? migrate deterministically:
npx sonor-setup codemod --check --json # exit 1 ⇒ work pending
npx sonor-setup codemod --write --json # apply (idempotent, minimal diffs)
next build # you run the build
npx sonor-setup verify --json # exit 0 === doneThe machine contract
- Every agent-native command supports
--jsonand prints exactly ONE JSON envelope to stdout (all human logs go to stderr). Parse stdout; branch onexitCode/ok/checks. - Exit codes:
0OK ·1FAILED (result is red, fix the code) ·2USAGE (bad invocation) ·3CONFIG (missing input — the error names the flag) ·4NETWORK (API/key) ·5INTERNAL. - Never interactive under
--json,--yes, or a non-TTY. A command that needs input exits3with an actionablefixinstead of hanging. - Each
checks[]finding has a stableid, afix, and often afixCommandyou can run directly.
The one env var
The only variable a site needs is:
SONOR_API_KEY=sonor_1a2b3c4d_... # server-side only — NO NEXT_PUBLIC_ prefixSiteKitLayout reads it server-side and derives the project id + all client
config. Never add NEXT_PUBLIC_SONOR_API_KEY, UPTRADE_API_KEY,
NEXT_PUBLIC_UPTRADE_API_KEY, or SONOR_PROJECT_ID. Uptrade keys/vars are fully
deprecated. Key format: sonor_{project-uuid-first8}_{secret}.
Blessed patterns
Root layout — SiteKitLayout (a server component), wrap {children}:
import { SiteKitLayout } from '@sonordev/site-kit/layout'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<SiteKitLayout>{children}</SiteKitLayout>
</body>
</html>
)
}Use SiteKitLayout, never the deprecated SiteKitProvider (it forces the
whole tree client-side and breaks RSC/SSR). codemod migrates it for you.
Analytics is already a deferred, childless sibling. Don't wrap {children},
and don't hand-roll it. Since 3.0.2, SiteKitLayout renders {children}
first and mounts analytics, chat, popups and the fleet heartbeat after it, deferred
to window load + idle (or the first interaction). 4.0.0 removed the last
wrappers. So the plain layout above is the whole pattern: configure analytics
with analytics={{ ... }}, and track custom events with the standalone
trackEvent / trackConversion from @sonordev/site-kit/analytics (no
provider needed; useAnalytics() throws outside one).
What still de-opts a statically prerenderable route to 100% client rendering
(the hero paints only after hydration; mobile LCP tanks) is a component you
wrap around {children} that skips server rendering, usually
next/dynamic({ ssr: false }). The old SiteKitLayout analytics={false} plus
a hand-rolled deferred sibling was the workaround for site-kit <3.0.2. On those
installs, upgrade.
Agent tools — the built-in Sonor MCP server (7.0). Don't hand-write
business-info, FAQ or review tools; npx sonor-setup mcp wires
sonorMcpServer from @sonordev/site-kit/mcp/sonor, and a site's own tools
go in its tools array. send_inquiry needs the person's go-ahead
(person_confirmed: true) and a form with "Agent inquiries" on in Sonor.
Pass onToolCall: reportToolCallsToSonor() to createMcpHandler so Sonor
sees which agents called. A site that already runs its own MCP server is a
custom implementation: sonor-setup mcp leaves it alone,
and so should you; add reporting to it rather than replacing it.
Imports: from a module's entry (@sonordev/site-kit/sync,
/website/images, /seo/llms), never the root, which is types-only. The
package is ESM only.
Common failure modes → the check that catches it
| Symptom | Cause | Fix | doctor/verify check |
|---|---|---|---|
Mobile LCP ~4s; server HTML is an empty shell + self.__next_f flight data, no <section>/<h1> | Something wraps {children} in a component that skips SSR, usually next/dynamic({ ssr: false }) (e.g. the site's own lazy Providers) → 100% CSR. Never a plain SiteKitLayout on site-kit ≥3.0.2 | Keep the layout. Import that wrapper statically or mount it as a childless sibling. On <3.0.2, upgrade | ssr.render |
| Build throws "runtime is not available in proxy" / codemod skips middleware.ts | Next 16's proxy.ts always runs on Node and rejects a runtime export; middleware.ts still accepts it | Remove the runtime export, then npx sonor-setup codemod --write moves the file to proxy.ts | middleware.netlify |
| Invalid-key console spam; data features paused (401/403) | Stale/rotated key, or an env change wasn't redeployed | Put the current sonor_ key in .env.local and redeploy | key.valid |
Deprecated SiteKitProvider in layout | 2.x integration | npx sonor-setup codemod --only provider-to-layout --write | layout.sitekit |
@uptrademedia/site-kit / UPTRADE_* remnants | pre-rebrand site | npx sonor-setup codemod --only uptrade-to-sonor --write | package.installed / env.api-key |
| Hero present but LCP late | above-the-fold element is opacity:0 + JS entrance-animated | Render hero fully static; gate reveal animations below the fold | — |
Verifying (definition of done)
npx sonor-setup verify --json # static health + key validity + SSR (from .next build)
npx sonor-setup verify --url https://your-site.com --json # SSR check against a live/preview URL (strongest)verify exits 0 only when the integration is genuinely green. If it exits
non-zero, read checks[] — each red check carries a fix (and often a
fixCommand), and nextSteps[] names what to run. Loop until green.