@sonordev/site-kit 4.2.2 → 4.2.4

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 (213) hide show
  1. package/agent-manifest.json +1 -1
  2. package/dist/AnalyticsProvider-HVPZPHZI.mjs +10 -0
  3. package/dist/{AnalyticsProvider-EQSG7FGL.mjs.map → AnalyticsProvider-HVPZPHZI.mjs.map} +1 -1
  4. package/dist/AnalyticsProvider-LFSSF7MQ.js +31 -0
  5. package/dist/{AnalyticsProvider-4BQIFRWP.js.map → AnalyticsProvider-LFSSF7MQ.js.map} +1 -1
  6. package/dist/ChatWidget-7E4PLIYQ.mjs +11 -0
  7. package/dist/{ChatWidget-ZTKWIKXR.mjs.map → ChatWidget-7E4PLIYQ.mjs.map} +1 -1
  8. package/dist/ChatWidget-RFVIYV5C.js +20 -0
  9. package/dist/{ChatWidget-EXJD6YVW.js.map → ChatWidget-RFVIYV5C.js.map} +1 -1
  10. package/dist/EngageWidget-LPMSXVT6.js +18 -0
  11. package/dist/{EngageWidget-HY523XM4.js.map → EngageWidget-LPMSXVT6.js.map} +1 -1
  12. package/dist/EngageWidget-N7EQGSSL.mjs +9 -0
  13. package/dist/{EngageWidget-BJLSXG2V.mjs.map → EngageWidget-N7EQGSSL.mjs.map} +1 -1
  14. package/dist/{FileField-5U4FYTLN.mjs → FileField-AS6TUTXA.mjs} +5 -5
  15. package/dist/{FileField-5U4FYTLN.mjs.map → FileField-AS6TUTXA.mjs.map} +1 -1
  16. package/dist/{FileField-FV46FKRM.js → FileField-Y7NOLT27.js} +6 -6
  17. package/dist/{FileField-FV46FKRM.js.map → FileField-Y7NOLT27.js.map} +1 -1
  18. package/dist/{FormSpotlight-W6BNVKNN.mjs → FormSpotlight-I6FEUYAA.mjs} +3 -3
  19. package/dist/{FormSpotlight-W6BNVKNN.mjs.map → FormSpotlight-I6FEUYAA.mjs.map} +1 -1
  20. package/dist/{FormSpotlight-TUHLSY3O.js → FormSpotlight-RMYOXT2T.js} +15 -15
  21. package/dist/{FormSpotlight-TUHLSY3O.js.map → FormSpotlight-RMYOXT2T.js.map} +1 -1
  22. package/dist/{FormStage-MNXNKSPP.js → FormStage-44OWEWZM.js} +13 -13
  23. package/dist/{FormStage-MNXNKSPP.js.map → FormStage-44OWEWZM.js.map} +1 -1
  24. package/dist/{FormStage-NKWP3C2S.mjs → FormStage-G3MUJMTC.mjs} +3 -3
  25. package/dist/{FormStage-NKWP3C2S.mjs.map → FormStage-G3MUJMTC.mjs.map} +1 -1
  26. package/dist/ManagedForm-LVBNVUZE.mjs +14 -0
  27. package/dist/{ManagedForm-M3XW66FH.mjs.map → ManagedForm-LVBNVUZE.mjs.map} +1 -1
  28. package/dist/ManagedForm-YC6WLYVJ.js +23 -0
  29. package/dist/{ManagedForm-K4UHRVYV.js.map → ManagedForm-YC6WLYVJ.js.map} +1 -1
  30. package/dist/{SignalCore-7PVCUG2S.js → SignalCore-AW5VDA4Y.js} +11 -11
  31. package/dist/{SignalCore-7PVCUG2S.js.map → SignalCore-AW5VDA4Y.js.map} +1 -1
  32. package/dist/{SignalCore-S6PCGTHS.mjs → SignalCore-DYBIG4JP.mjs} +5 -5
  33. package/dist/{SignalCore-S6PCGTHS.mjs.map → SignalCore-DYBIG4JP.mjs.map} +1 -1
  34. package/dist/SitemapSync-2JJOGL7D.mjs +8 -0
  35. package/dist/{SitemapSync-2HLGUZES.mjs.map → SitemapSync-2JJOGL7D.mjs.map} +1 -1
  36. package/dist/SitemapSync-S54TSZLM.js +17 -0
  37. package/dist/{SitemapSync-66HQ4SG2.js.map → SitemapSync-S54TSZLM.js.map} +1 -1
  38. package/dist/analytics/index.js +9 -9
  39. package/dist/analytics/index.mjs +4 -4
  40. package/dist/api-N5OLQXYN.js +202 -0
  41. package/dist/api-N5OLQXYN.js.map +1 -0
  42. package/dist/api-NTHZ6PAW.mjs +184 -0
  43. package/dist/api-NTHZ6PAW.mjs.map +1 -0
  44. package/dist/{chunk-3BXQP54K.js → chunk-2MFLOK2I.js} +3 -3
  45. package/dist/{chunk-3BXQP54K.js.map → chunk-2MFLOK2I.js.map} +1 -1
  46. package/dist/{chunk-SED7E3P5.mjs → chunk-2MLIU6OJ.mjs} +4 -4
  47. package/dist/{chunk-SED7E3P5.mjs.map → chunk-2MLIU6OJ.mjs.map} +1 -1
  48. package/dist/{chunk-OB6DNGYK.js → chunk-2XLWCYB3.js} +12 -12
  49. package/dist/{chunk-OB6DNGYK.js.map → chunk-2XLWCYB3.js.map} +1 -1
  50. package/dist/{chunk-2GUDSTNH.mjs → chunk-33OSUHPZ.mjs} +4 -4
  51. package/dist/{chunk-2GUDSTNH.mjs.map → chunk-33OSUHPZ.mjs.map} +1 -1
  52. package/dist/chunk-4JVFWTER.js +8 -0
  53. package/dist/{chunk-UUBW2NJZ.js.map → chunk-4JVFWTER.js.map} +1 -1
  54. package/dist/{chunk-55FJN7VQ.js → chunk-5K2CX5LK.js} +6 -6
  55. package/dist/{chunk-55FJN7VQ.js.map → chunk-5K2CX5LK.js.map} +1 -1
  56. package/dist/{chunk-W3QLA5MI.mjs → chunk-6ZQFCZPW.mjs} +3 -3
  57. package/dist/{chunk-W3QLA5MI.mjs.map → chunk-6ZQFCZPW.mjs.map} +1 -1
  58. package/dist/{chunk-UF33ENG6.js → chunk-A7O7IWL7.js} +3 -3
  59. package/dist/{chunk-UF33ENG6.js.map → chunk-A7O7IWL7.js.map} +1 -1
  60. package/dist/{chunk-7YVFFQWH.js → chunk-ARFWE2EL.js} +14 -14
  61. package/dist/{chunk-7YVFFQWH.js.map → chunk-ARFWE2EL.js.map} +1 -1
  62. package/dist/{chunk-LSW5QBW2.js → chunk-BDDTZJKC.js} +4 -4
  63. package/dist/{chunk-LSW5QBW2.js.map → chunk-BDDTZJKC.js.map} +1 -1
  64. package/dist/{chunk-YV2TG7JR.mjs → chunk-BJW6YR6W.mjs} +3 -3
  65. package/dist/{chunk-YV2TG7JR.mjs.map → chunk-BJW6YR6W.mjs.map} +1 -1
  66. package/dist/{chunk-ZXVMP6SY.mjs → chunk-C6CNCDIN.mjs} +3 -3
  67. package/dist/{chunk-ZXVMP6SY.mjs.map → chunk-C6CNCDIN.mjs.map} +1 -1
  68. package/dist/{chunk-HYRKVST7.js → chunk-C6HDI464.js} +14 -14
  69. package/dist/{chunk-HYRKVST7.js.map → chunk-C6HDI464.js.map} +1 -1
  70. package/dist/{chunk-DEPDYVUH.js → chunk-CLD3IE6D.js} +5 -5
  71. package/dist/{chunk-DEPDYVUH.js.map → chunk-CLD3IE6D.js.map} +1 -1
  72. package/dist/{chunk-WHHBOYAW.mjs → chunk-CPJA5XTO.mjs} +4 -4
  73. package/dist/{chunk-WHHBOYAW.mjs.map → chunk-CPJA5XTO.mjs.map} +1 -1
  74. package/dist/{chunk-YN5WYGHK.mjs → chunk-E3F6OOVQ.mjs} +3 -3
  75. package/dist/{chunk-YN5WYGHK.mjs.map → chunk-E3F6OOVQ.mjs.map} +1 -1
  76. package/dist/chunk-EFMPOC6L.mjs +6 -0
  77. package/dist/{chunk-NPCWPOCE.mjs.map → chunk-EFMPOC6L.mjs.map} +1 -1
  78. package/dist/{chunk-BEHHWRZM.mjs → chunk-FSSVIYQ7.mjs} +3 -3
  79. package/dist/{chunk-BEHHWRZM.mjs.map → chunk-FSSVIYQ7.mjs.map} +1 -1
  80. package/dist/{chunk-D6CPHVMV.mjs → chunk-HP7V2DI3.mjs} +3 -3
  81. package/dist/{chunk-D6CPHVMV.mjs.map → chunk-HP7V2DI3.mjs.map} +1 -1
  82. package/dist/{chunk-TWPLJ27S.js → chunk-ICJ7EU4Z.js} +7 -7
  83. package/dist/{chunk-TWPLJ27S.js.map → chunk-ICJ7EU4Z.js.map} +1 -1
  84. package/dist/{chunk-524SIFK7.js → chunk-JHOMOLZA.js} +12 -12
  85. package/dist/{chunk-524SIFK7.js.map → chunk-JHOMOLZA.js.map} +1 -1
  86. package/dist/{chunk-F46DCSUI.js → chunk-KDWKSL7U.js} +9 -9
  87. package/dist/{chunk-F46DCSUI.js.map → chunk-KDWKSL7U.js.map} +1 -1
  88. package/dist/{chunk-4MBD6PPV.js → chunk-KPSESCYA.js} +15 -15
  89. package/dist/{chunk-4MBD6PPV.js.map → chunk-KPSESCYA.js.map} +1 -1
  90. package/dist/{chunk-SS5S43I4.mjs → chunk-L5GUV4M7.mjs} +7 -7
  91. package/dist/{chunk-SS5S43I4.mjs.map → chunk-L5GUV4M7.mjs.map} +1 -1
  92. package/dist/{chunk-2LU7BV5K.js → chunk-LSNIDSLQ.js} +18 -18
  93. package/dist/{chunk-2LU7BV5K.js.map → chunk-LSNIDSLQ.js.map} +1 -1
  94. package/dist/{chunk-UIKGLLTU.js → chunk-LUXCT2NU.js} +4 -4
  95. package/dist/{chunk-UIKGLLTU.js.map → chunk-LUXCT2NU.js.map} +1 -1
  96. package/dist/{chunk-QQFWEHYU.js → chunk-NK6K4VHD.js} +4 -4
  97. package/dist/{chunk-QQFWEHYU.js.map → chunk-NK6K4VHD.js.map} +1 -1
  98. package/dist/{chunk-XMZQUC2J.js → chunk-OIY5O3SE.js} +8 -8
  99. package/dist/{chunk-XMZQUC2J.js.map → chunk-OIY5O3SE.js.map} +1 -1
  100. package/dist/{chunk-UEOGMRTL.js → chunk-QQCOIGLN.js} +8 -8
  101. package/dist/{chunk-UEOGMRTL.js.map → chunk-QQCOIGLN.js.map} +1 -1
  102. package/dist/{chunk-THBZCEA7.js → chunk-RH5WIBIA.js} +5 -5
  103. package/dist/{chunk-THBZCEA7.js.map → chunk-RH5WIBIA.js.map} +1 -1
  104. package/dist/{chunk-6TAXXKMX.js → chunk-SACGTFCK.js} +94 -4
  105. package/dist/chunk-SACGTFCK.js.map +1 -0
  106. package/dist/{chunk-DQO4UKFE.mjs → chunk-SRJSR6YP.mjs} +93 -5
  107. package/dist/chunk-SRJSR6YP.mjs.map +1 -0
  108. package/dist/{chunk-P37FFMOZ.mjs → chunk-SSRQHRZB.mjs} +8 -8
  109. package/dist/{chunk-P37FFMOZ.mjs.map → chunk-SSRQHRZB.mjs.map} +1 -1
  110. package/dist/{chunk-WNK2OK5G.mjs → chunk-SYR7YSB2.mjs} +3 -3
  111. package/dist/{chunk-WNK2OK5G.mjs.map → chunk-SYR7YSB2.mjs.map} +1 -1
  112. package/dist/{chunk-645TOMKQ.js → chunk-U7L7FW3K.js} +4 -4
  113. package/dist/{chunk-645TOMKQ.js.map → chunk-U7L7FW3K.js.map} +1 -1
  114. package/dist/{chunk-CM5QUX2T.mjs → chunk-UBSZ466T.mjs} +3 -3
  115. package/dist/{chunk-CM5QUX2T.mjs.map → chunk-UBSZ466T.mjs.map} +1 -1
  116. package/dist/{chunk-XFVI3SNM.mjs → chunk-UHVII7W7.mjs} +3 -3
  117. package/dist/{chunk-XFVI3SNM.mjs.map → chunk-UHVII7W7.mjs.map} +1 -1
  118. package/dist/{chunk-KJSYSRMD.mjs → chunk-ULV3DZ27.mjs} +3 -3
  119. package/dist/{chunk-KJSYSRMD.mjs.map → chunk-ULV3DZ27.mjs.map} +1 -1
  120. package/dist/{chunk-MOKM6STG.mjs → chunk-UVRB4SQU.mjs} +3 -3
  121. package/dist/{chunk-MOKM6STG.mjs.map → chunk-UVRB4SQU.mjs.map} +1 -1
  122. package/dist/{chunk-4E7NFYL5.mjs → chunk-V3HJLXAI.mjs} +3 -3
  123. package/dist/{chunk-4E7NFYL5.mjs.map → chunk-V3HJLXAI.mjs.map} +1 -1
  124. package/dist/{chunk-WARTV6ZP.mjs → chunk-WX6VKFHW.mjs} +4 -4
  125. package/dist/{chunk-WARTV6ZP.mjs.map → chunk-WX6VKFHW.mjs.map} +1 -1
  126. package/dist/{chunk-4SJITKBL.mjs → chunk-XKBADVXN.mjs} +3 -3
  127. package/dist/{chunk-4SJITKBL.mjs.map → chunk-XKBADVXN.mjs.map} +1 -1
  128. package/dist/{chunk-L4ZLL6TE.mjs → chunk-XLD6K3BV.mjs} +4 -4
  129. package/dist/{chunk-L4ZLL6TE.mjs.map → chunk-XLD6K3BV.mjs.map} +1 -1
  130. package/dist/{chunk-K7XOPTOE.mjs → chunk-XWBQLPTZ.mjs} +4 -4
  131. package/dist/{chunk-K7XOPTOE.mjs.map → chunk-XWBQLPTZ.mjs.map} +1 -1
  132. package/dist/{chunk-7723NZQW.mjs → chunk-YDOWCCE2.mjs} +4 -4
  133. package/dist/{chunk-7723NZQW.mjs.map → chunk-YDOWCCE2.mjs.map} +1 -1
  134. package/dist/{chunk-BUZSBUYJ.js → chunk-YHGMM2XF.js} +14 -14
  135. package/dist/{chunk-BUZSBUYJ.js.map → chunk-YHGMM2XF.js.map} +1 -1
  136. package/dist/{chunk-PLY5AP72.js → chunk-YZ6UYIXD.js} +3 -3
  137. package/dist/{chunk-PLY5AP72.js.map → chunk-YZ6UYIXD.js.map} +1 -1
  138. package/dist/{chunk-XBKNPBRB.js → chunk-ZJNIM4GQ.js} +15 -15
  139. package/dist/{chunk-XBKNPBRB.js.map → chunk-ZJNIM4GQ.js.map} +1 -1
  140. package/dist/cli/agent/checks.d.ts.map +1 -1
  141. package/dist/cli/commands/og.d.ts +46 -11
  142. package/dist/cli/commands/og.d.ts.map +1 -1
  143. package/dist/cli/index.js +529 -174
  144. package/dist/cli/index.js.map +1 -1
  145. package/dist/cli/index.mjs +527 -172
  146. package/dist/cli/index.mjs.map +1 -1
  147. package/dist/client/index.js +4 -4
  148. package/dist/client/index.mjs +3 -3
  149. package/dist/commerce/index.js +56 -56
  150. package/dist/commerce/index.mjs +5 -5
  151. package/dist/engage/index.js +10 -10
  152. package/dist/engage/index.mjs +7 -7
  153. package/dist/fleet/index.js +8 -8
  154. package/dist/fleet/index.mjs +4 -4
  155. package/dist/forms/index.js +16 -16
  156. package/dist/forms/index.mjs +9 -9
  157. package/dist/forms/server.js +10 -10
  158. package/dist/forms/server.mjs +8 -8
  159. package/dist/forms/static.js +7 -7
  160. package/dist/forms/static.mjs +6 -6
  161. package/dist/images/index.js +11 -11
  162. package/dist/images/index.mjs +4 -4
  163. package/dist/index.js +63 -63
  164. package/dist/index.mjs +9 -9
  165. package/dist/layout/client.js +8 -8
  166. package/dist/layout/client.mjs +7 -7
  167. package/dist/layout/index.js +10 -10
  168. package/dist/layout/index.mjs +8 -8
  169. package/dist/maps/index.js +5 -5
  170. package/dist/maps/index.mjs +3 -3
  171. package/dist/og/config.d.ts +46 -0
  172. package/dist/og/config.d.ts.map +1 -1
  173. package/dist/og/index.js +5 -5
  174. package/dist/og/index.mjs +1 -1
  175. package/dist/og/pages.d.ts +56 -0
  176. package/dist/og/pages.d.ts.map +1 -0
  177. package/dist/og/pages.test.d.ts +2 -0
  178. package/dist/og/pages.test.d.ts.map +1 -0
  179. package/dist/og/template.d.ts.map +1 -1
  180. package/dist/og/wiring.d.ts +56 -0
  181. package/dist/og/wiring.d.ts.map +1 -0
  182. package/dist/og/wiring.test.d.ts +2 -0
  183. package/dist/og/wiring.test.d.ts.map +1 -0
  184. package/dist/search/index.js +4 -4
  185. package/dist/search/index.mjs +3 -3
  186. package/dist/seo/client.js +5 -5
  187. package/dist/seo/client.mjs +4 -4
  188. package/dist/seo/index.js +5 -5
  189. package/dist/seo/index.mjs +4 -4
  190. package/dist/seo/register-sitemap-cli.js +1 -1
  191. package/dist/seo/register-sitemap-cli.mjs +1 -1
  192. package/dist/server/index.js +3 -3
  193. package/dist/server/index.mjs +2 -2
  194. package/dist/shared/version.d.ts +1 -1
  195. package/dist/signal/index.js +11 -11
  196. package/dist/signal/index.mjs +2 -2
  197. package/dist/sync/index.js +16 -16
  198. package/dist/sync/index.mjs +4 -4
  199. package/package.json +1 -1
  200. package/dist/AnalyticsProvider-4BQIFRWP.js +0 -31
  201. package/dist/AnalyticsProvider-EQSG7FGL.mjs +0 -10
  202. package/dist/ChatWidget-EXJD6YVW.js +0 -20
  203. package/dist/ChatWidget-ZTKWIKXR.mjs +0 -11
  204. package/dist/EngageWidget-BJLSXG2V.mjs +0 -9
  205. package/dist/EngageWidget-HY523XM4.js +0 -18
  206. package/dist/ManagedForm-K4UHRVYV.js +0 -23
  207. package/dist/ManagedForm-M3XW66FH.mjs +0 -14
  208. package/dist/SitemapSync-2HLGUZES.mjs +0 -8
  209. package/dist/SitemapSync-66HQ4SG2.js +0 -17
  210. package/dist/chunk-6TAXXKMX.js.map +0 -1
  211. package/dist/chunk-DQO4UKFE.mjs.map +0 -1
  212. package/dist/chunk-NPCWPOCE.mjs +0 -6
  213. package/dist/chunk-UUBW2NJZ.js +0 -8
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shared/publishCredential.ts"],"names":["normalizeSiteHost","configureSiteCredential","currentCredential"],"mappings":";;;;;;AAqCO,SAAS,gBAAgB,YAAA,EAA+B;AAC7D,EAAA,MAAM,aACJ,OAAO,OAAA,KAAY,WAAA,GAAc,OAAA,CAAQ,IAAI,oBAAA,GAAuB,MAAA;AAEtE,EAAA,IAAI,WAAW,YAAA,IAAgB,EAAA;AAC/B,EAAA,IAAI,CAAC,YAAY,UAAA,EAAY;AAC3B,IAAA,IAAI;AACF,MAAA,QAAA,GAAW,IAAI,GAAA,CAAI,UAAU,CAAA,CAAE,IAAA;AAAA,IACjC,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AACA,EAAA,IAAI,CAAC,QAAA,IAAY,OAAO,MAAA,KAAW,WAAA,EAAa;AAC9C,IAAA,QAAA,GAAW,OAAO,QAAA,CAAS,IAAA;AAAA,EAC7B;AACA,EAAA,OAAOA,kCAAA,CAAkB,QAAQ,CAAA,IAAK,EAAA;AACxC;AAkCO,SAAS,qBAAA,CAAsB;AAAA,EACpC,MAAA;AAAA,EACA,MAAA,GAAS,sBAAA;AAAA,EACT,IAAA;AAAA,EACA,KAAA,GAAQ,KAAA;AAAA,EACR;AACF,CAAA,EAAuC;AACrC,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAKnC,EAAAC,wCAAA,CAAwB,EAAE,UAAA,EAAY,MAAA,EAAQ,MAAA,EAAQ,CAAA;AACrD,EAAC,OAAe,oBAAA,GAAuB,MAAA;AACvC,EAAC,MAAA,CAAe,oBAAA,GAAuBC,kCAAA,CAAkB,MAAM,CAAA;AAC/D,EAAC,OAAe,kBAAA,GAAqB,KAAA;AACrC,EAAC,MAAA,CAAe,iBAAA,GAAoB,IAAA,IAAQ,eAAA,EAAgB;AAG7D,EAAA,IAAI,cAAA,EAAiB,MAAA,CAAe,uBAAA,GAA0B,cAAA;AAChE","file":"chunk-THBZCEA7.js","sourcesContent":["/**\n * @sonordev/site-kit shared — Client credential + window-global publishing\n *\n * SINGLE SOURCE OF TRUTH for seeding the browser-side token manager and the\n * `window.__SITE_KIT_*` globals that every client module reads. Used by:\n *\n * - `SiteKitClientProviders` (core `SiteKitLayout`)\n * - `SiteKitCredential` (`@sonordev/site-kit/client`) — the public component\n * for layouts that are NOT core `SiteKitLayout`, e.g.\n * `@sonordev/agency-site-kit`'s `AgencySiteKitLayout`.\n *\n * Do not re-fork this. Before it existed, agency sites hand-rolled their own\n * \"set the globals on mount\" component (upforge's `UptradeSiteKitInit`), and\n * because those copies had no access to the token manager they published the\n * RAW `sonor_` key into the client bundle — the exact exposure the mint exists\n * to prevent. Any layout that puts a credential on `window` goes through here.\n *\n * Client-safe: no `server-only` guard, SSR-safe (every write is window-gated).\n * The minting half is server-only and lives in `../server/mint-site-token`.\n */\n// Leaf module, NOT '../sites/contract' — see sites/normalize.ts. Importing the\n// full contract here shipped its JSON schema to every browser that loads a\n// credential-publishing entry.\nimport { normalizeSiteHost } from '../sites/normalize'\nimport { configureSiteCredential, currentCredential } from './siteToken'\n\n/**\n * Resolve the sub-site identifier — the dimension that lets one Sonor project\n * host many sites (e.g. truepowersystems.com + ohiopowerstudies.com under one\n * TPS project). Precedence, per the platform site contract:\n * explicit `site` > NEXT_PUBLIC_SITE_URL host > window.location.host\n *\n * Normalized with the shared contract (lowercase host, no protocol/path/port)\n * so `__SITE_KIT_SITE__` matches exactly what the Sonor API stores — servers\n * re-normalize with the same rule, so this only makes the client emit the\n * canonical value. Always returns a string (possibly empty).\n */\nexport function resolveSiteHost(explicitSite?: string): string {\n const envSiteUrl =\n typeof process !== 'undefined' ? process.env.NEXT_PUBLIC_SITE_URL : undefined\n\n let resolved = explicitSite ?? ''\n if (!resolved && envSiteUrl) {\n try {\n resolved = new URL(envSiteUrl).host\n } catch {\n /* malformed URL — fall through to client-side host */\n }\n }\n if (!resolved && typeof window !== 'undefined') {\n resolved = window.location.host\n }\n return normalizeSiteHost(resolved) ?? ''\n}\n\nexport interface PublishSiteCredentialOptions {\n /**\n * The credential the layout minted server-side. In 3.2+ this is a short-lived\n * `st1.` token that `sonorFetch` refreshes transparently. A raw `sonor_` key\n * still works (2.x kit, or the mint-failed fallback) but should never be\n * routed here from a client component — it would ship the secret to the\n * browser.\n */\n apiKey?: string\n apiUrl?: string\n /** Pre-resolved sub-site host. Omit to resolve via `resolveSiteHost`. */\n site?: string\n debug?: boolean\n /**\n * Referrer-restricted Google Maps BROWSER key for client map components.\n * Resolved server-side by SiteKitLayout (NEXT_PUBLIC_GOOGLE_MAPS_API_KEY /\n * GOOGLE_MAPS_BROWSER_KEY) because maps/api.ts runs client-side, where a bare\n * process.env read is dead code unless the app's bundler happens to inline it.\n * Never route a server secret here.\n */\n mapsBrowserKey?: string\n}\n\n/**\n * Seed the token manager and publish the `window.__SITE_KIT_*` globals.\n *\n * Called during RENDER, not in an effect: client modules read these globals in\n * their own mount effects, and effects run child-first, so an effect-based\n * publish would land after the modules that need it.\n *\n * No-op on the server.\n */\nexport function publishSiteCredential({\n apiKey,\n apiUrl = 'https://api.sonor.io',\n site,\n debug = false,\n mapsBrowserKey,\n}: PublishSiteCredentialOptions): void {\n if (typeof window === 'undefined') return\n\n // Seed the client token manager. The window global is set to the freshest\n // token held — a prior page this session may already have refreshed past the\n // seed the layout injected. In raw-key mode this is a passthrough.\n configureSiteCredential({ credential: apiKey, apiUrl })\n ;(window as any).__SITE_KIT_API_URL__ = apiUrl\n ;(window as any).__SITE_KIT_API_KEY__ = currentCredential(apiKey)\n ;(window as any).__SITE_KIT_DEBUG__ = debug\n ;(window as any).__SITE_KIT_SITE__ = site ?? resolveSiteHost()\n // maps/api.ts falls back to this global when the Sonor platform hasn't\n // provisioned a browser key. Don't clobber a value another layout already set.\n if (mapsBrowserKey) (window as any).__GOOGLE_MAPS_API_KEY__ = mapsBrowserKey\n}\n"]}
1
+ {"version":3,"sources":["../src/shared/publishCredential.ts"],"names":["normalizeSiteHost","configureSiteCredential","currentCredential"],"mappings":";;;;;;AAqCO,SAAS,gBAAgB,YAAA,EAA+B;AAC7D,EAAA,MAAM,aACJ,OAAO,OAAA,KAAY,WAAA,GAAc,OAAA,CAAQ,IAAI,oBAAA,GAAuB,MAAA;AAEtE,EAAA,IAAI,WAAW,YAAA,IAAgB,EAAA;AAC/B,EAAA,IAAI,CAAC,YAAY,UAAA,EAAY;AAC3B,IAAA,IAAI;AACF,MAAA,QAAA,GAAW,IAAI,GAAA,CAAI,UAAU,CAAA,CAAE,IAAA;AAAA,IACjC,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AACA,EAAA,IAAI,CAAC,QAAA,IAAY,OAAO,MAAA,KAAW,WAAA,EAAa;AAC9C,IAAA,QAAA,GAAW,OAAO,QAAA,CAAS,IAAA;AAAA,EAC7B;AACA,EAAA,OAAOA,kCAAA,CAAkB,QAAQ,CAAA,IAAK,EAAA;AACxC;AAkCO,SAAS,qBAAA,CAAsB;AAAA,EACpC,MAAA;AAAA,EACA,MAAA,GAAS,sBAAA;AAAA,EACT,IAAA;AAAA,EACA,KAAA,GAAQ,KAAA;AAAA,EACR;AACF,CAAA,EAAuC;AACrC,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAKnC,EAAAC,wCAAA,CAAwB,EAAE,UAAA,EAAY,MAAA,EAAQ,MAAA,EAAQ,CAAA;AACrD,EAAC,OAAe,oBAAA,GAAuB,MAAA;AACvC,EAAC,MAAA,CAAe,oBAAA,GAAuBC,kCAAA,CAAkB,MAAM,CAAA;AAC/D,EAAC,OAAe,kBAAA,GAAqB,KAAA;AACrC,EAAC,MAAA,CAAe,iBAAA,GAAoB,IAAA,IAAQ,eAAA,EAAgB;AAG7D,EAAA,IAAI,cAAA,EAAiB,MAAA,CAAe,uBAAA,GAA0B,cAAA;AAChE","file":"chunk-RH5WIBIA.js","sourcesContent":["/**\n * @sonordev/site-kit shared — Client credential + window-global publishing\n *\n * SINGLE SOURCE OF TRUTH for seeding the browser-side token manager and the\n * `window.__SITE_KIT_*` globals that every client module reads. Used by:\n *\n * - `SiteKitClientProviders` (core `SiteKitLayout`)\n * - `SiteKitCredential` (`@sonordev/site-kit/client`) — the public component\n * for layouts that are NOT core `SiteKitLayout`, e.g.\n * `@sonordev/agency-site-kit`'s `AgencySiteKitLayout`.\n *\n * Do not re-fork this. Before it existed, agency sites hand-rolled their own\n * \"set the globals on mount\" component (upforge's `UptradeSiteKitInit`), and\n * because those copies had no access to the token manager they published the\n * RAW `sonor_` key into the client bundle — the exact exposure the mint exists\n * to prevent. Any layout that puts a credential on `window` goes through here.\n *\n * Client-safe: no `server-only` guard, SSR-safe (every write is window-gated).\n * The minting half is server-only and lives in `../server/mint-site-token`.\n */\n// Leaf module, NOT '../sites/contract' — see sites/normalize.ts. Importing the\n// full contract here shipped its JSON schema to every browser that loads a\n// credential-publishing entry.\nimport { normalizeSiteHost } from '../sites/normalize'\nimport { configureSiteCredential, currentCredential } from './siteToken'\n\n/**\n * Resolve the sub-site identifier — the dimension that lets one Sonor project\n * host many sites (e.g. truepowersystems.com + ohiopowerstudies.com under one\n * TPS project). Precedence, per the platform site contract:\n * explicit `site` > NEXT_PUBLIC_SITE_URL host > window.location.host\n *\n * Normalized with the shared contract (lowercase host, no protocol/path/port)\n * so `__SITE_KIT_SITE__` matches exactly what the Sonor API stores — servers\n * re-normalize with the same rule, so this only makes the client emit the\n * canonical value. Always returns a string (possibly empty).\n */\nexport function resolveSiteHost(explicitSite?: string): string {\n const envSiteUrl =\n typeof process !== 'undefined' ? process.env.NEXT_PUBLIC_SITE_URL : undefined\n\n let resolved = explicitSite ?? ''\n if (!resolved && envSiteUrl) {\n try {\n resolved = new URL(envSiteUrl).host\n } catch {\n /* malformed URL — fall through to client-side host */\n }\n }\n if (!resolved && typeof window !== 'undefined') {\n resolved = window.location.host\n }\n return normalizeSiteHost(resolved) ?? ''\n}\n\nexport interface PublishSiteCredentialOptions {\n /**\n * The credential the layout minted server-side. In 3.2+ this is a short-lived\n * `st1.` token that `sonorFetch` refreshes transparently. A raw `sonor_` key\n * still works (2.x kit, or the mint-failed fallback) but should never be\n * routed here from a client component — it would ship the secret to the\n * browser.\n */\n apiKey?: string\n apiUrl?: string\n /** Pre-resolved sub-site host. Omit to resolve via `resolveSiteHost`. */\n site?: string\n debug?: boolean\n /**\n * Referrer-restricted Google Maps BROWSER key for client map components.\n * Resolved server-side by SiteKitLayout (NEXT_PUBLIC_GOOGLE_MAPS_API_KEY /\n * GOOGLE_MAPS_BROWSER_KEY) because maps/api.ts runs client-side, where a bare\n * process.env read is dead code unless the app's bundler happens to inline it.\n * Never route a server secret here.\n */\n mapsBrowserKey?: string\n}\n\n/**\n * Seed the token manager and publish the `window.__SITE_KIT_*` globals.\n *\n * Called during RENDER, not in an effect: client modules read these globals in\n * their own mount effects, and effects run child-first, so an effect-based\n * publish would land after the modules that need it.\n *\n * No-op on the server.\n */\nexport function publishSiteCredential({\n apiKey,\n apiUrl = 'https://api.sonor.io',\n site,\n debug = false,\n mapsBrowserKey,\n}: PublishSiteCredentialOptions): void {\n if (typeof window === 'undefined') return\n\n // Seed the client token manager. The window global is set to the freshest\n // token held — a prior page this session may already have refreshed past the\n // seed the layout injected. In raw-key mode this is a passthrough.\n configureSiteCredential({ credential: apiKey, apiUrl })\n ;(window as any).__SITE_KIT_API_URL__ = apiUrl\n ;(window as any).__SITE_KIT_API_KEY__ = currentCredential(apiKey)\n ;(window as any).__SITE_KIT_DEBUG__ = debug\n ;(window as any).__SITE_KIT_SITE__ = site ?? resolveSiteHost()\n // maps/api.ts falls back to this global when the Sonor platform hasn't\n // provisioned a browser key. Don't clobber a value another layout already set.\n if (mapsBrowserKey) (window as any).__GOOGLE_MAPS_API_KEY__ = mapsBrowserKey\n}\n"]}
@@ -6,8 +6,85 @@ function defineOgCard(config) {
6
6
  }
