@sonordev/site-kit 7.1.2 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +60 -0
  3. package/README.md +9 -8
  4. package/agent-manifest.json +17 -7
  5. package/dist/{AnalyticsProvider-ZB33W6MC.js → AnalyticsProvider-DJQA66AG.js} +4 -4
  6. package/dist/{ArticleViewTracker-GMHYLLLZ.js → ArticleViewTracker-GFOAXFV7.js} +3 -3
  7. package/dist/{BlocksPopup-EA6LYL25.js → BlocksPopup-AHDU34TL.js} +5 -5
  8. package/dist/ChatWidget-FHBAHFZX.js +17 -0
  9. package/dist/{FileField-QKA3JB2Q.js → FileField-J5W5MIAZ.js} +3 -3
  10. package/dist/{FormSpotlight-AQTIA5TP.js → FormSpotlight-PE5YZBFZ.js} +20 -6
  11. package/dist/{FormStage-FDTLTP3B.js → FormStage-OOPQC27J.js} +27 -7
  12. package/dist/ManagedForm-JRZAJ7SZ.js +16 -0
  13. package/dist/{ManagedNewsletterForm-J6QSAGGP.js → ManagedNewsletterForm-TC5542ZG.js} +7 -5
  14. package/dist/{SignalCore-JZNXOLNT.js → SignalCore-2YP5FCAS.js} +3 -3
  15. package/dist/SiteChat-EGEZJJVV.js +5 -0
  16. package/dist/{SiteDesignReporter-W4EJUSTE.js → SiteDesignReporter-YEEWJKWN.js} +5 -5
  17. package/dist/SitePopups-LQBIXIFV.js +10 -0
  18. package/dist/SitemapSync-LSZ2EYOF.js +8 -0
  19. package/dist/_client/booking-widget.js +5 -5
  20. package/dist/affiliates/index.js +3 -3
  21. package/dist/analytics/index.js +4 -4
  22. package/dist/analytics/send-gate.d.ts +1 -1
  23. package/dist/articles/index.js +2 -2
  24. package/dist/articles/server-ui.js +3 -2
  25. package/dist/articles/server.js +2 -1
  26. package/dist/{engage → chat}/ChatWidget.d.ts +7 -6
  27. package/dist/{engage → chat}/EchoUiActions.d.ts +1 -1
  28. package/dist/chat/SiteChat.d.ts +25 -0
  29. package/dist/{engage → chat}/brand-color.d.ts +1 -1
  30. package/dist/{engage → chat}/chat-messages.d.ts +1 -1
  31. package/dist/{engage → chat}/echo-config.d.ts +1 -1
  32. package/dist/chat/index.d.ts +10 -6
  33. package/dist/chat/index.js +14 -8
  34. package/dist/{engage → chat}/launcher-placement.d.ts +1 -7
  35. package/dist/{engage → chat}/socket-loader.d.ts +2 -2
  36. package/dist/chat/types.d.ts +136 -0
  37. package/dist/chunk-27FISYK4.js +4 -0
  38. package/dist/{chunk-HF57LG73.js → chunk-2AD3CDRR.js} +43 -29
  39. package/dist/{chunk-M3YEPKP7.js → chunk-2ECWEUHP.js} +1 -1
  40. package/dist/{chunk-LUXVWITO.js → chunk-4NTBQNHA.js} +247 -78
  41. package/dist/{chunk-AH5Q262S.js → chunk-56KNHB4U.js} +1 -1
  42. package/dist/{chunk-GLHQ3LRO.js → chunk-6U3VLV2C.js} +1 -1
  43. package/dist/{chunk-3SDOQUUF.js → chunk-BEBR4OJD.js} +2 -2
  44. package/dist/{chunk-JSZN6LDZ.js → chunk-BF7TZYC3.js} +38 -31
  45. package/dist/{chunk-AQSNPZY4.js → chunk-C2FZBUSS.js} +80 -1
  46. package/dist/{chunk-PHYMNUJU.js → chunk-CYUFBKJQ.js} +1 -1
  47. package/dist/{chunk-6WIQCXOO.js → chunk-DNPNYWVL.js} +1 -1
  48. package/dist/{chunk-V6FDQLR7.js → chunk-EPT6FJUY.js} +53 -21
  49. package/dist/chunk-HODO5BX5.js +28 -0
  50. package/dist/chunk-HW7B43E5.js +14 -0
  51. package/dist/{chunk-PGMVL4AB.js → chunk-I2YX3HVD.js} +2 -2
  52. package/dist/{chunk-JTU2GOM4.js → chunk-L7U23XI2.js} +18 -6
  53. package/dist/{chunk-YEMPUQVU.js → chunk-MKMD2HJW.js} +2 -2
  54. package/dist/{chunk-Q4W23JYM.js → chunk-MRQ2U4LZ.js} +1 -1
  55. package/dist/chunk-MVR4FY5R.js +187 -0
  56. package/dist/{chunk-VGH4MDTU.js → chunk-NI2XNEMR.js} +1 -1
  57. package/dist/{chunk-4Y5FWDPM.js → chunk-NZ4RXN3G.js} +55 -7
  58. package/dist/{chunk-RHNWA34F.js → chunk-O5RGXTWU.js} +41 -7
  59. package/dist/{chunk-TXBAOEKG.js → chunk-PKGN32AU.js} +5 -4
  60. package/dist/chunk-Q6E4OPGG.js +40 -0
  61. package/dist/chunk-RDPW4D33.js +45 -0
  62. package/dist/{chunk-UXOGCKL6.js → chunk-T4DLETKR.js} +1 -1
  63. package/dist/{chunk-NK4CSJNP.js → chunk-UPRSH6Q4.js} +2 -2
  64. package/dist/{chunk-ERXXITG4.js → chunk-VF4F5WXZ.js} +1 -1
  65. package/dist/chunk-VWWITFWM.js +300 -0
  66. package/dist/{chunk-WWSHFRDL.js → chunk-WDQB4OXY.js} +7 -3
  67. package/dist/{chunk-VDF5DFWT.js → chunk-WGG6GTKW.js} +1 -1
  68. package/dist/{chunk-CSTQDNGK.js → chunk-WGIDC6QP.js} +43 -24
  69. package/dist/{chunk-V3PUVBME.js → chunk-XLSQTTUE.js} +2 -4
  70. package/dist/{chunk-JBE46QJZ.js → chunk-Y6LP36UW.js} +1 -1
  71. package/dist/chunk-YJLS6JBI.js +370 -0
  72. package/dist/{chunk-QEDHRBAT.js → chunk-Z65DHMTW.js} +26 -9
  73. package/dist/{chunk-ILU5I6RW.js → chunk-ZHS2XFLV.js} +1 -1
  74. package/dist/{chunk-D5EC5KVO.js → chunk-ZKHI4E2B.js} +2 -2
  75. package/dist/{chunk-CIG5UL2S.js → chunk-ZTLMPUO6.js} +3 -2
  76. package/dist/client/index.js +3 -3
  77. package/dist/commerce/index.js +4 -4
  78. package/dist/contracts/color.d.ts +1 -1
  79. package/dist/contracts/entries.d.ts +1 -1
  80. package/dist/contracts/error-page.d.ts +183 -0
  81. package/dist/contracts/llms.d.ts +1 -0
  82. package/dist/contracts/proposal-sitemap.d.ts +130 -0
  83. package/dist/contracts/schema-placeholders.d.ts +87 -0
  84. package/dist/engage/EngageWidget.d.ts +13 -12
  85. package/dist/engage/index.d.ts +10 -9
  86. package/dist/engage/index.js +31 -35
  87. package/dist/engage/types.d.ts +19 -244
  88. package/dist/fleet/FleetHeartbeat.d.ts +1 -1
  89. package/dist/fleet/index.js +4 -4
  90. package/dist/forms/FormEnhancer.d.ts +25 -12
  91. package/dist/forms/ServerForm.d.ts +11 -10
  92. package/dist/forms/StaticForm.d.ts +3 -1
  93. package/dist/forms/field-autocomplete.d.ts +28 -0
  94. package/dist/forms/form-dom-values.d.ts +42 -0
  95. package/dist/forms/index.js +11 -9
  96. package/dist/forms/server.js +6 -4
  97. package/dist/forms/static.js +3 -2
  98. package/dist/forms/submitForm.d.ts +8 -1
  99. package/dist/forms/types.d.ts +41 -2
  100. package/dist/forms/useForm.d.ts +21 -4
  101. package/dist/forms/webmcp.d.ts +38 -0
  102. package/dist/images/index.js +4 -4
  103. package/dist/index.d.ts +4 -1
  104. package/dist/index.js +1 -1
  105. package/dist/layout/SiteKitClientProviders.d.ts +3 -3
  106. package/dist/layout/SiteKitLayout.d.ts +4 -4
  107. package/dist/layout/client.d.ts +3 -3
  108. package/dist/layout/client.js +7 -7
  109. package/dist/layout/index.js +8 -8
  110. package/dist/layout/types.d.ts +5 -4
  111. package/dist/llms/agent-access.d.ts +4 -0
  112. package/dist/llms/contract.js +1 -1
  113. package/dist/llms/index.js +6 -6
  114. package/dist/maps/index.js +3 -3
  115. package/dist/mcp/WebMcpTools.d.ts +5 -4
  116. package/dist/mcp/client.js +14 -7
  117. package/dist/mcp/discovery.d.ts +48 -0
  118. package/dist/mcp/handlers.d.ts +13 -7
  119. package/dist/mcp/index.d.ts +5 -3
  120. package/dist/mcp/index.js +62 -30
  121. package/dist/mcp/serverCard.d.ts +6 -7
  122. package/dist/mcp/sonor.js +14 -11
  123. package/dist/revalidate/index.js +2 -2
  124. package/dist/seo/client.js +4 -4
  125. package/dist/seo/index.js +15 -8
  126. package/dist/seo/llms/contract.js +1 -1
  127. package/dist/seo/llms.js +6 -6
  128. package/dist/seo/register-sitemap-cli.js +1 -1
  129. package/dist/seo/sitemap.js +4 -4
  130. package/dist/server/index.js +2 -2
  131. package/dist/shared/dialog.d.ts +27 -0
  132. package/dist/shared/identity.d.ts +1 -1
  133. package/dist/shared/layers.d.ts +7 -0
  134. package/dist/shared/mid-form.d.ts +53 -0
  135. package/dist/shared/reporting-gate.d.ts +1 -1
  136. package/dist/shared/version.d.ts +1 -1
  137. package/dist/shared/visual-viewport-gap.d.ts +1 -1
  138. package/dist/signal/index.js +2 -2
  139. package/dist/signal/types.d.ts +1 -1
  140. package/dist/sitemap/index.js +4 -4
  141. package/dist/{socket-loader-R24ZSRSQ.js → socket-loader-CGIPEG74.js} +1 -1
  142. package/dist/sync/index.js +5 -5
  143. package/dist/types.d.ts +3 -1
  144. package/dist/{web-vitals.attribution-GD6LGLVF.js → web-vitals.attribution-N2PCCF4Q.js} +1 -1
  145. package/dist/website/BlocksPopup.d.ts +1 -1
  146. package/dist/website/PopupBlocks.d.ts +5 -2
  147. package/dist/website/SitePopups.d.ts +25 -0
  148. package/dist/website/images.js +4 -4
  149. package/dist/website/index.js +6 -6
  150. package/dist/website/popup-rules.d.ts +46 -0
  151. package/dist/website/popup-types.d.ts +113 -0
  152. package/dist/website/popups.d.ts +2 -6
  153. package/dist/website/popups.js +6 -14
  154. package/dist/{writeLLMsTxt-G3JBNBC5.js → writeLLMsTxt-2NQVDGYI.js} +3 -3
  155. package/docs/MIGRATING-TO-7.md +41 -9
  156. package/docs.json +2 -1
  157. package/package.json +2 -2
  158. package/src/analytics/README.md +2 -2
  159. package/src/articles/README.md +4 -1
  160. package/src/{engage → chat}/README.md +63 -62
  161. package/src/forms/README.md +49 -3
  162. package/src/layout/README.md +8 -5
  163. package/src/llms/README.md +69 -0
  164. package/src/mcp/README.md +50 -16
  165. package/src/seo/README.md +3 -1
  166. package/src/sync/README.md +10 -0
  167. package/src/website/README.md +133 -0
  168. package/dist/ChatWidget-UCDWMWCW.js +0 -15
  169. package/dist/EngageWidget-7ZYSK4GE.js +0 -11
  170. package/dist/ManagedForm-H7BVAH2W.js +0 -14
  171. package/dist/SitemapSync-XBANGKHZ.js +0 -8
  172. package/dist/chunk-NDF4A5JM.js +0 -37
  173. package/dist/chunk-VLARKWZU.js +0 -100
  174. package/dist/chunk-YCJT4JJG.js +0 -837
  175. package/dist/engage/DesignRenderer.d.ts +0 -57
  176. package/dist/engage/element-rules.d.ts +0 -38
