@sonordev/site-kit 7.3.0 → 8.0.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 (239) hide show
  1. package/AGENTS.md +40 -12
  2. package/CHANGELOG.md +73 -0
  3. package/README.md +44 -18
  4. package/agent-manifest.json +111 -56
  5. package/dist/AnalyticsProvider-LYFDQRRJ.js +13 -0
  6. package/dist/{ArticleViewTracker-GFOAXFV7.js → ArticleViewTracker-HG7IIRJY.js} +6 -5
  7. package/dist/{BlocksPopup-AHDU34TL.js → BlocksPopup-GFX3GZIK.js} +6 -5
  8. package/dist/{ChatWidget-FHBAHFZX.js → ChatWidget-DVS7MLBN.js} +8 -5
  9. package/dist/{FileField-J5W5MIAZ.js → FileField-DVN7NUBI.js} +5 -5
  10. package/dist/{FormSpotlight-PE5YZBFZ.js → FormSpotlight-W4SXKKO6.js} +3 -3
  11. package/dist/{FormStage-OOPQC27J.js → FormStage-JTEE5MZD.js} +3 -3
  12. package/dist/ManagedForm-ZAUHIHGP.js +19 -0
  13. package/dist/{ManagedNewsletterForm-TC5542ZG.js → ManagedNewsletterForm-DQIB5Z4Y.js} +9 -6
  14. package/dist/{SignalCore-2YP5FCAS.js → SignalCore-W5PYYMGX.js} +7 -7
  15. package/dist/SiteChat-MY2GATWC.js +6 -0
  16. package/dist/SiteDesignReporter-ICZWEGCY.js +12 -0
  17. package/dist/SitePopups-TGULLAER.js +13 -0
  18. package/dist/SitemapSync-AN2OBJE6.js +9 -0
  19. package/dist/_client/booking-widget.js +6 -5
  20. package/dist/_client/testimonial-section.d.ts +1 -1
  21. package/dist/_client/testimonial-section.js +4 -2
  22. package/dist/affiliates/index.js +6 -6
  23. package/dist/analytics/index.js +73 -7
  24. package/dist/articles/index.js +2 -2
  25. package/dist/articles/server-ui.js +3 -3
  26. package/dist/articles/server.js +2 -2
  27. package/dist/brand.css +1 -1
  28. package/dist/chat/index.d.ts +1 -1
  29. package/dist/chat/index.js +39 -8
  30. package/dist/chunk-3G7YABRA.js +25 -0
  31. package/dist/{chunk-CGWUXUYZ.js → chunk-3RDJKIYO.js} +2 -2
  32. package/dist/{chunk-J4FSB4OG.js → chunk-3YKQLC7J.js} +5 -2
  33. package/dist/{chunk-BEBR4OJD.js → chunk-56VDS2RP.js} +2 -2
  34. package/dist/chunk-5ROTTMBL.js +19 -0
  35. package/dist/{chunk-56KNHB4U.js → chunk-5S2XDG5O.js} +1 -1
  36. package/dist/chunk-5TM3WPCI.js +7 -0
  37. package/dist/{chunk-BR4ZKUHH.js → chunk-6CJGZFOU.js} +18 -2
  38. package/dist/{chunk-RYVDGXC2.js → chunk-6KU7STM3.js} +3 -1
  39. package/dist/{chunk-XLSQTTUE.js → chunk-6QFINXLH.js} +3 -3
  40. package/dist/{chunk-CYUFBKJQ.js → chunk-6ZXJ3SXR.js} +1 -1
  41. package/dist/{chunk-NI2XNEMR.js → chunk-7YEEOIWI.js} +1 -1
  42. package/dist/{chunk-DNPNYWVL.js → chunk-BQ2CXKTQ.js} +5 -20
  43. package/dist/{chunk-X6F6SO4S.js → chunk-C7TLGL3R.js} +21 -1
  44. package/dist/{chunk-43OCZ3JA.js → chunk-CAH4Y4PY.js} +4 -2
  45. package/dist/chunk-CAT235HY.js +13 -0
  46. package/dist/{chunk-PXKIGZ2F.js → chunk-EATJPSTS.js} +1 -1
  47. package/dist/{chunk-O5RGXTWU.js → chunk-GNSXOTIK.js} +6 -4
  48. package/dist/{chunk-UPRSH6Q4.js → chunk-ICAVVF6D.js} +3 -3
  49. package/dist/{chunk-2ECWEUHP.js → chunk-IJQ767JS.js} +3 -5
  50. package/dist/{chunk-MKMD2HJW.js → chunk-J65N34YM.js} +3 -3
  51. package/dist/{chunk-6U3VLV2C.js → chunk-JIH32RH2.js} +4 -4
  52. package/dist/{chunk-T4DLETKR.js → chunk-K22OLAPC.js} +2 -2
  53. package/dist/{chunk-I2YX3HVD.js → chunk-KRBBH64S.js} +3 -3
  54. package/dist/{chunk-VF4F5WXZ.js → chunk-LBIQ5LTC.js} +1 -1
  55. package/dist/{chunk-WGIDC6QP.js → chunk-LKH44GI3.js} +5 -5
  56. package/dist/{chunk-ZTLMPUO6.js → chunk-LNSF6BJC.js} +1 -1
  57. package/dist/{chunk-WDQB4OXY.js → chunk-M62U7TJF.js} +1 -1
  58. package/dist/{chunk-EDYHROQ7.js → chunk-NFRJTOTB.js} +3 -3
  59. package/dist/chunk-OD2ULW5Z.js +57 -0
  60. package/dist/{chunk-Y6LP36UW.js → chunk-OE3NXEOF.js} +2 -2
  61. package/dist/{chunk-L7U23XI2.js → chunk-OGK7OL3T.js} +5 -4
  62. package/dist/{chunk-RDPW4D33.js → chunk-PD7K54EQ.js} +2 -2
  63. package/dist/{chunk-MVR4FY5R.js → chunk-PIX7FAMX.js} +2 -2
  64. package/dist/{chunk-2AD3CDRR.js → chunk-ROAOI6GG.js} +10 -10
  65. package/dist/{chunk-YJLS6JBI.js → chunk-S7SSYGUJ.js} +6 -5
  66. package/dist/{chunk-ZKHI4E2B.js → chunk-SBPAXWLF.js} +1 -1
  67. package/dist/{chunk-ZHS2XFLV.js → chunk-TOSZGJEQ.js} +1 -1
  68. package/dist/{chunk-4NTBQNHA.js → chunk-TYAVHRWH.js} +80 -81
  69. package/dist/{chunk-Z65DHMTW.js → chunk-TZ5RTANX.js} +3 -3
  70. package/dist/{chunk-VX5CMNMM.js → chunk-UAYAJPEP.js} +7 -1
  71. package/dist/{chunk-4YTYGG2C.js → chunk-URJN75ZG.js} +29 -16
  72. package/dist/{chunk-EPT6FJUY.js → chunk-UUWUAMUC.js} +13 -16
  73. package/dist/{chunk-PKGN32AU.js → chunk-XPTJRVL2.js} +1 -1
  74. package/dist/{chunk-662ILEZ6.js → chunk-Y35BWW2C.js} +3 -7
  75. package/dist/{chunk-MRQ2U4LZ.js → chunk-ZHNQFNO3.js} +2 -2
  76. package/dist/chunk-ZKADJNR2.js +52 -0
  77. package/dist/{chunk-WGG6GTKW.js → chunk-ZLFGXKLG.js} +1 -1
  78. package/dist/client/index.js +6 -5
  79. package/dist/contracts/entries.d.ts +1 -1
  80. package/dist/contracts/sentences.d.ts +46 -0
  81. package/dist/contracts/site-cache.d.ts +2 -0
  82. package/dist/cta-bar/index.js +1 -1
  83. package/dist/fleet/index.js +6 -5
  84. package/dist/forms/field-autocomplete.d.ts +16 -8
  85. package/dist/forms/index.d.ts +5 -0
  86. package/dist/forms/index.js +17 -17
  87. package/dist/forms/server.js +7 -7
  88. package/dist/forms/static.js +2 -2
  89. package/dist/forms/submitForm.d.ts +5 -0
  90. package/dist/images/index.js +4 -4
  91. package/dist/index.d.ts +3 -7
  92. package/dist/index.js +1 -1
  93. package/dist/layout/SiteKitLayout.d.ts +2 -1
  94. package/dist/layout/client.js +11 -9
  95. package/dist/layout/index.js +14 -12
  96. package/dist/llms/index.js +8 -7
  97. package/dist/maps/index.js +6 -6
  98. package/dist/mcp/sonor.d.ts +47 -3
  99. package/dist/mcp/sonor.js +31 -21
  100. package/dist/proxy/index.d.ts +3 -3
  101. package/dist/proxy/index.js +1 -1
  102. package/dist/reputation/TestimonialSection.d.ts +1 -1
  103. package/dist/reputation/VisualReviewCard.d.ts +6 -0
  104. package/dist/reputation/index.d.ts +2 -1
  105. package/dist/reputation/index.js +5 -2
  106. package/dist/reputation/review-photo.d.ts +2 -0
  107. package/dist/reputation/server.d.ts +3 -2
  108. package/dist/reputation/server.js +4 -1
  109. package/dist/reputation/types.d.ts +22 -0
  110. package/dist/revalidate/index.js +3 -3
  111. package/dist/robots/indexnow.js +2 -2
  112. package/dist/runtime/index.d.ts +32 -0
  113. package/dist/runtime/index.js +7 -0
  114. package/dist/seo/api.d.ts +0 -4
  115. package/dist/seo/client.js +6 -5
  116. package/dist/seo/getManagedMetadata.d.ts +24 -0
  117. package/dist/seo/index.d.ts +2 -3
  118. package/dist/seo/index.js +14 -132
  119. package/dist/seo/indexnow.js +2 -2
  120. package/dist/seo/llms.js +8 -7
  121. package/dist/seo/register-sitemap-cli.js +2 -2
  122. package/dist/seo/server-api.d.ts +0 -6
  123. package/dist/seo/server.d.ts +1 -1
  124. package/dist/seo/server.js +4 -4
  125. package/dist/seo/sitemap.js +5 -4
  126. package/dist/seo/types.d.ts +9 -30
  127. package/dist/server/index.d.ts +8 -1
  128. package/dist/server/index.js +5 -4
  129. package/dist/server/mint-site-token.d.ts +1 -13
  130. package/dist/server/server-fetch.d.ts +19 -0
  131. package/dist/server-api-EGOKC7Q3.js +9 -0
  132. package/dist/shared/build-entries.d.ts +21 -9
  133. package/dist/shared/clientApiConfig.d.ts +56 -0
  134. package/dist/shared/fresh-fetch.d.ts +9 -3
  135. package/dist/shared/identity-reader.d.ts +26 -0
  136. package/dist/shared/identity-storage.d.ts +30 -0
  137. package/dist/shared/identity.d.ts +13 -13
  138. package/dist/shared/import-specifiers.d.ts +26 -0
  139. package/dist/shared/sonorFetch.d.ts +4 -3
  140. package/dist/shared/version.d.ts +1 -1
  141. package/dist/signal/index.js +2 -2
  142. package/dist/sitemap/index.js +5 -4
  143. package/dist/slots/index.js +3 -3
  144. package/dist/sync/index.js +6 -5
  145. package/dist/types.d.ts +0 -1
  146. package/dist/website/cta-bar.js +1 -1
  147. package/dist/website/images.js +4 -4
  148. package/dist/website/index.js +7 -6
  149. package/dist/website/popups.js +10 -7
  150. package/dist/website/slots.js +3 -3
  151. package/dist/{writeLLMsTxt-2NQVDGYI.js → writeLLMsTxt-UZGN6IBU.js} +4 -3
  152. package/docs/MIGRATING-TO-7.md +5 -2
  153. package/docs/MIGRATING-TO-8.md +183 -0
  154. package/docs.json +4 -8
  155. package/package.json +15 -44
  156. package/skills/site-kit/SKILL.md +41 -11
  157. package/src/analytics/README.md +14 -14
  158. package/src/chat/README.md +7 -7
  159. package/src/forms/README.md +61 -6
  160. package/src/mcp/README.md +61 -20
  161. package/src/proxy/README.md +5 -4
  162. package/src/reputation/README.md +25 -0
  163. package/src/revalidate/README.md +2 -2
  164. package/src/runtime/README.md +51 -0
  165. package/src/seo/README.md +24 -8
  166. package/src/sync/README.md +2 -2
  167. package/src/website/README.md +3 -2
  168. package/dist/AnalyticsProvider-DJQA66AG.js +0 -11
  169. package/dist/ManagedForm-JRZAJ7SZ.js +0 -16
  170. package/dist/SiteChat-EGEZJJVV.js +0 -5
  171. package/dist/SiteDesignReporter-YEEWJKWN.js +0 -11
  172. package/dist/SitePopups-LQBIXIFV.js +0 -10
  173. package/dist/SitemapSync-LSZ2EYOF.js +0 -8
  174. package/dist/chunk-24QZEO3Q.js +0 -41
  175. package/dist/chunk-BU66S5B7.js +0 -1
  176. package/dist/chunk-G3NHWMP5.js +0 -233
  177. package/dist/chunk-HODO5BX5.js +0 -28
  178. package/dist/chunk-L3AQCDCY.js +0 -386
  179. package/dist/chunk-P5GB6NEO.js +0 -66
  180. package/dist/chunk-SWKE6FWH.js +0 -47
  181. package/dist/cms/CmsPage.d.ts +0 -17
  182. package/dist/cms/CmsPreview.d.ts +0 -37
  183. package/dist/cms/CmsSection.d.ts +0 -8
  184. package/dist/cms/PortableTextRenderer.d.ts +0 -7
  185. package/dist/cms/index.d.ts +0 -37
  186. package/dist/cms/index.js +0 -5
  187. package/dist/cms/sanity-image.d.ts +0 -47
  188. package/dist/cms/sections/CtaSection.d.ts +0 -3
  189. package/dist/cms/sections/CustomSection.d.ts +0 -7
  190. package/dist/cms/sections/FaqSection.d.ts +0 -3
  191. package/dist/cms/sections/FormSection.d.ts +0 -8
  192. package/dist/cms/sections/GallerySection.d.ts +0 -3
  193. package/dist/cms/sections/HeroSection.d.ts +0 -3
  194. package/dist/cms/sections/RichTextSection.d.ts +0 -3
  195. package/dist/cms/sections/TestimonialsSection.d.ts +0 -3
  196. package/dist/cms/sections/index.d.ts +0 -8
  197. package/dist/cms/server-api.d.ts +0 -49
  198. package/dist/cms/server.d.ts +0 -1
  199. package/dist/cms/server.js +0 -5
  200. package/dist/cms/types.d.ts +0 -104
  201. package/dist/commerce/CalendarView.d.ts +0 -43
  202. package/dist/commerce/CheckoutForm.d.ts +0 -11
  203. package/dist/commerce/EventCalendar.d.ts +0 -26
  204. package/dist/commerce/EventCheckout.d.ts +0 -42
  205. package/dist/commerce/EventEmbed.d.ts +0 -11
  206. package/dist/commerce/EventModal.d.ts +0 -43
  207. package/dist/commerce/EventTile.d.ts +0 -10
  208. package/dist/commerce/EventsAgenda.d.ts +0 -38
  209. package/dist/commerce/EventsWidget.d.ts +0 -84
  210. package/dist/commerce/OfferingCard.d.ts +0 -9
  211. package/dist/commerce/OfferingList.d.ts +0 -9
  212. package/dist/commerce/ProductDetail.d.ts +0 -38
  213. package/dist/commerce/ProductEmbed.d.ts +0 -11
  214. package/dist/commerce/ProductGrid.d.ts +0 -41
  215. package/dist/commerce/ProductPage.d.ts +0 -39
  216. package/dist/commerce/RegistrationForm.d.ts +0 -9
  217. package/dist/commerce/SizeChart.d.ts +0 -8
  218. package/dist/commerce/UpcomingEvents.d.ts +0 -9
  219. package/dist/commerce/api.d.ts +0 -179
  220. package/dist/commerce/events-extras.d.ts +0 -60
  221. package/dist/commerce/index.d.ts +0 -33
  222. package/dist/commerce/index.js +0 -8022
  223. package/dist/commerce/server.d.ts +0 -161
  224. package/dist/commerce/server.js +0 -2
  225. package/dist/commerce/types.d.ts +0 -340
  226. package/dist/commerce/useEventModal.d.ts +0 -20
  227. package/dist/commerce/utils.d.ts +0 -17
  228. package/dist/engage/EngageWidget.d.ts +0 -21
  229. package/dist/engage/index.d.ts +0 -14
  230. package/dist/engage/index.js +0 -45
  231. package/dist/engage/types.d.ts +0 -33
  232. package/dist/seo/ManagedContent.d.ts +0 -14
  233. package/dist/server-api-BVCBLJKL.js +0 -9
  234. package/dist/website/cms/server.d.ts +0 -2
  235. package/dist/website/cms/server.js +0 -5
  236. package/dist/website/cms-server.d.ts +0 -2
  237. package/dist/website/cms.d.ts +0 -2
  238. package/dist/website/cms.js +0 -5
  239. package/src/commerce/README.md +0 -109
