@sonordev/site-kit 7.1.1 → 7.2.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 (282) hide show
  1. package/AGENTS.md +3 -3
  2. package/CHANGELOG.md +68 -3426
  3. package/README.md +9 -8
  4. package/agent-manifest.json +12 -4
  5. package/dist/{AnalyticsProvider-3MI5RCTL.js → AnalyticsProvider-SLFFBRPE.js} +4 -4
  6. package/dist/{ArticleViewTracker-CMBSJR3N.js → ArticleViewTracker-TM5AMGN2.js} +3 -3
  7. package/dist/{BlocksPopup-X3OAAK2L.js → BlocksPopup-YJNJIVPO.js} +5 -5
  8. package/dist/ChatWidget-XKOSH66S.js +17 -0
  9. package/dist/{FileField-QRSEGO6D.js → FileField-CI4UL5SH.js} +3 -3
  10. package/dist/{FormSpotlight-XKV7VTN4.js → FormSpotlight-F2AAEGBN.js} +20 -6
  11. package/dist/{FormStage-C3YGYAUZ.js → FormStage-BI5R5CNC.js} +27 -7
  12. package/dist/ManagedForm-G6COGHXL.js +16 -0
  13. package/dist/{ManagedNewsletterForm-LPIKGJF4.js → ManagedNewsletterForm-BO3WCGXX.js} +7 -5
  14. package/dist/{SignalCore-ICXXZBYI.js → SignalCore-ZA6WHKBR.js} +3 -3
  15. package/dist/SiteChat-YJADDTKG.js +5 -0
  16. package/dist/{SiteDesignReporter-A7MS2CIP.js → SiteDesignReporter-5YAW36AQ.js} +5 -5
  17. package/dist/SitePopups-ST5PMKZE.js +10 -0
  18. package/dist/SitemapSync-RJTEBINX.js +8 -0
  19. package/dist/SitemapSync.d.ts +1 -1
  20. package/dist/_client/booking-widget.js +5 -5
  21. package/dist/_client/testimonial-section.d.ts +1 -1
  22. package/dist/affiliates/api.d.ts +1 -1
  23. package/dist/affiliates/index.js +3 -3
  24. package/dist/analytics/AnalyticsProvider.d.ts +1 -1
  25. package/dist/analytics/index.js +4 -4
  26. package/dist/analytics/send-gate.d.ts +13 -26
  27. package/dist/articles/Article.d.ts +1 -1
  28. package/dist/articles/ArticleList.d.ts +1 -1
  29. package/dist/articles/ClusterLandingPage.d.ts +2 -2
  30. package/dist/articles/PublicationLayout.d.ts +1 -1
  31. package/dist/articles/PublicationSidebar.d.ts +1 -1
  32. package/dist/articles/RelatedPosts.d.ts +1 -1
  33. package/dist/articles/author-schema.d.ts +1 -1
  34. package/dist/articles/excerpt.d.ts +3 -4
  35. package/dist/articles/index.d.ts +2 -2
  36. package/dist/articles/index.js +2 -2
  37. package/dist/articles/news-sitemap.d.ts +1 -1
  38. package/dist/articles/processArticleHtml.d.ts +1 -1
  39. package/dist/articles/server-core.d.ts +4 -4
  40. package/dist/articles/server-ui.js +3 -2
  41. package/dist/articles/server.d.ts +4 -4
  42. package/dist/articles/server.js +2 -1
  43. package/dist/articles/types.d.ts +1 -1
  44. package/dist/{engage → chat}/ChatWidget.d.ts +8 -7
  45. package/dist/{engage → chat}/EchoUiActions.d.ts +4 -4
  46. package/dist/chat/SiteChat.d.ts +25 -0
  47. package/dist/{engage → chat}/brand-color.d.ts +2 -2
  48. package/dist/{engage → chat}/chat-messages.d.ts +1 -1
  49. package/dist/{engage → chat}/echo-config.d.ts +1 -1
  50. package/dist/chat/index.d.ts +10 -6
  51. package/dist/chat/index.js +14 -8
  52. package/dist/{engage → chat}/launcher-placement.d.ts +1 -7
  53. package/dist/{engage → chat}/socket-loader.d.ts +2 -2
  54. package/dist/chat/types.d.ts +136 -0
  55. package/dist/chunk-27FISYK4.js +4 -0
  56. package/dist/{chunk-QPEEKFCA.js → chunk-4NTBQNHA.js} +249 -80
  57. package/dist/{chunk-RK7GF7PQ.js → chunk-5V2V5C3Y.js} +2 -4
  58. package/dist/{chunk-ICFRH2ED.js → chunk-5YCEMV2L.js} +1 -1
  59. package/dist/{chunk-V4HUSHZO.js → chunk-64DKGVEI.js} +41 -7
  60. package/dist/{chunk-GLHQ3LRO.js → chunk-6U3VLV2C.js} +1 -1
  61. package/dist/{chunk-ZETJTCMV.js → chunk-BC4Y3S3D.js} +1 -1
  62. package/dist/{chunk-JSZN6LDZ.js → chunk-BF7TZYC3.js} +38 -31
  63. package/dist/{chunk-EFPS56JA.js → chunk-BKY2ZNM7.js} +1 -1
  64. package/dist/{chunk-AQSNPZY4.js → chunk-C2FZBUSS.js} +80 -1
  65. package/dist/{chunk-H42PZKVC.js → chunk-CF5QNFYU.js} +2 -2
  66. package/dist/{chunk-JC7KXNNM.js → chunk-D5DCBD4Y.js} +2 -2
  67. package/dist/{chunk-PXZ7LAOY.js → chunk-D6ZC7XKZ.js} +2 -2
  68. package/dist/chunk-EJVQJCG5.js +370 -0
  69. package/dist/{chunk-6OL23QYD.js → chunk-F24FNCTK.js} +1 -1
  70. package/dist/{chunk-NEKEH4CK.js → chunk-FR6BYFPC.js} +53 -21
  71. package/dist/{chunk-KOPVJQ2P.js → chunk-GEXT7BTE.js} +1 -1
  72. package/dist/{chunk-ZAWANWUS.js → chunk-GGORNBSC.js} +18 -6
  73. package/dist/{chunk-DUQMSNF2.js → chunk-HGFAEDG3.js} +1 -1
  74. package/dist/chunk-HODO5BX5.js +28 -0
  75. package/dist/chunk-HW7B43E5.js +14 -0
  76. package/dist/{chunk-EQBZ24VH.js → chunk-IAM3D3RL.js} +43 -24
  77. package/dist/{chunk-ODADQM6Z.js → chunk-JJ3DELUL.js} +2 -2
  78. package/dist/{chunk-542MIHNP.js → chunk-JMLK6MH7.js} +1 -1
  79. package/dist/{chunk-7FWA3I6A.js → chunk-JWTQS5F5.js} +1 -1
  80. package/dist/{chunk-OUNRFXDC.js → chunk-K4ZHAURD.js} +1 -1
  81. package/dist/chunk-NV72HZ4W.js +45 -0
  82. package/dist/{chunk-4Y5FWDPM.js → chunk-NZ4RXN3G.js} +55 -7
  83. package/dist/chunk-OWZ7ATLZ.js +187 -0
  84. package/dist/{chunk-TXBAOEKG.js → chunk-PKGN32AU.js} +5 -4
  85. package/dist/chunk-Q6E4OPGG.js +40 -0
  86. package/dist/{chunk-J4D6ZXRW.js → chunk-QL6XNPSD.js} +3 -3
  87. package/dist/{chunk-HAG4YIZY.js → chunk-R46IQIGK.js} +6 -6
  88. package/dist/{chunk-R3VKF43H.js → chunk-RPZ7OQGD.js} +1 -1
  89. package/dist/{chunk-Q3S26EQA.js → chunk-S54ECQGY.js} +1 -1
  90. package/dist/{chunk-JIL5VY2R.js → chunk-VHKEF5ZB.js} +1 -1
  91. package/dist/{chunk-ILAW3XKD.js → chunk-VO3JYRT5.js} +43 -29
  92. package/dist/chunk-VWWITFWM.js +300 -0
  93. package/dist/{chunk-OPRBF7VE.js → chunk-ZTLMPUO6.js} +3 -2
  94. package/dist/client/index.js +3 -3
  95. package/dist/cms/CmsPreview.d.ts +2 -2
  96. package/dist/cms/index.d.ts +3 -3
  97. package/dist/cms/server-api.d.ts +1 -1
  98. package/dist/cms/types.d.ts +1 -1
  99. package/dist/commerce/EventCheckout.d.ts +1 -1
  100. package/dist/commerce/EventsAgenda.d.ts +3 -3
  101. package/dist/commerce/api.d.ts +1 -1
  102. package/dist/commerce/index.js +4 -4
  103. package/dist/commerce/server.d.ts +3 -4
  104. package/dist/contracts/color.d.ts +1 -1
  105. package/dist/contracts/entries.d.ts +1 -1
  106. package/dist/contracts/error-page.d.ts +183 -0
  107. package/dist/contracts/fleet.d.ts +9 -9
  108. package/dist/contracts/forms.d.ts +4 -10
  109. package/dist/contracts/llms.d.ts +2 -1
  110. package/dist/contracts/portfolio.d.ts +11 -15
  111. package/dist/contracts/proposal-sitemap.d.ts +130 -0
  112. package/dist/contracts/schema-placeholders.d.ts +87 -0
  113. package/dist/contracts/seo-meta.d.ts +11 -12
  114. package/dist/contracts/seo-pages.d.ts +12 -18
  115. package/dist/contracts/site-cache.d.ts +4 -4
  116. package/dist/contracts/sites-normalize.d.ts +2 -4
  117. package/dist/contracts/sites.d.ts +3 -5
  118. package/dist/contracts/slot-content.d.ts +2 -2
  119. package/dist/contracts/slots.d.ts +6 -6
  120. package/dist/contracts/voice.d.ts +56 -0
  121. package/dist/contracts/website.d.ts +2 -2
  122. package/dist/engage/EngageWidget.d.ts +13 -12
  123. package/dist/engage/index.d.ts +10 -9
  124. package/dist/engage/index.js +31 -35
  125. package/dist/engage/types.d.ts +19 -244
  126. package/dist/fleet/FleetHeartbeat.d.ts +2 -2
  127. package/dist/fleet/index.js +4 -4
  128. package/dist/forms/FormEnhancer.d.ts +25 -12
  129. package/dist/forms/FormSpotlight.d.ts +2 -2
  130. package/dist/forms/ManagedForm.d.ts +1 -1
  131. package/dist/forms/ServerForm.d.ts +11 -10
  132. package/dist/forms/StaticForm.d.ts +4 -2
  133. package/dist/forms/field-autocomplete.d.ts +28 -0
  134. package/dist/forms/field-interactions.d.ts +2 -2
  135. package/dist/forms/form-dom-values.d.ts +42 -0
  136. package/dist/forms/form-loaded-at.d.ts +2 -2
  137. package/dist/forms/formsApi.d.ts +2 -2
  138. package/dist/forms/index.d.ts +1 -1
  139. package/dist/forms/index.js +11 -9
  140. package/dist/forms/server.d.ts +1 -1
  141. package/dist/forms/server.js +6 -4
  142. package/dist/forms/static.js +3 -2
  143. package/dist/forms/submitForm.d.ts +9 -2
  144. package/dist/forms/types.d.ts +44 -5
  145. package/dist/forms/useForm.d.ts +21 -4
  146. package/dist/forms/webmcp.d.ts +38 -0
  147. package/dist/images/ManagedFavicon.d.ts +1 -1
  148. package/dist/images/ManagedImage.d.ts +4 -4
  149. package/dist/images/api.d.ts +1 -1
  150. package/dist/images/index.d.ts +2 -2
  151. package/dist/images/index.js +4 -4
  152. package/dist/index.d.ts +4 -1
  153. package/dist/index.js +1 -1
  154. package/dist/landing/contract.d.ts +3 -4
  155. package/dist/layout/SiteKitClientProviders.d.ts +4 -4
  156. package/dist/layout/SiteKitLayout.d.ts +11 -11
  157. package/dist/layout/client.d.ts +4 -4
  158. package/dist/layout/client.js +7 -7
  159. package/dist/layout/index.js +8 -8
  160. package/dist/layout/types.d.ts +7 -6
  161. package/dist/llms/SpeakableSchema.d.ts +2 -2
  162. package/dist/llms/agent-access.d.ts +2 -2
  163. package/dist/llms/aiRobots.d.ts +4 -4
  164. package/dist/llms/api.d.ts +2 -2
  165. package/dist/llms/contract.js +1 -1
  166. package/dist/llms/generateLLMsTxt.d.ts +1 -1
  167. package/dist/llms/index.js +6 -6
  168. package/dist/llms/links.d.ts +1 -1
  169. package/dist/llms/revalidate.d.ts +1 -1
  170. package/dist/llms/seo-revalidate.d.ts +2 -3
  171. package/dist/llms/types.d.ts +6 -6
  172. package/dist/llms/writeLLMsTxt.d.ts +10 -10
  173. package/dist/manifest/index.d.ts +8 -8
  174. package/dist/maps/index.js +3 -3
  175. package/dist/mcp/WebMcpTools.d.ts +1 -1
  176. package/dist/mcp/index.js +2 -14
  177. package/dist/mcp/report.d.ts +2 -2
  178. package/dist/mcp/serverCard.d.ts +1 -1
  179. package/dist/mcp/sonor.d.ts +5 -6
  180. package/dist/mcp/sonor.js +14 -11
  181. package/dist/mcp/transport.d.ts +1 -2
  182. package/dist/mcp/types.d.ts +4 -5
  183. package/dist/motion/engine.d.ts +2 -3
  184. package/dist/motion/failsafe.d.ts +1 -2
  185. package/dist/motion/gsap.d.ts +2 -2
  186. package/dist/motion/three.d.ts +1 -1
  187. package/dist/og/config.d.ts +4 -4
  188. package/dist/og/contrast.d.ts +2 -2
  189. package/dist/og/pages.d.ts +5 -5
  190. package/dist/og/route-fit.d.ts +2 -2
  191. package/dist/og/route.d.ts +1 -2
  192. package/dist/og/template.d.ts +1 -1
  193. package/dist/proxy/index.d.ts +2 -2
  194. package/dist/proxy/securityHeaders.d.ts +10 -12
  195. package/dist/redirects/index.d.ts +7 -7
  196. package/dist/reputation/TestimonialSection.d.ts +1 -1
  197. package/dist/reputation/api.d.ts +1 -1
  198. package/dist/revalidate/index.js +2 -2
  199. package/dist/robots/index.d.ts +5 -5
  200. package/dist/robots/indexnow.d.ts +2 -3
  201. package/dist/seo/ManagedScripts.d.ts +2 -2
  202. package/dist/seo/client.js +4 -4
  203. package/dist/seo/getManagedMetadata.d.ts +1 -1
  204. package/dist/seo/groundingSchema.d.ts +2 -2
  205. package/dist/seo/index.js +15 -8
  206. package/dist/seo/llms/contract.js +1 -1
  207. package/dist/seo/llms.js +6 -6
  208. package/dist/seo/register-sitemap-cli-impl.d.ts +1 -1
  209. package/dist/seo/register-sitemap-cli.d.ts +1 -1
  210. package/dist/seo/register-sitemap-cli.js +2 -2
  211. package/dist/seo/schema-filter.d.ts +5 -6
  212. package/dist/seo/server-api.d.ts +1 -1
  213. package/dist/seo/sitemap.js +4 -4
  214. package/dist/seo/types.d.ts +4 -4
  215. package/dist/seo/withManagedMetadata.d.ts +4 -4
  216. package/dist/server/index.js +2 -2
  217. package/dist/server/mint-site-token.d.ts +1 -2
  218. package/dist/server/server-fetch.d.ts +5 -5
  219. package/dist/shared/build-entries.d.ts +4 -4
  220. package/dist/shared/dialog.d.ts +27 -0
  221. package/dist/shared/frame.d.ts +8 -9
  222. package/dist/shared/fresh-fetch.d.ts +1 -2
  223. package/dist/shared/identity.d.ts +1 -1
  224. package/dist/shared/layers.d.ts +7 -0
  225. package/dist/shared/mid-form.d.ts +53 -0
  226. package/dist/shared/next-files.d.ts +1 -1
  227. package/dist/shared/publishCredential.d.ts +1 -1
  228. package/dist/shared/reporting-gate.d.ts +1 -1
  229. package/dist/shared/uuid.d.ts +1 -2
  230. package/dist/shared/version.d.ts +1 -1
  231. package/dist/shared/visual-viewport-gap.d.ts +3 -3
  232. package/dist/signal/index.js +2 -2
  233. package/dist/signal/types.d.ts +1 -1
  234. package/dist/site-config/index.d.ts +2 -2
  235. package/dist/sitemap/index.d.ts +13 -13
  236. package/dist/sitemap/index.js +4 -4
  237. package/dist/sitemap/shared.d.ts +3 -3
  238. package/dist/sites/site-param.d.ts +2 -2
  239. package/dist/slots/ManagedRichText.d.ts +1 -1
  240. package/dist/slots/ManagedSlot.d.ts +1 -1
  241. package/dist/slots/index.d.ts +1 -3
  242. package/dist/{socket-loader-R24ZSRSQ.js → socket-loader-CGIPEG74.js} +1 -1
  243. package/dist/sync/index.js +5 -5
  244. package/dist/types.d.ts +4 -2
  245. package/dist/website/BlocksPopup.d.ts +1 -1
  246. package/dist/website/PopupBlocks.d.ts +8 -6
  247. package/dist/website/SitePopups.d.ts +25 -0
  248. package/dist/website/images.js +4 -4
  249. package/dist/website/index.js +6 -6
  250. package/dist/website/popup-rules.d.ts +46 -0
  251. package/dist/website/popup-types.d.ts +113 -0
  252. package/dist/website/popups.d.ts +2 -6
  253. package/dist/website/popups.js +6 -14
  254. package/dist/{writeLLMsTxt-QR23OQUE.js → writeLLMsTxt-OV24LQVL.js} +3 -3
  255. package/docs/MIGRATING-TO-7.md +50 -22
  256. package/docs.json +2 -1
  257. package/package.json +2 -2
  258. package/src/admin-auth/README.md +3 -3
  259. package/src/analytics/README.md +9 -11
  260. package/src/articles/README.md +10 -7
  261. package/src/{engage → chat}/README.md +63 -62
  262. package/src/cta-bar/README.md +11 -11
  263. package/src/forms/README.md +60 -12
  264. package/src/layout/README.md +8 -5
  265. package/src/llms/README.md +3 -3
  266. package/src/mcp/README.md +7 -7
  267. package/src/motion/README.md +1 -1
  268. package/src/og/README.md +26 -27
  269. package/src/proxy/README.md +14 -14
  270. package/src/seo/README.md +4 -2
  271. package/src/slots/README.md +1 -1
  272. package/src/sync/README.md +10 -0
  273. package/src/website/README.md +133 -0
  274. package/dist/ChatWidget-JMFBXJ75.js +0 -15
  275. package/dist/EngageWidget-A3QKZ3SU.js +0 -11
  276. package/dist/ManagedForm-2GSSDJPP.js +0 -14
  277. package/dist/SitemapSync-4U654SEA.js +0 -8
  278. package/dist/chunk-B73QPZPH.js +0 -837
  279. package/dist/chunk-NDF4A5JM.js +0 -37
  280. package/dist/chunk-RTMMHTEQ.js +0 -100
  281. package/dist/engage/DesignRenderer.d.ts +0 -57
  282. package/dist/engage/element-rules.d.ts +0 -38