7
7
  var OG_WIDTH = 1200;
8
8
  var OG_HEIGHT = 630;
9
+ var TITLE_MAX_PX = 104;
10
+ var TITLE_MIN_PX = 56;
11
+ var OG_REPORT_ATTR = "data-og-report";
9
12
 
10
13
  // src/og/template.ts
14
+ function fitScript() {
15
+ return `<script>
16
+ (async function () {
17
+ var report = { fits: true, titlePx: ${TITLE_MAX_PX}, issues: [] };
18
+ function push(msg) { report.fits = false; report.issues.push(msg); }
19
+ try {
20
+ try { await document.fonts.ready; } catch (e) {}
21
+ var h1 = document.querySelector('h1');
22
+ var copy = document.querySelector('.copy');
23
+ var wrap = document.querySelector('.wrap');
24
+ var bar = document.querySelector('.bar');
25
+
26
+ if (h1 && copy && wrap) {
27
+ var cs = getComputedStyle(wrap);
28
+ var avail = wrap.clientHeight - parseFloat(cs.paddingTop || '0') - parseFloat(cs.paddingBottom || '0');
29
+
30
+ // Fit BOTH axes. Height alone is not enough: a single long word
31
+ // ("WORKBENCHES") cannot wrap, so it keeps overflowing the column
32
+ // sideways at a size that fits vertically. Shrinking for either
33
+ // condition is what makes an arbitrary managed title safe to render.
34
+ var tooTall = function () { return copy.scrollHeight > avail; };
35
+ var tooWide = function () { return h1.scrollWidth > copy.clientWidth + 1; };
36
+ var size = ${TITLE_MAX_PX};
37
+ while (size > ${TITLE_MIN_PX} && (tooTall() || tooWide())) {
38
+ size -= 2;
39
+ h1.style.fontSize = size + 'px';
40
+ }
41
+ report.titlePx = size;
42
+
43
+ if (tooTall()) {
44
+ push('title/subtitle block overflows the card by ' + Math.ceil(copy.scrollHeight - avail) +
45
+ 'px even at the ' + ${TITLE_MIN_PX} + 'px legibility floor - shorten content.title or content.subtitle');
46
+ }
47
+ if (tooWide()) {
48
+ push('the longest title word still overflows the copy column by ' + Math.ceil(h1.scrollWidth - copy.clientWidth) +
49
+ 'px at the ' + ${TITLE_MIN_PX} + 'px floor - shorten it or add a line break');
50
+ }
51
+ }
52
+
53
+ // The bar is checked on BOTH axes. Width alone missed a real failure: a
54
+ // long segment wrapped INSIDE its span, so scrollWidth never exceeded
55
+ // clientWidth while the text grew past the bar's fixed height and spilled
56
+ // over the subtitle. Segments are nowrap now, which turns that into
57
+ // horizontal overflow the width check sees, and the height check is the
58
+ // belt-and-braces for anything else that grows the bar.
59
+ if (bar) {
60
+ if (bar.scrollWidth > bar.clientWidth + 1) {
61
+ push('bottom bar content overflows by ' + Math.ceil(bar.scrollWidth - bar.clientWidth) +
62
+ 'px - drop a content.bar segment or shorten one');
63
+ }
64
+ if (bar.scrollHeight > bar.clientHeight + 1) {
65
+ push('bottom bar content is ' + Math.ceil(bar.scrollHeight - bar.clientHeight) +
66
+ 'px taller than the bar - a segment is wrapping; shorten it');
67
+ }
68
+ }
69
+
70
+ // Nothing may sit outside the frame platforms actually crop to.
71
+ var named = [['kicker', '.kicker'], ['title', 'h1'], ['subtitle', '.sub'], ['hand', '.hand'], ['bar', '.bar']];
72
+ for (var i = 0; i < named.length; i++) {
73
+ var el = document.querySelector(named[i][1]);
74
+ if (!el) continue;
75
+ var r = el.getBoundingClientRect();
76
+ if (r.top < -1 || r.bottom > ${OG_HEIGHT} + 1) {
77
+ push(named[i][0] + ' sits outside the ' + ${OG_HEIGHT} + 'px frame (top ' +
78
+ Math.round(r.top) + ', bottom ' + Math.round(r.bottom) + ')');
79
+ }
80
+ }
81
+ } catch (e) {
82
+ push('fitter failed: ' + (e && e.message ? e.message : String(e)));
83
+ }
84
+ document.documentElement.setAttribute('${OG_REPORT_ATTR}', JSON.stringify(report));
85
+ })();
86
+ </script>`;
87
+ }
11
88
  function esc(s) {
12
89
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13
90
  }