@@ -1,4 +1,4 @@
1
- # Forms — `@sonordev/site-kit/forms`
1
+ # Forms: `@sonordev/site-kit/forms`
2
2
 
3
3
  Sonor-managed forms with multi-step support, conditional logic, validation, anti-bot protection, and automatic CRM routing.
4
4
 
@@ -21,7 +21,7 @@ Fetches form config from Sonor, renders fields, handles submission, routes to CR
21
21
  Since 4.0 that one line gets you the **spotlight** experience by default: the
22
22
  familiar layout, alive. A glowing ring visits the field you're in, completed
23
23
  fields earn a check, and each finished row collapses into a sentence the form
24
- says back — "Nice to meet you, Jordan." / "We'll follow up at jordan@…" — with
24
+ says back ("Nice to meet you, Jordan." or "We'll follow up at jordan@…"), with
25
25
  an edit control to reopen it.
26
26
 
27
27
  | Experience | What it is | How to get it |
@@ -36,7 +36,7 @@ an edit control to reopen it.
36
36
  <ManagedForm formId="contact-form" experience="classic" /> // opt out
37
37
  ```
38
38
 
39
- Sonor can decide instead of the site, with no deploy on either side — set the
39
+ Sonor can decide instead of the site, with no deploy on either side: set the
40
40
  form's `layout` to `classic`, `stage`, or `spotlight`. An explicit
41
41
  `experience` prop always wins over the config.
42
42
 
@@ -68,7 +68,7 @@ Welcome aboard, {first_name}. We'll reach you at {email}.
68
68
  ```