@@ -1,58 +1,69 @@
1
- # Engage — `@sonordev/site-kit/engage`
1
+ # Website chat — `@sonordev/site-kit/chat`
2
2
 
3
- Popups, nudges, bars, slide-ins, and chat widgets — all configured from the Sonor dashboard.
3
+ Echo's chat launcher and conversation on your site, configured in Sonor
4
+ (Messages → Chat settings). AI answers, live handoff to your team, and an
5
+ offline form when nobody's around.
4
6
 
5
7
  ## Usage
6
8
 
7
- Auto-included by `SiteKitLayout`. For standalone use:
9
+ `SiteKitLayout` mounts it for you, after the page goes idle:
10
+
11
+ ```tsx
12
+ <SiteKitLayout>{children}</SiteKitLayout> // chat on (default)
13
+ <SiteKitLayout chat={{ offsetBottom: '88px' }}>{children}</SiteKitLayout>
14
+ <SiteKitLayout chat={false}>{children}</SiteKitLayout> // no chat
15
+ ```
16
+
17
+ On a site without `SiteKitLayout`, mount `SiteChat` the same way:
8
18
 
9
19
  ```tsx
10
20
  'use client'
11
- import { EngageWidget } from '@sonordev/site-kit/engage'
12
-
13
- export default function Layout({ children }) {
14
- return (
15
- <>
16
- {children}
17
- <EngageWidget />
18
- </>
19
- )
21
+ import { SiteChat } from '@sonordev/site-kit/chat'
22
+
23
+ export function Chat() {
24
+ return <SiteChat position="bottom-right" />
20
25
  }
21
26
  ```