@@ -78,12 +78,12 @@ import { useForm } from '@sonordev/site-kit/forms'
78
78
 
79
79
  export function ContactForm() {
80
80
  const {
81
- fields, values, errors, setFieldValue, submit, isSubmitting,
81
+ fields, values, errors, setFieldValue, handleSubmit, toolAttributes, isSubmitting,
82
82
  step, totalSteps, isMultiStep, nextStep, prevStep, isLastStep,
83
83
  } = useForm('contact-form')
84
84
 
85
85
  return (
86
- <form onSubmit={(e) => { e.preventDefault(); submit() }}>
86
+ <form {...toolAttributes} onSubmit={handleSubmit}>
87
87
  {fields.map(field => (
88
88
  <div key={field.slug}>
89
89
  <label>{field.label}</label>
@@ -166,7 +166,12 @@ interface UseFormReturn {
166
166
  canGoPrev: boolean
167
167
  isLastStep: boolean
168
168
  validate: () => boolean
169
- submit: () => Promise<void>
169
+ // Resolves with what happened: { status: 'sent' | 'invalid' | 'failed' | 'busy' | 'next_step', ... }
170
+ submit: (options?: FormSubmitOptions) => Promise<FormSubmitOutcome>
171
+ // A ready-made <form onSubmit>: steps or submits, and answers an AI agent (see below)
172
+ handleSubmit: (event: FormEvent<HTMLFormElement>) => void
173
+ // WebMCP attributes for your <form>; null until the config loads
174
+ toolAttributes: { toolname: string; tooldescription: string } | null
170
175
  isSubmitting: boolean
171
176
  isComplete: boolean
172
177
  reset: () => void
@@ -212,6 +217,47 @@ it, so Sonor refuses those before anything is written: the visitor sees an
212
217
  error and you get no lead. Submit through site-kit, and define the form's
213
218
  fields in Sonor so both can render and validate them.
214
219
 
220
+ ## Agent-ready forms (7.2.0)
221
+
222
+ People increasingly ask an AI assistant to fill out a form for them: a quote
223
+ request, a booking, a newsletter signup. Assistants act on what the page's
224
+ markup says each control is, so managed forms now say it clearly. There's
225
+ nothing to configure.
226
+
227
+ - **Autocomplete tokens.** Name, email, phone, company, website and address
228
+ fields carry the right `autocomplete` token (`given-name`, `email`, `tel`,
229
+ `organization`, `url`, `postal-code`, ...), read from the field's CRM
230
+ destination in Sonor, then its type, slug and label. Browser autofill and
231
+ password managers use the same tokens.
232
+ - **Accessible wiring.** Error and help text are tied to their control
233
+ (`aria-describedby`), an errored control says so (`aria-invalid`), radio and
234
+ checkbox groups are named by their question, and rating stars by their
235
+ value.
236
+ - **WebMCP.** Every interactive managed form carries the declarative WebMCP
237
+ attributes (`toolname`, `tooldescription`), so a browser agent that supports
238
+ WebMCP can treat it as a tool. It fills the same fields a person would, and
239
+ the submit runs the same validation. There's no `toolautosubmit`: the
240
+ assistant fills the form and the person it's helping presses Send.
241
+ Browsers without WebMCP ignore the attributes.
242
+ - **Agent-sent leads are tagged.** When the browser reports that an agent
243
+ pressed submit (`SubmitEvent.agentInvoked`), the submission carries that
244
+ flag and the lead is tagged in Sonor, so you can see how those leads compare
245
+ over time, and Sonor's spam screening takes it into account. The agent is
246
+ handed the outcome of its submit (`respondWith`).
247
+ - **Nothing typed before the form loads is lost.** `ServerForm` renders the
248
+ form in the page HTML and loads the interactive version at idle. Since
249
+ 7.2.0, typing into the server-rendered form starts that upgrade at once and
250
+ what was typed carries over, and a Send pressed before the upgrade is held
251
+ and sent the moment it lands. The server-rendered Send button stays disabled
252
+ until the page can catch the click.
253
+
254
+ With `useForm`, spread `toolAttributes` on your `<form>` and use
255
+ `handleSubmit` as its `onSubmit` to get the same behavior.
256
+
257
+ `ServerForm`'s `enhance` prop is ignored since 7.2.0. Only the interactive
258
+ form can send a managed form, so `enhance={false}` only ever produced a form
259
+ that silently went nowhere.
260
+
215
261
  ## Styles
216
262
 
217
263
  ```tsx
@@ -28,9 +28,11 @@ interface SiteKitLayoutProps {
28
28
  children: React.ReactNode
29
29
  apiKey?: string // Defaults to SONOR_API_KEY env var
30
30
  apiUrl?: string // Defaults to SONOR_API_URL, then https://api.sonor.io
31
- projectId?: string // For Engage chat routing (auto-resolved if omitted)
31
+ projectId?: string // For chat routing (auto-resolved if omitted)
32
32
  analytics?: boolean | AnalyticsConfig // Default: true
33
- engage?: boolean | EngageConfig // Default: true
33
+ chat?: boolean | ChatLayoutConfig // Default: true (website chat; config = launcher placement)
34
+ popups?: boolean // Default: true (Website → Popups & Banners)
35
+ engage?: boolean | EngageConfig // Deprecated: false turns chat and popups off; an object configures the launcher
34
36
  signal?: boolean | SignalConfig // Default: false
35
37
  sitemapSync?: boolean // Default: false (build-time + server reconciler own this)
36
38
  fleet?: boolean // Default: true (once-per-session kit version heartbeat)
@@ -43,7 +45,7 @@ interface SiteKitLayoutProps {
43
45
  }
44
46
  ```
45
47
 
46
- Module options live with each module: [Analytics](../analytics/README.md) (`trackPageViews`, `excludePaths`, `site`, `allowInFrame`, `allowLocalhost`) and [Engage](../engage/README.md) (`position`, `chatEnabled`, and launcher placement).
48
+ Module options live with each module: [Analytics](../analytics/README.md) (`trackPageViews`, `excludePaths`, `site`, `allowInFrame`, `allowLocalhost`) [Website chat](../chat/README.md) (`position`, `offsetBottom`, `zIndex`, `allowInFrame`) and [Popups and banners](../website/README.md).
47
49
 
48
50
  ## What It Composes
49
51
 
@@ -54,12 +56,13 @@ Module options live with each module: [Analytics](../analytics/README.md) (`trac
54
56
 
55
57
  **Client-side (lazy-loaded island):**
56
58
  - `AnalyticsProvider` — page views, scroll depth, heatmap clicks, Web Vitals
57
- - `EngageWidget` — popups, nudges, chat
59
+ - `SitePopups` — popups, banners and toasts
60
+ - `SiteChat` — website chat (Echo)
58
61
  - `SignalBridge` — A/B experiments, behavior tracking (opt-in)
59
62
  - `SitemapSync` — parses `/sitemap.xml` and syncs to Sonor (opt-in)
60
63
  - `FleetHeartbeat` — reports the kit version and enabled modules once per session
61
64
 
62
- 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, Engage, 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.
65
+ 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.
63
66
 
64
67
  ## Note
65
68
 
@@ -229,6 +229,75 @@ with `optimizedLLMsTxt: false` in `createSitemap`, so one writer owns the file.
229
229
  Reads behind this write always bypass Next's Data Cache — hosts persist it
230
230
  between builds, and a cached read is how a site shipped a months-old llms.txt.
231
231
 
232
+ #### Agent discovery for a custom MCP server
233
+
234
+ A custom server stays opt-in. Share the same `mcp` options between the
235
+ build-time writer and both route handlers so the short and full files point
236
+ at the same tools. This adds links to an existing server; it doesn't create
237
+ the endpoint, card, or catalog.
238
+
239
+ ```ts
240
+ // lib/agent-access.ts
241
+ import type { AgentAccessOptions } from '@sonordev/site-kit/seo/llms'
242
+
243
+ export const agentAccess: AgentAccessOptions = {
244
+ endpoint: '/api/mcp',
245
+ card: '/api/mcp/server-card',
246
+ catalog: '/.well-known/ai-catalog.json', // omit unless this document is served
247
+ position: 'after-intro',
248
+ }
249
+ ```
250
+
251
+ ```ts
252
+ // The site's build-time writer
253
+ import { writeLLMsTxtToPublic } from '@sonordev/site-kit/seo/llms'
254
+ import { agentAccess } from '../lib/agent-access'
255
+
256
+ await writeLLMsTxtToPublic({
257
+ full: true,
258
+ site: 'example.com',
259
+ mcp: agentAccess,
260
+ })
261
+ ```
262
+
263
+ ```ts
264
+ // app/llms.txt/route.ts
265
+ import { createLLMsTxtHandler } from '@sonordev/site-kit/seo/llms'
266
+ import { agentAccess } from '@/lib/agent-access'
267
+
268
+ export const GET = createLLMsTxtHandler({
269
+ baseUrl: 'https://example.com',
270
+ mcp: agentAccess,
271
+ })
272
+ ```
273
+
274
+ ```ts
275
+ // app/llms-full.txt/route.ts — use the same options
276
+ import { createLLMsFullTxtHandler } from '@sonordev/site-kit/seo/llms'
277
+ import { agentAccess } from '@/lib/agent-access'
278
+
279
+ export const GET = createLLMsFullTxtHandler({
280
+ baseUrl: 'https://example.com',
281
+ mcp: agentAccess,
282
+ })
283
+ ```
284
+
285
+ `after-intro` preserves the title and introduction and puts Agent access
286
+ before the first content section. `end` places it at the end. Existing
287
+ markdown that already contains Agent access or a recognized server-card link
288
+ is preserved, so update a site's existing discovery section directly when
289
+ its URLs change. The optional catalog is an experimental discovery document,
290
+ not a requirement for using the MCP endpoint.
291
+
292
+ The writer applies these options to both files, whether Sonor generated the
293
+ content or local data supplied it. It resolves the origin from
294
+ `NEXT_PUBLIC_SITE_URL`, falling back to the `site` host. The handlers apply
295
+ explicit options even when serving an older file through `preferStatic`;
296
+ they use `baseUrl` or the configured site URL and never read the incoming
297
+ request. A CDN serving `public/llms.txt` directly still needs the build-time
298
+ writer to update that file. `mcp: false` disables automatic insertion; it
299
+ doesn't remove a site's existing discovery text.
300
+
232
301
  ### Step 5: On-Demand Revalidation (optional)
233
302
 
234
303
  When Sonor data changes (page summaries, FAQs, etc.), bust the ISR cache instantly instead of waiting for `s-maxage` to expire:
package/src/mcp/README.md CHANGED
@@ -9,7 +9,7 @@ definitions:
9
9
  | Surface | Who uses it | Entry point |
10
10
  |---|---|---|
11
11
  | Remote MCP endpoint (Streamable HTTP) | Off-browser agents — Claude, Cursor, any MCP client | `createMcpHandler` |
12
- | MCP Server Card (SEP-2127) | Crawlers and clients discovering the endpoint | `createMcpServerCardHandler` |
12
+ | Server card and experimental AI catalog | Crawlers and clients discovering the endpoint | `createMcpServerCardHandler`, `createMcpAiCatalogHandler` |
13
13
  | In-page WebMCP | Browser-driving agents | `<WebMcpTools>`, `declarativeToolForm` |
14
14
 
15
15
  One definition feeding all three is the point. The alternative — a tool list for
@@ -163,22 +163,45 @@ export const { POST, GET, DELETE, OPTIONS } = createMcpHandler({
163
163
  export const dynamic = 'force-dynamic'
164
164
  ```
165
165
 
166
- ### 3. Mount the card at BOTH well-known paths
166
+ ### 3. Mount the card and optional experimental catalog
167
+
168
+ The [ext-server-card discovery draft](https://github.com/modelcontextprotocol/ext-server-card/blob/main/docs/discovery.md)
169
+ is experimental. It recommends the card beside the endpoint at
170
+ `<streamable-http-url>/server-card` and a domain catalog at
171
+ `/.well-known/ai-catalog.json`. With the default endpoint, use
172
+ `/api/mcp/server-card` as the site's canonical card URL:
167
173
 
168
174
  ```ts
169
- // app/.well-known/mcp-server-card/route.ts ← canonical (SEP-2127)
170
- // app/.well-known/mcp.json/route.ts ← superseded SEP-1649, still probed
171
- import { createMcpServerCardHandler } from '@sonordev/site-kit/mcp'
175
+ // app/api/mcp/server-card/route.ts
176
+ import { createMcpServerCardHandler, MCP_SERVER_CARD_MEDIA_TYPE } from '@sonordev/site-kit/mcp'
172
177
  import { mcpServer } from '@/lib/mcp/server'
173
178
 
174
179
  export const { GET, OPTIONS } = createMcpServerCardHandler({
175
180
  info: mcpServer.info,
176
181
  tools: mcpServer.tools,
177
182
  baseUrl: 'https://example.com',
183
+ contentType: MCP_SERVER_CARD_MEDIA_TYPE, // application/mcp-server-card+json
184
+ })
185
+ ```
186
+
187
+ ```ts
188
+ // app/.well-known/ai-catalog.json/route.ts
189
+ import { createMcpAiCatalogHandler } from '@sonordev/site-kit/mcp'
190
+ import { mcpServer } from '@/lib/mcp/server'
191
+
192
+ export const { GET, OPTIONS } = createMcpAiCatalogHandler({
193
+ info: mcpServer.info,
194
+ baseUrl: 'https://example.com',
195
+ serverCardPath: '/api/mcp/server-card',
178
196
  })
179
- export const revalidate = 3600
180
197
  ```
181
198
 
199
+ The catalog returns `application/ai-catalog+json`. Both handlers use explicit
200
+ configuration and stay statically renderable without reading request data.
201
+ Keep existing `/.well-known/mcp-server-card` and `/.well-known/mcp.json`
202
+ aliases for clients that already use them. The card handler's default remains
203
+ `application/json`; set the vendor content type explicitly on the new path.
204
+
182
205
  ### 4. Register in-page (optional, for browser agents)
183
206
 
184
207
  ```tsx
@@ -188,10 +211,17 @@ export const revalidate = 3600
188
211
  ```
189
212
 
190
213
  Pass no tool list. The component checks for WebMCP support first and only then
191
- fetches `tools/list` from the endpoint — so an ordinary visitor does no work
192
- and the page carries no extra bytes, while an agent gets the same catalog the
193
- endpoint serves. Handing it descriptors from a server component instead would
194
- put the whole catalog in every page's RSC payload.
214
+ fetches `tools/list` from the endpoint. In a supported browser it starts when
215
+ the component mounts, without waiting for window load, an idle period, or a
216
+ human interaction. A browser without WebMCP makes no discovery request and
217
+ receives no catalog. Handing it descriptors from a server component instead
218
+ would put the whole catalog in every page's RSC payload.
219
+
220
+ `defer={true}` preserves the optional load + idle activation. Ordinary parent
221
+ renders keep the current registration; unmounting aborts discovery, remote
222
+ calls, and registrations that support the registration signal. If passing
223
+ `tools` or `localTools` explicitly, keep their arrays stable until the tools
224
+ change.
195
225
 
196
226
  ### 5. Rate-limit the public endpoint (Netlify)
197
227
 
@@ -268,8 +298,8 @@ is not re-exported from `@sonordev/site-kit/mcp`, which stays runtime-neutral.
268
298
 
269
299
  ### The card does not list tools — on purpose
270
300
 
271
- SEP-2127 deliberately omits primitives from the card: what a server exposes can
272
- vary with auth state and flags, so the authoritative list is whatever
301
+ The card's discovery shape omits primitives: what a server exposes can
302
+ vary with auth state and configuration, so the authoritative list is whatever
273
303
  `tools/list` returns at call time. A card that inlined tools would be a second
274
304
  source of truth that goes stale silently.
275
305
 
@@ -279,11 +309,15 @@ point and requires a reverse-DNS prefix, so the hint rides along without
279
309
  pretending to be standard — useful for crawlers that index the card and never
280
310
  connect.
281
311
 
282
- ### Two well-known paths
312
+ ### Experimental discovery and compatibility paths
283
313
 
284
- SEP-1649 proposed `/.well-known/mcp.json`; the ratified SEP-2127 moved to
285
- `/.well-known/mcp-server-card`. Deployed validators still probe the old path.
286
- Serving one document from two URLs costs nothing, so mount both.
314
+ Site-kit keeps `/.well-known/mcp-server-card` and `/.well-known/mcp.json`
315
+ for compatibility with existing integrations. The experimental
316
+ ext-server-card draft adds domain discovery through
317
+ `/.well-known/ai-catalog.json`, which links to the recommended endpoint-local
318
+ card at `/api/mcp/server-card`. These draft conventions aren't a guarantee
319
+ that every agent discovers or uses them. Keep the endpoint and existing card
320
+ URLs working while adding the catalog.
287
321
 
288
322
  ### Dual-era protocol support
289
323
 
package/src/seo/README.md CHANGED
@@ -143,6 +143,8 @@ It renders one `application/ld+json` script (an `@graph` when there's more than
143
143
  - a `BreadcrumbList` built from the path, when there isn't one already and the project has a site URL (skipped on `/`)
144
144
  - a speakable `WebPage` or `Article` node, when `speakable`, `pageName` and `pageUrl` are all set
145
145
 
146
+ 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`.
147
+
146
148
  | Prop | Default | Notes |
147
149
  |------|---------|-------|
148
150
  | `path` | | Required. |
@@ -157,7 +159,7 @@ It's wrapped in `Suspense`, so the fetch never holds up the rest of the page. Th
157
159
 
158
160
  ### `<LLMSchema path>`
159
161
 
160
- 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.
162
+ 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 `<ManagedSchema>`).
161
163
 
162
164
  ### Schema helpers
163
165
 
@@ -59,6 +59,16 @@ Expired meeting details and time reservations show a recovery action. Guests can
59
59
 
60
60
  Navigation is disabled while a reservation or booking request is running. Stale responses cannot reopen a previous step, and holds created after the widget is removed are released.
61
61
 
62
+ ## Accessibility
63
+
64
+ Screen readers and AI agent browsers (which act on elements by their accessible names) can book without guessing:
65
+
66
+ - **Days** are buttons named with the full date, year included: "Wednesday, October 7, 2026". The picked day is pressed (`aria-pressed="true"`), and days that can't be booked are disabled.
67
+ - **Times** are buttons named with the time and the date, "10:30 AM, Wednesday, October 7, 2026", in the booking's time zone. They still show just the time. The picked time is pressed.
68
+ - **Confirm**, the button that reserves the picked time, is named "Confirm 10:30 AM, Wednesday, October 7, 2026".
69
+
70
+ Every name contains what its button shows (a time's and Confirm's start with it), so voice control can still target a button by its visible text (WCAG 2.5.3). None of this changes what the widget sends.
71
+
62
72
  ## API Functions
63
73
 
64
74
  ```ts
@@ -0,0 +1,133 @@
1
+ # Popups and banners — `@sonordev/site-kit/website/popups`
2
+
3
+ The popups, banners and toasts you publish in Sonor (Website → Popups &
4
+ Banners), shown on your site where and when you set them, in the site's own
5
+ design.
6
+
7
+ ## Usage
8
+
9
+ `SiteKitLayout` mounts it for you, after the page goes idle:
10
+
11
+ ```tsx
12
+ <SiteKitLayout>{children}</SiteKitLayout> // popups on (default)
13
+ <SiteKitLayout popups={false}>{children}</SiteKitLayout> // no popups
14
+ ```
15
+
16
+ On a site without `SiteKitLayout`, mount `SitePopups` the same way:
17
+
18
+ ```tsx
19
+ 'use client'
20
+ import { SitePopups } from '@sonordev/site-kit/website/popups'
21
+
22
+ export function Popups() {
23
+ return <SitePopups />
24
+ }
25
+ ```
26
+
27
+ A project with nothing published costs one request and draws nothing. The
28
+ renderer downloads only when a popup is about to open.
29
+
30
+ ## Props
31
+
32
+ ```ts
33
+ interface SitePopupsProps {
34
+ apiUrl?: string // Default: from SiteKitLayout, else https://api.sonor.io
35
+ apiKey?: string // Default: from SiteKitLayout
36
+ zIndex?: number // The layer popups, banners and toasts sit on. Default: 9999
37
+ allowInFrame?: boolean // Show inside a cross-origin frame. Default: false
38
+ allowLocalhost?: boolean // Show on localhost. Default: false
39
+ debug?: boolean
40
+ }
41
+ ```
42
+
43
+ Through `SiteKitLayout`, `zIndex` comes from `chat={{ zIndex }}`, so the chat
44
+ launcher and popups share one layer.
45
+
46
+ ## What shows
47
+
48
+ Each popup is a set of blocks (heading, formatted text, image, button,
49
+ divider) drawn in your site's design: its colors, fonts, radius and buttons.
50
+ `SiteKitLayout` measures the design from the home page at idle and reports it
51
+ to Sonor, so the popup builder previews popups the way they'll look. Its
52
+ `design` prop declares any part of it yourself
53
+ (`design={{ primary: 'var(--brand)', fontHeading: 'Fraunces, serif' }}`).
54
+
55
+ | Placement | What it is |
56
+ |-----------|------------|
57
+ | Popup | Centred over a dimmed page. One at a time: a second waits until the first closes |
58
+ | Banner | A bar along the top or bottom edge |
59
+ | Toast | A small card in a corner (slide-in) |
60
+
61
+ ## Targeting and triggers
62
+
63
+ All set in Sonor, no code changes:
64
+
65
+ - **Pages**: include and exclude paths, with `*` for a prefix (`/services/*`)
66
+ - **Devices**: desktop, mobile, tablet
67
+ - **Schedule**: start and end dates
68
+ - **Trigger**: immediately, after a delay, at a scroll depth, or on exit intent
69
+ - **Frequency**: once, once per session, or every N days. Closing a popup and
70
+ pressing its button both count as seen.
71
+
72
+ A popup targeted at one page leaves when the visitor navigates away from it.
73
+
74
+ ### Popups wait for forms
75
+
76
+ A popup or toast that opens on its own first checks whether the visitor is in
77
+ the middle of a form, and waits while they are. Mid-form means:
78
+
79
+ - focus is in a form field, or in a card field embedded in a form;
80
+ - a form has a field they've filled in and haven't sent yet;
81
+ - a site-kit form is sending.
82
+
83
+ It opens a moment after focus leaves the form, or once the form has been sent
84
+ and cleared. It never gives up: while the visitor stays mid-form, it keeps
85
+ waiting. Banners show at once, since they don't take focus or hide the page.
86
+
87
+ Why it matters: a modal popup takes focus and hides the rest of the page from
88
+ assistive technology while it's open. Opening one mid-form throws a person out
89
+ of the field they're typing in, and an AI agent browser filling the form for
90
+ someone (agents act on the accessibility tree) loses every field it was about
91
+ to fill.
92
+
93
+ ## Accessibility
94
+
95
+ Popups, toasts and banners are built so people and agent browsers can find
96
+ them and their controls by name:
97
+
98
+ - **A popup** is a modal dialog (`role="dialog"`, `aria-modal="true"`) named
99
+ by its heading. It takes focus when it opens, keeps Tab inside, closes on
100
+ Escape or a click outside it, and puts focus back where it was.
101
+ - **A toast** is a named, non-modal dialog, and **a banner** is a named
102
+ region. Both close on Escape and leave focus where it is.
103
+ - **Close buttons are named "Close"**, whatever they show.
104
+
105
+ ## Tracking
106
+
107
+ Each popup's impression (once half of it is on screen) and button presses are
108
+ counted in Sonor, against the same visitor ID (`_sk_vid`) analytics uses.
109
+
110
+ ## Drawing one popup yourself
111
+
112
+ `PopupBlocks` is the renderer `SitePopups` uses, for drawing one popup from
113
+ its blocks (a preview, a custom placement):
114
+
115
+ ```tsx
116
+ import { PopupBlocks } from '@sonordev/site-kit/website/popups'
117
+
118
+ <PopupBlocks
119
+ blocks={[{ type: 'heading', text: 'Spring cleanups are booking' }]}
120
+ placement="popup"
121
+ onClose={() => setOpen(false)}
122
+ />
123
+ ```
124
+
125
+ `inline` renders it in place, without the overlay or focus handling.
126
+
127
+ ## Engage (retired)
128
+
129
+ Engage was retired in Sonor, and its popups with it. Popups made in Engage
130
+ Studio (a `design_json` without blocks) aren't drawn by site-kit 7.2 or later;
131
+ make them again in Website → Popups & Banners. `@sonordev/site-kit/engage`
132
+ still builds through 7.x: its `EngageWidget` draws `SitePopups` and the
133
+ website chat.
@@ -1,15 +0,0 @@
1
- export { ChatWidget, EchoChat, isChatEnabled } from './chunk-RHNWA34F.js';
2
- import './chunk-JE4A5S4F.js';
3
- import './chunk-NDF4A5JM.js';
4
- import './chunk-6T6CQVNL.js';
5
- import './chunk-V6FDQLR7.js';
6
- import './chunk-AQSNPZY4.js';
7
- import './chunk-5SQK3D53.js';
8
- import './chunk-NJCTQH2P.js';
9
- import './chunk-S22FSH7C.js';
10
- import './chunk-L2V5PUFN.js';
11
- import './chunk-GJWI74ZZ.js';
12
- import './chunk-3SDOQUUF.js';
13
- import './chunk-VDF5DFWT.js';
14
- import './chunk-AH5Q262S.js';
15
- import './chunk-PKBMQBKP.js';
@@ -1,11 +0,0 @@
1
- export { EngageWidget } from './chunk-YCJT4JJG.js';
2
- import './chunk-NDF4A5JM.js';
3
- import './chunk-6T6CQVNL.js';
4
- import './chunk-24QZEO3Q.js';
5
- import './chunk-GJWI74ZZ.js';
6
- import './chunk-43OCZ3JA.js';
7
- import './chunk-EKBEOXTH.js';
8
- import './chunk-3SDOQUUF.js';
9
- import './chunk-VDF5DFWT.js';
10
- import './chunk-AH5Q262S.js';
11
- import './chunk-PKBMQBKP.js';
@@ -1,14 +0,0 @@
1
- export { ManagedForm } from './chunk-CSTQDNGK.js';
2
- import './chunk-D5EC5KVO.js';
3
- import './chunk-DXO6SBMD.js';
4
- import './chunk-LUXVWITO.js';
5
- import './chunk-V6FDQLR7.js';
6
- import './chunk-AQSNPZY4.js';
7
- import './chunk-5SQK3D53.js';
8
- import './chunk-NJCTQH2P.js';
9
- import './chunk-L2V5PUFN.js';
10
- import './chunk-GJWI74ZZ.js';
11
- import './chunk-3SDOQUUF.js';
12
- import './chunk-VDF5DFWT.js';
13
- import './chunk-AH5Q262S.js';
14
- import './chunk-PKBMQBKP.js';
@@ -1,8 +0,0 @@
1
- export { SitemapSync } from './chunk-UXOGCKL6.js';
2
- import './chunk-YVKRYQRG.js';
3
- import './chunk-43OCZ3JA.js';
4
- import './chunk-EKBEOXTH.js';
5
- import './chunk-3SDOQUUF.js';
6
- import './chunk-VDF5DFWT.js';
7
- import './chunk-AH5Q262S.js';
8
- import './chunk-PKBMQBKP.js';
@@ -1,37 +0,0 @@
1
- import { VISUAL_VIEWPORT_GAP_PROPERTY } from './chunk-6T6CQVNL.js';
2
-
3
- // src/engage/launcher-placement.ts
4
- var DEFAULT_LAUNCHER_OFFSET_BOTTOM = "20px";
5
- var DEFAULT_ENGAGE_Z_INDEX = 9999;
6
- var LAUNCHER_OFFSET_PROPERTY = "--sk-echo-offset-bottom";
7
- var SIDE_GUTTER = "20px";
8
- var POPUP_ABOVE_LAUNCHER = "70px";
9
- var POPUP_VERTICAL_RESERVE = "100px";
10
- function toCssLength(value) {
11
- if (typeof value === "number") return Number.isFinite(value) ? `${value}px` : void 0;
12
- const trimmed = value?.trim();
13
- return trimmed ? trimmed : void 0;
14
- }
15
- function chatLauncherStyles({ position, offsetBottom, zIndex } = {}) {
16
- const side = position === "bottom-left" ? { left: SIDE_GUTTER } : { right: SIDE_GUTTER };
17
- const offset = toCssLength(offsetBottom) ?? DEFAULT_LAUNCHER_OFFSET_BOTTOM;
18
- const layer = typeof zIndex === "number" && Number.isFinite(zIndex) ? zIndex : DEFAULT_ENGAGE_Z_INDEX;
19
- const clearance = `var(${LAUNCHER_OFFSET_PROPERTY}, ${offset}) + env(safe-area-inset-bottom, 0px) + var(${VISUAL_VIEWPORT_GAP_PROPERTY}, 0px)`;
20
- return {
21
- launcher: {
22
- ...side,
23
- bottom: `calc(${clearance})`
24
- },
25
- popup: {
26
- ...side,
27
- bottom: `calc(${clearance} + ${POPUP_ABOVE_LAUNCHER})`,
28
- maxHeight: `calc(100dvh - (${clearance}) - ${POPUP_VERTICAL_RESERVE})`
29
- },
30
- // One beneath the launcher. At 0 or below, the same layer instead: -1
31
- // would paint the popup behind the page's own content, and at a shared
32
- // layer the launcher, which renders after the popup, still paints on top.
33
- zIndex: { launcher: layer, popup: layer > 0 ? layer - 1 : layer }
34
- };
35
- }
36
-
37
- export { DEFAULT_ENGAGE_Z_INDEX, DEFAULT_LAUNCHER_OFFSET_BOTTOM, chatLauncherStyles };