69
69
 
70
70
  Leave it empty to keep the built-in sentence. Unknown or unanswered tokens
71
- render as nothing — never raw braces.
71
+ render as nothing, never raw braces.
72
72
 
73
73
  ### Option 2: Headless Hook (full UI control)
74
74
 
@@ -101,6 +101,59 @@ export function ContactForm() {
101
101
  }
102
102
  ```
103
103
 
104
+ #### Autocomplete on your own controls
105
+
106
+ `ManagedForm` puts the right `autocomplete` token on each field that has one, so
107
+ browser autofill, password managers and AI assistants know what the field wants.
108
+ When you draw the controls yourself, with `useForm` or the render prop below, ask
109
+ for the same token with `autocompleteFor` (since 8.0.0) and pass it to
110
+ `autoComplete`:
111
+
112
+ ```tsx
113
+ 'use client'
114
+ import { useForm, autocompleteFor } from '@sonordev/site-kit/forms'
115
+
116
+ export function QuoteForm() {
117
+ const { fields, values, setFieldValue, handleSubmit, toolAttributes } = useForm('quote-request')
118
+
119
+ return (
120
+ <form {...toolAttributes} onSubmit={handleSubmit}>
121
+ {fields.map(field => (
122
+ <label key={field.slug}>
123
+ {field.label}
124
+ <input
125
+ name={field.slug}
126
+ autoComplete={autocompleteFor(field)}
127
+ value={String(values[field.slug] || '')}
128
+ onChange={(e) => setFieldValue(field.slug, e.target.value)}
129
+ />
130
+ </label>
131
+ ))}
132
+ <button type="submit">Request a quote</button>
133
+ </form>
134
+ )
135
+ }
136
+ ```
137
+
138
+ It returns a token such as `given-name`, `email`, `tel`, `organization` or
139
+ `postal-code`, or `undefined` when a field has no confident match, and React
140
+ leaves the attribute off for `undefined`. Only text, email and phone fields and
141
+ selects get a token, so a message box, a number, a date or a checkbox gets
142
+ `undefined`. To stay short, the example draws every field as a text input and
143
+ leaves out error messages and steps; draw each field with the control it calls
144
+ for and pass the same `autoComplete={autocompleteFor(field)}` to all of them.
145
+
146
+ Use it instead of typing tokens per field. It works the token out from the
147
+ field's type, slug and label, so your markup doesn't keep a list of its own that
148
+ can drift. `@sonordev/site-kit/forms` is a client module, like `useForm`, so call
149
+ `autocompleteFor` from a client component (a file that starts with
150
+ `'use client'`). Calling it in a Server Component fails the build.
151
+
152
+ To type a field you pass around, use `UseFormReturn['fields'][number]`, or
153
+ `AutocompleteField` for a helper that only needs the token. Don't use the
154
+ `FormField` this entry exports: that's the shape `formsApi` takes (camelCase keys
155
+ such as `fieldType`), not what `useForm` returns.
156
+
104
157
  ### Option 3: Render Prop
105
158
 
106
159
  ```tsx