22
27
 
28
+ `SiteChat` waits for the browser to go idle, skips cross-origin frames and
29
+ localhost (like analytics), and downloads the chat widget only when it
30
+ renders. `ChatWidget` is the widget itself, for a page that resolves those
31
+ choices on its own.
32
+
23
33
  ## Props
24
34
 
25
35
  ```ts
26
- interface EngageWidgetProps {
27
- apiUrl?: string // Default: https://api.sonor.io
28
- apiKey?: string // From SiteKitLayout or env
29
- projectId?: string // For chat routing
36
+ interface SiteChatProps {
37
+ apiUrl?: string // Default: from SiteKitLayout, else https://api.sonor.io
38
+ apiKey?: string // Default: from SiteKitLayout
39
+ projectId?: string // For chat routing. Resolved from the key when absent
30
40
  position?: 'bottom-right' | 'bottom-left' // Default: 'bottom-right'
31
41
  offsetBottom?: string | number // Default: '20px'. See "Launcher placement"
32
- zIndex?: number // Default: 9999. Popups, nudges, bars and the chat. See "Stacking"
33
- chatEnabled?: boolean // Default: true. See "The chat switch"
34
- debug?: boolean
42
+ zIndex?: number // Default: 9999. See "Stacking"
43
+ allowInFrame?: boolean // Show inside a cross-origin frame. Default: false
44
+ allowLocalhost?: boolean // Show on localhost. Default: false
35
45
  }
36
46
  ```
