# Get started
A Sonor site is a Next.js site with `@sonordev/site-kit` installed and one API key. Everything it shows or tracks (SEO, analytics, forms, articles, reviews, chat, agent tools) is managed from the dashboard at [app.sonor.io](https://app.sonor.io), so the code stays small and the content stays editable.
You'll need Next.js 16 and Node 20.19 or later. site-kit is ESM only.
## 1. Install
```bash
npm install @sonordev/site-kit
```
## 2. Add your key
Copy the key from [app.sonor.io](https://app.sonor.io): **Projects, then your project, then Settings, then API Keys**. Keys start with `sonor_`.
```bash
# .env.local
SONOR_API_KEY=sonor_xxxxxxxx_xxxxx
```
That's the only variable a site needs. Keep it server-side, with no `NEXT_PUBLIC_` prefix: `SiteKitLayout` reads it on the server and hands the client what it needs.
## 3. Add the layout
```tsx
// app/layout.tsx
import { SiteKitLayout } from '@sonordev/site-kit/layout'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
`SiteKitLayout` is a server component. Your page renders first, and analytics, chat and the rest mount after it as deferred siblings, so nothing pushes a route into client rendering. Its props are in [Layout](https://sonor.dev/site-kit/layout).
## 4. Scaffold the rest
```bash
npx sonor-setup init # the layout, the key and the postbuild step, if you'd rather not do 2 and 3 by hand
npx sonor-setup scaffold # sitemap, robots, llms.txt, proxy and an OG card
```
The proxy adds Sonor-managed redirects, security headers and AI discovery headers. See [Proxy](https://sonor.dev/site-kit/proxy).
## 5. Build and verify
```bash
next build
npx sonor-setup verify
```
`verify` exits 0 only when the integration is genuinely done: the key works, the layout's in place and the built pages server-render real content. When it doesn't, each failing check says how to fix it. Point it at a deploy for the strongest check:
```bash
npx sonor-setup verify --url https://your-site.com
```
## What next
- **An agency or real estate site?** An industry kit adds case studies or listings on top of site-kit. See [Industry kits](https://sonor.dev/guides/kits).
- **Forms**: `` renders a form you define in Sonor, with spam defense built in. See [Forms](https://sonor.dev/site-kit/forms).
- **SEO**: managed metadata, schema and FAQs per page. See [SEO](https://sonor.dev/site-kit/seo).
- **AI visibility and agents**: llms.txt, answer-engine blocks and MCP tools. See [Agents and AI visibility](https://sonor.dev/guides/agents).
- **An older site?** `npx sonor-setup codemod --write` moves any 2.x to 6.x site to site-kit 7. See [Migrating to 7](https://sonor.dev/site-kit/migrating-to-7).
- **Building with a coding agent?** Every CLI command takes `--json`, and site-kit ships a guide written for agents. See [For coding agents](https://sonor.dev/site-kit/agents).
---
# Industry kits
site-kit covers what every Sonor site needs. An industry kit adds what one kind of site needs on top of it: an agency's case studies, a brokerage's listings. A kit never rebuilds what site-kit already does. It fetches through site-kit's server data plane, tracks through its analytics and submits through its managed forms, so a kit site still runs on one `SONOR_API_KEY` and shows up in the same Sonor project.
| Kit | For | What it adds |
| ---------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [agency-site-kit](https://sonor.dev/agency-site-kit) | Agencies showing client work | Case studies from Sonor's portfolio, the rules for which numbers a case study may claim, JSON-LD, live device frames, instant publish and draft preview |
| [re-site-kit](https://sonor.dev/re-site-kit) | Real estate sites | MLS listings with search and filters, listing pages, IDX attribution, buildings, trending listings, and MCP tools for a buyer's AI assistant |
## How a kit fits
- **site-kit comes first.** Each kit lists `@sonordev/site-kit` as a peer dependency and uses the copy your site already installs. Wire site-kit as in [Get started](https://sonor.dev/guides/getting-started), then add the kit.
- **Nothing new to configure.** The kit reads the same `SONOR_API_KEY`, server-side, and Sonor works out the project from it.
- **Server-rendered by default.** Kit components render on the server. The few client pieces (a gallery, a view tracker, a form) are small islands that never wrap your page.
- **The content lives in Sonor.** Case studies and listings are edited in the dashboard, and the [live updates route](https://sonor.dev/site-kit/live-updates) refreshes the pages that show them.
## agency-site-kit
For an agency's own site: the work index, a page per case study, and the proof behind every number on it.
```bash
pnpm add @sonordev/agency-site-kit @sonordev/site-kit
npx agency-site-kit-setup --site-url https://youragency.com --agency-name "Your Agency"
```
The setup command scaffolds a `/work` index, case-study pages, category routes, and the two routes Sonor calls (live updates and draft preview). The renderer it writes is yours to restyle; the kit supplies the rules every case study follows, so a number is always credited to whoever reported it and never overstated.
[Read the agency-site-kit docs](https://sonor.dev/agency-site-kit)
## re-site-kit
For a brokerage or team site: searchable listings, a page per listing, and the MLS attribution IDX rules require.
```bash
pnpm add @sonordev/re-site-kit
```
```tsx
// app/listings/page.tsx
import { searchListings } from '@sonordev/re-site-kit/server'
import { ListingGrid, ListingFilters, ListingPagination } from '@sonordev/re-site-kit'
export const revalidate = 60
export default async function ListingsPage({ searchParams }) {
const params = await searchParams
const { listings, pagination } = await searchListings({ city: params.city, page: Number(params.page ?? 1) })
return (
`/listings/${l.slug}`} />
)
}
```
The site never holds an MLS credential: Sonor pulls the feed and the kit reads it through the same key as everything else.
[Read the re-site-kit docs](https://sonor.dev/re-site-kit)
---
# Agents and AI visibility
People increasingly meet a business through an assistant: ChatGPT, Claude, Perplexity, Google's AI answers. Sonor gives a site three ways to be understood and used by them, and tells you which agents showed up.
| Layer | What it does | Where it lives |
| --------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| **Read** | llms.txt, answer-engine blocks and schema, so an assistant quotes the business correctly | [llms.txt and AEO](https://sonor.dev/site-kit/llms), [SEO](https://sonor.dev/site-kit/seo) |
| **Use** | MCP tools, so an agent can look up services, hours, FAQs and reviews, and send an inquiry | [MCP and agent tools](https://sonor.dev/site-kit/mcp) |
| **Build** | A machine-readable contract, so a coding agent can wire a site correctly and prove it | [For coding agents](https://sonor.dev/site-kit/agents), [sonor-setup CLI](https://sonor.dev/cli) |
## Read: llms.txt and answer engines
`npx sonor-setup scaffold` writes `/llms.txt` and `/llms-full.txt` at build time from what Sonor knows about the business: its summary, services, pages and FAQs. Answer-engine blocks (`AEOBlock`, `AEOSummary`, `AEOSteps`) and Speakable schema put direct answers in the page itself, and the proxy's discovery headers point crawlers at all of it. Everything's in [llms.txt and AEO](https://sonor.dev/site-kit/llms).
## Use: agent tools over MCP
```bash
npx sonor-setup mcp --inquiry-form contact
```
That gives the site a Model Context Protocol endpoint at `/api/mcp`, a server card at `/.well-known/mcp-server-card`, and the built-in Sonor tools: the business profile, services, FAQ search, pages, articles and reviews. With `--inquiry-form`, an agent can also send an inquiry for a person.
A few rules hold for every site:
- **An inquiry has a person behind it.** `send_inquiry` refuses unless the person asked to be contacted and agreed to share their details, and only for a form with "Agent inquiries" turned on in Sonor. Each one arrives with the agent's name on it.
- **Tools read what visitors read.** The built-in tools use the same data the site's pages do, so an agent never learns something a visitor couldn't.
- **You can bring your own server.** A site that runs its own MCP server keeps it; site-kit won't overwrite it, and you can still mix in the built-in tools.
The full setup, including rate limiting on Netlify and writing good tool descriptions, is in [MCP and agent tools](https://sonor.dev/site-kit/mcp).
## See which agents came
Pass `onToolCall: reportToolCallsToSonor()` to the MCP handler, and Sonor records each call: which agent, which tool, and whether it worked. Never the arguments or the answer. The **AI Visibility** tab in Sonor lists them, and Echo flags a tool that keeps failing along with the fix.
## Build: coding agents
site-kit ships a guide written for coding agents, a machine-readable manifest, and a CLI where every command answers in one JSON envelope with a stable exit code:
```bash
npx sonor-setup manifest --json # modules, env, patterns and failure modes
npx sonor-setup verify --json # exit 0 means done
```
An agent can go from a bare Next.js repo to a verified Sonor site without guessing. Start with [For coding agents](https://sonor.dev/site-kit/agents).
## This site is agent-readable too
Every page on sonor.dev is available as markdown: add `.md` to its URL. [llms.txt](https://sonor.dev/llms.txt) indexes them, [llms-full.txt](https://sonor.dev/llms-full.txt) has all of them in one file, and `https://sonor.dev/api/mcp` serves `search_docs` and `get_doc` tools so an agent building a Sonor site can read these docs directly.
---
# The public API
Every site-kit module talks to Sonor over one public API at `https://api.sonor.io/api/public`. site-kit is the supported way to use it: it handles auth, caching, the site host, retries and spam defense for you. This page is for when you need to know what's underneath.
## Authentication
Every request carries the project's key in the `x-api-key` header:
```bash
curl https://api.sonor.io/api/public/... \
-H 'x-api-key: sonor_xxxxxxxx_xxxxx'
```
The key identifies the project, so a request never sends a project id. Keys look like `sonor_{first 8 characters of the project id}_{secret}`. A site sets one variable, `SONOR_API_KEY`, and `SiteKitLayout` passes the browser a short-lived credential for the calls that happen there.
## Multi-site projects
One Sonor project can serve many domains: a company site plus regional microsites, say. Reads and writes carry the site host (`?site=` on reads, a `site` field on writes) so each domain gets its own pages, forms and analytics. site-kit sends it automatically from `NEXT_PUBLIC_SITE_URL`.
## What's there
The API is grouped the way the site-kit modules are:
| Area | site-kit module |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| SEO metadata, schema, FAQs, sitemaps, redirects | [SEO](https://sonor.dev/site-kit/seo), [Sitemap](https://sonor.dev/site-kit/sitemap), [Redirects](https://sonor.dev/site-kit/redirects) |
| Analytics events, page views and Web Vitals | [Analytics](https://sonor.dev/site-kit/analytics) |
| Forms and submissions | [Forms](https://sonor.dev/site-kit/forms) |
| Articles | [Articles](https://sonor.dev/site-kit/articles) |
| Reviews and testimonials | [Reputation](https://sonor.dev/site-kit/reputation) |
| Products, services, events and checkout | [Commerce](https://sonor.dev/site-kit/commerce) |
| Booking availability | [Booking](https://sonor.dev/site-kit/booking) |
| Chat, popups and banners | [Website chat](https://sonor.dev/site-kit/chat) |
| llms.txt and answer-engine data | [llms.txt and AEO](https://sonor.dev/site-kit/llms) |
| Agent tool-call reports | [MCP and agent tools](https://sonor.dev/site-kit/mcp) |
| Listings and search | [re-site-kit](https://sonor.dev/re-site-kit) |
| Portfolio and case studies | [agency-site-kit](https://sonor.dev/agency-site-kit) |
## Forms go through site-kit
A form submission is only accepted with evidence that a real browser rendered the page it came from. `` and the headless `useForm` hook send that evidence automatically; a hand-rolled `fetch`, or a proxy through your own API route, can't. Sonor refuses those before anything is written, so the visitor sees an error and you get no lead.
So:
- Use ``, or `useForm` when the design needs its own markup.
- Define the form's fields in Sonor, so both can render and validate them.
- Send routing to different inboxes from Sonor (one form per destination), not from a proxy.
- For agents, turn on "Agent inquiries" for the form and use the MCP `send_inquiry` tool. See [Agents and AI visibility](https://sonor.dev/guides/agents).
## Using the API from something other than Next.js
site-kit targets Next.js 16. From another stack, the data reads (SEO, articles, reviews, llms data) work over plain HTTP with the key. Forms don't, for the reason above. If you're planning a non-Next integration, talk to us first at [sonor.io](https://sonor.io/contact).
---
# @sonordev/site-kit
All-in-one integration kit for [Sonor](https://sonor.io)-powered Next.js sites. One package, one env var, every module: SEO, Analytics, Forms, Articles, Commerce, Website chat, Popups, GEO/AEO, Booking, Reputation, A/B Testing, and more — all managed from the Sonor dashboard at [app.sonor.io](https://app.sonor.io).
## Install
```bash
npm install @sonordev/site-kit
```
## What's new in 7.0
- **Agents can use the site.** `npx sonor-setup mcp` serves the built-in
Sonor tools over MCP (business profile, services, FAQ, pages, articles,
reviews, and inquiries when you turn them on), and Sonor's AI Visibility
tab shows which agents called them. See [src/mcp/README.md](https://sonor.dev/site-kit/mcp).
- **Organised the way Sonor's dashboard is**: one entry per module
(`./website/*`, `./seo/*`, `./chat`), a types-only root entry, and the
setup CLI in its own package, [`sonor-setup`](https://sonor.dev/cli).
- **ESM only, Next 16 only, 0.6 MB** (from 4 MB), with no react-markdown in
your install and support for Cache Components.
Move a site when you next touch it: `npx sonor-setup codemod --write`, then
build. See [docs/MIGRATING-TO-7.md](https://sonor.dev/site-kit/migrating-to-7).
## Setup
**One environment variable:**
```bash
# .env.local
SONOR_API_KEY=sonor_xxxxxxxx_xxxxx
```
Copy the key from [app.sonor.io](https://app.sonor.io): **Projects → your project → Settings → API Keys**. Keys start with `sonor_`. Keep it server-side, with no `NEXT_PUBLIC_` prefix.
**One layout component:**
```tsx
// app/layout.tsx
import { SiteKitLayout } from '@sonordev/site-kit/layout'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
`SiteKitLayout` is an RSC-compatible async Server Component that auto-composes: analytics tracking, website chat, popups, favicons, and managed scripts. No client-side provider wrapping needed: client modules mount as deferred siblings after `{children}`, so the page stays server-rendered. Props and module options: [src/layout/README.md](https://sonor.dev/site-kit/layout).
**Proxy (optional but recommended):**
```ts
// proxy.ts
import { createProxy } from '@sonordev/site-kit/proxy'
export default createProxy()
// Inlined on purpose: Next statically parses `config.matcher` at build time
// and rejects an imported value. `siteKitMatcher` is a reference value to
// copy from, never a binding to export.
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:ico|png|jpg|jpeg|gif|webp|svg|woff2?)$).*)',
],
}
```
Handles Sonor-managed redirects, security headers, and AI discovery headers.
**CLI (optional — scaffolds everything):**
```bash
npx sonor-setup init
npx sonor-setup scaffold # sitemap, robots, llms.txt, middleware, manifest
npx sonor-setup status # health check
```
***
## Modules
Every module has its own README in `src//README.md` with full API docs, types, and examples.
### Core (every site)
| Module | Import | Purpose | Docs |
| ------------- | ---------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------- |
| **Layout** | `@sonordev/site-kit/layout` | RSC layout that composes all features | [README](https://sonor.dev/site-kit/layout) |
| **SEO** | `@sonordev/site-kit/seo` | Managed metadata, schemas, FAQs, internal links | [README](https://sonor.dev/site-kit/seo) |
| **Analytics** | `@sonordev/site-kit/analytics` | Page views, events, conversions, Web Vitals | [README](https://sonor.dev/site-kit/analytics) |
| **Sitemap** | `@sonordev/site-kit/seo/sitemap` | Auto-generated sitemap with Sonor sync. URLs follow next.config `trailingSlash` | [README](https://sonor.dev/site-kit/sitemap) |
| **Proxy** | `@sonordev/site-kit/proxy` | Redirects, security headers, AI discovery (`./middleware` is its 7.x alias) | [README](https://sonor.dev/site-kit/proxy) |
| **Redirects** | `@sonordev/site-kit/seo/redirects` | Sonor-managed 301/302 redirect rules | [README](https://sonor.dev/site-kit/redirects) |
### Content
| Module | Import | Purpose | Docs |
| -------------- | ----------------------------------- | -------------------------------------------------------- | ----------------------------------------------- |
| **Articles** | `@sonordev/site-kit/articles` | Sonor-managed articles with SSG, topic clusters, E-E-A-T | [README](https://sonor.dev/site-kit/articles) |
| **Images** | `@sonordev/site-kit/website/images` | Managed image slots with dev-mode editing | [README](https://sonor.dev/site-kit/images) |
| **Reputation** | `@sonordev/site-kit/reputation` | Reviews, testimonials, rating stats | [README](https://sonor.dev/site-kit/reputation) |
### Engagement
| Module | Import | Purpose | Docs |
| -------------------- | ----------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------- |
| **Website chat** | `@sonordev/site-kit/chat` | The chat launcher and Echo (Sonor → Messages) | [README](https://sonor.dev/site-kit/chat) |
| **Popups & Banners** | `@sonordev/site-kit/website/popups` | Popups, banners and toasts from Sonor → Website, drawn in the site's own design | [README](https://sonor.dev/site-kit/popups) |
| **Forms** | `@sonordev/site-kit/forms` | Managed forms with CRM routing, multi-step, validation | [README](https://sonor.dev/site-kit/forms) |
| **Signal** | `@sonordev/site-kit/signal` | A/B experiments, behavior tracking, real-time config | [README](https://sonor.dev/site-kit/signal) |
### Commerce
| Module | Import | Purpose | Docs |
| ------------ | ----------------------------- | ----------------------------------------- | --------------------------------------------- |
| **Commerce** | `@sonordev/site-kit/commerce` | Products, services, events, checkout | [README](https://sonor.dev/site-kit/commerce) |
| **Sync** | `@sonordev/site-kit/sync` | Booking/scheduling widget (Calendly-like) | [README](https://sonor.dev/site-kit/booking) |
### GEO / AEO (AI Visibility)
| Module | Import | Purpose | Docs |
| ------------------- | -------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------- |
| **LLMs** | `@sonordev/site-kit/seo/llms` | llms.txt, AEO components, Speakable schema | [README](https://sonor.dev/site-kit/llms) |
| **LLMs Contract** | `@sonordev/site-kit/seo/llms/contract` | Shared types/sanitizers (the APIs use `@sonordev/contracts/llms`, the same code) | README |
| **MCP** | `@sonordev/site-kit/mcp` | MCP endpoint, server card, in-page WebMCP tools | [README](https://sonor.dev/site-kit/mcp) |
| **Sonor MCP tools** | `@sonordev/site-kit/mcp/sonor` | The built-in tools (`sonorMcpServer`) and tool-call reporting to Sonor | [README](https://sonor.dev/site-kit/mcp) |
### Motion
Three tiers as three subpaths — a site only installs and ships what it imports. Server HTML always stays visible; above-the-fold content is never animated in.
| Module | Import | Purpose | Docs |
| ------------------ | --------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
| **Motion** | `@sonordev/site-kit/motion` | ``, ``, `` and the scroll engine — zero deps, \~2KB | [README](https://sonor.dev/site-kit/motion) |
| **Motion / GSAP** | `@sonordev/site-kit/motion/gsap` | Lazy-loaded GSAP timelines (`useGsap`) — optional peer `gsap` | [README](https://sonor.dev/site-kit/motion) |
| **Motion / three** | `@sonordev/site-kit/motion/three` | WebGL stages on the engine (`useThreeStage`) — optional peer `three` | [README](https://sonor.dev/site-kit/motion) |
### Liquid Glass chrome
| Module | Import | Purpose | Docs |
| ----------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| **CTA bar** | `@sonordev/site-kit/website/cta-bar` | `` + ``: the floating glass mobile CTA bar. Hides over its form and while typing, compacts on scroll, lifts the Echo launcher, emits `cta_click`. Server component. | [README](https://sonor.dev/site-kit/cta-bar) |
Echo's launcher and chat window use the same glass recipe; see [Website chat](https://sonor.dev/site-kit/chat#liquid-glass-610).
***
## How It Works
```
SONOR_API_KEY in .env.local
│
▼
SiteKitLayout (RSC server component)
├── Server-side:
│ ├── ManagedFavicon (Sonor logo → tags)
│ ├── ManagedScripts (tracking pixels, analytics tags)
│ └── API preconnect hints
│
├── Client-side (childless siblings after {children}, deferred to idle):
│ ├── AnalyticsProvider (page views, scroll depth, Web Vitals)
│ ├── SitePopups (popups, banners, toasts)
│ ├── SiteChat (website chat: Echo)
│ ├── SignalBridge (A/B experiments, opt-in, not deferred)
│ ├── SitemapSync (browser sitemap fallback, opt-in)
│ └── FleetHeartbeat (kit version + modules, once per session)
│
└── Middleware (separate):
├── Redirects (Sonor-managed 301/302)
├── Security headers (CSP frame-ancestors, nosniff, etc.)
└── AI discovery (Link: rel="describedby" → /llms.txt)
```
All data flows through the Sonor API (`api.sonor.io`) authenticated by your API key. No direct database access, no Supabase keys exposed to the client.
***
## Import Paths
```ts
// Core
import { SiteKitLayout } from '@sonordev/site-kit/layout'
import { createProxy, siteKitMatcher } from '@sonordev/site-kit/proxy' // in proxy.ts
// SEO
import { getManagedMetadata, ManagedSchema, ManagedFAQ } from '@sonordev/site-kit/seo'
// Analytics
import { AnalyticsProvider, useAnalytics, WebVitals } from '@sonordev/site-kit/analytics'
// Forms
import { ManagedForm, useForm, formsApi, field } from '@sonordev/site-kit/forms'
// Articles (server components — keeps server-only data helpers out of client bundles)
import { Article, ArticleList, ClusterLandingPage } from '@sonordev/site-kit/articles/server-ui'
import { getArticle, generateArticleStaticParams } from '@sonordev/site-kit/articles/server'
// Commerce
import { OfferingCard, ProductPage, EventCalendar } from '@sonordev/site-kit/commerce'
// Booking
import { BookingWidget } from '@sonordev/site-kit/sync'
// Website chat, and popups (SiteKitLayout mounts both; its `chat` and `popups` props turn them off)
import { SiteChat, ChatWidget } from '@sonordev/site-kit/chat'
import { SitePopups, PopupBlocks } from '@sonordev/site-kit/website/popups'
// Signal (A/B)
import { SignalBridge, SignalExperiment, useSignal } from '@sonordev/site-kit/signal'
// GEO / AEO
import { createLLMsTxtHandler, buildAiDiscoveryHeaders } from '@sonordev/site-kit/seo/llms'
import { AEOBlock, AEOSummary, AEOSteps, SpeakableSchema } from '@sonordev/site-kit/seo/llms'
// Agent tools (MCP): see src/mcp/README.md, or run `npx sonor-setup mcp`
import { createMcpHandler, createMcpServerCardHandler } from '@sonordev/site-kit/mcp'
import { sonorMcpServer, reportToolCallsToSonor } from '@sonordev/site-kit/mcp/sonor'
import { WebMcpTools } from '@sonordev/site-kit/mcp/client'
// Sitemap
import { createSitemap } from '@sonordev/site-kit/seo/sitemap'
// Images
import { ManagedImage } from '@sonordev/site-kit/website/images'
// Reputation
import { TestimonialSection, fetchReviews } from '@sonordev/site-kit/reputation'
// Redirects
import { handleManagedRedirects } from '@sonordev/site-kit/seo/redirects'
// Robots (the AI crawler helpers live in /llms; /robots re-exports them)
import { createRobots, buildAiCrawlerRules, createRobotsTxtHandler } from '@sonordev/site-kit/seo/robots'
// Motion (tier 0, zero deps) — tiers 1/2 need `npm i gsap` / `npm i three`
import { Reveal, Parallax, ScrollScene, registerScene } from '@sonordev/site-kit/motion'
import { useGsap } from '@sonordev/site-kit/motion/gsap'
import { useThreeStage, canRunWebGL } from '@sonordev/site-kit/motion/three'
// Liquid Glass mobile CTA bar (server component; pass Next Link via `as`)
import { CtaBar, CtaBarAction } from '@sonordev/site-kit/website/cta-bar'
// Styles (optional)
import '@sonordev/site-kit/brand.css'
import '@sonordev/site-kit/forms/styles.css'
```
***
## Environment Variables
```bash
# Required (server-only — SiteKitLayout injects into client automatically):
SONOR_API_KEY=sonor_xxxxxxxx_xxxxx
# Optional:
SONOR_API_URL=https://api.sonor.io # Sonor API (default)
NEXT_PUBLIC_SITE_URL=https://example.com # For CLI status checks + sitemap
REVALIDATION_SECRET=your_secret # On-demand ISR for llms.txt
MCP_TRANSPORT_SECRET=... # Netlify MCP relay (npx sonor-setup mcp writes it)
```
Uptrade-era variables (`UPTRADE_API_KEY`, `NEXT_PUBLIC_UPTRADE_API_KEY`) aren't read, and `uptrade_` keys aren't accepted. A site that only sets those runs with no key. Move it to `SONOR_API_KEY` with `npx sonor-setup codemod --only uptrade-to-sonor --write`.
***
## CLI
The setup CLI is its own package since 7.0, [`sonor-setup`](https://sonor.dev/cli).
`npx` fetches it; a site that runs it from package.json scripts adds it as a
dev dependency. (`sonor-register-sitemap`, the postbuild step, stays here.)
```bash
npx sonor-setup
Commands:
init Initialize site-kit in a Next.js project
scaffold Scaffold sitemap, robots, llms.txt, proxy, manifest
mcp Give agents the site's tools (MCP endpoint, server card, relay)
setup AI-powered SEO setup (metadata, schemas, FAQs)
scan Scan codebase for integration opportunities
migrate Migrate detected components to site-kit
sync Sync local content to Sonor
status Health check (API, llms.txt, sitemap, layout)
geo Check GEO/llms.txt wiring
images Scan, upload, and manage images
locations Generate location pages
faqs Sync ManagedFAQ paths
api-routes Generate API proxy routes
install Install @sonordev/site-kit
upgrade Upgrade to latest version
codemod Move an older site to the current site-kit (--check, --write)
next16 Move middleware.ts to proxy.ts
verify Exit 0 when the integration is done
```
***
## TypeScript
Fully typed. All types are exported from their respective module paths:
```ts
import type { LLMsDataResponse, GenerateLLMSTxtOptions } from '@sonordev/site-kit/seo/llms'
import type { ManagedFormConfig, UseFormReturn } from '@sonordev/site-kit/forms'
import type { Article, TopicCluster } from '@sonordev/site-kit/articles'
import type { CommerceOffering, SizeChart } from '@sonordev/site-kit/commerce'
import type { SiteKitLayoutProps } from '@sonordev/site-kit/layout'
```
***
## Architecture Note
`@sonordev/site-kit` is the client bridge between Next.js marketing sites and the Sonor platform. All persistent data (forms, articles, SEO config, analytics) lives in Sonor — site-kit fetches, renders, and tracks.
- **Server components** (SEO, Articles, Images): Import directly, RSC-compatible, no provider needed
- **Client modules** (Analytics, chat, popups, Signal): Lazy-loaded via `SiteKitLayout`, tree-shaken
- **Build-time** (Sitemap, llms.txt): Run during `next build`, sync to Sonor
- **Middleware** (Redirects, Security, AI Discovery): Runs on every request edge
Full docs for every module: [sonor.dev](https://sonor.dev).
---
# Moving a site to site-kit 7
site-kit 7 is organised the way Sonor's dashboard is: one entry per module.
Most sites need no changes at all. Move a site when you next touch it;
nothing forces it before then, because a
site's `^6` range never picks up 7.
## The short version
```bash
pnpm add @sonordev/site-kit@^7
npx sonor-setup codemod --check # lists what needs moving (writes nothing)
npx sonor-setup codemod --write # moves it
pnpm build # a bump you didn't build is a guess
```
Run the codemod in each app or workspace package that imports site-kit (in a
monorepo, inside each workspace package too). It's
idempotent: running it twice changes nothing the second time. It works from
any 2.x-6.x site: copies of sites on 4.2, 5.8 and 6.5 were migrated with it
and built.
It leaves nothing in the project but the change. Since sonor-setup 7.1.2 the
originals of the files it writes go to a folder in your OS temp directory
(it prints where, and git has them too), not `.bak` files beside them, and
it never creates or edits `.env.example`: `.env.local` is the one env file.
An old fallback such as `process.env.SONOR_API_KEY || process.env.UPTRADE_API_KEY`
becomes `process.env.SONOR_API_KEY`.
Requirements: Next 16 and Node 20.19+ or 22.12+
(Netlify's default is 22).
## What changed, and what the codemod does about it
### 1. The root entry is types-only
`@sonordev/site-kit` exports types and `SITE_KIT_VERSION`, nothing that runs.
A stray root import used to be able to pull commerce, signal or redirect code
into a page's bundle.
| Was | Now |
| ---------------------------------------------------------------------- | --------------------------------------------------------- |
| `import { BookingWidget } from '@sonordev/site-kit'` | `@sonordev/site-kit/sync` |
| `import { AffiliatesWidget, useAffiliates } from '@sonordev/site-kit'` | `@sonordev/site-kit/affiliates` (new entry) |
| commerce components and fetchers | `@sonordev/site-kit/commerce` |
| `ManagedImage` and the image helpers | `@sonordev/site-kit/website/images` |
| `SignalBridge`, experiments, `useSignal*` | `@sonordev/site-kit/signal` |
| `TestimonialSection`, `fetchReviews` | `@sonordev/site-kit/reputation` |
| `handleManagedRedirects` and friends | `@sonordev/site-kit/seo/redirects` |
| `LandingPage`, `landingPageMetadata` | `@sonordev/site-kit/website/landing` |
| `formatBookingTime`, `formatBookingDate` | `formatTime`, `formatDate` from `@sonordev/site-kit/sync` |
**The codemod rewrites these imports**, including the two renamed ones, and
keeps type imports on the root. It flags, rather than rewrites, a namespace
import (`import * as SK`), a `require`, or a re-export from the root. The full
list is `src/shared/module-map.json` (`rootRuntime`).
### 2. The setup CLI is its own package: `sonor-setup`
The CLI (init, scaffold, codemods, OG cards, GEO wiring, verify) was 3 MB of
every site's install and forced a site-kit release for every setup-only fix.
It's now [`sonor-setup`](https://sonor.dev/cli).
- **`npx sonor-setup …` keeps working unchanged:** npx fetches the package.
- **A site whose package.json scripts run `sonor-setup`** (an OG step that
runs `sonor-setup og`, say) needs it as a dev dependency. The codemod adds
`"sonor-setup": "^7.0.0"` for you.
- **`sonor-register-sitemap` stays in site-kit**, since sites run it from
their postbuild. Nothing to change.
- The `site-kit` bin alias for the CLI is gone (no site used it).
### 3. One entry per Sonor module (old paths still work)
New homes, each the same module as before:
- Website: `./website/{popups,images,slots,cms,landing,cta-bar}`
- SEO: `./seo/{sitemap,robots,indexnow,redirects,og,og/route,llms,llms/client,llms/contract,meta/contract,pages/contract}`
- Website chat: `./chat`, and popups: `./website/popups` (both out of
`./engage`, which Sonor retired with the Engage module)
**The old paths keep working through 7.x** and are removed in 8.0. The
codemod moves them anyway, so a touched site is done in one pass: chat
imports go to `./chat`, popup types to `./website/popups` under their new
names (`EngageElement` is `SitePopup`). It flags `EngageWidget`, which draws
both and which `SiteKitLayout` already mounts. `SiteKitLayout` gains `chat`
and `popups` props; `engage` is an alias (`engage={false}` still turns both
off). Since 7.2.0, popups render only from blocks: one made in Engage Studio
isn't drawn, so make it again in Sonor (Website → Popups & Banners).
### 4. ESM only, Next 16 only
site-kit ships ES modules alone (`"type": "module"`). Next, Turbopack and the
kits don't notice. Node 20.19+/22.12+ can `require()` it too, so a
`next.config.ts` that imports `@sonordev/site-kit/config` keeps working. A
site-side script that `require()`s site-kit on an older Node would break.
The `next` peer is `^16`. Next 16 renamed `middleware.ts` to `proxy.ts`, and
**the codemod moves it**: the file, a named `middleware` export (to `proxy`),
and `createMiddleware` from `@sonordev/site-kit/middleware` (to `createProxy`
from `/proxy`). The old names work through 7.x.
**The proxy goes beside the app directory.** Next reads it from the project
root, or from `src/` when the app is `src/app`, and silently ignores one
anywhere else: the build passes, but the site sends no security headers and
runs no managed redirects. So on a `src/app` site the codemod writes
`src/proxy.ts`, with the file's relative imports re-pointed. Before
sonor-setup 7.1.2 it renamed the file in place, which left a root `proxy.ts`
on those sites; rerunning the codemod moves it, and `npx sonor-setup doctor`
warns about any proxy Next doesn't read.
It won't move a file that sets `runtime` (a proxy file can't; the build
throws), and says so: remove the export, check the logic still runs on your
host, rerun. It never moves onto a proxy Next already runs: with one in each
place, it reports both and changes neither. `npx sonor-setup next16` runs
just this part.
### 5. Removed (nothing used them)
- `@sonordev/site-kit/search` and `/search/contract`
- `@sonordev/site-kit/redirects/not-found` (`resolveManagedRedirect`), and
with it the `x-sk-path` request header the proxy stamped for it and the
`SK_PATH_HEADER` export. Managed redirects run in the proxy.
- The postinstall GEO bootstrap (`SITE_KIT_AUTO_GEO=1`). Run
`npx sonor-setup geo` instead. site-kit no longer runs anything at install.
- `@sonordev/site-kit/setup` (an empty stub since 2.0), `LocationPageContent`
and `getLocationSection` (they sent a `project_id`), `identifyTopicClusters`
(use `getTopicCluster`), `formatContentSignals`, and the `SiteKitConfig`
type (not `withSiteKitConfig`, which stays).
- Source maps. The package no longer ships `.map` files (7.3 of 6.x's 11 MB).
Still accepted and ignored until 8.0, because sites pass them:
`contentSignals` on `createRobotsTxtHandler`, `nativeReturnTo` on forms, and
``.
### 6. Behavior worth knowing
- **react-markdown is no longer installed with site-kit.** The chat widget
renders markdown with `marked`, already a dependency. A site that imports
`react-markdown` itself without listing it (it only worked because npm
hoisted site-kit's copy) must add it.
- **`sonor-register-sitemap` reads .env files the way Next does**: a variable
the host set wins over every file, then `.env.production.local`,
`.env.local`, `.env.production`, `.env`. It used to let `.env.local` beat
the host.
## Optional, and new in 7.x
- **Cache Components.** `cacheComponents: true` in next.config works with
site-kit now (it failed every page through 6.x). Remove route segment
configs (`dynamic`, `revalidate`) when you turn it on; Next rejects them.
- **Agent tools.** `npx sonor-setup mcp` gives the site an MCP endpoint with
the built-in Sonor tools (business profile, services, FAQ, pages, articles,
reviews; inquiries, availability and offerings when you ask), the server
card, and on Netlify the rate-limited relay. Sonor's AI Visibility tab then
shows which agents used them. A site that already runs its own MCP server
(a hand-written one) is left exactly as it is:
nothing in 7.0 turns anything on for it. To have Sonor see its calls, add
`onToolCall: reportToolCallsToSonor()` to its `createMcpHandler`.
- **`@sonordev/contracts`** is for the APIs and the dashboard; sites keep
importing `@sonordev/site-kit/*/contract`, which is the same code.
- **Live updates (7.1).** `npx sonor-setup scaffold` writes the route Sonor
calls when content changes, `app/api/seo-revalidate/route.ts`, and
`npx sonor-setup doctor` checks for it. A site with articles gets
`createRevalidateRoute({ publicationBasePath: '/insights' })` with its own
path, read from the app directory (a dynamic page that renders site-kit
articles, or a `[slug]` route under a folder like `/blog` or `/news`).
When more than one folder looks like the publication, it writes the
one-line route and lists them.
## Kits built on site-kit
agency-site-kit and re-site-kit import only entries 7 keeps unchanged
(`/client`, `/server`, `/llms`, `/forms`, `/mcp`, `/portfolio/contract`),
so their `>=` peer ranges already accept 7.
---
# 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?** See `CONTRIBUTING.md`.
For the full machine-readable contract (modules, env, blessed patterns, failure
modes, exit codes) run:
```bash
npx sonor-setup manifest --json
```
Everything 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
```bash
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 === done
```
## The machine contract
- **Every agent-native command supports `--json`** and prints exactly ONE JSON
envelope to stdout (all human logs go to stderr). Parse stdout; branch on
`exitCode` / `ok` / `checks`.
- **Exit codes:** `0` OK · `1` FAILED (result is red, fix the code) · `2` USAGE
(bad invocation) · `3` CONFIG (missing input — the error names the flag) · `4`
NETWORK (API/key) · `5` INTERNAL.
- **Never interactive under `--json`, `--yes`, or a non-TTY.** A command that
needs input exits `3` with an actionable `fix` instead of hanging.
- Each `checks[]` finding has a stable `id`, a `fix`, and often a `fixCommand`
you can run directly.
## The one env var
The **only** variable a site needs is:
```bash
SONOR_API_KEY=sonor_1a2b3c4d_... # server-side only — NO NEXT_PUBLIC_ prefix
```
`SiteKitLayout` 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}`:**
```tsx
import { SiteKitLayout } from '@sonordev/site-kit/layout'
export default function RootLayout({ children }) {
return (
{children}
)
}
```
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 ``/`
` | 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)
```bash
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.
---
# Layout — `@sonordev/site-kit/layout`
RSC-compatible master layout that auto-composes all site-kit features.
## Usage
```tsx
// app/layout.tsx
import { SiteKitLayout } from '@sonordev/site-kit/layout'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
Zero-config: reads `SONOR_API_KEY` from env, injects it into client modules automatically.
## Props
```ts
interface SiteKitLayoutProps {
children: React.ReactNode
apiKey?: string // Defaults to SONOR_API_KEY env var
apiUrl?: string // Defaults to SONOR_API_URL, then https://api.sonor.io
projectId?: string // For chat routing (auto-resolved if omitted)
analytics?: boolean | AnalyticsConfig // Default: true
chat?: boolean | ChatLayoutConfig // Default: true (website chat; config = launcher placement)
popups?: boolean // Default: true (Website → Popups & Banners)
engage?: boolean | EngageConfig // Deprecated: false turns chat and popups off; an object configures the launcher
signal?: boolean | SignalConfig // Default: false
sitemapSync?: boolean // Default: false (build-time + server reconciler own this)
fleet?: boolean // Default: true (once-per-session kit version heartbeat)
defer?: boolean // Default: true (client modules wait for load + idle)
favicon?: boolean // Default: true
managedScripts?: boolean // Default: true
debug?: boolean // Default: false
showLlmsTxtFooterLink?: boolean // Default: false (prefer middleware discovery headers)
speculation?: boolean | { mode?: 'prerender' | 'prefetch'; exclude?: string[] } // Default: false
}
```
Module options live with each module: [Analytics](https://sonor.dev/site-kit/analytics) (`trackPageViews`, `excludePaths`, `site`, `allowInFrame`, `allowLocalhost`) [Website chat](https://sonor.dev/site-kit/chat) (`position`, `offsetBottom`, `zIndex`, `allowInFrame`) and [Popups and banners](https://sonor.dev/site-kit/popups).
## What It Composes
**Server-side (RSC):**
- `ManagedFavicon` — Sonor logo as `` tags
- `ManagedScripts` — tracking pixels/analytics tags in `` and body-end positions
- API preconnect/dns-prefetch hints
**Client-side (lazy-loaded island):**
- `AnalyticsProvider` — page views, scroll depth, heatmap clicks, Web Vitals
- `SitePopups` — popups, banners and toasts
- `SiteChat` — website chat (Echo)
- `SignalBridge` — A/B experiments, behavior tracking (opt-in)
- `SitemapSync` — parses `/sitemap.xml` and syncs to Sonor (opt-in)
- `FleetHeartbeat` — reports the kit version and enabled modules once per session
Since 4.0.0 none of these wrap your page. `{children}` renders first and every module mounts after it as a childless sibling, so `SiteKitLayout` never pushes a route to client rendering. Analytics, chat, popups, SitemapSync and the heartbeat also wait for window load + idle (or the first interaction) unless you pass `defer={false}`. `SignalBridge` isn't deferred, so experiment variants apply early. Visitor and session IDs come from a shared storage singleton rather than a provider.
## Note
`SiteKitProvider` was **removed in 4.0.0**. It wrapped the whole tree client-side, which broke RSC. Migrate an older layout with `npx sonor-setup codemod --only provider-to-layout --write`.
---
# Live updates
Your pages stay cached, and when someone changes content in Sonor, the pages
that use it refresh within seconds. It takes one route file:
```ts
// app/api/seo-revalidate/route.ts
export { POST } from '@sonordev/site-kit/revalidate'
```
`npx sonor-setup scaffold` writes it for you, and `npx sonor-setup doctor`
tells you when it's missing.
## How it works
Everything site-kit fetches from Sonor is cached, and pages built from those
fetches are served from your host's CDN. When content changes in Sonor
(managed copy, a page's title or description, an article, a portfolio item),
Sonor sends this route the paths and cache tags that changed. The route checks
the call was signed with your project's API key, then expires only those
pages and tags. The next visitor gets the new version, and every other page
stays cached.
It's the same `SONOR_API_KEY` the rest of site-kit reads, so there's no
extra secret to set. Sonor finds the route on its own: the first sitemap sync
after a deploy registers `https://your-domain/api/seo-revalidate` and checks
that it answers.
Without the route, nothing breaks. Edits still show up, but only once each
cached fetch ages out: about five minutes for managed copy, up to a day for
metadata.
## Sites with a publication
If your site publishes articles, tell the route where they live so the index
and feeds refresh with each article:
```ts
// app/api/seo-revalidate/route.ts
import { createRevalidateRoute } from '@sonordev/site-kit/revalidate'
export const POST = createRevalidateRoute({ publicationBasePath: '/insights' })
```
| Option | What it does |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `publicationBasePath` | Where your articles live, e.g. `/insights`. The index and its RSS and Atom feeds refresh on every call. |
| `extraPaths` | Local paths to refresh on every call, e.g. a hub page like `/work`. |
| `extendPayload` | Map extra fields Sonor sends to more paths or tags. The result is validated again, so it can't widen what a caller may refresh. |
| `secret` | The key calls are signed with. Defaults to `SONOR_API_KEY`, read on every call. |
## What Sonor sends
A POST with `Authorization: Bearer ` and a JSON body:
```json
{ "paths": ["/services/roofing"], "tags": ["sonor-slots"] }
```
| Field | Meaning |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paths` | Local paths to regenerate. `/sitemap.xml`, `/llms.txt` and `/llms-full.txt` are always refreshed too. |
| `tags` | Cache tags to expire. The ones Sonor uses are `SITE_CACHE_TAGS`: `sonor-slots` (managed copy), `seo` (metadata, schema, FAQs), `blog` (articles), `editorial-taxonomy` and `portfolio`. |
| `revalidateAll` | Regenerate every page. |
| `ping` | `{ "ping": true }` on its own regenerates nothing and answers `{ "ok": true, "ping": true, "version": "…" }`. Sonor uses it to confirm the route is installed. |
The route refuses a bad key (401), a body over 16 KB (413) and any path that
isn't a plain local path or any malformed tag (400). A refused call refreshes
nothing.
## Checking it
```bash
curl -s -X POST https://your-domain/api/seo-revalidate \
-H "Authorization: Bearer $SONOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ping":true}'
```
`{"ok":true,"ping":true,...}` means Sonor's edits will go live in seconds. A
404 means the route isn't deployed. A 401 means the key on the site doesn't
match the project's key.
---
# SEO: `@sonordev/site-kit/seo`
Server Components and server helpers that render what you manage in the SEO module at [app.sonor.io](https://app.sonor.io): page metadata, JSON-LD, FAQs, internal links, content blocks, redirects and robots directives.
The project comes from `SONOR_API_KEY`. Nothing in this module takes a project ID. A few option types and props still carry an optional `projectId` from older versions; it's ignored, so leave it out.
## Setup
```bash
# .env.local
SONOR_API_KEY=sonor_xxxxxxxx_xxxxx
```
That's the only variable you need. Keep it server-side, with no `NEXT_PUBLIC_` prefix. `SONOR_API_URL` is optional and defaults to `https://api.sonor.io`.
If the key is missing, the server helpers throw:
```
@sonordev/seo: SONOR_API_KEY environment variable is required for server-side SEO functions
```
## Entry points
| Import | Runs on | Contains |
| ------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `@sonordev/site-kit/seo` | Server only | Everything on this page except `registerLocalSitemap` |
| `@sonordev/site-kit/seo/server` | Server only | The [data fetchers](#data-fetchers), `getManagedMetadata`, `getManagedMetadataWithAB`, `generateSitemap`, `registerLocalSitemap`, and the types |
| `@sonordev/site-kit/seo/client` | Client | `SitemapSync` only |
Both server entries import `server-only`, so importing either one from a Client Component fails the build. That's deliberate: it keeps the key out of the browser bundle.
## Page metadata
### `getManagedMetadata(options)`
```tsx
// app/services/[slug]/page.tsx
import { getManagedMetadata } from '@sonordev/site-kit/seo'
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
return getManagedMetadata({
path: `/services/${slug}`,
fallback: {
title: 'Our Services',
description: 'What we do and where we do it.',
},
})
}
```
| Option | Type | Notes |
| ----------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path` | `string` | Required. The page path as Sonor has it. |
| `fallback` | `Metadata` | Fills any field Sonor has no managed value for. Used in full when the page isn't in Sonor at all. |
| `overrides` | `Partial` | Applied last, so it wins over managed values. |
| `favicon` | `'metadata' \| 'component'` | `'metadata'` (default) adds `icons` from the project logo. Pass `'component'` when your layout already renders the favicon, which `SiteKitLayout` does by default, so icons aren't emitted twice. |
It returns a Next.js `Metadata` object with two extra flags, `_managed` and `_source`. Managed fields map like this:
| Sonor field | Metadata field |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `managed_title` | `title` |
| `managed_meta_description` | `description` |
| `managed_keywords` | `keywords` |
| `managed_robots` | `robots` |
| `managed_canonical` | `alternates.canonical` |
| `language_alternates` | `alternates.languages` |
| `managed_og_title`, `managed_og_description`, `managed_og_image` | `openGraph` and `twitter` (`summary_large_image`), falling back to the title and description |
When the page exists in Sonor but has neither a title nor a description, the call asks Signal to write them in the background and returns your fallback for now. The generated copy shows up once the cached response refreshes (see [Caching](#caching)).
**Title templates.** The managed title comes back as a plain string, so a root-layout `title.template` still applies to it. If your managed titles already include the brand, you'll get it twice. Mark the title absolute:
```ts
const metadata = await getManagedMetadata({ path: '/about' })
return typeof metadata.title === 'string'
? { ...metadata, title: { absolute: metadata.title } }
: metadata
```
### `withManagedMetadata(path, pageMetadata?)`
Builds the `generateMetadata` function for you. Pick one of these forms:
```ts
import { withManagedMetadata } from '@sonordev/site-kit/seo'
// A fixed path
export const generateMetadata = withManagedMetadata('/about')
// A path built from params
export const generateMetadata = withManagedMetadata(
async ({ params }) => `/services/${(await params).slug}`,
)
// Page-level values on top of Sonor's
export const generateMetadata = withManagedMetadata('/about', async () => ({
title: 'About Us',
}))
```
Whatever `pageMetadata` returns wins over Sonor's values. `openGraph` and `twitter` merge one level deep. It calls `getManagedMetadata` with the default `favicon: 'metadata'`.
### A/B-tested titles and descriptions
`getManagedMetadataWithAB` works like `getManagedMetadata`, then swaps in the assigned variant of any running title or description test for that path. `getABVariant({ path, field, sessionId? })` does the same for one field (`'title' | 'description' | 'content'`) and returns `{ testId, variant, value }`, or `null` when nothing's running.
```ts
import { cookies } from 'next/headers'
import { getManagedMetadataWithAB } from '@sonordev/site-kit/seo'
export async function generateMetadata() {
const sessionId = (await cookies()).get('visitor_id')?.value
return getManagedMetadataWithAB({ path: '/pricing', sessionId })
}
```
Pass a stable visitor ID your site already keeps. The kit doesn't set a cookie for this, and without one every request gets a random variant. Reading cookies makes the route dynamic, and each variant lookup records an impression.
## JSON-LD
### ``
```tsx
import { ManagedSchema } from '@sonordev/site-kit/seo'
export default function Page() {
return (
<>
{/* ... */}
>
)
}
```
It renders one `application/ld+json` script (an `@graph` when there's more than one node) that combines:
- the schema Sonor has for the path, filtered by `includeTypes` / `excludeTypes`
- the page's Signal-generated `managed_schema`
- anything you pass in `additionalSchemas`
- a `BreadcrumbList` built from the path, when there isn't one already and the project has a site URL (skipped on `/`)
- a speakable `WebPage` or `Article` node, when `speakable`, `pageName` and `pageUrl` are all set
Everything Sonor supplies (its schema rows, `managed_schema` and the entity graph) loses any template placeholder first. A node whose values are a template's unfilled slots is dropped: a URL on a reserved example domain (`example.com`), a name like "Example" or "Your Business Name", a placeholder phone such as `+1-000-000-0000`, a slot like `[Resident Name]` or `{plan.name}`, or an object that's only a note about what goes there. Its real siblings and parents stay, so an `FAQPage` keeps its questions when only its publisher was a placeholder, and the `BreadcrumbList` fallback still applies when a placeholder breadcrumb is dropped. Your `additionalSchemas` are never touched. The rule is `@sonordev/contracts/schema-placeholders`.
| Prop | Default | Notes |
| ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `path` | | Required. |
| `includeTypes` / `excludeTypes` | | Type allow and deny lists. `includeTypes` keeps Sonor schema rows by `schema_type`. `excludeTypes` drops a node of that `@type` wherever it sits in Sonor's schema: a whole row, an `@graph` member, or a nested value like `mainEntity`, and in `managed_schema` too. A row left empty is dropped. Your `additionalSchemas` are never filtered. |
| `additionalSchemas` | `[]` | Extra nodes to merge in. |
| `speakable` | | `true` for the default selectors (`h1`, `[data-speakable="true"]`, `.page-summary`, `.key-points`, `.aeo-block[data-speakable="true"]`), or `{ cssSelector }` / `{ xpath }`. |
| `pageType` | `'WebPage'` | `'WebPage'` or `'Article'`, for the speakable node. |
| `pageName`, `pageUrl` | | Required for the speakable node. |
| `includeEntityGraph` | `true` | Meant to add nodes from Signal's entity graph. It adds nothing today; see [Entity graph](#entity-graph-and-ai-visibility). |
It's wrapped in `Suspense`, so the fetch never holds up the rest of the page. The script streams in when it's ready.
### ``
Renders the page's `managed_llm_schema` as a `WebPage` JSON-LD script marked `data-llm-optimized="true"`, linked to the site's `WebSite` node when the project has a site URL. It renders nothing when the page has no LLM schema, or when that schema is a template placeholder (see ``).
### Schema helpers
- `createSchema(type, data)` returns `{ '@context': 'https://schema.org', '@type': type, ...data }`.
- `createBreadcrumbSchema(baseUrl, path, labels?)` builds a `BreadcrumbList`. `labels` maps a path segment to its display name.
- `createWebSiteOrganizationStub({ name, url, sameAs?, knowsAbout? })` returns a minimal `Organization` and `WebSite` pair with stable `@id`s. Only reach for it when Sonor isn't already emitting those nodes.
Hand the result to `ManagedSchema` through `additionalSchemas`, so it's serialized and escaped in the same script as everything else.
## FAQs: ``
```tsx
import { ManagedFAQ } from '@sonordev/site-kit/seo'
```
| Prop | Default | Notes |
| --------------- | ---------- | --------------------------------------------------------------------------------------- |
| `path` | | Required. |
| `showTitle` | `true` | Renders the FAQ's title as an `
`. |
| `includeSchema` | `true` | Emits `FAQPage` JSON-LD, but only when the FAQ is also set to include schema in Sonor. |
| `renderItem` | | `(item, index) => ReactNode`, for your own markup. |
| `className` | `'sk-faq'` | Wrapper class. |
| `site` | | Sub-site host on a multi-site project. See [Multi-site projects](#multi-site-projects). |
The default markup is native `` / `` with its own small `