@@ -228,7 +281,8 @@ nothing to configure.
228
281
  fields carry the right `autocomplete` token (`given-name`, `email`, `tel`,
229
282
  `organization`, `url`, `postal-code`, ...), read from the field's CRM
230
283
  destination in Sonor, then its type, slug and label. Browser autofill and
231
- password managers use the same tokens.
284
+ password managers use the same tokens. A headless form adds them with
285
+ `autocompleteFor` (see [Autocomplete on your own controls](#autocomplete-on-your-own-controls)).
232
286
  - **Accessible wiring.** Error and help text are tied to their control
233
287
  (`aria-describedby`), an errored control says so (`aria-invalid`), radio and
234
288
  checkbox groups are named by their question, and rating stars by their
@@ -252,7 +306,8 @@ nothing to configure.
252
306
  until the page can catch the click.
253
307
 
254
308
  With `useForm`, spread `toolAttributes` on your `<form>` and use
255
- `handleSubmit` as its `onSubmit` to get the same behavior.
309
+ `handleSubmit` as its `onSubmit` to get the WebMCP behavior and the agent-sent
310
+ tag, and add `autocompleteFor` to your controls for the autofill tokens.
256
311
 
257
312
  `ServerForm`'s `enhance` prop is ignored since 7.2.0. Only the interactive
258
313
  form can send a managed form, so `enhance={false}` only ever produced a form
package/src/mcp/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # `@sonordev/site-kit/mcp` — WebMCP & Model Context Protocol
1
+ # `@sonordev/site-kit/mcp`: WebMCP & Model Context Protocol
2
2
 
3
3
  Make a marketing site something an AI agent can **use**, not just read.
4
4
 
@@ -8,12 +8,12 @@ definitions:
8
8
 
9
9
  | Surface | Who uses it | Entry point |
10
10
  |---|---|---|
11
- | Remote MCP endpoint (Streamable HTTP) | Off-browser agents — Claude, Cursor, any MCP client | `createMcpHandler` |
11
+ | Remote MCP endpoint (Streamable HTTP) | Off-browser agents: Claude, Cursor, any MCP client | `createMcpHandler` |
12
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
- One definition feeding all three is the point. The alternative — a tool list for
16
- the endpoint and a separate one for the page — drifts, and a stale tool
15
+ One definition feeding all three is the point. The alternative, a tool list for
16
+ the endpoint and a separate one for the page, drifts, and a stale tool
17
17
  definition is worse than none, because the agent believes it.
18
18
 
19
19
  ---
@@ -36,7 +36,7 @@ site's pages use, so an agent gets what a visitor gets.
36
36
  | `find_pages` | The page that covers a topic |
37
37
  | `list_articles`, `get_article` | Its articles (`articles: false` drops them) |
38
38
  | `get_reviews` | Reviews verbatim, with who wrote them, and the rating |
39
- | `list_offerings` | Priced products, services, events (opt-in: `offerings: { path }`); private prices are left out |
39
+ | `list_offerings` | Priced products, services, events (opt-in: `offerings: { path, read }`, see below); private prices are left out |
40
40
  | `check_availability` | Open appointment times, read only (opt-in: `booking: { path }`) |
41
41
  | `get_inquiry_form`, `send_inquiry` | An inquiry for a person (opt-in: `inquiry: { form }`) |
42
42
 
@@ -63,6 +63,47 @@ export const { POST, GET, DELETE, OPTIONS } = createMcpHandler({
63
63
  })
64
64
  ```
65
65
 
66
+ **`list_offerings` needs a reader (8.0).** The catalog it lists belongs to
67
+ [commerce-kit](https://sonor.dev/commerce-kit), so you hand the tool commerce-kit's
68
+ fetcher instead of site-kit importing it:
69
+
70
+ ```ts
71
+ // lib/mcp.ts
72
+ import 'server-only'
73
+ import { sonorMcpServer } from '@sonordev/site-kit/mcp/sonor'
74
+ import { getOfferingsResult } from '@sonordev/commerce-kit/commerce/server'
75
+
76
+ export const mcpServer = sonorMcpServer({
77
+ businessName: 'Example Studio',
78
+ offerings: { path: '/shop', read: getOfferingsResult },
79
+ })
80
+ ```
81
+
82
+ `read` is required and is `getOfferingsResult` from
83
+ `@sonordev/commerce-kit/commerce/server`, so the site needs commerce-kit installed
84
+ (`npm install @sonordev/commerce-kit`). When a request fails it tells the agent the
85
+ catalog is unavailable; the plain `getOfferings` from the same entry also works, but
86
+ it reads an outage as an empty catalog. The other tools, `list_services` included,
87
+ don't. `path` is where the offerings' pages live: each offering links to
88
+ `<path>/<slug>` on the site, and `path` defaults to `/shop`. An agent can narrow the
89
+ list with `type` (`product`, `service` or `event`), search it with `query`, and cap
90
+ it with `limit` (at most 20). The tool, how it ranks a query, and its rule that a
91
+ price the business keeps private is never shown all stay in site-kit.
92
+
93
+ `read` is typed `McpOfferingsReader`, and what it returns is a list of `McpOffering`s
94
+ (a name, type, slug, descriptions and a price), or `{ ok, data }` around that list. Both
95
+ types are exported from `@sonordev/site-kit/mcp/sonor`. commerce-kit's fetchers satisfy
96
+ them as they are, so you only meet the types if you supply the catalog from somewhere
97
+ else: write a function that takes `({ apiUrl, apiKey, projectId }, { type, limit })` and
98
+ returns the offerings, and answer `{ ok: false, data: [] }` when the request fails, so
99
+ the agent hears "unavailable" instead of "nothing for sale".
100
+
101
+ Without `read`, `sonorMcpServer` throws when the server is built, with a message that
102
+ names the fix. `npx sonor-setup mcp --offerings /shop` writes this wiring for a new
103
+ site, import included, and tells you to install commerce-kit. Before 8.0 the option
104
+ was `offerings: { path }`; see
105
+ [Moving a site to site-kit 8](../../docs/MIGRATING-TO-8.md#the-mcp-change).
106
+
66
107
  **`send_inquiry` has a person behind it.** It refuses unless
67
108
  `person_confirmed` is true (the person asked to be contacted and agreed to
68
109
  share their details), files through Sonor's agent-inquiry door with the
@@ -205,7 +246,7 @@ aliases for clients that already use them. The card handler's default remains
205
246
  ### 4. Register in-page (optional, for browser agents)
206
247
 
207
248
  ```tsx
208
- // app/layout.tsx — a CHILDLESS SIBLING, never a wrapper
249
+ // app/layout.tsx: a CHILDLESS SIBLING, never a wrapper
209
250
  <SiteKitLayout>{children}</SiteKitLayout>
210
251
  <WebMcpTools endpoint="/api/mcp" />
211
252
  ```
@@ -290,13 +331,13 @@ The header and label default to `x-site-mcp-transport` and
290
331
  `{ header, label }` to all three factories (for example
291
332
  `x-example-mcp-transport` / `example-mcp-transport-v1`). This entry is Node
292
333
  only and imports no `server-only`, so the plain-Node function can load it. It
293
- is not re-exported from `@sonordev/site-kit/mcp`, which stays runtime-neutral.
334
+ isn't re-exported from `@sonordev/site-kit/mcp`, which stays runtime-neutral.
294
335
 
295
336
  ---
296
337
 
297
338
  ## Design notes
298
339
 
299
- ### The card does not list tools — on purpose
340
+ ### The card doesn't list tools, on purpose
300
341
 
301
342
  The card's discovery shape omits primitives: what a server exposes can
302
343
  vary with auth state and configuration, so the authoritative list is whatever
@@ -306,8 +347,8 @@ source of truth that goes stale silently.
306
347
  We still publish a **summary** (name + description + read-only flag) under
307
348
  `_meta['io.sonor.site-kit/tools']`. `_meta` is the spec's sanctioned extension
308
349
  point and requires a reverse-DNS prefix, so the hint rides along without
309
- pretending to be standard — useful for crawlers that index the card and never
310
- connect.
350
+ pretending to be standard. That's useful for crawlers that index the card and
351
+ never connect.
311
352
 
312
353
  ### Experimental discovery and compatibility paths
313
354
 
@@ -323,21 +364,21 @@ URLs working while adding the catalog.
323
364
 
324
365
  `dispatch()` answers both protocol eras on one endpoint:
325
366
 
326
- - **Modern (`2026-07-28`)** — stateless, per-request `_meta` carrying the
367
+ - **Modern (`2026-07-28`)**: stateless, per-request `_meta` carrying the
327
368
  protocol version, mirrored into `MCP-Protocol-Version`. `server/discover`
328
369
  replaces the handshake. No sessions, no GET stream (both return `405`).
329
- - **Legacy (`≤ 2025-11-25`)** — the `initialize` handshake, which is what most
370
+ - **Legacy (`≤ 2025-11-25`)**: the `initialize` handshake, which is what most
330
371
  shipped clients and SDKs still speak.
331
372
 
332
373
  Supporting only the current revision would be spec-correct and unusable today.
333
374
 
334
- ### Header mirroring: mismatch is fatal, absence is not
375
+ ### Header mirroring: mismatch is fatal, absence isn't
335
376
 
336
377
  The modern revision mirrors `method` and `params.name` into `Mcp-Method` and
337
378
  `Mcp-Name` so intermediaries can route without parsing bodies, and requires
338
379
  servers to reject disagreements (`-32020`).
339
380
 
340
- We always reject a **mismatch** — that is the real security property, stopping
381
+ We always reject a **mismatch**; that's the real security property, stopping
341
382
  a load balancer and the server from acting on different values. A merely
342
383
  **absent** header is tolerated unless you set `strictHeaders: true`, because a
343
384
  public marketing endpoint exists to be reachable and today's clients frequently
@@ -360,13 +401,13 @@ one or the other makes you invisible to a large slice of the ecosystem.
360
401
 
361
402
  `<WebMcpTools>` does nothing at all unless `document.modelContext` exists. That
362
403
  gate comes before the `tools/list` fetch, so the cost for a human visitor is a
363
- single property check — not a request, and not a byte of page weight.
404
+ single property check: not a request, and not a byte of page weight.
364
405
 
365
406
  ### In-page tools are proxied, not re-implemented
366
407
 
367
408
  `<WebMcpTools>` registers thin wrappers that POST `tools/call` to this site's
368
409
  own endpoint, so the in-page tool and the remote tool run the same server-side
369
- handler. It also keeps `SONOR_API_KEY` out of the client bundle — registering
410
+ handler. It also keeps `SONOR_API_KEY` out of the client bundle: registering
370
411
  real handlers client-side would pull server code, and the key with it, into a
371
412
  `'use client'` graph and a public chunk.
372
413
 
@@ -376,13 +417,13 @@ Tools that genuinely need live DOM state go in `localTools` and run in-page.
376
417
 
377
418
  `<WebMcpTools>` waits for the page to go quiet (`useDeferredActivation`) before
378
419
  touching `document.modelContext`. Agents poll or listen for `toolchange`, so a
379
- few hundred milliseconds costs nothing — a blocked LCP costs a lot.
420
+ few hundred milliseconds costs nothing, while a blocked LCP costs a lot.
380
421
 
381
422
  ---
382
423
 
383
424
  ## Writing good tools
384
425
 
385
- The `description` is the highest-leverage field in this module. It is the only
426
+ The `description` is the highest-leverage field in this module. It's the only
386
427
  thing a model reads when deciding whether to call the tool.
387
428
 
388
429
  - **Verb-led `snake_case` names**: `get_services`, `request_site_audit`.
@@ -391,10 +432,10 @@ thing a model reads when deciding whether to call the tool.
391
432
  - **Set `annotations` honestly.** `readOnlyHint` on lookups; `destructiveHint`
392
433
  on anything creating a record. Good agents use these to decide what needs a
393
434
  human.
394
- - **Never `toolautosubmit` a lead form.** That is how an agent files fifty
435
+ - **Never `toolautosubmit` a lead form.** That's how an agent files fifty
395
436
  audit requests by accident.
396
437
  - **Return the caveat with the data.** A pricing tool should return the ranges
397
- *and* the fact that they are ranges — otherwise the model quotes a number as
438
+ *and* the fact that they're ranges; otherwise the model quotes a number as
398
439
  a commitment.
399
440
 
400
441
  ## Testing an endpoint by hand
@@ -2,10 +2,11 @@
2
2
 
3
3
  Composable Next.js Proxy factory. Zero-config redirects + security headers. Opt-in AI discovery headers.
4
4
 
5
- Next 16 renamed the `middleware` file convention to `proxy`. Through 7.x this
6
- module is also served on `@sonordev/site-kit/middleware`, with `createMiddleware`
7
- as an alias of `createProxy`; both go in 8.0. `npx sonor-setup codemod --write`
8
- moves a site over (the file too).
5
+ Next 16 renamed the `middleware` file convention to `proxy`. This module is
6
+ also served on `@sonordev/site-kit/middleware`, with `createMiddleware` as an
7
+ alias of `createProxy`. Both are deprecated and still work in 8; they'll go in a
8
+ later major, so move off them when you next touch the file.
9
+ `npx sonor-setup codemod --write` moves a site over (the file too).
9
10
 
10
11
  ## Usage
11
12
 
@@ -27,6 +27,7 @@ interface TestimonialSectionProps {
27
27
  autoplay?: boolean // Auto-rotate reviews
28
28
  autoplayInterval?: number // ms between rotations
29
29
  showRating?: boolean // Display star ratings
30
+ showPhoto?: boolean // Show attached business photo (default true)
30
31
  maxReviews?: number // Limit displayed count
31
32
  featuredOnly?: boolean // Show featured reviews only
32
33
  service?: string // Filter by service tag
@@ -34,6 +35,29 @@ interface TestimonialSectionProps {
34
35
  }
35
36
  ```
36
37
 
38
+ ## Visual review cards
39
+
40
+ Render a photo and its original review together with a responsive, server-rendered gallery:
41
+
42
+ ```tsx
43
+ import { fetchReviews, VisualReviewGallery } from '@sonordev/site-kit/reputation/server'
44
+
45
+ export default async function ReviewsPage() {
46
+ const reviews = await fetchReviews({ limit: 12 })
47
+ return <VisualReviewGallery reviews={reviews} title="What Our Clients Say" />
48
+ }
49
+ ```
50
+
51
+ `VisualReviewCard` accepts one `review`, optional `showRating` and `className`.
52
+ `VisualReviewGallery` accepts `reviews`, optional `title`, `subtitle`, `showRating`
53
+ and `className`. Both use the site's `--sk-*` brand tokens and work in React
54
+ Server Components. Import `@sonordev/site-kit/brand.css` once for token defaults,
55
+ then override them in your site's CSS.
56
+
57
+ Text-only reviews stay visible while a photo is pending. Adding or removing a
58
+ photo in Sonor updates the public review feed; `image` continues to mean the
59
+ reviewer's avatar. Only HTTPS photo URLs are displayed.
60
+
37
61
  ## API Functions
38
62
 
39
63
  ```ts
@@ -50,6 +74,7 @@ interface Review {
50
74
  id: string; quote: string; name: string; role?: string;
51
75
  rating: number; image?: string; date?: string;
52
76
  platform?: string; isFeatured?: boolean; serviceTags?: string[];
77
+ photo?: { fileId: string; url: string; altText: string; caption: string } | null;
53
78
  }
54
79
 
55
80
  interface ReviewStats {
@@ -15,7 +15,7 @@ tells you when it's missing.
15
15
 
16
16
  Everything site-kit fetches from Sonor is cached, and pages built from those
17
17
  fetches are served from your host's CDN. When content changes in Sonor
18
- (managed copy, a page's title or description, an article, a portfolio item),
18
+ (managed copy, a page's title or description, an article, a portfolio item, a product or event),
19
19
  Sonor sends this route the paths and cache tags that changed. The route checks
20
20
  the call was signed with your project's API key, then expires only those
21
21
  pages and tags. The next visitor gets the new version, and every other page
@@ -60,7 +60,7 @@ A POST with `Authorization: Bearer <SONOR_API_KEY>` and a JSON body:
60
60
  | Field | Meaning |
61
61
  |---|---|
62
62
  | `paths` | Local paths to regenerate. `/sitemap.xml`, `/llms.txt` and `/llms-full.txt` are always refreshed too. |
63
- | `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`. |
63
+ | `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`, `portfolio` and `commerce` (products, services, events and membership plans, from [commerce-kit](https://sonor.dev/commerce-kit)). |
64
64
  | `revalidateAll` | Regenerate every page. |
65
65
  | `ping` | `{ "ping": true }` on its own regenerates nothing and answers `{ "ok": true, "ping": true, "version": "…" }`. Sonor uses it to confirm the route is installed. |
66
66
 
@@ -0,0 +1,51 @@
1
+ # Runtime
2
+
3
+ `@sonordev/site-kit/runtime` is how a package that builds on site-kit talks to Sonor from the browser, so it never needs a transport of its own. [commerce-kit](https://sonor.dev/commerce-kit) is built on it.
4
+
5
+ Building a site? You don't need this entry. Forms, analytics, articles and the rest of site-kit already use the same transport.
6
+
7
+ ## What it gives a kit
8
+
9
+ | Export | Use it to |
10
+ |---|---|
11
+ | `sonorFetch(url, init?, options?)` | Call the Sonor API from the browser. It sends the site credential you pass as `apiKey`, retries transient failures with a timeout, refreshes an expired site token once, and pauses for a few minutes after an authentication failure, so a misconfigured site doesn't hammer the API. |
12
+ | `getClientApiConfig(options?)` | Find the API origin and credential. In the browser that's what `SiteKitLayout` published. During server rendering it's the server's `SONOR_API_URL`, and with `{ serverKey: true }` its `SONOR_API_KEY`. |
13
+ | `readPublishedApiConfig()` | Exactly what `SiteKitLayout` published, with no fallbacks. For code that must stay quiet on a page without the layout. |
14
+ | `DEFAULT_SONOR_API_URL` | `https://api.sonor.io`. |
15
+ | `readAnalyticsIdentity()` | Get the visitor's analytics ids, `{ sessionId, visitorId }`, so what you send to Sonor ties to the visit analytics recorded. It only reads: either id is `null` during server rendering and before analytics has started a session in the tab. It never throws, including when the browser blocks site storage. |
16
+
17
+ The types `SonorFetchOptions`, `FetchRetryOptions`, `ClientApiConfigOptions` and `AnalyticsIdentity` come with them.
18
+
19
+ ```ts
20
+ import { getClientApiConfig, sonorFetch } from '@sonordev/site-kit/runtime'
21
+
22
+ export async function startCheckout(payload: unknown) {
23
+ const { apiUrl, apiKey } = getClientApiConfig()
24
+ // Anything that isn't safe to repeat (checkout, payments, uploads) passes retries: 0.
25
+ const response = await sonorFetch(
26
+ `${apiUrl}/api/public/example/checkout`,
27
+ { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) },
28
+ { apiKey, retries: 0 },
29
+ )
30
+ return response.json()
31
+ }
32
+ ```
33
+
34
+ Pass `apiKey` from `getClientApiConfig()` and `sonorFetch` sets the `x-api-key` header itself, using the site's freshest token when the site uses one. Don't set `x-api-key` or `Authorization` yourself: on a site that uses tokens, a stale header you set is replaced by the fresh one.
35
+
36
+ ## Rules for kits
37
+
38
+ - **Use this entry for browser calls.** Don't read `window.__SITE_KIT_*` and don't call `fetch` on the Sonor API directly. A second transport is a second copy of the credential, retry and breaker logic, and it drifts.
39
+ - **Get the visitor's ids from `readAnalyticsIdentity()`.** Where site-kit keeps them is its own business and can change.
40
+ - **Server data goes through `@sonordev/site-kit/server`.** `serverApiFetch` sends the key, bounds each call in time and retries transient failures. Tag what you fetch with your content's entry in `SITE_CACHE_TAGS` so an edit in Sonor refreshes it, and if you catch errors around your own server calls, rethrow the ones `isNextControlFlow` recognizes: they're Next steering the render (a page turning dynamic, `redirect()`, `notFound()`), not failures.
41
+ - **Events go through `trackEvent` and `trackConversion`** from `@sonordev/site-kit/analytics` (`@sonordev/site-kit/client` exports them too). They wait for the analytics provider, which mounts after the page. Don't use the `useAnalytics` hooks in a kit: under `SiteKitLayout` your components sit outside the provider, so the hooks have nothing to return.
42
+ - **Lead capture goes through `ManagedForm`** from `@sonordev/site-kit/forms`.
43
+ - **List site-kit as a peer dependency** and mark it external in your bundler, so every kit shares the site's one copy.
44
+
45
+ ## Why it's its own entry
46
+
47
+ `@sonordev/site-kit/client` is stamped `'use client'`. A function imported from a stamped module into a server component becomes a client reference there and can't be called, and `getClientApiConfig` is built to run during server rendering. So `runtime` isn't stamped, imports neither React nor Next, and works the same from a server component, a route handler or a client component.
48
+
49
+ ## Stability
50
+
51
+ This entry is public API under semver. New exports arrive in minor releases; nothing in the list above is removed or changed in a minor or patch release.
package/src/seo/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # SEO: `@sonordev/site-kit/seo`
2
2
 
3
- 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.
3
+ 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, redirects and robots directives.
4
4
 
5
5
  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.
6
6
 
@@ -55,6 +55,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
55
55
  | `fallback` | `Metadata` | Fills any field Sonor has no managed value for. Used in full when the page isn't in Sonor at all. |
56
56
  | `overrides` | `Partial<Metadata>` | Applied last, so it wins over managed values. |
57
57
  | `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. |
58
+ | `scope` | `'page' \| 'layout'` | `'page'` (default) applies Sonor's managed canonical and hreflang for `path`. Pass `'layout'` when you call this from a root or shared layout, whose metadata every route beneath it inherits. See [Canonical URLs](#canonical-urls). |
58
59
 
59
60
  It returns a Next.js `Metadata` object with two extra flags, `_managed` and `_source`. Managed fields map like this:
60
61
 
@@ -70,6 +71,27 @@ It returns a Next.js `Metadata` object with two extra flags, `_managed` and `_so
70
71
 
71
72
  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)).