37
47
 
38
- Through `SiteKitLayout`, the same options go in `engage={{ ... }}`.
48
+ Through `SiteKitLayout`, the placement options go in `chat={{ ... }}`. The
49
+ deprecated `engage={{ ... }}` still works through 7.x.
39
50
 
40
51
  ## The chat switch
41
52
 
42
- Echo follows the project's **Enable Chat Widget** switch in Sonor (Engage,
43
- Chat settings). A project that has never saved chat settings counts as on, so
44
- Echo is on by default and only an owner who switches it off hides it. It also
45
- needs the project's **Engage** module on (Project Settings); without it Sonor
46
- answers off, since the chat has nowhere to take a visitor's message.
53
+ Echo follows the project's **Show the chat widget** switch in Sonor
54
+ (Messages → Chat settings). A project that has never saved chat settings
55
+ counts as on, so Echo is on by default and only an owner who switches it off
56
+ hides it. It also needs the project's **Website chat** module on (Project
57
+ Settings); without it Sonor answers off, since the chat has nowhere to take a
58
+ visitor's message.
47
59
 
48
60
  - The launcher appears once `GET /engage/widget/config` answers. Nothing
49
61
  renders before that, so a switched-off site never flashes a launcher, and
50
62
  it never starts Echo's availability polling.
