# 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 `