72
73
 
74
+ ### Canonical URLs
75
+
76
+ Every page gets a canonical, so you never have to set one by hand. When Sonor has no `managed_canonical` for the page, `getManagedMetadata` returns `alternates.canonical: './'`. Next resolves that against your `metadataBase` and the route being rendered, so each page names itself (`https://example.com/about` for `/about`). Without a `metadataBase`, Next emits the path alone (`/about`), which is still valid. Set `metadataBase` in your root layout for absolute URLs.
77
+
78
+ The order, lowest to highest: the self-referencing default, then Sonor's managed canonical and hreflang, then your `fallback`, then your `overrides`. `alternates` merges key by key, so passing only `languages` keeps the canonical, and `canonical: null` opts out.
79
+
80
+ **Calling it from a layout.** A layout's metadata is inherited by every page under it. If your root layout calls `getManagedMetadata({ path: '/' })`, pass `scope: 'layout'`. Otherwise a managed canonical set for the homepage in Sonor is stamped on every page without its own, and each one tells search engines it's a copy of the homepage:
81
+
82
+ ```tsx
83
+ // app/layout.tsx
84
+ export async function generateMetadata() {
85
+ return getManagedMetadata({
86
+ path: '/',
87
+ scope: 'layout',
88
+ fallback: { title: 'Example Co', description: 'Plumbing in Cincinnati.' },
89
+ })
90
+ }
91
+ ```
92
+
93
+ Don't set an absolute `alternates.canonical` in a layout for the same reason. Use `'./'`, or leave it to this helper.
94
+
73
95
  **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:
74
96
 
75
97
  ```ts
@@ -100,7 +122,7 @@ export const generateMetadata = withManagedMetadata('/about', async () => ({
100
122
  }))
101
123
  ```
102
124
 
103
- Whatever `pageMetadata` returns wins over Sonor's values. `openGraph` and `twitter` merge one level deep. It calls `getManagedMetadata` with the default `favicon: 'metadata'`.
125
+ Whatever `pageMetadata` returns wins over Sonor's values. `openGraph`, `twitter` and `alternates` merge one level deep, so a page that returns only `alternates.languages` keeps its canonical. It calls `getManagedMetadata` with the default `favicon: 'metadata'`.
104
126
 
105
127
  ### A/B-tested titles and descriptions
106
128
 
@@ -221,10 +243,6 @@ Renders the internal links Sonor has for `path` at that position, or nothing whe
221
243
 
222
244
  `limit` defaults to 5. `renderLink(link)` replaces the default `<a>`, and `className` replaces the default `sk-internal-links sk-internal-links--{position}`. `site` pins the sub-site host (see [Multi-site projects](#multi-site-projects)).
223
245
 
224
- ## Content blocks: `<ManagedContent>` (deprecated)
225
-
226
- Deprecated in 7.1 and removed in 8.0. Page copy is [managed copy](../slots/README.md) now: wrap the text in `<ManagedSlot>` or `<ManagedRichText>` and edit it in Sonor under Website → Content, with drafts, history and Edit on page. Sonor no longer creates content blocks, so `ManagedContent` renders its `fallback`.
227
-
228
246
  ## Multi-site projects
229
247
 
230
248
  One Sonor project can serve many domains (example.com plus its city microsites). Managed FAQs and internal links can be project-wide (every host) or tagged with one host (that host only). `ManagedFAQ` and `ManagedInternalLinks` send the site host with every read, so each microsite gets its own rows plus the project-wide ones, never a sibling's.
@@ -235,7 +253,6 @@ The host resolves from `NEXT_PUBLIC_SITE_URL`, which every microsite already set
235
253
  <ManagedFAQ path="/contact" site="charlotte.example.com" />
236
254
  await getFAQData('/contact', 'charlotte.example.com')
237
255
  await getInternalLinks('/contact', { position: 'bottom', site: 'charlotte.example.com' })
238
- await getContentBlock('/contact', 'hero', 'charlotte.example.com')
239
256
  ```
240
257
 
241
258
  When no host resolves, `site` is left off and the API answers for the project's primary domain. Single-site projects and older API servers ignore it.
@@ -288,7 +305,6 @@ Every component above is built on these. They're server-only, take paths rather
288
305
  | `getSchemaMarkups(path, { includeTypes?, excludeTypes? })` | Schema rows (`schema_type`, `schema_json`, ...). `excludeTypes` also prunes matching nodes inside each row. |
289
306
  | `getFAQData(path, site?)` | The FAQ (`title`, `description`, `items`, `include_schema`), or `null` |
290
307
  | `getInternalLinks(path, { position?, limit?, site? })` | Link rows |
291
- | `getContentBlock(path, section, site?)` | The content block, or `null` |
292
308
  | `getABTest(path, field)` | The running test for that field, or `null` |
293
309
  | `recordABImpression(testId, variant, sessionId?)` | `void` |
294
310
  | `getRedirectData(path)` | The raw redirect row, or `null` |
@@ -1,6 +1,6 @@
1
- # Sync (Booking) — `@sonordev/site-kit/sync`
1
+ # Sync (Booking): `@sonordev/site-kit/sync`
2
2
 
3
- Embeddable booking/scheduling widget — like Calendly, built into Sonor. Appointments, consultations, classes.
3
+ Embeddable booking/scheduling widget, like Calendly, built into Sonor. Appointments, consultations, classes.
4
4
 
5
5
  ## Usage
6
6
 
@@ -129,5 +129,6 @@ import { PopupBlocks } from '@sonordev/site-kit/website/popups'
129
129
  Engage was retired in Sonor, and its popups with it. Popups made in Engage
130
130
  Studio (a `design_json` without blocks) aren't drawn by site-kit 7.2 or later;
131
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.
132
+ was removed in site-kit 8: `SiteKitLayout` mounts `SitePopups` and the website
133
+ chat for you, and `npx sonor-setup codemod --write` moves the names that have
134
+ a home.
@@ -1,11 +0,0 @@
1
- export { AnalyticsProvider, useAnalytics, useAnalyticsOptional, useTrackEvent } from './chunk-MKMD2HJW.js';
2
- import './chunk-LCDVPAMR.js';
3
- import './chunk-24QZEO3Q.js';
4
- import './chunk-L2V5PUFN.js';
5
- import './chunk-GJWI74ZZ.js';
6
- import './chunk-43OCZ3JA.js';
7
- import './chunk-EKBEOXTH.js';
8
- import './chunk-BEBR4OJD.js';
9
- import './chunk-WGG6GTKW.js';
10
- import './chunk-56KNHB4U.js';
11
- import './chunk-PKBMQBKP.js';
@@ -1,16 +0,0 @@
1
- export { ManagedForm } from './chunk-WGIDC6QP.js';
2
- import './chunk-ZKHI4E2B.js';
3
- import './chunk-DXO6SBMD.js';
4
- import './chunk-4NTBQNHA.js';
5
- import './chunk-EPT6FJUY.js';
6
- import './chunk-Q6E4OPGG.js';
7
- import './chunk-C2FZBUSS.js';
8
- import './chunk-5SQK3D53.js';
9
- import './chunk-HW7B43E5.js';
10
- import './chunk-NJCTQH2P.js';
11
- import './chunk-L2V5PUFN.js';
12
- import './chunk-GJWI74ZZ.js';
13
- import './chunk-BEBR4OJD.js';
14
- import './chunk-WGG6GTKW.js';
15
- import './chunk-56KNHB4U.js';
16
- import './chunk-PKBMQBKP.js';
@@ -1,5 +0,0 @@
1
- export { SiteChat } from './chunk-RDPW4D33.js';
2
- import './chunk-27FISYK4.js';
3
- import './chunk-43OCZ3JA.js';
4
- import './chunk-EKBEOXTH.js';
5
- import './chunk-PKBMQBKP.js';
@@ -1,11 +0,0 @@
1
- export { SiteDesignReporter } from './chunk-MRQ2U4LZ.js';
2
- import './chunk-I2YX3HVD.js';
3
- import './chunk-IHG36STL.js';
4
- import './chunk-YC7ELZS3.js';
5
- import './chunk-S22FSH7C.js';
6
- import './chunk-43OCZ3JA.js';
7
- import './chunk-EKBEOXTH.js';
8
- import './chunk-BEBR4OJD.js';
9
- import './chunk-WGG6GTKW.js';
10
- import './chunk-56KNHB4U.js';
11
- import './chunk-PKBMQBKP.js';
@@ -1,10 +0,0 @@
1
- export { SitePopups } from './chunk-YJLS6JBI.js';
2
- import './chunk-27FISYK4.js';
3
- import './chunk-24QZEO3Q.js';
4
- import './chunk-GJWI74ZZ.js';
5
- import './chunk-43OCZ3JA.js';
6
- import './chunk-EKBEOXTH.js';
7
- import './chunk-BEBR4OJD.js';
8
- import './chunk-WGG6GTKW.js';
9
- import './chunk-56KNHB4U.js';
10
- import './chunk-PKBMQBKP.js';
@@ -1,8 +0,0 @@
1
- export { SitemapSync } from './chunk-T4DLETKR.js';
2
- import './chunk-YVKRYQRG.js';
3
- import './chunk-43OCZ3JA.js';
4
- import './chunk-EKBEOXTH.js';
5
- import './chunk-BEBR4OJD.js';
6
- import './chunk-WGG6GTKW.js';
7
- import './chunk-56KNHB4U.js';
8
- import './chunk-PKBMQBKP.js';
@@ -1,41 +0,0 @@
1
- 'use client';
2
- import { uuid } from './chunk-GJWI74ZZ.js';
3
- import { useState } from 'react';
4
-
5
- function getOrCreateVisitorId() {
6
- if (typeof window === "undefined") return "";
7
- let visitorId = localStorage.getItem("_sk_vid");
8
- if (!visitorId) {
9
- visitorId = uuid();
10
- }
11
- localStorage.setItem("_sk_vid", visitorId);
12
- return visitorId;
13
- }
14
- function getOrCreateSessionId(timeoutMinutes = 30) {
15
- if (typeof window === "undefined") return "";
16
- const now = Date.now();
17
- const timeoutMs = timeoutMinutes * 60 * 1e3;
18
- const existingSession = sessionStorage.getItem("_sk_sid");
19
- const lastActivity = sessionStorage.getItem("_sk_stime");
20
- if (existingSession && lastActivity) {
21
- const elapsed = now - parseInt(lastActivity, 10);
22
- if (elapsed < timeoutMs) {
23
- sessionStorage.setItem("_sk_stime", now.toString());
24
- sessionStorage.setItem("_sk_sid", existingSession);
25
- return existingSession;
26
- }
27
- }
28
- const newSession = uuid();
29
- sessionStorage.setItem("_sk_sid", newSession);
30
- sessionStorage.setItem("_sk_stime", now.toString());
31
- return newSession;
32
- }
33
- function useSiteKitIdentity() {
34
- const [identity] = useState(() => ({
35
- visitorId: getOrCreateVisitorId(),
36
- sessionId: getOrCreateSessionId()
37
- }));
38
- return identity;
39
- }
40
-
41
- export { getOrCreateSessionId, getOrCreateVisitorId, useSiteKitIdentity };
@@ -1 +0,0 @@
1
-