51
63
  - If the config can't be fetched, the launcher stays hidden. The chat can't
52
64
  run without the API anyway.
53
- - `chatEnabled: false` (or `engage={false}`) in code still turns chat off
54
- whatever the switch says. Code can turn Echo off; it can't force it on over
55
- the owner's switch.
65
+ - `chat={false}` in code still turns chat off whatever the switch says. Code
66
+ can turn Echo off; it can't force it on over the owner's switch.
56
67
 
57
68
  ## Liquid Glass (6.1.0)
58
69
 
@@ -75,7 +86,7 @@ recipe (`src/shared/glass.tsx`), the same material as the mobile CTA bar
75
86
  panel, and otherwise pulls it toward black (light panel) or white (dark
76
87
  panel) just far enough to clear it: `#d4af37` gold becomes `#887023`. Fills
77
88
  (launcher, avatar, the visitor's bubbles, buttons) keep the raw brand. One
78
- helper decides this, `src/engage/brand-color.ts`; route any new brand
89
+ helper decides this, `src/chat/brand-color.ts`; route any new brand
79
90
  foreground through it. `--sk-primary` can be hex, `rgb()` or `hsl()`.
80
91
  - **Fields are 16px.** iOS Safari zooms the whole page into any focused field
81
92
  under 16px; the chat input and the inline Echo forms were 13.5-14px.
@@ -104,7 +115,7 @@ it without `!important`. Don't write that override; declare the offset instead.
104
115
  **A fixed offset, every page:** pass `offsetBottom`.
105
116
 
106
117
  ```tsx
107
- <SiteKitLayout engage={{ offsetBottom: '88px' }}>{children}</SiteKitLayout>
118
+ <SiteKitLayout chat={{ offsetBottom: '88px' }}>{children}</SiteKitLayout>
108
119
  ```
109
120
 
110
121
  Any CSS length works (`'5.5rem'`, `'calc(4rem + 8px)'`); a number is pixels.
@@ -148,13 +159,14 @@ popup maxHeight = 100dvh - launcher bottom - 100px
148
159
 
149
160
  ### Stacking
150
161
 
151
- `zIndex` is the one layer everything Engage renders sits on: popups, nudges,
152
- bars and the chat launcher. The chat popup sits one layer beneath the
153
- launcher. The default is 9999, above almost anything a site draws, so lower it
154
- when your own fixed UI (a mobile menu, a cookie banner) has to cover the chat:
162
+ `zIndex` is the layer the chat launcher sits on, with the chat window one
163
+ layer beneath it. Through `SiteKitLayout` the same number also stacks the
164
+ site's popups, banners and toasts. The default is 9999, above almost anything
165
+ a site draws, so lower it when your own fixed UI (a mobile menu, a cookie
166
+ banner) has to cover the chat:
155
167
 
156
168
  ```tsx
157
- <SiteKitLayout engage={{ zIndex: 40 }}>{children}</SiteKitLayout>
169
+ <SiteKitLayout chat={{ zIndex: 40 }}>{children}</SiteKitLayout>
158
170
  ```
159
171
 
160
172
  ```
@@ -162,9 +174,9 @@ launcher z-index = zIndex (default 9999)
162
174
  popup z-index = zIndex - 1
163
175
  ```
164
176
 
165
- At `zIndex` 0 or below the popup shares the launcher's layer instead, because
166
- -1 would put it behind the page's own content. The launcher still paints on
167
- top.
177
+ At `zIndex` 0 or below the chat window shares the launcher's layer instead,
178
+ because -1 would put it behind the page's own content. The launcher still
179
+ paints on top.
168
180
 
169
181
  ### Layout vs visual viewport on phones
170
182
 
@@ -194,29 +206,9 @@ It's opt-in: while a field has focus the gap grows to the keyboard's height,
194
206
  so everything that reads it rides above the keyboard. `useVisualViewportGap()`
195
207
  is the hook form for an existing client component.
196
208
 
197
- ## Element Types
198
-
199
- | Type | Description |
200
- |------|-------------|
201
- | `popup` | Modal overlay with CTA |
202
- | `nudge` | Small corner notification |
203
- | `bar` | Top/bottom sticky bar |
204
- | `slide-in` | Side panel |
205
- | `chat` | AI/live chat widget |
209
+ ## Chat config
206
210
 
207
- ## Targeting & Triggers
208
-
209
- All configured in the Sonor dashboard — no code changes needed:
210
-
211
- - **Page targeting** — include/exclude paths with wildcard support
212
- - **Device targeting** — desktop, mobile, tablet
213
- - **Visitor targeting** — new vs returning visitors
214
- - **Triggers** — immediate, delay (seconds), scroll (%), exit-intent, click, custom
215
- - **Frequency capping** — once, once-per-session, every N days
216
-
217
- ## Chat Widget
218
-
219
- Supports AI mode (Echo), live mode, and hybrid (AI + human handoff):
211
+ AI mode (Echo), live mode, and hybrid (AI with a human handoff). `ChatWidget` takes these as `config`; Sonor sends the rest from Chat settings:
220
212
 
221
213
  ```ts
222
214
  interface ChatConfig {
@@ -236,6 +228,15 @@ interface ChatConfig {
236
228
  }
237
229
  ```
238
230
 
239
- ## Tracking
231
+ ## Popups
232
+
233
+ Popups, banners and toasts are their own module now:
234
+ [Popups and banners](../website/README.md) (`@sonordev/site-kit/website/popups`).
235
+
236
+ ## `@sonordev/site-kit/engage` (deprecated)
240
237
 
241
- Impressions and clicks are automatically tracked via the Sonor API. Shares visitor ID (`_sk_vid`) with Analytics for cross-module attribution.
238
+ Engage was retired in Sonor. Its entry stays through 7.x so old imports keep
239
+ building: `ChatWidget` and the chat types re-export from here, and
240
+ `EngageWidget` draws `SiteChat` and `SitePopups`. Engage Studio's renderer
241
+ (`DesignRenderer`) is gone; popups render from blocks. Import
242
+ `@sonordev/site-kit/chat` and `@sonordev/site-kit/website/popups` instead.
@@ -3,17 +3,17 @@
3
3
  The Liquid Glass mobile CTA bar (6.1.0). A floating frosted capsule that
4
4
  keeps a site's one or two highest-intent actions a thumb away on phones.
5
5
 
6
- It replaces every hand-rolled sticky mobile bar in the fleet. Those eleven
7
- copies had each solved part of the same problem set; this one does all of it:
6
+ It replaces a hand-rolled sticky mobile bar, and handles what each of those
7
+ ends up solving on its own:
8
8
 
9
- | Behaviour | Before | Here |
10
- |---|---|---|
11
- | Hide while the form it points at is on screen | two sites, hand-built | `hideOver` |
12
- | Stay off the hero until the hero CTA scrolls away | one site | `showAfter` |
13
- | Get out from under the Echo launcher | two sites (`body:has`, a 72px dead corner) | automatic |
14
- | Ride out the iOS toolbar collapsing | one site (a GSAP tween) | `--sk-vv-layout-gap` |
15
- | Get out of the way of the keyboard | nobody | `hideWhileTyping` |
16
- | Tell Sonor which action converts | one site (hand-wired) | `cta_click` event |
9
+ | Behaviour | How |
10
+ |---|---|
11
+ | Hide while the form it points at is on screen | `hideOver` |
12
+ | Stay off the hero until the hero CTA scrolls away | `showAfter` |
13
+ | Get out from under the Echo launcher | automatic |
14
+ | Ride out the iOS toolbar collapsing | `--sk-vv-layout-gap` |
15
+ | Get out of the way of the keyboard | `hideWhileTyping` |
16
+ | Tell Sonor which action converts | `cta_click` event |
17
17
 
18
18
  ## Use it
19
19
 
@@ -28,7 +28,7 @@ import { CtaBar, CtaBarAction } from '@sonordev/site-kit/cta-bar'
28
28
  <main>{children}</main>
29
29
  <Footer />
30
30
  <CtaBar label="Call or request a quote" hideOver="#quote">
31
- <CtaBarAction href="tel:+15135550100" variant="secondary" icon={<Phone />}>
31
+ <CtaBarAction href="tel:+15555550100" variant="secondary" icon={<Phone />}>
32
32
  Call now
33
33
  </CtaBarAction>
34
34
  <CtaBarAction as={Link} href="/quote" icon={<ClipboardCheck />}>
@@ -47,8 +47,8 @@ Three things hold for every experience:
47
47
  the form still works and still submits.
48
48
  - **Nobody pays for what they don't use.** Experience code is loaded on
49
49
  demand, so a `classic` form downloads none of it.
50
- - **Same engine.** Identical validation, honeypot, reCAPTCHA, and CRM routing
51
- — an experience is a rendering, not a fork.
50
+ - **Same engine.** Identical validation, spam protection and CRM routing.
51
+ An experience is a rendering, not a fork.
52
52
 
53
53
  ### Editing what the form says back
54
54
 
@@ -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
@@ -202,13 +207,56 @@ Submissions auto-route based on `form_type`:
202
207
  | `newsletter` | Email Subscribers | Newsletter signups |
203
208
  | `custom` | Form Submissions only | Custom handling |
204
209
 
205
- ## Anti-Bot Protection
206
-
207
- - **Honeypot fields** — hidden fields that bots fill (server-side rejection)
208
- - **reCAPTCHA Enterprise** — optional Google reCAPTCHA v3 scoring
209
- - **Submission timing** — forms that submit in < 3 seconds are flagged
210
- - **Composite spam scoring** — name patterns, email domain, user-agent, IP rate, message content
211
- - **NestJS ThrottlerGuard** — 10 requests/minute per IP at HTTP level
210
+ ## Spam protection
211
+
212
+ Sonor screens every submission on its side, and there's nothing to configure.
213
+ It only accepts a submission with evidence that a real browser rendered the
214
+ page, which `<ManagedForm>` and `useForm` send automatically. A hand-rolled
215
+ `fetch` to the forms API, or a proxy through your own API route, can't send
216
+ it, so Sonor refuses those before anything is written: the visitor sees an
217
+ error and you get no lead. Submit through site-kit, and define the form's
218
+ fields in Sonor so both can render and validate them.
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. It's the browser's word, so it never changes how a submission is
246
+ checked. The agent is 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.
212
260
 
213
261
  ## Styles
214
262
 
@@ -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
 
@@ -315,7 +315,7 @@ Business description...
315
315
 
316
316
  ## Portfolio & Case Studies
317
317
 
318
- ### [Project Title](https://live-url.com)
318
+ ### [Project Title](https://example.com/work/project)
319
319
 
320
320
  Description of the project.
321
321
 
@@ -355,7 +355,7 @@ We follow a proven process...
355
355
 
356
356
  ### Family Law Basics
357
357
  Topic: Family Law
358
- Area: Cincinnati, OH
358
+ Area: Springfield
359
359
  Articles: 8
360
360
  Service page: https://example.com/services/family-law
361
361
  Pillar: [Complete Guide to Family Law](/article/family-law-guide)
@@ -515,7 +515,7 @@ export default function DivorcePage() {
515
515
  title="Key Facts"
516
516
  points={[
517
517
  '25+ years of family law experience',
518
- 'Serving Northern Kentucky and Cincinnati',
518
+ 'Serving Springfield and the surrounding counties',
519
519
  'Free initial consultation available',
520
520
  ]}
521
521
  speakable
package/src/mcp/README.md CHANGED
@@ -82,11 +82,11 @@ endpoint and card (automatic in the build-time file once `/api/mcp` exists),
82
82
  and `createProxy({ llmsDiscovery: { siteUrl, mcpServerCard: true } })` adds
83
83
  `Link: <.../.well-known/mcp-server-card>; rel="service-desc"`.
84
84
 
85
- ### A custom MCP server (upforge.io) is left alone
85
+ ### A custom MCP server is left alone
86
86
 
87
87
  The built-in tools are opt-in, never automatic. A site that runs its own MCP
88
- server (its own tools, transport names or llms.txt section, like upforge.io
89
- or a re-site-kit site) is a **custom implementation**, and site-kit keeps its
88
+ server (its own tools, transport names or llms.txt section, like a
89
+ re-site-kit site) is a **custom implementation**, and site-kit keeps its
90
90
  hands off:
91
91
 
92
92
  | What | Built-in server | Custom server |
@@ -257,8 +257,8 @@ What the relay does, so you don't have to re-derive it:
257
257
 
258
258
  The header and label default to `x-site-mcp-transport` and
259
259
  `site-mcp-transport-v1`. A site with names already live passes the same
260
- `{ header, label }` to all three factories (upforge.io uses
261
- `x-upforge-mcp-transport` / `upforge-mcp-transport-v1`). This entry is Node
260
+ `{ header, label }` to all three factories (for example
261
+ `x-example-mcp-transport` / `example-mcp-transport-v1`). This entry is Node
262
262
  only and imports no `server-only`, so the plain-Node function can load it. It
263
263
  is not re-exported from `@sonordev/site-kit/mcp`, which stays runtime-neutral.
264
264
 
@@ -333,8 +333,8 @@ single property check — not a request, and not a byte of page weight.
333
333
  `<WebMcpTools>` registers thin wrappers that POST `tools/call` to this site's
334
334
  own endpoint, so the in-page tool and the remote tool run the same server-side
335
335
  handler. It also keeps `SONOR_API_KEY` out of the client bundle — registering
336
- real handlers client-side would pull server code into a `'use client'` graph,
337
- which is how a raw `sonor_` key once shipped in a public chunk.
336
+ real handlers client-side would pull server code, and the key with it, into a
337
+ `'use client'` graph and a public chunk.
338
338
 
339
339
  Tools that genuinely need live DOM state go in `localTools` and run in-page.
340
340
 
@@ -348,7 +348,7 @@ Per-site components map onto `<Reveal>` almost one to one:
348
348
  | Site prop | `<Reveal>` |
349
349
  |-----------|-----------|
350
350
  | `from="up"`, `direction="up"` | `from="up"` |
351
- | `y={30}` (upforge.io) | `distance={30}` |
351
+ | `y={30}` | `distance={30}` |
352
352
  | `stagger` (boolean) | `stagger` |
353
353
  | `delay`, `duration` | same, in seconds |
354
354
  | `threshold`, `rootMargin` | `threshold` |
package/src/og/README.md CHANGED
@@ -47,8 +47,8 @@ to child segments.** A card at `services/` is not inherited by
47
47
  param), which the generator does automatically.
48
48
 
49
49
  `sonor-setup og` and `sonor-setup doctor` both check this through one shared
50
- rule (`og/wiring.ts`). Do not re-implement it — there were two copies once and
51
- both told sites to do the wrong thing. The rule reads the site's code through
50
+ rule (`og/wiring.ts`), so the two never disagree. The rule reads the site's
51
+ code through
52
52
  `shared/source-graph.ts`, which follows imports into `lib/` helpers and monorepo
53
53
  workspace packages, so `twitter.card` set in a metadata helper counts. It used
54
54
  to read the root layout alone.
@@ -62,16 +62,16 @@ zero-config seed for sites that have no config yet.
62
62
  import { defineOgCard } from '@sonordev/site-kit/og'
63
63
 
64
64
  export default defineOgCard({
65
- theme: { bg: '#0A0A0A', text: '#f5f5f7', accent: '#C41E3A', surface: '#141418' },
66
- fonts: [{ family: 'Playfair Display', weights: [800] }],
65
+ theme: { bg: '#0F172A', text: '#F8FAFC', accent: '#F59E0B', surface: '#1E293B' },
66
+ fonts: [{ family: 'Fraunces', weights: [800] }],
67
67
  logo: '/logo-white.svg',
68
68
  layout: 'split',
69
- photo: { src: '/team.jpg' },
69
+ photo: { src: '/crew.jpg' },
70
70
  content: {
71
- kicker: 'Custom Closets · Cincinnati',
72
- title: 'Built by\nbrothers',
73
- subtitle: 'Designed, built, and installed by the same two people.',
74
- bar: ['Free design', 'example.com'],
71
+ kicker: 'Remodeling · Springfield',
72
+ title: 'Built by\nneighbors',
73
+ subtitle: 'Designed, built and installed by one local crew.',
74
+ bar: ['Free estimate', 'example.com'],
75
75
  },
76
76
  })
77
77
  ```
@@ -86,10 +86,10 @@ The title opens at 104px and the renderer steps it down until it fits **both**
86
86
  axes, stopping at a 56px legibility floor — below that a headline stops reading
87
87
  at the ~300px thumbnail width platforms actually show.
88
88
 
89
- If copy still does not fit at the floor, the command **fails** and names the
90
- element and the overflow in pixels. That is deliberate: the failure it exists to
91
- catch was a card that shipped with the kicker off-canvas and the subtitle buried
92
- under the bottom bar, while the CLI printed a tick.
89
+ If copy still doesn't fit at the floor, the command **fails** and names the
90
+ element and the overflow in pixels. That's deliberate: otherwise a card can ship
91
+ with the kicker off-canvas and the subtitle buried under the bottom bar while
92
+ the CLI prints a tick.
93
93
 
94
94
  Rules of thumb: about 10 uppercase characters per title line, and about 29 for
95
95
  the kicker. `\n` in a title is a hard break, so choose the wrap yourself rather
@@ -106,8 +106,8 @@ Hand-write the few that deserve it:
106
106
 
107
107
  ```ts
108
108
  cards: {
109
- '/free-3d-design': {
110
- content: { kicker: 'Free 3D design', title: 'See it\nfirst' },
109
+ '/free-estimate': {
110
+ content: { kicker: 'Free estimate', title: 'Know the\ncost first' },
111
111
  photo: { src: '/lp/hero.jpg' },
112
112
  },
113
113
  },
@@ -119,8 +119,8 @@ so an entry only states what differs.
119
119
  Managed titles carry the brand for the SERP (`About Us | Acme`); the card drops
120
120
  it. It also drops the two broken suffixes managed titles turn up with: a dangling
121
121
  separator with no brand after it (`About Us |`) and a domain after a comma
122
- (`Privacy Policy, abbeyglenapts.com`). A hyphen inside a word is never a
123
- separator (`Walk-In Closets` stays whole).
122
+ (`Privacy Policy, example.com`). A hyphen inside a word is never a
123
+ separator (`Walk-In Showers` stays whole).
124
124
 
125
125
  ### Dynamic routes are keyed by pattern, one `*` per level
126
126
 
@@ -141,23 +141,22 @@ cards: {
141
141
  },
142
142
  ```
143
143
 
144
- This was a real bug: both directories keyed to `/services/*`, so 120
145
- service-by-metro pages shipped the service pages' card and the `/services/*/*`
146
- override matched nothing — silently. A `cards` key that matches no rendered
147
- route is now reported by `sonor-setup og` (`og.cards`, a warning).
144
+ The generator keys each depth separately, so a `/services/*/*` entry reaches
145
+ the service-by-metro pages and nothing else. A `cards` key that matches no
146
+ rendered route is reported by `sonor-setup og` (`og.cards`, a warning), since
147
+ otherwise it would match nothing, silently.
148
148
 
149
149
  > **Writing a nested pattern in a comment.** `/services/*/*` contains `*/`,
150
150
  > which **closes a `/* */` block comment early**. In TypeScript put it in a
151
- > string, a `//` line comment, or spell the depth out in prose — this bit the
152
- > fix for the bug above, inside the comment explaining the bug.
151
+ > string, a `//` line comment, or spell the depth out in prose. It's an easy
152
+ > slip in exactly the comment that explains the pattern.
153
153
 
154
154
  ### Static routes under a dynamic segment get a card too
155
155
 
156
156
  `properties/[slug]/about` is a static route under a dynamic one. It gets its
157
157
  own card, keyed `/properties/*/about` and titled from its own name ("About",
158
158
  kicker "properties"). The generator used to stop at the first dynamic segment,
159
- so these pages shipped with no og:image at all, 30 of them on two
160
- property sites.
159
+ which left these pages with no og:image at all.
161
160
 
162
161
  ### Per-URL cards: one per floor plan, one per community
163
162
 
@@ -213,7 +212,7 @@ The URL ends in `.jpg`, so a `trailingSlash: true` site never redirects it.
213
212
  Per-URL cards need `sharp` (they have to be `.jpg`); without it the command
214
213
  fails those cards and says so. Their copy comes from Sonor's managed title for
215
214
  that URL, like any route card. A per-URL key matches a pattern one segment per
216
- `*`, so `/properties/tall-pines/about` falls under `/properties/*/about`.
215
+ `*`, so `/properties/maple-court/about` falls under `/properties/*/about`.
217
216
 
218
217
  Cards are re-encoded to JPEG when `sharp` resolves — on a real 18-card site that
219
218
  was 5.6 MB → 1.4 MB. Without sharp they stay PNG, which is correct, just heavier.
@@ -249,7 +248,7 @@ export default createOgImage<{ slug: string }>(async ({ slug }) => {
249
248
  kicker: 'From the publication',
250
249
  title: post.title,
251
250
  photoUrl: post.featured_image, // must be absolute
252
- bar: 'acme.com',
251
+ bar: 'example.com',
253
252
  }
254
253
  })
255
254
  ```
@@ -114,7 +114,7 @@ into a dynamic one. The resolver is deprecated.
114
114
  | Header | Value |
115
115
  |--------|-------|
116
116
  | X-DNS-Prefetch-Control | `on` |
117
- | Content-Security-Policy | `frame-ancestors 'self' https://upforge.io https://*.upforge.io` |
117
+ | Content-Security-Policy | `frame-ancestors` from `DEFAULT_FRAME_ANCESTORS` (below) |
118
118
  | X-Content-Type-Options | `nosniff` |
119
119
  | X-XSS-Protection | `1; mode=block` |
120
120
  | Referrer-Policy | `strict-origin-when-cross-origin` |
@@ -124,23 +124,23 @@ Note there is no `X-Frame-Options` row — that is deliberate, see below.
124
124
 
125
125
  ### Framing / `frame-ancestors`
126
126
 
127
- Upforge showcases live client work in iframes on its portfolio and area
128
- pages, so managed sites ship a CSP `frame-ancestors` allowlist instead of
129
- `X-Frame-Options`. XFO has no allowlist form (`ALLOW-FROM` is dead), and
130
- emitting both would let a stray `DENY` silently re-block the embed — so
131
- `X-Frame-Options` is **omitted** whenever `frameAncestors` is active.
132
- Every origin not on the list is still blocked.
127
+ Managed sites ship a CSP `frame-ancestors` allowlist instead of
128
+ `X-Frame-Options`. The default, `DEFAULT_FRAME_ANCESTORS`, is `'self'`,
129
+ Sonor's dashboard (`https://app.sonor.io`, for Edit on page) and the Upforge
130
+ showcase origins (`upforge.io`, `upforgelabs.com` and their subdomains), where
131
+ live client sites appear in portfolio frames. XFO has no allowlist form
132
+ (`ALLOW-FROM` is dead), and emitting both would let a stray `DENY` silently
133
+ re-block the embed, so `X-Frame-Options` is **omitted** whenever
134
+ `frameAncestors` is active. Every origin not on the list is still blocked.
135
+
136
+ `frameAncestors` replaces the default, so spread it to add an origin:
133
137
 
134
138
  ```ts
139
+ import { createProxy, DEFAULT_FRAME_ANCESTORS } from '@sonordev/site-kit/proxy'
140
+
135
141
  createProxy({
136
- // extend the allowlist
137
142
  securityHeaders: {
138
- frameAncestors: [
139
- "'self'",
140
- 'https://upforge.io',
141
- 'https://*.upforge.io',
142
- 'https://partner.example.com',
143
- ],
143
+ frameAncestors: [...DEFAULT_FRAME_ANCESTORS, 'https://partner.example.com'],
144
144
  },
145
145
  })
146
146