@@ -41,8 +118,10 @@ function barHtml(content, theme) {
41
118
  function copyHtml(config) {
42
119
  const { content, theme } = config;
43
120
  const titleHtml = esc(content.title).replace(/\n/g, "<br>");
121
+ const mark = config.logo && (config.layout ?? (config.logo ? "plate-left" : "banner")) === "split" ? `<img class="mark" src="${esc(config.logo)}">` : "";
44
122
  return `
45
123
  <div class="copy">
124
+ ${mark}
46
125
  ${content.kicker ? `<div class="kicker">${esc(content.kicker)}</div>` : ""}
47
126
  <h1>${titleHtml}</h1>
48
127
  ${content.subtitle ? `<div class="sub" style="color:${esc(muted(theme))}">${esc(content.subtitle)}</div>` : ""}
@@ -85,25 +164,36 @@ ${fontLink(config)}
85
164
  .photo { flex:0 0 auto; height:76%; overflow:hidden; box-shadow:0 8px 40px rgba(0,0,0,.25); }
86
165
  .photo img { width:100%; height:100%; object-fit:cover; display:block; }
87
166
  .copy { flex:1 1 auto; min-width:0; }
167
+ /* split-layout brand mark: the photo owns the right pane, so the logo rides
168
+ above the kicker rather than being dropped. */
169
+ .mark { display:block; height:44px; width:auto; margin-bottom:20px; }
88
170
  .kicker { font-weight:800; text-transform:uppercase; letter-spacing:.22em; font-size:24px; color:${theme.accent}; margin-bottom:14px; }
89
- h1 { font-weight:800; text-transform:uppercase; font-size:104px; line-height:.9; letter-spacing:-.01em; }
171
+ /* Opening size only \u2014 the in-page fitter steps this down to fit the frame. */
172
+ h1 { font-weight:800; text-transform:uppercase; font-size:${TITLE_MAX_PX}px; line-height:.9; letter-spacing:-.01em; }
90
173
  .sub { margin-top:22px; font-size:29px; font-weight:500; line-height:1.32; }
91
174
  /* Never below 40px: script faces are illegible at thumbnail width under that. */
92
175
  .hand { font-size:42px; margin-top:14px; transform:rotate(-1.5deg); display:inline-block; }
93
176
  .bar { position:absolute; left:0; right:0; bottom:0; height:74px; display:flex; align-items:center; justify-content:center; gap:26px;
94
177
  font-weight:800; text-transform:uppercase; letter-spacing:.1em; font-size:31px; color:${theme.surface}; }
95
- .bar .dot { width:10px; height:10px; border-radius:50%; background:currentColor; opacity:.55; }
178
+ /* nowrap so an over-long segment overflows the bar sideways, where the
179
+ fitter's width check sees it, instead of silently wrapping to a second
180
+ line and spilling over the copy above. */
181
+ .bar > span { white-space:nowrap; }
182
+ .bar .dot { width:10px; height:10px; border-radius:50%; background:currentColor; opacity:.55; flex:0 0 auto; }
96
183
  </style></head>
97
184
  <body>
98
185
  ${backgroundLayers(config)}
99
186
  ${body}
100
187
  ${barHtml(config.content, theme)}
188
+ ${fitScript()}
101
189
  </body></html>`;
102
190
  }
103
191
 
104
192
  exports.OG_HEIGHT = OG_HEIGHT;
193
+ exports.OG_REPORT_ATTR = OG_REPORT_ATTR;
105
194
  exports.OG_WIDTH = OG_WIDTH;
195
+ exports.TITLE_MAX_PX = TITLE_MAX_PX;
106
196
  exports.buildOgHtml = buildOgHtml;
107
197
  exports.defineOgCard = defineOgCard;
108
- //# sourceMappingURL=chunk-6TAXXKMX.js.map
109
- //# sourceMappingURL=chunk-6TAXXKMX.js.map
198
+ //# sourceMappingURL=chunk-SACGTFCK.js.map
199
+ //# sourceMappingURL=chunk-SACGTFCK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/og/config.ts","../src/og/template.ts"],"names":[],"mappings":"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","file":"chunk-SACGTFCK.js","sourcesContent":["/**\n * @sonordev/site-kit/og — OG card config contract.\n *\n * THE SITE OWNS THE THEME. Sonor stores one brand_primary/secondary per\n * project; real sites have full palettes (Ludlow: red, ink, cream, scrim —\n * four tokens where Sonor stores two). So the card's colors, fonts, and\n * layout come from a repo-owned og.config.ts, typically importing the same\n * constants the site itself uses. Sonor brand data is only the zero-config\n * seed used by `sonor-setup og` when no og.config.ts exists.\n *\n * Rendering is BUILD-TIME via headless Chrome (`sonor-setup og`), writing a\n * static public/og.png: full real CSS (no Satori subset), Google Fonts load\n * in the template (no font vendoring), zero runtime cost — the right shape\n * for a force-static fleet. Per-entity runtime cards are a future tier.\n */\n\nexport interface OgTheme {\n /** Page background behind everything. */\n bg: string\n /** Primary text color. */\n text: string\n /** Accent (kicker, bar). */\n accent: string\n /** Plate/surface color behind the logo. */\n surface: string\n /** Muted text (subtitle). Defaults derived from `text` when omitted. */\n muted?: string\n}\n\nexport interface OgFont {\n /** Google Fonts family name, e.g. 'Barlow Condensed'. */\n family: string\n weights?: number[]\n}\n\nexport interface OgBackground {\n /** Path under public/, e.g. '/hero-frost.avif'. */\n image: string\n /** CSS blur radius in px. */\n blur?: number\n /** Overlay color, e.g. 'rgba(17,17,16,.68)'. */\n scrim?: string\n}\n\nexport interface OgPhoto {\n /** Path under public/ ('/team.jpg') or an absolute https URL. */\n src: string\n /** Pane width in px (default 430). */\n width?: number\n /** Corner radius in px (default 24). */\n radius?: number\n}\n\nexport interface OgContent {\n /** Small all-caps line above the title. */\n kicker?: string\n /** The big line. `\\n` for a manual break. Keep it short — previews truncate. */\n title: string\n /** One supporting line. */\n subtitle?: string\n /** Handwritten-style accent line (rendered in the last configured font). */\n hand?: string\n /** Bottom bar segments, e.g. ['Give an hour', 'ludlowpto.org']. */\n bar?: string[]\n}\n\n/**\n * A per-route entry in `cards`. Layered over the derived card, so it only\n * states what differs — including inside `content`: a route can override just\n * the subtitle and keep the title Sonor derived. The runtime always merged\n * field by field; this type used to demand a full OgContent, which forced\n * casts in every site that used the documented pattern.\n */\nexport type OgCardOverride = Partial<Omit<OgCardConfig, 'cards' | 'content'>> & {\n content?: Partial<OgContent>\n}\n\nexport interface OgCardConfig {\n theme: OgTheme\n /** First font = display face, second (optional) = hand/accent face. */\n fonts?: OgFont[]\n /** Path under public/ to the logo shown on the plate, e.g. '/logo-mark.svg'. */\n logo?: string\n background?: OgBackground\n /**\n * 'plate-left': logo plate left, copy right (the Ludlow layout).\n * 'centered': stacked, logo above title.\n * 'banner': no plate, full-bleed copy.\n * 'split': copy left, content photo right — for cards that show the page's\n * own imagery (a project photo, a product, the storefront).\n */\n layout?: 'plate-left' | 'centered' | 'banner' | 'split'\n /** Content photo for the 'split' layout (also rendered in 'plate-left' when set, replacing the plate). */\n photo?: OgPhoto\n content: OgContent\n /**\n * Per-page overrides, keyed by route path (`/services/garage`) or its slug\n * (`services-garage`). Every static route already gets its own card built\n * from its managed title and description; this is for the handful worth\n * writing by hand — a campaign landing page, the flagship service.\n *\n * Each entry is layered over the site card, so it only states what differs.\n * Build-time only: never rendered as part of a card.\n */\n cards?: Record<string, OgCardOverride>\n /**\n * MIDDLE escape hatch: keep the loader/renderer/verifier pipeline but\n * produce your own document. Receives the config and the same helpers the\n * stock template uses (esc, fontLink). Root-relative src attributes are\n * still rewritten to public/ files automatically.\n */\n template?: (config: OgCardConfig, helpers: OgTemplateHelpers) => string\n /** FULL escape hatch: raw HTML replaces everything, no helpers. */\n html?: string\n}\n\nexport interface OgTemplateHelpers {\n /** HTML-escape user text. */\n esc: (s: string) => string\n /** <link> tag loading the config's Google Fonts. */\n fontLink: string\n width: number\n height: number\n}\n\n/** Identity helper for typed configs: `export default defineOgCard({...})`. */\nexport function defineOgCard(config: OgCardConfig): OgCardConfig {\n return config\n}\n\nexport const OG_WIDTH = 1200\nexport const OG_HEIGHT = 630\n\n/**\n * Title type scale. The card opens at TITLE_MAX and the in-page fitter steps\n * down until the copy column fits, stopping at TITLE_MIN.\n *\n * The floor is a legibility floor, not a layout one: below ~56px the headline\n * stops reading at the ~300px thumbnail width platforms actually show. A card\n * that would need to go under it is a card whose copy is too long, so the\n * renderer reports that rather than shrinking into mush.\n */\nexport const TITLE_MAX_PX = 104\nexport const TITLE_MIN_PX = 56\n\n/**\n * The attribute the in-page fitter writes its findings to, read back by\n * `sonor-setup og` via Chrome's --dump-dom. Shared so the template and the CLI\n * cannot drift on the name.\n */\nexport const OG_REPORT_ATTR = 'data-og-report'\n\n/** What the in-page fitter reports back about a rendered card. */\nexport interface OgRenderReport {\n /** Did everything land inside the frame, at or above the legibility floor? */\n fits: boolean\n /** Final title size after fitting. Below TITLE_MAX means the copy ran long. */\n titlePx: number\n /** Human-readable problems, each naming the element and the overflow. */\n issues: string[]\n}\n","/**\n * OG card HTML template builder.\n *\n * Turns an OgCardConfig into a self-contained 1200x630 HTML document rendered\n * by headless Chrome. Layout rules baked in from the Ludlow card:\n * - content held inside the middle ~80% (platforms crop toward 2:1 and the\n * bottom bar is the first casualty)\n * - script/hand faces never below 40px — under that they turn to mush at\n * iMessage thumbnail width (~300px)\n * - the plate + title must survive alone if everything else is cropped\n */\n\nimport type { OgCardConfig, OgContent, OgTheme } from './config'\nimport {\n OG_WIDTH,\n OG_HEIGHT,\n TITLE_MAX_PX,\n TITLE_MIN_PX,\n OG_REPORT_ATTR,\n} from './config'\n\n/**\n * The in-page fitter. Runs inside the render, after webfonts settle, and does\n * the two things a Node-side template cannot:\n *\n * 1. FITS THE TITLE. The h1 opens at TITLE_MAX_PX and steps down until the\n * copy column fits the frame. Before this the size was fixed, so title\n * length silently decided whether a card worked — a long one did not\n * shrink, it pushed the kicker off the top and the subtitle under the\n * bottom bar, and the CLI still reported success.\n * 2. MEASURES WHAT IS LEFT. Anything still clipped is written to the report\n * attribute, which `sonor-setup og` reads back through --dump-dom and\n * turns into a failure. Real measurement, not a character-count estimate.\n *\n * Fonts matter: measuring before `document.fonts.ready` measures the fallback\n * face and produces a size that is wrong for the face that actually renders.\n */\nfunction fitScript(): string {\n return `<script>\n(async function () {\n var report = { fits: true, titlePx: ${TITLE_MAX_PX}, issues: [] };\n function push(msg) { report.fits = false; report.issues.push(msg); }\n try {\n try { await document.fonts.ready; } catch (e) {}\n var h1 = document.querySelector('h1');\n var copy = document.querySelector('.copy');\n var wrap = document.querySelector('.wrap');\n var bar = document.querySelector('.bar');\n\n if (h1 && copy && wrap) {\n var cs = getComputedStyle(wrap);\n var avail = wrap.clientHeight - parseFloat(cs.paddingTop || '0') - parseFloat(cs.paddingBottom || '0');\n\n // Fit BOTH axes. Height alone is not enough: a single long word\n // (\"WORKBENCHES\") cannot wrap, so it keeps overflowing the column\n // sideways at a size that fits vertically. Shrinking for either\n // condition is what makes an arbitrary managed title safe to render.\n var tooTall = function () { return copy.scrollHeight > avail; };\n var tooWide = function () { return h1.scrollWidth > copy.clientWidth + 1; };\n var size = ${TITLE_MAX_PX};\n while (size > ${TITLE_MIN_PX} && (tooTall() || tooWide())) {\n size -= 2;\n h1.style.fontSize = size + 'px';\n }\n report.titlePx = size;\n\n if (tooTall()) {\n push('title/subtitle block overflows the card by ' + Math.ceil(copy.scrollHeight - avail) +\n 'px even at the ' + ${TITLE_MIN_PX} + 'px legibility floor - shorten content.title or content.subtitle');\n }\n if (tooWide()) {\n push('the longest title word still overflows the copy column by ' + Math.ceil(h1.scrollWidth - copy.clientWidth) +\n 'px at the ' + ${TITLE_MIN_PX} + 'px floor - shorten it or add a line break');\n }\n }\n\n // The bar is checked on BOTH axes. Width alone missed a real failure: a\n // long segment wrapped INSIDE its span, so scrollWidth never exceeded\n // clientWidth while the text grew past the bar's fixed height and spilled\n // over the subtitle. Segments are nowrap now, which turns that into\n // horizontal overflow the width check sees, and the height check is the\n // belt-and-braces for anything else that grows the bar.\n if (bar) {\n if (bar.scrollWidth > bar.clientWidth + 1) {\n push('bottom bar content overflows by ' + Math.ceil(bar.scrollWidth - bar.clientWidth) +\n 'px - drop a content.bar segment or shorten one');\n }\n if (bar.scrollHeight > bar.clientHeight + 1) {\n push('bottom bar content is ' + Math.ceil(bar.scrollHeight - bar.clientHeight) +\n 'px taller than the bar - a segment is wrapping; shorten it');\n }\n }\n\n // Nothing may sit outside the frame platforms actually crop to.\n var named = [['kicker', '.kicker'], ['title', 'h1'], ['subtitle', '.sub'], ['hand', '.hand'], ['bar', '.bar']];\n for (var i = 0; i < named.length; i++) {\n var el = document.querySelector(named[i][1]);\n if (!el) continue;\n var r = el.getBoundingClientRect();\n if (r.top < -1 || r.bottom > ${OG_HEIGHT} + 1) {\n push(named[i][0] + ' sits outside the ' + ${OG_HEIGHT} + 'px frame (top ' +\n Math.round(r.top) + ', bottom ' + Math.round(r.bottom) + ')');\n }\n }\n } catch (e) {\n push('fitter failed: ' + (e && e.message ? e.message : String(e)));\n }\n document.documentElement.setAttribute('${OG_REPORT_ATTR}', JSON.stringify(report));\n})();\n</script>`\n}\n\nfunction esc(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\nfunction fontLink(config: OgCardConfig): string {\n const fonts = config.fonts?.length\n ? config.fonts\n : [{ family: 'Barlow Condensed', weights: [800] }]\n const families = fonts\n .map(f => `family=${f.family.replace(/ /g, '+')}:wght@${(f.weights || [700]).join(';')}`)\n .join('&')\n return `<link href=\"https://fonts.googleapis.com/css2?${families}&display=block\" rel=\"stylesheet\">`\n}\n\nfunction displayFamily(config: OgCardConfig): string {\n return config.fonts?.[0]?.family || 'Barlow Condensed'\n}\n\nfunction handFamily(config: OgCardConfig): string {\n const fonts = config.fonts || []\n return fonts.length > 1 ? fonts[fonts.length - 1].family : displayFamily(config)\n}\n\nfunction muted(theme: OgTheme): string {\n return theme.muted || theme.text\n}\n\nfunction backgroundLayers(config: OgCardConfig): string {\n if (!config.background) return ''\n const { image, blur = 0, scrim } = config.background\n return `\n <div class=\"bg\"><img src=\"${esc(image)}\" style=\"filter:blur(${blur}px)\"></div>\n ${scrim ? `<div class=\"scrim\" style=\"background:${esc(scrim)}\"></div>` : ''}`\n}\n\nfunction barHtml(content: OgContent, theme: OgTheme): string {\n if (!content.bar?.length) return ''\n const segs = content.bar\n .map(s => `<span>${esc(s)}</span>`)\n .join('<span class=\"dot\"></span>')\n return `<div class=\"bar\" style=\"background:${esc(theme.accent)}\">${segs}</div>`\n}\n\nfunction copyHtml(config: OgCardConfig): string {\n const { content, theme } = config\n const titleHtml = esc(content.title).replace(/\\n/g, '<br>')\n // In `split` the photo owns the right pane, so a configured logo has nowhere\n // to go as a plate. It used to be dropped silently. Render it small above the\n // kicker instead: the brand mark still appears, and the plate invariant that\n // split has no `.plate` is preserved.\n const mark =\n config.logo && (config.layout ?? (config.logo ? 'plate-left' : 'banner')) === 'split'\n ? `<img class=\"mark\" src=\"${esc(config.logo)}\">`\n : ''\n return `\n <div class=\"copy\">\n ${mark}\n ${content.kicker ? `<div class=\"kicker\">${esc(content.kicker)}</div>` : ''}\n <h1>${titleHtml}</h1>\n ${content.subtitle ? `<div class=\"sub\" style=\"color:${esc(muted(theme))}\">${esc(content.subtitle)}</div>` : ''}\n ${content.hand ? `<div class=\"hand\" style=\"font-family:'${esc(handFamily(config))}',cursive\">${esc(content.hand)}</div>` : ''}\n </div>`\n}\n\nexport function buildOgHtml(config: OgCardConfig): string {\n if (config.html) return config.html\n if (config.template) {\n return config.template(config, {\n esc,\n fontLink: fontLink(config),\n width: OG_WIDTH,\n height: OG_HEIGHT,\n })\n }\n\n const { theme, logo, layout = logo ? 'plate-left' : 'banner' } = config\n const display = displayFamily(config)\n\n const plate = logo\n ? `<div class=\"plate\" style=\"background:${esc(theme.surface)}\"><img src=\"${esc(logo)}\"></div>`\n : ''\n\n const photo = config.photo\n ? `<div class=\"photo\" style=\"flex-basis:${config.photo.width ?? 430}px;border-radius:${config.photo.radius ?? 24}px\"><img src=\"${esc(config.photo.src)}\"></div>`\n : ''\n\n const body =\n layout === 'centered'\n ? `<div class=\"wrap centered\">${plate}${copyHtml(config)}</div>`\n : layout === 'banner'\n ? `<div class=\"wrap banner\">${copyHtml(config)}</div>`\n : layout === 'split'\n ? `<div class=\"wrap\">${copyHtml(config)}${photo}</div>`\n : `<div class=\"wrap\">${photo || plate}${copyHtml(config)}</div>`\n\n return `<!doctype html>\n<html><head><meta charset=\"utf-8\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n${fontLink(config)}\n<style>\n * { margin:0; padding:0; box-sizing:border-box; }\n html,body { width:${OG_WIDTH}px; height:${OG_HEIGHT}px; overflow:hidden; }\n body { position:relative; background:${theme.bg}; font-family:'${display}',sans-serif; color:${theme.text}; }\n .bg { position:absolute; inset:0; }\n .bg img { width:100%; height:100%; object-fit:cover; transform:scale(1.05); }\n .scrim { position:absolute; inset:0; }\n .wrap { position:relative; height:100%; display:flex; align-items:center; gap:52px; padding:0 68px ${config.content.bar?.length ? '74px' : '0'}; }\n .wrap.centered { flex-direction:column; justify-content:center; text-align:center; gap:28px; }\n .wrap.banner { padding-left:90px; }\n .plate { border-radius:28px; padding:26px; flex:0 0 auto; }\n .plate img { display:block; width:250px; height:auto; }\n .centered .plate img { width:180px; }\n .photo { flex:0 0 auto; height:76%; overflow:hidden; box-shadow:0 8px 40px rgba(0,0,0,.25); }\n .photo img { width:100%; height:100%; object-fit:cover; display:block; }\n .copy { flex:1 1 auto; min-width:0; }\n /* split-layout brand mark: the photo owns the right pane, so the logo rides\n above the kicker rather than being dropped. */\n .mark { display:block; height:44px; width:auto; margin-bottom:20px; }\n .kicker { font-weight:800; text-transform:uppercase; letter-spacing:.22em; font-size:24px; color:${theme.accent}; margin-bottom:14px; }\n /* Opening size only — the in-page fitter steps this down to fit the frame. */\n h1 { font-weight:800; text-transform:uppercase; font-size:${TITLE_MAX_PX}px; line-height:.9; letter-spacing:-.01em; }\n .sub { margin-top:22px; font-size:29px; font-weight:500; line-height:1.32; }\n /* Never below 40px: script faces are illegible at thumbnail width under that. */\n .hand { font-size:42px; margin-top:14px; transform:rotate(-1.5deg); display:inline-block; }\n .bar { position:absolute; left:0; right:0; bottom:0; height:74px; display:flex; align-items:center; justify-content:center; gap:26px;\n font-weight:800; text-transform:uppercase; letter-spacing:.1em; font-size:31px; color:${theme.surface}; }\n /* nowrap so an over-long segment overflows the bar sideways, where the\n fitter's width check sees it, instead of silently wrapping to a second\n line and spilling over the copy above. */\n .bar > span { white-space:nowrap; }\n .bar .dot { width:10px; height:10px; border-radius:50%; background:currentColor; opacity:.55; flex:0 0 auto; }\n</style></head>\n<body>\n${backgroundLayers(config)}\n${body}\n${barHtml(config.content, theme)}\n${fitScript()}\n</body></html>`\n}\n"]}
@@ -4,8 +4,85 @@ function defineOgCard(config) {
4
4
  }
5
5
  var OG_WIDTH = 1200;
6
6
  var OG_HEIGHT = 630;
7
+ var TITLE_MAX_PX = 104;
8
+ var TITLE_MIN_PX = 56;
9
+ var OG_REPORT_ATTR = "data-og-report";
7
10
 
8
11
  // src/og/template.ts
12
+ function fitScript() {
13
+ return `<script>
14
+ (async function () {
15
+ var report = { fits: true, titlePx: ${TITLE_MAX_PX}, issues: [] };
16
+ function push(msg) { report.fits = false; report.issues.push(msg); }
17
+ try {
18
+ try { await document.fonts.ready; } catch (e) {}
19
+ var h1 = document.querySelector('h1');
20
+ var copy = document.querySelector('.copy');
21
+ var wrap = document.querySelector('.wrap');
22
+ var bar = document.querySelector('.bar');
23
+
24
+ if (h1 && copy && wrap) {
25
+ var cs = getComputedStyle(wrap);
26
+ var avail = wrap.clientHeight - parseFloat(cs.paddingTop || '0') - parseFloat(cs.paddingBottom || '0');
27
+
28
+ // Fit BOTH axes. Height alone is not enough: a single long word
29
+ // ("WORKBENCHES") cannot wrap, so it keeps overflowing the column
30
+ // sideways at a size that fits vertically. Shrinking for either
31
+ // condition is what makes an arbitrary managed title safe to render.
32
+ var tooTall = function () { return copy.scrollHeight > avail; };
33
+ var tooWide = function () { return h1.scrollWidth > copy.clientWidth + 1; };
34
+ var size = ${TITLE_MAX_PX};
35
+ while (size > ${TITLE_MIN_PX} && (tooTall() || tooWide())) {
36
+ size -= 2;
37
+ h1.style.fontSize = size + 'px';
38
+ }
39
+ report.titlePx = size;
40
+
41
+ if (tooTall()) {
42
+ push('title/subtitle block overflows the card by ' + Math.ceil(copy.scrollHeight - avail) +
43
+ 'px even at the ' + ${TITLE_MIN_PX} + 'px legibility floor - shorten content.title or content.subtitle');
44
+ }
45
+ if (tooWide()) {
46
+ push('the longest title word still overflows the copy column by ' + Math.ceil(h1.scrollWidth - copy.clientWidth) +
47
+ 'px at the ' + ${TITLE_MIN_PX} + 'px floor - shorten it or add a line break');
48
+ }
49
+ }
50
+
51
+ // The bar is checked on BOTH axes. Width alone missed a real failure: a
52
+ // long segment wrapped INSIDE its span, so scrollWidth never exceeded
53
+ // clientWidth while the text grew past the bar's fixed height and spilled
54
+ // over the subtitle. Segments are nowrap now, which turns that into
55
+ // horizontal overflow the width check sees, and the height check is the
56
+ // belt-and-braces for anything else that grows the bar.
57
+ if (bar) {
58
+ if (bar.scrollWidth > bar.clientWidth + 1) {
59
+ push('bottom bar content overflows by ' + Math.ceil(bar.scrollWidth - bar.clientWidth) +
60
+ 'px - drop a content.bar segment or shorten one');
61
+ }
62
+ if (bar.scrollHeight > bar.clientHeight + 1) {
63
+ push('bottom bar content is ' + Math.ceil(bar.scrollHeight - bar.clientHeight) +
64
+ 'px taller than the bar - a segment is wrapping; shorten it');
65
+ }
66
+ }
67
+
68
+ // Nothing may sit outside the frame platforms actually crop to.
69
+ var named = [['kicker', '.kicker'], ['title', 'h1'], ['subtitle', '.sub'], ['hand', '.hand'], ['bar', '.bar']];
70
+ for (var i = 0; i < named.length; i++) {
71
+ var el = document.querySelector(named[i][1]);
72
+ if (!el) continue;
73
+ var r = el.getBoundingClientRect();
74
+ if (r.top < -1 || r.bottom > ${OG_HEIGHT} + 1) {
75
+ push(named[i][0] + ' sits outside the ' + ${OG_HEIGHT} + 'px frame (top ' +
76
+ Math.round(r.top) + ', bottom ' + Math.round(r.bottom) + ')');
77
+ }
78
+ }
79
+ } catch (e) {
80
+ push('fitter failed: ' + (e && e.message ? e.message : String(e)));
81
+ }
82
+ document.documentElement.setAttribute('${OG_REPORT_ATTR}', JSON.stringify(report));
83
+ })();
84
+ </script>`;
85
+ }
9
86
  function esc(s) {
10
87
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
11
88
  }
@@ -39,8 +116,10 @@ function barHtml(content, theme) {
39
116
  function copyHtml(config) {
40
117
  const { content, theme } = config;
41
118
  const titleHtml = esc(content.title).replace(/\n/g, "<br>");
119
+ const mark = config.logo && (config.layout ?? (config.logo ? "plate-left" : "banner")) === "split" ? `<img class="mark" src="${esc(config.logo)}">` : "";
42
120
  return `
43
121
  <div class="copy">
122
+ ${mark}
44
123
  ${content.kicker ? `<div class="kicker">${esc(content.kicker)}</div>` : ""}
45
124
  <h1>${titleHtml}</h1>
46
125
  ${content.subtitle ? `<div class="sub" style="color:${esc(muted(theme))}">${esc(content.subtitle)}</div>` : ""}
@@ -83,22 +162,31 @@ ${fontLink(config)}
83
162
  .photo { flex:0 0 auto; height:76%; overflow:hidden; box-shadow:0 8px 40px rgba(0,0,0,.25); }
84
163
  .photo img { width:100%; height:100%; object-fit:cover; display:block; }
85
164
  .copy { flex:1 1 auto; min-width:0; }
165
+ /* split-layout brand mark: the photo owns the right pane, so the logo rides
166
+ above the kicker rather than being dropped. */
167
+ .mark { display:block; height:44px; width:auto; margin-bottom:20px; }
86
168
  .kicker { font-weight:800; text-transform:uppercase; letter-spacing:.22em; font-size:24px; color:${theme.accent}; margin-bottom:14px; }
87
- h1 { font-weight:800; text-transform:uppercase; font-size:104px; line-height:.9; letter-spacing:-.01em; }
169
+ /* Opening size only \u2014 the in-page fitter steps this down to fit the frame. */
170
+ h1 { font-weight:800; text-transform:uppercase; font-size:${TITLE_MAX_PX}px; line-height:.9; letter-spacing:-.01em; }
88
171
  .sub { margin-top:22px; font-size:29px; font-weight:500; line-height:1.32; }
89
172
  /* Never below 40px: script faces are illegible at thumbnail width under that. */
90
173
  .hand { font-size:42px; margin-top:14px; transform:rotate(-1.5deg); display:inline-block; }
91
174
  .bar { position:absolute; left:0; right:0; bottom:0; height:74px; display:flex; align-items:center; justify-content:center; gap:26px;
92
175
  font-weight:800; text-transform:uppercase; letter-spacing:.1em; font-size:31px; color:${theme.surface}; }
93
- .bar .dot { width:10px; height:10px; border-radius:50%; background:currentColor; opacity:.55; }
176
+ /* nowrap so an over-long segment overflows the bar sideways, where the
177
+ fitter's width check sees it, instead of silently wrapping to a second
178
+ line and spilling over the copy above. */
179
+ .bar > span { white-space:nowrap; }
180
+ .bar .dot { width:10px; height:10px; border-radius:50%; background:currentColor; opacity:.55; flex:0 0 auto; }
94
181
  </style></head>
95
182
  <body>
96
183
  ${backgroundLayers(config)}
97
184
  ${body}
98
185
  ${barHtml(config.content, theme)}
186
+ ${fitScript()}
99
187
  </body></html>`;
100
188
  }
101
189
 
102
- export { OG_HEIGHT, OG_WIDTH, buildOgHtml, defineOgCard };
103
- //# sourceMappingURL=chunk-DQO4UKFE.mjs.map
104
- //# sourceMappingURL=chunk-DQO4UKFE.mjs.map
190
+ export { OG_HEIGHT, OG_REPORT_ATTR, OG_WIDTH, TITLE_MAX_PX, buildOgHtml, defineOgCard };
191
+ //# sourceMappingURL=chunk-SRJSR6YP.mjs.map
192
+ //# sourceMappingURL=chunk-SRJSR6YP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/og/config.ts","../src/og/template.ts"],"names":[],"mappings":"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","file":"chunk-SRJSR6YP.mjs","sourcesContent":["/**\n * @sonordev/site-kit/og — OG card config contract.\n *\n * THE SITE OWNS THE THEME. Sonor stores one brand_primary/secondary per\n * project; real sites have full palettes (Ludlow: red, ink, cream, scrim —\n * four tokens where Sonor stores two). So the card's colors, fonts, and\n * layout come from a repo-owned og.config.ts, typically importing the same\n * constants the site itself uses. Sonor brand data is only the zero-config\n * seed used by `sonor-setup og` when no og.config.ts exists.\n *\n * Rendering is BUILD-TIME via headless Chrome (`sonor-setup og`), writing a\n * static public/og.png: full real CSS (no Satori subset), Google Fonts load\n * in the template (no font vendoring), zero runtime cost — the right shape\n * for a force-static fleet. Per-entity runtime cards are a future tier.\n */\n\nexport interface OgTheme {\n /** Page background behind everything. */\n bg: string\n /** Primary text color. */\n text: string\n /** Accent (kicker, bar). */\n accent: string\n /** Plate/surface color behind the logo. */\n surface: string\n /** Muted text (subtitle). Defaults derived from `text` when omitted. */\n muted?: string\n}\n\nexport interface OgFont {\n /** Google Fonts family name, e.g. 'Barlow Condensed'. */\n family: string\n weights?: number[]\n}\n\nexport interface OgBackground {\n /** Path under public/, e.g. '/hero-frost.avif'. */\n image: string\n /** CSS blur radius in px. */\n blur?: number\n /** Overlay color, e.g. 'rgba(17,17,16,.68)'. */\n scrim?: string\n}\n\nexport interface OgPhoto {\n /** Path under public/ ('/team.jpg') or an absolute https URL. */\n src: string\n /** Pane width in px (default 430). */\n width?: number\n /** Corner radius in px (default 24). */\n radius?: number\n}\n\nexport interface OgContent {\n /** Small all-caps line above the title. */\n kicker?: string\n /** The big line. `\\n` for a manual break. Keep it short — previews truncate. */\n title: string\n /** One supporting line. */\n subtitle?: string\n /** Handwritten-style accent line (rendered in the last configured font). */\n hand?: string\n /** Bottom bar segments, e.g. ['Give an hour', 'ludlowpto.org']. */\n bar?: string[]\n}\n\n/**\n * A per-route entry in `cards`. Layered over the derived card, so it only\n * states what differs — including inside `content`: a route can override just\n * the subtitle and keep the title Sonor derived. The runtime always merged\n * field by field; this type used to demand a full OgContent, which forced\n * casts in every site that used the documented pattern.\n */\nexport type OgCardOverride = Partial<Omit<OgCardConfig, 'cards' | 'content'>> & {\n content?: Partial<OgContent>\n}\n\nexport interface OgCardConfig {\n theme: OgTheme\n /** First font = display face, second (optional) = hand/accent face. */\n fonts?: OgFont[]\n /** Path under public/ to the logo shown on the plate, e.g. '/logo-mark.svg'. */\n logo?: string\n background?: OgBackground\n /**\n * 'plate-left': logo plate left, copy right (the Ludlow layout).\n * 'centered': stacked, logo above title.\n * 'banner': no plate, full-bleed copy.\n * 'split': copy left, content photo right — for cards that show the page's\n * own imagery (a project photo, a product, the storefront).\n */\n layout?: 'plate-left' | 'centered' | 'banner' | 'split'\n /** Content photo for the 'split' layout (also rendered in 'plate-left' when set, replacing the plate). */\n photo?: OgPhoto\n content: OgContent\n /**\n * Per-page overrides, keyed by route path (`/services/garage`) or its slug\n * (`services-garage`). Every static route already gets its own card built\n * from its managed title and description; this is for the handful worth\n * writing by hand — a campaign landing page, the flagship service.\n *\n * Each entry is layered over the site card, so it only states what differs.\n * Build-time only: never rendered as part of a card.\n */\n cards?: Record<string, OgCardOverride>\n /**\n * MIDDLE escape hatch: keep the loader/renderer/verifier pipeline but\n * produce your own document. Receives the config and the same helpers the\n * stock template uses (esc, fontLink). Root-relative src attributes are\n * still rewritten to public/ files automatically.\n */\n template?: (config: OgCardConfig, helpers: OgTemplateHelpers) => string\n /** FULL escape hatch: raw HTML replaces everything, no helpers. */\n html?: string\n}\n\nexport interface OgTemplateHelpers {\n /** HTML-escape user text. */\n esc: (s: string) => string\n /** <link> tag loading the config's Google Fonts. */\n fontLink: string\n width: number\n height: number\n}\n\n/** Identity helper for typed configs: `export default defineOgCard({...})`. */\nexport function defineOgCard(config: OgCardConfig): OgCardConfig {\n return config\n}\n\nexport const OG_WIDTH = 1200\nexport const OG_HEIGHT = 630\n\n/**\n * Title type scale. The card opens at TITLE_MAX and the in-page fitter steps\n * down until the copy column fits, stopping at TITLE_MIN.\n *\n * The floor is a legibility floor, not a layout one: below ~56px the headline\n * stops reading at the ~300px thumbnail width platforms actually show. A card\n * that would need to go under it is a card whose copy is too long, so the\n * renderer reports that rather than shrinking into mush.\n */\nexport const TITLE_MAX_PX = 104\nexport const TITLE_MIN_PX = 56\n\n/**\n * The attribute the in-page fitter writes its findings to, read back by\n * `sonor-setup og` via Chrome's --dump-dom. Shared so the template and the CLI\n * cannot drift on the name.\n */\nexport const OG_REPORT_ATTR = 'data-og-report'\n\n/** What the in-page fitter reports back about a rendered card. */\nexport interface OgRenderReport {\n /** Did everything land inside the frame, at or above the legibility floor? */\n fits: boolean\n /** Final title size after fitting. Below TITLE_MAX means the copy ran long. */\n titlePx: number\n /** Human-readable problems, each naming the element and the overflow. */\n issues: string[]\n}\n","/**\n * OG card HTML template builder.\n *\n * Turns an OgCardConfig into a self-contained 1200x630 HTML document rendered\n * by headless Chrome. Layout rules baked in from the Ludlow card:\n * - content held inside the middle ~80% (platforms crop toward 2:1 and the\n * bottom bar is the first casualty)\n * - script/hand faces never below 40px — under that they turn to mush at\n * iMessage thumbnail width (~300px)\n * - the plate + title must survive alone if everything else is cropped\n */\n\nimport type { OgCardConfig, OgContent, OgTheme } from './config'\nimport {\n OG_WIDTH,\n OG_HEIGHT,\n TITLE_MAX_PX,\n TITLE_MIN_PX,\n OG_REPORT_ATTR,\n} from './config'\n\n/**\n * The in-page fitter. Runs inside the render, after webfonts settle, and does\n * the two things a Node-side template cannot:\n *\n * 1. FITS THE TITLE. The h1 opens at TITLE_MAX_PX and steps down until the\n * copy column fits the frame. Before this the size was fixed, so title\n * length silently decided whether a card worked — a long one did not\n * shrink, it pushed the kicker off the top and the subtitle under the\n * bottom bar, and the CLI still reported success.\n * 2. MEASURES WHAT IS LEFT. Anything still clipped is written to the report\n * attribute, which `sonor-setup og` reads back through --dump-dom and\n * turns into a failure. Real measurement, not a character-count estimate.\n *\n * Fonts matter: measuring before `document.fonts.ready` measures the fallback\n * face and produces a size that is wrong for the face that actually renders.\n */\nfunction fitScript(): string {\n return `<script>\n(async function () {\n var report = { fits: true, titlePx: ${TITLE_MAX_PX}, issues: [] };\n function push(msg) { report.fits = false; report.issues.push(msg); }\n try {\n try { await document.fonts.ready; } catch (e) {}\n var h1 = document.querySelector('h1');\n var copy = document.querySelector('.copy');\n var wrap = document.querySelector('.wrap');\n var bar = document.querySelector('.bar');\n\n if (h1 && copy && wrap) {\n var cs = getComputedStyle(wrap);\n var avail = wrap.clientHeight - parseFloat(cs.paddingTop || '0') - parseFloat(cs.paddingBottom || '0');\n\n // Fit BOTH axes. Height alone is not enough: a single long word\n // (\"WORKBENCHES\") cannot wrap, so it keeps overflowing the column\n // sideways at a size that fits vertically. Shrinking for either\n // condition is what makes an arbitrary managed title safe to render.\n var tooTall = function () { return copy.scrollHeight > avail; };\n var tooWide = function () { return h1.scrollWidth > copy.clientWidth + 1; };\n var size = ${TITLE_MAX_PX};\n while (size > ${TITLE_MIN_PX} && (tooTall() || tooWide())) {\n size -= 2;\n h1.style.fontSize = size + 'px';\n }\n report.titlePx = size;\n\n if (tooTall()) {\n push('title/subtitle block overflows the card by ' + Math.ceil(copy.scrollHeight - avail) +\n 'px even at the ' + ${TITLE_MIN_PX} + 'px legibility floor - shorten content.title or content.subtitle');\n }\n if (tooWide()) {\n push('the longest title word still overflows the copy column by ' + Math.ceil(h1.scrollWidth - copy.clientWidth) +\n 'px at the ' + ${TITLE_MIN_PX} + 'px floor - shorten it or add a line break');\n }\n }\n\n // The bar is checked on BOTH axes. Width alone missed a real failure: a\n // long segment wrapped INSIDE its span, so scrollWidth never exceeded\n // clientWidth while the text grew past the bar's fixed height and spilled\n // over the subtitle. Segments are nowrap now, which turns that into\n // horizontal overflow the width check sees, and the height check is the\n // belt-and-braces for anything else that grows the bar.\n if (bar) {\n if (bar.scrollWidth > bar.clientWidth + 1) {\n push('bottom bar content overflows by ' + Math.ceil(bar.scrollWidth - bar.clientWidth) +\n 'px - drop a content.bar segment or shorten one');\n }\n if (bar.scrollHeight > bar.clientHeight + 1) {\n push('bottom bar content is ' + Math.ceil(bar.scrollHeight - bar.clientHeight) +\n 'px taller than the bar - a segment is wrapping; shorten it');\n }\n }\n\n // Nothing may sit outside the frame platforms actually crop to.\n var named = [['kicker', '.kicker'], ['title', 'h1'], ['subtitle', '.sub'], ['hand', '.hand'], ['bar', '.bar']];\n for (var i = 0; i < named.length; i++) {\n var el = document.querySelector(named[i][1]);\n if (!el) continue;\n var r = el.getBoundingClientRect();\n if (r.top < -1 || r.bottom > ${OG_HEIGHT} + 1) {\n push(named[i][0] + ' sits outside the ' + ${OG_HEIGHT} + 'px frame (top ' +\n Math.round(r.top) + ', bottom ' + Math.round(r.bottom) + ')');\n }\n }\n } catch (e) {\n push('fitter failed: ' + (e && e.message ? e.message : String(e)));\n }\n document.documentElement.setAttribute('${OG_REPORT_ATTR}', JSON.stringify(report));\n})();\n</script>`\n}\n\nfunction esc(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\nfunction fontLink(config: OgCardConfig): string {\n const fonts = config.fonts?.length\n ? config.fonts\n : [{ family: 'Barlow Condensed', weights: [800] }]\n const families = fonts\n .map(f => `family=${f.family.replace(/ /g, '+')}:wght@${(f.weights || [700]).join(';')}`)\n .join('&')\n return `<link href=\"https://fonts.googleapis.com/css2?${families}&display=block\" rel=\"stylesheet\">`\n}\n\nfunction displayFamily(config: OgCardConfig): string {\n return config.fonts?.[0]?.family || 'Barlow Condensed'\n}\n\nfunction handFamily(config: OgCardConfig): string {\n const fonts = config.fonts || []\n return fonts.length > 1 ? fonts[fonts.length - 1].family : displayFamily(config)\n}\n\nfunction muted(theme: OgTheme): string {\n return theme.muted || theme.text\n}\n\nfunction backgroundLayers(config: OgCardConfig): string {\n if (!config.background) return ''\n const { image, blur = 0, scrim } = config.background\n return `\n <div class=\"bg\"><img src=\"${esc(image)}\" style=\"filter:blur(${blur}px)\"></div>\n ${scrim ? `<div class=\"scrim\" style=\"background:${esc(scrim)}\"></div>` : ''}`\n}\n\nfunction barHtml(content: OgContent, theme: OgTheme): string {\n if (!content.bar?.length) return ''\n const segs = content.bar\n .map(s => `<span>${esc(s)}</span>`)\n .join('<span class=\"dot\"></span>')\n return `<div class=\"bar\" style=\"background:${esc(theme.accent)}\">${segs}</div>`\n}\n\nfunction copyHtml(config: OgCardConfig): string {\n const { content, theme } = config\n const titleHtml = esc(content.title).replace(/\\n/g, '<br>')\n // In `split` the photo owns the right pane, so a configured logo has nowhere\n // to go as a plate. It used to be dropped silently. Render it small above the\n // kicker instead: the brand mark still appears, and the plate invariant that\n // split has no `.plate` is preserved.\n const mark =\n config.logo && (config.layout ?? (config.logo ? 'plate-left' : 'banner')) === 'split'\n ? `<img class=\"mark\" src=\"${esc(config.logo)}\">`\n : ''\n return `\n <div class=\"copy\">\n ${mark}\n ${content.kicker ? `<div class=\"kicker\">${esc(content.kicker)}</div>` : ''}\n <h1>${titleHtml}</h1>\n ${content.subtitle ? `<div class=\"sub\" style=\"color:${esc(muted(theme))}\">${esc(content.subtitle)}</div>` : ''}\n ${content.hand ? `<div class=\"hand\" style=\"font-family:'${esc(handFamily(config))}',cursive\">${esc(content.hand)}</div>` : ''}\n </div>`\n}\n\nexport function buildOgHtml(config: OgCardConfig): string {\n if (config.html) return config.html\n if (config.template) {\n return config.template(config, {\n esc,\n fontLink: fontLink(config),\n width: OG_WIDTH,\n height: OG_HEIGHT,\n })\n }\n\n const { theme, logo, layout = logo ? 'plate-left' : 'banner' } = config\n const display = displayFamily(config)\n\n const plate = logo\n ? `<div class=\"plate\" style=\"background:${esc(theme.surface)}\"><img src=\"${esc(logo)}\"></div>`\n : ''\n\n const photo = config.photo\n ? `<div class=\"photo\" style=\"flex-basis:${config.photo.width ?? 430}px;border-radius:${config.photo.radius ?? 24}px\"><img src=\"${esc(config.photo.src)}\"></div>`\n : ''\n\n const body =\n layout === 'centered'\n ? `<div class=\"wrap centered\">${plate}${copyHtml(config)}</div>`\n : layout === 'banner'\n ? `<div class=\"wrap banner\">${copyHtml(config)}</div>`\n : layout === 'split'\n ? `<div class=\"wrap\">${copyHtml(config)}${photo}</div>`\n : `<div class=\"wrap\">${photo || plate}${copyHtml(config)}</div>`\n\n return `<!doctype html>\n<html><head><meta charset=\"utf-8\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n${fontLink(config)}\n<style>\n * { margin:0; padding:0; box-sizing:border-box; }\n html,body { width:${OG_WIDTH}px; height:${OG_HEIGHT}px; overflow:hidden; }\n body { position:relative; background:${theme.bg}; font-family:'${display}',sans-serif; color:${theme.text}; }\n .bg { position:absolute; inset:0; }\n .bg img { width:100%; height:100%; object-fit:cover; transform:scale(1.05); }\n .scrim { position:absolute; inset:0; }\n .wrap { position:relative; height:100%; display:flex; align-items:center; gap:52px; padding:0 68px ${config.content.bar?.length ? '74px' : '0'}; }\n .wrap.centered { flex-direction:column; justify-content:center; text-align:center; gap:28px; }\n .wrap.banner { padding-left:90px; }\n .plate { border-radius:28px; padding:26px; flex:0 0 auto; }\n .plate img { display:block; width:250px; height:auto; }\n .centered .plate img { width:180px; }\n .photo { flex:0 0 auto; height:76%; overflow:hidden; box-shadow:0 8px 40px rgba(0,0,0,.25); }\n .photo img { width:100%; height:100%; object-fit:cover; display:block; }\n .copy { flex:1 1 auto; min-width:0; }\n /* split-layout brand mark: the photo owns the right pane, so the logo rides\n above the kicker rather than being dropped. */\n .mark { display:block; height:44px; width:auto; margin-bottom:20px; }\n .kicker { font-weight:800; text-transform:uppercase; letter-spacing:.22em; font-size:24px; color:${theme.accent}; margin-bottom:14px; }\n /* Opening size only — the in-page fitter steps this down to fit the frame. */\n h1 { font-weight:800; text-transform:uppercase; font-size:${TITLE_MAX_PX}px; line-height:.9; letter-spacing:-.01em; }\n .sub { margin-top:22px; font-size:29px; font-weight:500; line-height:1.32; }\n /* Never below 40px: script faces are illegible at thumbnail width under that. */\n .hand { font-size:42px; margin-top:14px; transform:rotate(-1.5deg); display:inline-block; }\n .bar { position:absolute; left:0; right:0; bottom:0; height:74px; display:flex; align-items:center; justify-content:center; gap:26px;\n font-weight:800; text-transform:uppercase; letter-spacing:.1em; font-size:31px; color:${theme.surface}; }\n /* nowrap so an over-long segment overflows the bar sideways, where the\n fitter's width check sees it, instead of silently wrapping to a second\n line and spilling over the copy above. */\n .bar > span { white-space:nowrap; }\n .bar .dot { width:10px; height:10px; border-radius:50%; background:currentColor; opacity:.55; flex:0 0 auto; }\n</style></head>\n<body>\n${backgroundLayers(config)}\n${body}\n${barHtml(config.content, theme)}\n${fitScript()}\n</body></html>`\n}\n"]}
@@ -1,23 +1,23 @@
1
1
  'use client';
2
- import { resolveSiteHost, publishSiteCredential } from './chunk-D6CPHVMV.mjs';
3
- import { FleetHeartbeat } from './chunk-WARTV6ZP.mjs';
2
+ import { resolveSiteHost, publishSiteCredential } from './chunk-HP7V2DI3.mjs';
3
+ import { FleetHeartbeat } from './chunk-WX6VKFHW.mjs';
4
4
  import { useDeferredActivation } from './chunk-ZVBLXXNJ.mjs';
5
- import { SignalBridge } from './chunk-YV2TG7JR.mjs';
5
+ import { SignalBridge } from './chunk-BJW6YR6W.mjs';
6
6
  import { getOrCreateVisitorId, getOrCreateSessionId } from './chunk-ZZQAGCSI.mjs';
7
7
  import React, { useState, useRef, useCallback, Suspense } from 'react';
8
8
  import dynamic from 'next/dynamic';
9
9
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
10
 
11
11
  var LazyAnalyticsProvider = dynamic(
12
- () => import('./AnalyticsProvider-EQSG7FGL.mjs').then((m) => ({ default: m.AnalyticsProvider })),
12
+ () => import('./AnalyticsProvider-HVPZPHZI.mjs').then((m) => ({ default: m.AnalyticsProvider })),
13
13
  { ssr: false }
14
14
  );
15
15
  var LazyEngageWidget = dynamic(
16
- () => import('./EngageWidget-BJLSXG2V.mjs').then((m) => ({ default: m.EngageWidget })),
16
+ () => import('./EngageWidget-N7EQGSSL.mjs').then((m) => ({ default: m.EngageWidget })),
17
17
  { ssr: false }
18
18
  );
19
19
  var LazySitemapSync = dynamic(
20
- () => import('./SitemapSync-2HLGUZES.mjs').then((m) => ({ default: m.SitemapSync })),
20
+ () => import('./SitemapSync-2JJOGL7D.mjs').then((m) => ({ default: m.SitemapSync })),
21
21
  { ssr: false }
22
22
  );
23
23
  function SiteKitClientProviders({
@@ -133,5 +133,5 @@ function SiteKitClientProviders({
133
133
  }
134
134
 
135
135
  export { SiteKitClientProviders };
136
- //# sourceMappingURL=chunk-P37FFMOZ.mjs.map
137
- //# sourceMappingURL=chunk-P37FFMOZ.mjs.map
136
+ //# sourceMappingURL=chunk-SSRQHRZB.mjs.map
137
+ //# sourceMappingURL=chunk-SSRQHRZB.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/layout/SiteKitClientProviders.tsx"],"names":[],"mappings":"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","file":"chunk-P37FFMOZ.mjs","sourcesContent":["'use client'\n\n/**\n * SiteKitClientProviders — Client Island\n *\n * This is the 'use client' boundary for SiteKitLayout.\n * It composes the client-only providers (Analytics, Engage, Signal, SitemapSync)\n * that require React context or browser APIs.\n *\n * All modules are lazy-loaded via next/dynamic so their JS is only downloaded\n * when the feature is enabled — keeping the initial bundle lightweight.\n *\n * All modules share a unified identity (visitor ID + session ID) read from\n * the storage-backed singleton (shared/identity), ensuring consistent\n * tracking across Analytics, Signal, and Engage.\n *\n * 4.0 INVARIANT — CHILDREN ARE NEVER WRAPPED. Every module here renders as a\n * childless sibling AFTER {children}; nothing provides context around page\n * content. That makes the prerender-bailout / remount-flash class of bug\n * structurally impossible. Do not reintroduce a wrapper.\n *\n * Props are passed down from the server-side SiteKitLayout component,\n * which reads env vars at render time.\n */\n\nimport React, { Suspense, useCallback, useRef, useState, type ReactNode } from 'react'\nimport dynamic from 'next/dynamic'\nimport {\n getOrCreateVisitorId,\n getOrCreateSessionId,\n} from '../shared/identity'\n// Defer non-critical module mounting until the page is quiet: the lazy\n// modules below are dynamic(ssr:false) and never render on the server, but\n// rendering them during hydration still kicks off their chunk downloads +\n// mount effects inside the critical startup window. useDeferredActivation\n// holds them until window.load + idle or first interaction — the same\n// strategy as the hand-rolled per-site deferrals this replaces (upforge\n// `deferred-analytics.tsx`, TPS `DeferredAnalytics.tsx`). The hook is public\n// (`@sonordev/site-kit/client`) so sites can reuse it for their own scripts.\nimport { useDeferredActivation } from '../shared/defer'\n// Single source of truth for seeding the token manager + the __SITE_KIT_*\n// globals — shared with the public `SiteKitCredential` component that\n// non-core layouts (agency-site-kit) use. Do not re-inline it here.\nimport { publishSiteCredential, resolveSiteHost } from '../shared/publishCredential'\nimport type { AnalyticsConfig, EngageConfig, SignalConfig } from './types'\n\n// Lazy-load feature modules — JS only downloaded when feature is enabled\nconst LazyAnalyticsProvider = dynamic(\n () => import('../analytics/AnalyticsProvider').then(m => ({ default: m.AnalyticsProvider })),\n { ssr: false },\n)\nconst LazyEngageWidget = dynamic(\n () => import('../engage/EngageWidget').then(m => ({ default: m.EngageWidget })),\n { ssr: false },\n)\nconst LazySitemapSync = dynamic(\n () => import('../SitemapSync').then(m => ({ default: m.SitemapSync })),\n { ssr: false },\n)\n// SignalBridge is imported STATICALLY, not via dynamic(ssr:false): it is a\n// tiny SSR-safe context shell that renders children immediately and\n// lazy-loads its machinery (SignalCore) as a childless sibling internally.\n// Wrapping children in a dynamic(ssr:false) component was the old SSR-bailout\n// failure mode — do not reintroduce it.\nimport { SignalBridge } from '../signal/SignalBridge'\n// Fleet heartbeat: a deferred, CHILDLESS SIBLING (like Analytics/Engage) that\n// reports this site's kit version + active modules once per session, so the\n// platform can see version drift across the fleet. Tiny and SSR-safe — renders\n// null until the shared idle gate fires; never wraps page content.\nimport { FleetHeartbeat } from '../fleet/FleetHeartbeat'\nimport type { FleetModule } from '../fleet/contract'\n\nexport interface SiteKitClientProvidersProps {\n children: ReactNode\n apiKey: string\n apiUrl: string\n projectId?: string\n analytics?: boolean | AnalyticsConfig\n engage?: boolean | EngageConfig\n signal?: boolean | SignalConfig\n /**\n * Runtime SitemapSync from the visitor's browser. Default FALSE — sitemap\n * registration belongs to build time and to sonor-api's nightly reconciler,\n * not to whoever happens to load the page. See SiteKitLayout for the full\n * reasoning; in short, its guards live in session/localStorage, so every new\n * visitor missed them and re-POSTed the whole sitemap.\n */\n sitemapSync?: boolean\n /**\n * Fleet heartbeat — reports kit version + active modules to Sonor once per\n * session so the platform can track version drift and key state across every\n * site. Default true. Deferred exactly like Analytics; never in the hydration\n * path, carries no visitor data.\n */\n fleet?: boolean\n /**\n * Defer Analytics / Engage / SitemapSync until window load + idle or first\n * interaction (default true). Pass false to restore legacy eager mounting\n * on hydration. SignalBridge is never deferred — it must apply experiment\n * variants as early as possible.\n */\n defer?: boolean\n debug?: boolean\n /** Browser-safe Google Maps key, resolved server-side by SiteKitLayout. */\n mapsBrowserKey?: string\n}\n\nexport function SiteKitClientProviders({\n children,\n apiKey,\n apiUrl,\n projectId,\n analytics = true,\n engage = true,\n signal = false,\n sitemapSync = false,\n fleet = true,\n defer = true,\n debug = false,\n mapsBrowserKey,\n}: SiteKitClientProvidersProps) {\n // One shared activation gate for all deferred siblings, so they mount in a\n // single commit instead of staggered re-renders.\n const modulesReady = useDeferredActivation(defer)\n // Hoist analytics config so the `site` dimension is resolvable up here.\n const analyticsConfig: AnalyticsConfig =\n typeof analytics === 'object' ? analytics : {}\n\n // Resolve the sub-site identifier — the dimension that lets one Sonor project\n // host many sites (e.g. truepowersystems.com + ohiopowerstudies.com under one\n // TPS project). Precedence lives in resolveSiteHost (shared contract):\n // explicit `analytics.site` > NEXT_PUBLIC_SITE_URL host > window.location.host.\n // Needed as a local too — SignalBridge and the analytics config read it below.\n const site = resolveSiteHost(analyticsConfig.site)\n\n // Seed the token manager + publish the window globals modules read from.\n // No-op on the server; runs during render so it lands before child effects.\n publishSiteCredential({ apiKey, apiUrl, site, debug, mapsBrowserKey })\n\n // Unified identity — single source of truth for all modules\n const [visitorId] = useState(() => getOrCreateVisitorId())\n const [sessionId] = useState(() => getOrCreateSessionId())\n\n // Page metadata bridge: Analytics writes, Signal reads\n const pageMetadataRef = useRef<Record<string, unknown> | null>(null)\n const onPageMetadata = useCallback((metadata: Record<string, unknown>) => {\n pageMetadataRef.current = metadata\n }, [])\n\n // Children render FIRST and are never re-parented. Siblings accumulate after.\n const siblings: ReactNode[] = []\n\n // SignalBridge — childless sibling (4.0: useSignal reads the module store,\n // so it no longer needs to wrap anything). Not gated on modulesReady:\n // experiment variants must apply as early as possible.\n if (signal) {\n const signalConfig: SignalConfig =\n typeof signal === 'object' ? signal : {}\n siblings.push(\n <SignalBridge\n key=\"sk-signal\"\n enabled\n realtime={signalConfig.realtime !== false}\n experiments={signalConfig.experiments !== false}\n behaviorTracking={signalConfig.behaviorTracking !== false}\n visitorId={visitorId}\n sessionId={sessionId}\n pageMetadataRef={pageMetadataRef}\n />,\n )\n }\n\n // Add Analytics if enabled (renders alongside, doesn't wrap).\n //\n // CRITICAL: LazyAnalyticsProvider is dynamic(..., { ssr: false }). Wrapping\n // {content} inside an ssr:false component makes the entire page tree skip\n // server rendering — the route de-opts to 100% client rendering and the\n // hero / LCP element only paints after hydration (mobile LCP ~4s, Lighthouse\n // ~85). Analytics tracks via effects + a metadata ref, so it never needs to\n // wrap page content — mount it as a childless sibling, exactly like Engage.\n // Each deferred sibling is appended as `{modulesReady && <Module/>}` so the\n // children slot never moves in the tree — flipping ready only mounts the\n // sibling, it never remounts page content.\n if (analytics) {\n siblings.push(\n <React.Fragment key=\"sk-analytics\">\n {modulesReady && (\n <Suspense fallback={null}>\n <LazyAnalyticsProvider\n apiUrl={apiUrl}\n apiKey={apiKey}\n site={site}\n trackPageViews={analyticsConfig.trackPageViews !== false}\n trackWebVitals={analyticsConfig.trackWebVitals !== false}\n trackScrollDepth={analyticsConfig.trackScrollDepth !== false}\n trackClicks={analyticsConfig.trackClicks !== false}\n sessionTimeout={analyticsConfig.sessionDuration}\n excludePaths={analyticsConfig.excludePaths}\n debug={debug}\n externalVisitorId={visitorId}\n externalSessionId={sessionId}\n onPageMetadata={onPageMetadata}\n />\n </Suspense>\n )}\n </React.Fragment>,\n )\n }\n\n // Engage widget — childless sibling\n if (engage) {\n const engageConfig: EngageConfig =\n typeof engage === 'object' ? engage : {}\n siblings.push(\n <React.Fragment key=\"sk-engage\">\n {modulesReady && (\n <LazyEngageWidget\n apiUrl={apiUrl}\n apiKey={apiKey}\n projectId={projectId}\n position={engageConfig.position || 'bottom-right'}\n zIndex={engageConfig.zIndex}\n chatEnabled={engageConfig.chatEnabled !== false}\n />\n )}\n </React.Fragment>,\n )\n }\n\n // SitemapSync — childless sibling\n if (sitemapSync) {\n siblings.push(\n <React.Fragment key=\"sk-sitemap\">\n {modulesReady && <LazySitemapSync debug={debug} />}\n </React.Fragment>,\n )\n }\n\n // Fleet heartbeat — childless sibling on the same modulesReady idle gate.\n // Reports the modules actually enabled on THIS layout (honest client-observable\n // set) so the platform can answer \"which version / modules is each site\n // running\". defer={false}: the modulesReady gate already handled deferral, so\n // the heartbeat fires on mount (still bounded to once per session internally).\n if (fleet) {\n const activeModules: FleetModule[] = []\n if (analytics) activeModules.push('analytics')\n if (engage) activeModules.push('engage')\n if (signal) activeModules.push('signal')\n if (sitemapSync) activeModules.push('sitemapSync')\n siblings.push(\n <React.Fragment key=\"sk-fleet\">\n {modulesReady && (\n <FleetHeartbeat\n apiUrl={apiUrl}\n apiKey={apiKey}\n site={site}\n modules={activeModules}\n defer={false}\n debug={debug}\n />\n )}\n </React.Fragment>,\n )\n }\n\n return (\n <>\n {children}\n {siblings}\n </>\n )\n}\n"]}
1
+ {"version":3,"sources":["../src/layout/SiteKitClientProviders.tsx"],"names":[],"mappings":"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","file":"chunk-SSRQHRZB.mjs","sourcesContent":["'use client'\n\n/**\n * SiteKitClientProviders — Client Island\n *\n * This is the 'use client' boundary for SiteKitLayout.\n * It composes the client-only providers (Analytics, Engage, Signal, SitemapSync)\n * that require React context or browser APIs.\n *\n * All modules are lazy-loaded via next/dynamic so their JS is only downloaded\n * when the feature is enabled — keeping the initial bundle lightweight.\n *\n * All modules share a unified identity (visitor ID + session ID) read from\n * the storage-backed singleton (shared/identity), ensuring consistent\n * tracking across Analytics, Signal, and Engage.\n *\n * 4.0 INVARIANT — CHILDREN ARE NEVER WRAPPED. Every module here renders as a\n * childless sibling AFTER {children}; nothing provides context around page\n * content. That makes the prerender-bailout / remount-flash class of bug\n * structurally impossible. Do not reintroduce a wrapper.\n *\n * Props are passed down from the server-side SiteKitLayout component,\n * which reads env vars at render time.\n */\n\nimport React, { Suspense, useCallback, useRef, useState, type ReactNode } from 'react'\nimport dynamic from 'next/dynamic'\nimport {\n getOrCreateVisitorId,\n getOrCreateSessionId,\n} from '../shared/identity'\n// Defer non-critical module mounting until the page is quiet: the lazy\n// modules below are dynamic(ssr:false) and never render on the server, but\n// rendering them during hydration still kicks off their chunk downloads +\n// mount effects inside the critical startup window. useDeferredActivation\n// holds them until window.load + idle or first interaction — the same\n// strategy as the hand-rolled per-site deferrals this replaces (upforge\n// `deferred-analytics.tsx`, TPS `DeferredAnalytics.tsx`). The hook is public\n// (`@sonordev/site-kit/client`) so sites can reuse it for their own scripts.\nimport { useDeferredActivation } from '../shared/defer'\n// Single source of truth for seeding the token manager + the __SITE_KIT_*\n// globals — shared with the public `SiteKitCredential` component that\n// non-core layouts (agency-site-kit) use. Do not re-inline it here.\nimport { publishSiteCredential, resolveSiteHost } from '../shared/publishCredential'\nimport type { AnalyticsConfig, EngageConfig, SignalConfig } from './types'\n\n// Lazy-load feature modules — JS only downloaded when feature is enabled\nconst LazyAnalyticsProvider = dynamic(\n () => import('../analytics/AnalyticsProvider').then(m => ({ default: m.AnalyticsProvider })),\n { ssr: false },\n)\nconst LazyEngageWidget = dynamic(\n () => import('../engage/EngageWidget').then(m => ({ default: m.EngageWidget })),\n { ssr: false },\n)\nconst LazySitemapSync = dynamic(\n () => import('../SitemapSync').then(m => ({ default: m.SitemapSync })),\n { ssr: false },\n)\n// SignalBridge is imported STATICALLY, not via dynamic(ssr:false): it is a\n// tiny SSR-safe context shell that renders children immediately and\n// lazy-loads its machinery (SignalCore) as a childless sibling internally.\n// Wrapping children in a dynamic(ssr:false) component was the old SSR-bailout\n// failure mode — do not reintroduce it.\nimport { SignalBridge } from '../signal/SignalBridge'\n// Fleet heartbeat: a deferred, CHILDLESS SIBLING (like Analytics/Engage) that\n// reports this site's kit version + active modules once per session, so the\n// platform can see version drift across the fleet. Tiny and SSR-safe — renders\n// null until the shared idle gate fires; never wraps page content.\nimport { FleetHeartbeat } from '../fleet/FleetHeartbeat'\nimport type { FleetModule } from '../fleet/contract'\n\nexport interface SiteKitClientProvidersProps {\n children: ReactNode\n apiKey: string\n apiUrl: string\n projectId?: string\n analytics?: boolean | AnalyticsConfig\n engage?: boolean | EngageConfig\n signal?: boolean | SignalConfig\n /**\n * Runtime SitemapSync from the visitor's browser. Default FALSE — sitemap\n * registration belongs to build time and to sonor-api's nightly reconciler,\n * not to whoever happens to load the page. See SiteKitLayout for the full\n * reasoning; in short, its guards live in session/localStorage, so every new\n * visitor missed them and re-POSTed the whole sitemap.\n */\n sitemapSync?: boolean\n /**\n * Fleet heartbeat — reports kit version + active modules to Sonor once per\n * session so the platform can track version drift and key state across every\n * site. Default true. Deferred exactly like Analytics; never in the hydration\n * path, carries no visitor data.\n */\n fleet?: boolean\n /**\n * Defer Analytics / Engage / SitemapSync until window load + idle or first\n * interaction (default true). Pass false to restore legacy eager mounting\n * on hydration. SignalBridge is never deferred — it must apply experiment\n * variants as early as possible.\n */\n defer?: boolean\n debug?: boolean\n /** Browser-safe Google Maps key, resolved server-side by SiteKitLayout. */\n mapsBrowserKey?: string\n}\n\nexport function SiteKitClientProviders({\n children,\n apiKey,\n apiUrl,\n projectId,\n analytics = true,\n engage = true,\n signal = false,\n sitemapSync = false,\n fleet = true,\n defer = true,\n debug = false,\n mapsBrowserKey,\n}: SiteKitClientProvidersProps) {\n // One shared activation gate for all deferred siblings, so they mount in a\n // single commit instead of staggered re-renders.\n const modulesReady = useDeferredActivation(defer)\n // Hoist analytics config so the `site` dimension is resolvable up here.\n const analyticsConfig: AnalyticsConfig =\n typeof analytics === 'object' ? analytics : {}\n\n // Resolve the sub-site identifier — the dimension that lets one Sonor project\n // host many sites (e.g. truepowersystems.com + ohiopowerstudies.com under one\n // TPS project). Precedence lives in resolveSiteHost (shared contract):\n // explicit `analytics.site` > NEXT_PUBLIC_SITE_URL host > window.location.host.\n // Needed as a local too — SignalBridge and the analytics config read it below.\n const site = resolveSiteHost(analyticsConfig.site)\n\n // Seed the token manager + publish the window globals modules read from.\n // No-op on the server; runs during render so it lands before child effects.\n publishSiteCredential({ apiKey, apiUrl, site, debug, mapsBrowserKey })\n\n // Unified identity — single source of truth for all modules\n const [visitorId] = useState(() => getOrCreateVisitorId())\n const [sessionId] = useState(() => getOrCreateSessionId())\n\n // Page metadata bridge: Analytics writes, Signal reads\n const pageMetadataRef = useRef<Record<string, unknown> | null>(null)\n const onPageMetadata = useCallback((metadata: Record<string, unknown>) => {\n pageMetadataRef.current = metadata\n }, [])\n\n // Children render FIRST and are never re-parented. Siblings accumulate after.\n const siblings: ReactNode[] = []\n\n // SignalBridge — childless sibling (4.0: useSignal reads the module store,\n // so it no longer needs to wrap anything). Not gated on modulesReady:\n // experiment variants must apply as early as possible.\n if (signal) {\n const signalConfig: SignalConfig =\n typeof signal === 'object' ? signal : {}\n siblings.push(\n <SignalBridge\n key=\"sk-signal\"\n enabled\n realtime={signalConfig.realtime !== false}\n experiments={signalConfig.experiments !== false}\n behaviorTracking={signalConfig.behaviorTracking !== false}\n visitorId={visitorId}\n sessionId={sessionId}\n pageMetadataRef={pageMetadataRef}\n />,\n )\n }\n\n // Add Analytics if enabled (renders alongside, doesn't wrap).\n //\n // CRITICAL: LazyAnalyticsProvider is dynamic(..., { ssr: false }). Wrapping\n // {content} inside an ssr:false component makes the entire page tree skip\n // server rendering — the route de-opts to 100% client rendering and the\n // hero / LCP element only paints after hydration (mobile LCP ~4s, Lighthouse\n // ~85). Analytics tracks via effects + a metadata ref, so it never needs to\n // wrap page content — mount it as a childless sibling, exactly like Engage.\n // Each deferred sibling is appended as `{modulesReady && <Module/>}` so the\n // children slot never moves in the tree — flipping ready only mounts the\n // sibling, it never remounts page content.\n if (analytics) {\n siblings.push(\n <React.Fragment key=\"sk-analytics\">\n {modulesReady && (\n <Suspense fallback={null}>\n <LazyAnalyticsProvider\n apiUrl={apiUrl}\n apiKey={apiKey}\n site={site}\n trackPageViews={analyticsConfig.trackPageViews !== false}\n trackWebVitals={analyticsConfig.trackWebVitals !== false}\n trackScrollDepth={analyticsConfig.trackScrollDepth !== false}\n trackClicks={analyticsConfig.trackClicks !== false}\n sessionTimeout={analyticsConfig.sessionDuration}\n excludePaths={analyticsConfig.excludePaths}\n debug={debug}\n externalVisitorId={visitorId}\n externalSessionId={sessionId}\n onPageMetadata={onPageMetadata}\n />\n </Suspense>\n )}\n </React.Fragment>,\n )\n }\n\n // Engage widget — childless sibling\n if (engage) {\n const engageConfig: EngageConfig =\n typeof engage === 'object' ? engage : {}\n siblings.push(\n <React.Fragment key=\"sk-engage\">\n {modulesReady && (\n <LazyEngageWidget\n apiUrl={apiUrl}\n apiKey={apiKey}\n projectId={projectId}\n position={engageConfig.position || 'bottom-right'}\n zIndex={engageConfig.zIndex}\n chatEnabled={engageConfig.chatEnabled !== false}\n />\n )}\n </React.Fragment>,\n )\n }\n\n // SitemapSync — childless sibling\n if (sitemapSync) {\n siblings.push(\n <React.Fragment key=\"sk-sitemap\">\n {modulesReady && <LazySitemapSync debug={debug} />}\n </React.Fragment>,\n )\n }\n\n // Fleet heartbeat — childless sibling on the same modulesReady idle gate.\n // Reports the modules actually enabled on THIS layout (honest client-observable\n // set) so the platform can answer \"which version / modules is each site\n // running\". defer={false}: the modulesReady gate already handled deferral, so\n // the heartbeat fires on mount (still bounded to once per session internally).\n if (fleet) {\n const activeModules: FleetModule[] = []\n if (analytics) activeModules.push('analytics')\n if (engage) activeModules.push('engage')\n if (signal) activeModules.push('signal')\n if (sitemapSync) activeModules.push('sitemapSync')\n siblings.push(\n <React.Fragment key=\"sk-fleet\">\n {modulesReady && (\n <FleetHeartbeat\n apiUrl={apiUrl}\n apiKey={apiKey}\n site={site}\n modules={activeModules}\n defer={false}\n debug={debug}\n />\n )}\n </React.Fragment>,\n )\n }\n\n return (\n <>\n {children}\n {siblings}\n </>\n )\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { sonorFetch } from './chunk-K7XOPTOE.mjs';
2
+ import { sonorFetch } from './chunk-XWBQLPTZ.mjs';
3
3
  import { useState, useCallback, useEffect } from 'react';
4
4
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
5
 
@@ -521,5 +521,5 @@ async function clearImageSlot(config, slotId, pagePath) {
521
521
  }
522
522
 
523
523
  export { ManagedImage, assignImageToSlot, clearImageSlot, fetchManagedImage, fetchManagedImages, listImageFiles, uploadImage };
524
- //# sourceMappingURL=chunk-WNK2OK5G.mjs.map
525
- //# sourceMappingURL=chunk-WNK2OK5G.mjs.map
524
+ //# sourceMappingURL=chunk-SYR7YSB2.mjs.map
525
+ //# sourceMappingURL=chunk-SYR7YSB2.mjs.map