@sonordev/site-kit 2.8.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/EngageWidget-LRE4VD7O.js +14 -0
  2. package/dist/{EngageWidget-JIXUBG47.js.map → EngageWidget-LRE4VD7O.js.map} +1 -1
  3. package/dist/EngageWidget-OAOLB6ED.mjs +5 -0
  4. package/dist/{EngageWidget-4W5EM4UO.mjs.map → EngageWidget-OAOLB6ED.mjs.map} +1 -1
  5. package/dist/blog/index.js +2 -1
  6. package/dist/blog/index.js.map +1 -1
  7. package/dist/blog/index.mjs +2 -1
  8. package/dist/blog/index.mjs.map +1 -1
  9. package/dist/chunk-2FEQ6TYD.mjs +37 -0
  10. package/dist/chunk-2FEQ6TYD.mjs.map +1 -0
  11. package/dist/{chunk-5SQ4NRPH.mjs → chunk-3AEOGV6V.mjs} +6 -4
  12. package/dist/chunk-3AEOGV6V.mjs.map +1 -0
  13. package/dist/chunk-662YHKWW.mjs +10 -0
  14. package/dist/chunk-662YHKWW.mjs.map +1 -0
  15. package/dist/{chunk-MNOVPHL6.mjs → chunk-72MRAYAS.mjs} +19 -14
  16. package/dist/chunk-72MRAYAS.mjs.map +1 -0
  17. package/dist/{chunk-5QLM6LZ2.mjs → chunk-APRAGA5G.mjs} +3 -3
  18. package/dist/{chunk-5QLM6LZ2.mjs.map → chunk-APRAGA5G.mjs.map} +1 -1
  19. package/dist/{chunk-MWE2HRPU.js → chunk-D5XTX5EB.js} +19 -14
  20. package/dist/chunk-D5XTX5EB.js.map +1 -0
  21. package/dist/{chunk-HANRPNKG.js → chunk-FMI65E5Y.js} +3 -3
  22. package/dist/{chunk-HANRPNKG.js.map → chunk-FMI65E5Y.js.map} +1 -1
  23. package/dist/{chunk-SLJJJWMV.js → chunk-FPU2QPO4.js} +338 -33
  24. package/dist/chunk-FPU2QPO4.js.map +1 -0
  25. package/dist/{chunk-PKUTYMN7.js → chunk-J25JPU77.js} +2 -2
  26. package/dist/chunk-J25JPU77.js.map +1 -0
  27. package/dist/chunk-J7QI3GDX.js +12 -0
  28. package/dist/chunk-J7QI3GDX.js.map +1 -0
  29. package/dist/chunk-MLJXELPA.js +693 -0
  30. package/dist/chunk-MLJXELPA.js.map +1 -0
  31. package/dist/{chunk-L7XPPFSS.mjs → chunk-QDKQZ5XH.mjs} +2 -2
  32. package/dist/chunk-QDKQZ5XH.mjs.map +1 -0
  33. package/dist/{chunk-IAV32YHL.mjs → chunk-RV45YMBJ.mjs} +338 -33
  34. package/dist/chunk-RV45YMBJ.mjs.map +1 -0
  35. package/dist/chunk-TCVHI3JP.mjs +686 -0
  36. package/dist/chunk-TCVHI3JP.mjs.map +1 -0
  37. package/dist/{chunk-PAF5IGGF.js → chunk-XNUPJC6R.js} +6 -4
  38. package/dist/chunk-XNUPJC6R.js.map +1 -0
  39. package/dist/chunk-YBKALLBH.js +46 -0
  40. package/dist/chunk-YBKALLBH.js.map +1 -0
  41. package/dist/engage/index.d.mts +2 -2
  42. package/dist/engage/index.d.ts +2 -2
  43. package/dist/engage/index.js +5 -4
  44. package/dist/engage/index.js.map +1 -1
  45. package/dist/engage/index.mjs +2 -1
  46. package/dist/engage/index.mjs.map +1 -1
  47. package/dist/forms/index.js +19 -683
  48. package/dist/forms/index.js.map +1 -1
  49. package/dist/forms/index.mjs +4 -676
  50. package/dist/forms/index.mjs.map +1 -1
  51. package/dist/index.d.mts +2 -2
  52. package/dist/index.d.ts +2 -2
  53. package/dist/layout/client.js +2 -2
  54. package/dist/layout/client.mjs +1 -1
  55. package/dist/layout/index.js +5 -5
  56. package/dist/layout/index.mjs +2 -2
  57. package/dist/llms/index.d.mts +18 -4
  58. package/dist/llms/index.d.ts +18 -4
  59. package/dist/llms/index.js +26 -51
  60. package/dist/llms/index.js.map +1 -1
  61. package/dist/llms/index.mjs +16 -41
  62. package/dist/llms/index.mjs.map +1 -1
  63. package/dist/middleware/index.d.mts +62 -1
  64. package/dist/middleware/index.d.ts +62 -1
  65. package/dist/middleware/index.js +105 -0
  66. package/dist/middleware/index.js.map +1 -1
  67. package/dist/middleware/index.mjs +103 -1
  68. package/dist/middleware/index.mjs.map +1 -1
  69. package/dist/seo/index.d.mts +10 -205
  70. package/dist/seo/index.d.ts +10 -205
  71. package/dist/seo/index.js +16 -15
  72. package/dist/seo/index.js.map +1 -1
  73. package/dist/seo/index.mjs +7 -6
  74. package/dist/seo/index.mjs.map +1 -1
  75. package/dist/seo/server.d.mts +2 -106
  76. package/dist/seo/server.d.ts +2 -106
  77. package/dist/seo/server.js +56 -197
  78. package/dist/seo/server.js.map +1 -1
  79. package/dist/seo/server.mjs +2 -183
  80. package/dist/seo/server.mjs.map +1 -1
  81. package/dist/server-l6LMNvF1.d.ts +360 -0
  82. package/dist/server-mX5At_6x.d.mts +360 -0
  83. package/dist/sitemap/index.d.mts +1 -1
  84. package/dist/sitemap/index.d.ts +1 -1
  85. package/dist/sitemap/index.js +2 -2
  86. package/dist/sitemap/index.mjs +1 -1
  87. package/dist/slots/contract.d.mts +83 -0
  88. package/dist/slots/contract.d.ts +83 -0
  89. package/dist/slots/contract.js +41 -0
  90. package/dist/slots/contract.js.map +1 -0
  91. package/dist/slots/contract.mjs +4 -0
  92. package/dist/slots/contract.mjs.map +1 -0
  93. package/dist/slots/index.d.mts +104 -0
  94. package/dist/slots/index.d.ts +104 -0
  95. package/dist/slots/index.js +186 -0
  96. package/dist/slots/index.js.map +1 -0
  97. package/dist/slots/index.mjs +146 -0
  98. package/dist/slots/index.mjs.map +1 -0
  99. package/dist/{types-DYyIAgQg.d.ts → types-DPsbBKiu.d.mts} +1 -1
  100. package/dist/{types-DYyIAgQg.d.mts → types-DPsbBKiu.d.ts} +1 -1
  101. package/dist/{types-BnCwwUX3.d.mts → types-D_1iUxOQ.d.mts} +18 -1
  102. package/dist/{types-BnCwwUX3.d.ts → types-D_1iUxOQ.d.ts} +18 -1
  103. package/dist/{types-J7Z_FqmV.d.mts → types-DhCAWUmd.d.mts} +8 -0
  104. package/dist/{types-0NuBL1Gg.d.ts → types-xcP_owwn.d.ts} +8 -0
  105. package/package.json +11 -1
  106. package/dist/EngageWidget-4W5EM4UO.mjs +0 -4
  107. package/dist/EngageWidget-JIXUBG47.js +0 -13
  108. package/dist/chunk-5SQ4NRPH.mjs.map +0 -1
  109. package/dist/chunk-IAV32YHL.mjs.map +0 -1
  110. package/dist/chunk-L7XPPFSS.mjs.map +0 -1
  111. package/dist/chunk-MNOVPHL6.mjs.map +0 -1
  112. package/dist/chunk-MWE2HRPU.js.map +0 -1
  113. package/dist/chunk-PAF5IGGF.js.map +0 -1
  114. package/dist/chunk-PKUTYMN7.js.map +0 -1
  115. package/dist/chunk-SLJJJWMV.js.map +0 -1
  116. package/dist/routing-C7gmHWm9.d.ts +0 -163
  117. package/dist/routing-trNzR1Pz.d.mts +0 -163
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/slots/server-api.ts","../../src/slots/ManagedSlot.tsx","../../src/slots/revalidate.ts"],"names":["isValidSlotId","SLOTS_CONTRACT_VERSION","SLOT_CONTENT_MAX_LENGTH","verifySlotContent","cache","jsx","Fragment"],"mappings":";;;;;;;;;;AA4CA,SAAS,YAAA,GAA0D;AACjE,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,aAAA,IAAiB,sBAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,aAAA,IAAiB,EAAA;AAC5C,EAAA,IAAI,CAAC,QAAQ,OAAO,IAAA;AACpB,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC1B;AAEA,IAAM,MAAA,uBAAa,GAAA,EAAY;AAC/B,SAAS,QAAA,CAAS,KAAa,OAAA,EAAuB;AACpD,EAAA,IAAI,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA,EAAG;AACrB,EAAA,MAAA,CAAO,IAAI,GAAG,CAAA;AACd,EAAA,OAAA,CAAQ,KAAK,OAAO,CAAA;AACtB;AAEA,eAAe,SAAA,CACb,EAAA,EACA,IAAA,EACA,UAAA,EACA,UACA,IAAA,EAC6B;AAC7B,EAAA,IAAI,CAACA,8BAAA,CAAc,EAAE,CAAA,EAAG;AACtB,IAAA,QAAA,CAAS,CAAA,GAAA,EAAM,EAAE,CAAA,CAAA,EAAI,CAAA,kCAAA,EAAqC,EAAE,CAAA,YAAA,EAAe,GAAG,CAAA,YAAA,EAAe,GAAG,CAAA,sCAAA,CAAmC,CAAA;AACnI,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,SAAS,YAAA,EAAa;AAC5B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,QAAA,CAAS,UAAU,mFAA8E,CAAA;AACjG,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,WAAW,MAAM,KAAA,CAAM,CAAA,EAAG,MAAA,CAAO,MAAM,CAAA,yBAAA,CAAA,EAA6B;AAAA,MACxE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,aAAa,MAAA,CAAO;AAAA,OACtB;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACnB,gBAAA,EAAkBC,uCAAA;AAAA,QAClB,KAAA,EAAO;AAAA,UACL;AAAA,YACE,EAAA;AAAA,YACA,GAAI,QAAA,GAAW,EAAE,QAAA,EAAU,QAAA,CAAS,MAAM,CAAA,EAAGC,wCAAuB,CAAA,EAAE,GAAI,EAAC;AAAA,YAC3E,GAAI,IAAA,GAAO,EAAE,IAAA,KAAS;AAAC;AACzB,SACF;AAAA,QACA,GAAI,IAAA,GAAO,EAAE,IAAA,KAAS;AAAC,OACxB,CAAA;AAAA,MACD,MAAM,EAAE,UAAA,EAAY,IAAA,EAAM,CAAC,aAAa,CAAA;AAAE,KAC5B,CAAA;AAEhB,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAEhB,MAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,QAAA,QAAA;AAAA,UACE,CAAA,KAAA,EAAQ,SAAS,MAAM,CAAA,CAAA;AAAA,UACvB,CAAA,kCAAA,EAAqC,SAAS,MAAM,CAAA,2BAAA;AAAA,SACtD;AAAA,MACF;AACA,MAAA,MAAM,QAAA,CAAS,IAAA,EAAK,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AACpC,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,MAAM,IAAA,GAAQ,MAAM,QAAA,CAAS,IAAA,EAAK;AAClC,IAAA,MAAM,OAAA,GAAU,IAAA,EAAM,KAAA,GAAQ,EAAE,CAAA;AAChC,IAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AAErB,IAAA,IAAI,CAACC,kCAAA,CAAkB,OAAA,EAAS,MAAA,CAAO,MAAM,CAAA,EAAG;AAC9C,MAAA,QAAA;AAAA,QACE,OAAO,EAAE,CAAA,CAAA;AAAA,QACT,kCAAkC,EAAE,CAAA,wEAAA;AAAA,OACtC;AACA,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,OAAO,OAAA;AAAA,EACT,SAAS,GAAA,EAAK;AACZ,IAAA,QAAA,CAAS,SAAA,EAAW,CAAA,iCAAA,EAAqC,GAAA,EAAe,OAAO,CAAA,4BAAA,CAAyB,CAAA;AACxG,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAOA,IAAM,aAAA,GAAgBC,WAAA;AAAA,EACpB,CACE,EAAA,EACA,IAAA,EACA,UAAA,EACA,QAAA,EACA,IAAA,KACG,SAAA,CAAU,EAAA,EAAI,IAAA,EAAM,UAAA,EAAY,QAAA,EAAU,IAAI;AACrD,CAAA;AAGO,SAAS,OAAA,CAAQ,EAAA,EAAY,OAAA,GAA8B,EAAC,EAAgC;AACjG,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,QAAA,EAAU,IAAA,EAAK,IAAK,MAAA;AAC7C,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,EAAM,IAAA,EAAK,IAAK,MAAA;AACrC,EAAA,OAAO,aAAA,CAAc,IAAI,OAAA,CAAQ,IAAA,EAAM,QAAQ,UAAA,IAAc,GAAA,EAAK,UAAU,IAAI,CAAA;AAClF;AAQA,eAAsB,YAAA,CACpB,GAAA,EACA,OAAA,GAA8B,EAAC,EACc;AAC7C,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,GAAA;AAAA,IAC5B,GAAA,CAAI,GAAA,CAAI,OAAO,EAAA,KAAO,CAAC,EAAA,EAAI,MAAM,OAAA,CAAQ,EAAA,EAAI,OAAO,CAAC,CAAU;AAAA,GACjE;AACA,EAAA,OAAO,MAAA,CAAO,YAAY,OAAO,CAAA;AACnC;ACpIA,eAAsB,WAAA,CAAY;AAAA,EAChC,EAAA;AAAA,EACA,QAAA;AAAA,EACA,IAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAAkD;AAGhD,EAAA,MAAM,QAAA,GAAW,OAAO,QAAA,KAAa,QAAA,GAAW,QAAA,GAAW,MAAA;AAC3D,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,EAAA,EAAI,EAAE,IAAA,EAAM,UAAA,EAAY,QAAA,EAAU,IAAA,EAAM,CAAA;AAEtE,EAAA,uBAAOC,cAAA,CAAAC,mBAAA,EAAA,EAAG,QAAA,EAAA,OAAA,GAAU,OAAA,CAAQ,OAAA,GAAU,QAAA,EAAS,CAAA;AACjD;AAEA,IAAO,mBAAA,GAAQ;;;AC7BR,IAAM,eAAA,GAAkB;AAOxB,SAAS,4BAAA,CACd,QACA,OAAA,EAMyC;AACzC,EAAA,OAAO,eAAe,KAAK,OAAA,EAAqC;AAC9D,IAAA,IAAI;AACF,MAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA;AACtD,MAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAC/B,MAAA,MAAM,cAAA,GAAiB,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,QAAQ,CAAA;AACpD,MAAA,MAAM,KAAA,GAAQ,UAAA,EAAY,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA,IAAK,cAAA;AAExD,MAAA,IAAI,CAAC,MAAA,IAAU,KAAA,KAAU,MAAA,EAAQ;AAC/B,QAAA,OAAO,IAAI,SAAS,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,gBAAA,EAAkB,CAAA,EAAG;AAAA,UAC/D,MAAA,EAAQ,GAAA;AAAA,UACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA;AAAmB,SAC/C,CAAA;AAAA,MACH;AAEA,MAAA,IAAI,aAAA;AACJ,MAAA,IAAI,SAAS,aAAA,EAAe;AAC1B,QAAA,aAAA,GAAgB,OAAA,CAAQ,aAAA;AAAA,MAC1B,CAAA,MAAO;AAEL,QAAA,MAAM,SAAA,GAAY,MAAM,OAAO,YAAY,CAAA;AAC3C,QAAA,aAAA,GAAgB,SAAA,CAAU,aAAA;AAAA,MAC5B;AAEA,MAAA,MAAM,OAAO,CAAC,eAAA,EAAiB,GAAI,OAAA,EAAS,SAAA,IAAa,EAAG,CAAA;AAC5D,MAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,QAAA,aAAA,CAAc,GAAG,CAAA;AAAA,MACnB;AAEA,MAAA,OAAO,IAAI,SAAS,IAAA,CAAK,SAAA,CAAU,EAAE,WAAA,EAAa,IAAA,EAAM,IAAA,EAAM,CAAA,EAAG;AAAA,QAC/D,MAAA,EAAQ,GAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA;AAAmB,OAC/C,CAAA;AAAA,IACH,SAAS,GAAA,EAAK;AACZ,MAAA,OAAA,CAAQ,KAAA,CAAM,yCAAyC,GAAG,CAAA;AAC1D,MAAA,OAAO,IAAI,SAAS,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,qBAAA,EAAuB,CAAA,EAAG;AAAA,QACpE,MAAA,EAAQ,GAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA;AAAmB,OAC/C,CAAA;AAAA,IACH;AAAA,EACF,CAAA;AACF","file":"index.js","sourcesContent":["/**\n * @sonordev/site-kit/slots — Server-Only Resolution\n *\n * SECURITY: uses the private SONOR_API_KEY. Import only in server code\n * (RSC, route handlers, server actions).\n *\n * Design rules (see docs/SITE-KIT-3.0-SPEC-SLOTS.md):\n * - Static-safe: plain fetch with ISR caching; never touches request state,\n * so pages using slots stay prerenderable.\n * - Fallback-first: EVERY failure mode resolves to null (component renders\n * its children). Slots must never break a build or a page.\n * - Signed: unsigned/tampered payloads are dropped with a warning.\n */\n\nimport 'server-only'\nimport { cache } from 'react'\nimport {\n SLOTS_CONTRACT_VERSION,\n SLOT_CONTENT_MAX_LENGTH,\n isValidSlotId,\n verifySlotContent,\n type SlotContent,\n type SlotsResolveResponse,\n} from './contract'\n\nexport interface SlotResolveOptions {\n /** Multi-site sub-dimension: normalized lowercase host. */\n site?: string\n /** ISR revalidate window in seconds (default 300; instant updates come from the revalidate webhook). */\n revalidate?: number\n /**\n * The slot's current static text, reported on resolve so the dashboard\n * editor shows the live copy. ManagedSlot supplies this automatically when\n * its children are a plain string.\n */\n fallback?: string\n /**\n * The route this slot renders on (static literal, e.g. \"/pricing\") — feeds\n * the Copy Evidence Loop's slot↔page mapping.\n */\n page?: string\n}\n\n// New 3.0 modules are Sonor-only: no deprecated UPTRADE_* fallbacks.\nfunction getApiConfig(): { apiUrl: string; apiKey: string } | null {\n const apiUrl = process.env.SONOR_API_URL || 'https://api.sonor.io'\n const apiKey = process.env.SONOR_API_KEY || ''\n if (!apiKey) return null\n return { apiUrl, apiKey }\n}\n\nconst warned = new Set<string>()\nfunction warnOnce(key: string, message: string): void {\n if (warned.has(key)) return\n warned.add(key)\n console.warn(message)\n}\n\nasync function fetchSlot(\n id: string,\n site: string | undefined,\n revalidate: number,\n fallback: string | undefined,\n page: string | undefined,\n): Promise<SlotContent | null> {\n if (!isValidSlotId(id)) {\n warnOnce(`id:${id}`, `[site-kit/slots] Invalid slot id \"${id}\" (expected ${'`'}[a-z0-9-_.:]${'`'}, lowercase) — rendering fallback`)\n return null\n }\n\n const config = getApiConfig()\n if (!config) {\n warnOnce('no-key', '[site-kit/slots] SONOR_API_KEY is not set — all slots render their fallbacks')\n return null\n }\n\n try {\n const response = await fetch(`${config.apiUrl}/api/public/slots/resolve`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': config.apiKey,\n },\n body: JSON.stringify({\n contract_version: SLOTS_CONTRACT_VERSION,\n slots: [\n {\n id,\n ...(fallback ? { fallback: fallback.slice(0, SLOT_CONTENT_MAX_LENGTH) } : {}),\n ...(page ? { page } : {}),\n },\n ],\n ...(site ? { site } : {}),\n }),\n next: { revalidate, tags: ['sonor-slots'] },\n } as RequestInit)\n\n if (!response.ok) {\n // 404 = endpoint not deployed yet; expected during rollout. Anything else is worth one warning.\n if (response.status !== 404) {\n warnOnce(\n `http:${response.status}`,\n `[site-kit/slots] Resolve returned ${response.status} — rendering fallbacks`,\n )\n }\n await response.text().catch(() => {})\n return null\n }\n\n const data = (await response.json()) as SlotsResolveResponse\n const payload = data?.slots?.[id]\n if (!payload) return null\n\n if (!verifySlotContent(payload, config.apiKey)) {\n warnOnce(\n `sig:${id}`,\n `[site-kit/slots] Dropped slot \"${id}\": payload failed shape/signature verification — rendering fallback`,\n )\n return null\n }\n\n return payload\n } catch (err) {\n warnOnce('network', `[site-kit/slots] Resolve failed (${(err as Error)?.message}) — rendering fallbacks`)\n return null\n }\n}\n\n/**\n * Per-render deduped single-slot resolution. Multiple <ManagedSlot id=\"x\">\n * in one render produce one fetch; Next's data cache + the 'sonor-slots' tag\n * make steady-state cost ~one request per unique slot per revalidate window.\n */\nconst getSlotCached = cache(\n (\n id: string,\n site: string | undefined,\n revalidate: number,\n fallback: string | undefined,\n page: string | undefined,\n ) => fetchSlot(id, site, revalidate, fallback, page),\n)\n\n/** Resolve one slot. Null on any failure → caller renders fallback. */\nexport function getSlot(id: string, options: SlotResolveOptions = {}): Promise<SlotContent | null> {\n const fallback = options.fallback?.trim() || undefined\n const page = options.page?.trim() || undefined\n return getSlotCached(id, options.site, options.revalidate ?? 300, fallback, page)\n}\n\n/**\n * Batch resolution for layout-level prefetch. v1 fans out to the per-slot\n * cached resolver (one wire shape, no behavioral difference); the batch\n * endpoint exists in the contract so this can become a single request\n * without a wire change.\n */\nexport async function resolveSlots(\n ids: string[],\n options: SlotResolveOptions = {},\n): Promise<Record<string, SlotContent | null>> {\n const entries = await Promise.all(\n ids.map(async (id) => [id, await getSlot(id, options)] as const),\n )\n return Object.fromEntries(entries)\n}\n","import * as React from 'react'\nimport { getSlot } from './server-api'\nimport type { SlotResolveOptions } from './server-api'\n\nexport interface ManagedSlotProps extends Omit<SlotResolveOptions, 'fallback'> {\n /** Stable slot id, lowercase, page-scoped by convention (e.g. \"home-hero-headline\"). */\n id: string\n /**\n * The developer-owned static content — exactly what the page ships without\n * Sonor. Rendered verbatim whenever no managed content resolves.\n */\n children: React.ReactNode\n}\n\n/**\n * ManagedSlot — a Sonor-managed text region inside an element the developer\n * still owns. Renders a fragment: no wrapper tag, no className opinion — put\n * it INSIDE your element and keep your markup.\n *\n * Server component. Resolution is static-safe (ISR-cached fetch, no request\n * access) and fallback-first: any failure renders the children. Managed\n * content is signature-verified and rendered as escaped text, never HTML.\n *\n * @example\n * ```tsx\n * <h1 className=\"hero-title\">\n * <ManagedSlot id=\"home-hero-headline\" page=\"/\">\n * New Homes in Cincinnati & Northern Kentucky\n * </ManagedSlot>\n * </h1>\n * ```\n */\nexport async function ManagedSlot({\n id,\n children,\n site,\n revalidate,\n page,\n}: ManagedSlotProps): Promise<React.ReactElement> {\n // When the fallback is a plain string, report it on resolve so the\n // dashboard editor shows the site's live copy next to the slot id.\n const fallback = typeof children === 'string' ? children : undefined\n const managed = await getSlot(id, { site, revalidate, fallback, page })\n // Escaped text by contract (type 'text'); React renders it inert.\n return <>{managed ? managed.content : children}</>\n}\n\nexport default ManagedSlot\n","/**\n * @sonordev/site-kit/slots — On-demand Revalidation\n *\n * Sonor calls this when slot content changes (owner edit or approved Signal\n * proposal) so the change is live in seconds instead of at the next ISR\n * window. Mirrors the llms revalidation handler pattern.\n *\n * @example\n * ```ts\n * // app/api/revalidate-slots/route.ts\n * import { createSlotsRevalidateHandler } from '@sonordev/site-kit/slots'\n *\n * export const POST = createSlotsRevalidateHandler(process.env.REVALIDATION_SECRET!)\n * ```\n */\n\ntype RevalidateTagFn = (tag: string) => void\n\nexport const SLOTS_CACHE_TAG = 'sonor-slots'\n\n/**\n * Create a POST handler that busts the slots data cache.\n * Protected by a shared secret. (POST handlers are never statically\n * prerendered, so reading the request here is fine.)\n */\nexport function createSlotsRevalidateHandler(\n secret: string,\n options?: {\n /** Additional cache tags to revalidate alongside slots. */\n extraTags?: string[]\n /** Custom revalidateTag (for testing or frameworks other than Next.js). */\n revalidateTag?: RevalidateTagFn\n },\n): (request: Request) => Promise<Response> {\n return async function POST(request: Request): Promise<Response> {\n try {\n const authHeader = request.headers.get('authorization')\n const url = new URL(request.url)\n const tokenFromQuery = url.searchParams.get('secret')\n const token = authHeader?.replace(/^Bearer\\s+/i, '') || tokenFromQuery\n\n if (!secret || token !== secret) {\n return new Response(JSON.stringify({ error: 'Invalid secret' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n let revalidateTag: RevalidateTagFn\n if (options?.revalidateTag) {\n revalidateTag = options.revalidateTag\n } else {\n // Dynamic import keeps next/cache out of non-Next consumers.\n const nextCache = await import('next/cache')\n revalidateTag = nextCache.revalidateTag\n }\n\n const tags = [SLOTS_CACHE_TAG, ...(options?.extraTags || [])]\n for (const tag of tags) {\n revalidateTag(tag)\n }\n\n return new Response(JSON.stringify({ revalidated: true, tags }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (err) {\n console.error('[site-kit/slots] Revalidation failed:', err)\n return new Response(JSON.stringify({ error: 'Revalidation failed' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n}\n"]}
@@ -0,0 +1,146 @@
1
+ import { isValidSlotId, SLOT_CONTENT_MAX_LENGTH, SLOTS_CONTRACT_VERSION, verifySlotContent } from '../chunk-2FEQ6TYD.mjs';
2
+ export { SLOTS_CONTRACT_VERSION, SLOT_CONTENT_MAX_LENGTH, SLOT_CONTENT_TYPES, SLOT_ID_PATTERN, isValidSlotId, signSlotContent, slotSigningPayload, verifySlotContent } from '../chunk-2FEQ6TYD.mjs';
3
+ import '../chunk-4XPGGLVP.mjs';
4
+ import 'server-only';
5
+ import { cache } from 'react';
6
+ import { jsx, Fragment } from 'react/jsx-runtime';
7
+
8
+ function getApiConfig() {
9
+ const apiUrl = process.env.SONOR_API_URL || "https://api.sonor.io";
10
+ const apiKey = process.env.SONOR_API_KEY || "";
11
+ if (!apiKey) return null;
12
+ return { apiUrl, apiKey };
13
+ }
14
+ var warned = /* @__PURE__ */ new Set();
15
+ function warnOnce(key, message) {
16
+ if (warned.has(key)) return;
17
+ warned.add(key);
18
+ console.warn(message);
19
+ }
20
+ async function fetchSlot(id, site, revalidate, fallback, page) {
21
+ if (!isValidSlotId(id)) {
22
+ warnOnce(`id:${id}`, `[site-kit/slots] Invalid slot id "${id}" (expected ${"`"}[a-z0-9-_.:]${"`"}, lowercase) \u2014 rendering fallback`);
23
+ return null;
24
+ }
25
+ const config = getApiConfig();
26
+ if (!config) {
27
+ warnOnce("no-key", "[site-kit/slots] SONOR_API_KEY is not set \u2014 all slots render their fallbacks");
28
+ return null;
29
+ }
30
+ try {
31
+ const response = await fetch(`${config.apiUrl}/api/public/slots/resolve`, {
32
+ method: "POST",
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ "x-api-key": config.apiKey
36
+ },
37
+ body: JSON.stringify({
38
+ contract_version: SLOTS_CONTRACT_VERSION,
39
+ slots: [
40
+ {
41
+ id,
42
+ ...fallback ? { fallback: fallback.slice(0, SLOT_CONTENT_MAX_LENGTH) } : {},
43
+ ...page ? { page } : {}
44
+ }
45
+ ],
46
+ ...site ? { site } : {}
47
+ }),
48
+ next: { revalidate, tags: ["sonor-slots"] }
49
+ });
50
+ if (!response.ok) {
51
+ if (response.status !== 404) {
52
+ warnOnce(
53
+ `http:${response.status}`,
54
+ `[site-kit/slots] Resolve returned ${response.status} \u2014 rendering fallbacks`
55
+ );
56
+ }
57
+ await response.text().catch(() => {
58
+ });
59
+ return null;
60
+ }
61
+ const data = await response.json();
62
+ const payload = data?.slots?.[id];
63
+ if (!payload) return null;
64
+ if (!verifySlotContent(payload, config.apiKey)) {
65
+ warnOnce(
66
+ `sig:${id}`,
67
+ `[site-kit/slots] Dropped slot "${id}": payload failed shape/signature verification \u2014 rendering fallback`
68
+ );
69
+ return null;
70
+ }
71
+ return payload;
72
+ } catch (err) {
73
+ warnOnce("network", `[site-kit/slots] Resolve failed (${err?.message}) \u2014 rendering fallbacks`);
74
+ return null;
75
+ }
76
+ }
77
+ var getSlotCached = cache(
78
+ (id, site, revalidate, fallback, page) => fetchSlot(id, site, revalidate, fallback, page)
79
+ );
80
+ function getSlot(id, options = {}) {
81
+ const fallback = options.fallback?.trim() || void 0;
82
+ const page = options.page?.trim() || void 0;
83
+ return getSlotCached(id, options.site, options.revalidate ?? 300, fallback, page);
84
+ }
85
+ async function resolveSlots(ids, options = {}) {
86
+ const entries = await Promise.all(
87
+ ids.map(async (id) => [id, await getSlot(id, options)])
88
+ );
89
+ return Object.fromEntries(entries);
90
+ }
91
+ async function ManagedSlot({
92
+ id,
93
+ children,
94
+ site,
95
+ revalidate,
96
+ page
97
+ }) {
98
+ const fallback = typeof children === "string" ? children : void 0;
99
+ const managed = await getSlot(id, { site, revalidate, fallback, page });
100
+ return /* @__PURE__ */ jsx(Fragment, { children: managed ? managed.content : children });
101
+ }
102
+ var ManagedSlot_default = ManagedSlot;
103
+
104
+ // src/slots/revalidate.ts
105
+ var SLOTS_CACHE_TAG = "sonor-slots";
106
+ function createSlotsRevalidateHandler(secret, options) {
107
+ return async function POST(request) {
108
+ try {
109
+ const authHeader = request.headers.get("authorization");
110
+ const url = new URL(request.url);
111
+ const tokenFromQuery = url.searchParams.get("secret");
112
+ const token = authHeader?.replace(/^Bearer\s+/i, "") || tokenFromQuery;
113
+ if (!secret || token !== secret) {
114
+ return new Response(JSON.stringify({ error: "Invalid secret" }), {
115
+ status: 401,
116
+ headers: { "Content-Type": "application/json" }
117
+ });
118
+ }
119
+ let revalidateTag;
120
+ if (options?.revalidateTag) {
121
+ revalidateTag = options.revalidateTag;
122
+ } else {
123
+ const nextCache = await import('next/cache');
124
+ revalidateTag = nextCache.revalidateTag;
125
+ }
126
+ const tags = [SLOTS_CACHE_TAG, ...options?.extraTags || []];
127
+ for (const tag of tags) {
128
+ revalidateTag(tag);
129
+ }
130
+ return new Response(JSON.stringify({ revalidated: true, tags }), {
131
+ status: 200,
132
+ headers: { "Content-Type": "application/json" }
133
+ });
134
+ } catch (err) {
135
+ console.error("[site-kit/slots] Revalidation failed:", err);
136
+ return new Response(JSON.stringify({ error: "Revalidation failed" }), {
137
+ status: 500,
138
+ headers: { "Content-Type": "application/json" }
139
+ });
140
+ }
141
+ };
142
+ }
143
+
144
+ export { ManagedSlot, SLOTS_CACHE_TAG, createSlotsRevalidateHandler, ManagedSlot_default as default, getSlot, resolveSlots };
145
+ //# sourceMappingURL=index.mjs.map
146
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/slots/server-api.ts","../../src/slots/ManagedSlot.tsx","../../src/slots/revalidate.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @sonordev/site-kit/slots — Server-Only Resolution\n *\n * SECURITY: uses the private SONOR_API_KEY. Import only in server code\n * (RSC, route handlers, server actions).\n *\n * Design rules (see docs/SITE-KIT-3.0-SPEC-SLOTS.md):\n * - Static-safe: plain fetch with ISR caching; never touches request state,\n * so pages using slots stay prerenderable.\n * - Fallback-first: EVERY failure mode resolves to null (component renders\n * its children). Slots must never break a build or a page.\n * - Signed: unsigned/tampered payloads are dropped with a warning.\n */\n\nimport 'server-only'\nimport { cache } from 'react'\nimport {\n SLOTS_CONTRACT_VERSION,\n SLOT_CONTENT_MAX_LENGTH,\n isValidSlotId,\n verifySlotContent,\n type SlotContent,\n type SlotsResolveResponse,\n} from './contract'\n\nexport interface SlotResolveOptions {\n /** Multi-site sub-dimension: normalized lowercase host. */\n site?: string\n /** ISR revalidate window in seconds (default 300; instant updates come from the revalidate webhook). */\n revalidate?: number\n /**\n * The slot's current static text, reported on resolve so the dashboard\n * editor shows the live copy. ManagedSlot supplies this automatically when\n * its children are a plain string.\n */\n fallback?: string\n /**\n * The route this slot renders on (static literal, e.g. \"/pricing\") — feeds\n * the Copy Evidence Loop's slot↔page mapping.\n */\n page?: string\n}\n\n// New 3.0 modules are Sonor-only: no deprecated UPTRADE_* fallbacks.\nfunction getApiConfig(): { apiUrl: string; apiKey: string } | null {\n const apiUrl = process.env.SONOR_API_URL || 'https://api.sonor.io'\n const apiKey = process.env.SONOR_API_KEY || ''\n if (!apiKey) return null\n return { apiUrl, apiKey }\n}\n\nconst warned = new Set<string>()\nfunction warnOnce(key: string, message: string): void {\n if (warned.has(key)) return\n warned.add(key)\n console.warn(message)\n}\n\nasync function fetchSlot(\n id: string,\n site: string | undefined,\n revalidate: number,\n fallback: string | undefined,\n page: string | undefined,\n): Promise<SlotContent | null> {\n if (!isValidSlotId(id)) {\n warnOnce(`id:${id}`, `[site-kit/slots] Invalid slot id \"${id}\" (expected ${'`'}[a-z0-9-_.:]${'`'}, lowercase) — rendering fallback`)\n return null\n }\n\n const config = getApiConfig()\n if (!config) {\n warnOnce('no-key', '[site-kit/slots] SONOR_API_KEY is not set — all slots render their fallbacks')\n return null\n }\n\n try {\n const response = await fetch(`${config.apiUrl}/api/public/slots/resolve`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': config.apiKey,\n },\n body: JSON.stringify({\n contract_version: SLOTS_CONTRACT_VERSION,\n slots: [\n {\n id,\n ...(fallback ? { fallback: fallback.slice(0, SLOT_CONTENT_MAX_LENGTH) } : {}),\n ...(page ? { page } : {}),\n },\n ],\n ...(site ? { site } : {}),\n }),\n next: { revalidate, tags: ['sonor-slots'] },\n } as RequestInit)\n\n if (!response.ok) {\n // 404 = endpoint not deployed yet; expected during rollout. Anything else is worth one warning.\n if (response.status !== 404) {\n warnOnce(\n `http:${response.status}`,\n `[site-kit/slots] Resolve returned ${response.status} — rendering fallbacks`,\n )\n }\n await response.text().catch(() => {})\n return null\n }\n\n const data = (await response.json()) as SlotsResolveResponse\n const payload = data?.slots?.[id]\n if (!payload) return null\n\n if (!verifySlotContent(payload, config.apiKey)) {\n warnOnce(\n `sig:${id}`,\n `[site-kit/slots] Dropped slot \"${id}\": payload failed shape/signature verification — rendering fallback`,\n )\n return null\n }\n\n return payload\n } catch (err) {\n warnOnce('network', `[site-kit/slots] Resolve failed (${(err as Error)?.message}) — rendering fallbacks`)\n return null\n }\n}\n\n/**\n * Per-render deduped single-slot resolution. Multiple <ManagedSlot id=\"x\">\n * in one render produce one fetch; Next's data cache + the 'sonor-slots' tag\n * make steady-state cost ~one request per unique slot per revalidate window.\n */\nconst getSlotCached = cache(\n (\n id: string,\n site: string | undefined,\n revalidate: number,\n fallback: string | undefined,\n page: string | undefined,\n ) => fetchSlot(id, site, revalidate, fallback, page),\n)\n\n/** Resolve one slot. Null on any failure → caller renders fallback. */\nexport function getSlot(id: string, options: SlotResolveOptions = {}): Promise<SlotContent | null> {\n const fallback = options.fallback?.trim() || undefined\n const page = options.page?.trim() || undefined\n return getSlotCached(id, options.site, options.revalidate ?? 300, fallback, page)\n}\n\n/**\n * Batch resolution for layout-level prefetch. v1 fans out to the per-slot\n * cached resolver (one wire shape, no behavioral difference); the batch\n * endpoint exists in the contract so this can become a single request\n * without a wire change.\n */\nexport async function resolveSlots(\n ids: string[],\n options: SlotResolveOptions = {},\n): Promise<Record<string, SlotContent | null>> {\n const entries = await Promise.all(\n ids.map(async (id) => [id, await getSlot(id, options)] as const),\n )\n return Object.fromEntries(entries)\n}\n","import * as React from 'react'\nimport { getSlot } from './server-api'\nimport type { SlotResolveOptions } from './server-api'\n\nexport interface ManagedSlotProps extends Omit<SlotResolveOptions, 'fallback'> {\n /** Stable slot id, lowercase, page-scoped by convention (e.g. \"home-hero-headline\"). */\n id: string\n /**\n * The developer-owned static content — exactly what the page ships without\n * Sonor. Rendered verbatim whenever no managed content resolves.\n */\n children: React.ReactNode\n}\n\n/**\n * ManagedSlot — a Sonor-managed text region inside an element the developer\n * still owns. Renders a fragment: no wrapper tag, no className opinion — put\n * it INSIDE your element and keep your markup.\n *\n * Server component. Resolution is static-safe (ISR-cached fetch, no request\n * access) and fallback-first: any failure renders the children. Managed\n * content is signature-verified and rendered as escaped text, never HTML.\n *\n * @example\n * ```tsx\n * <h1 className=\"hero-title\">\n * <ManagedSlot id=\"home-hero-headline\" page=\"/\">\n * New Homes in Cincinnati & Northern Kentucky\n * </ManagedSlot>\n * </h1>\n * ```\n */\nexport async function ManagedSlot({\n id,\n children,\n site,\n revalidate,\n page,\n}: ManagedSlotProps): Promise<React.ReactElement> {\n // When the fallback is a plain string, report it on resolve so the\n // dashboard editor shows the site's live copy next to the slot id.\n const fallback = typeof children === 'string' ? children : undefined\n const managed = await getSlot(id, { site, revalidate, fallback, page })\n // Escaped text by contract (type 'text'); React renders it inert.\n return <>{managed ? managed.content : children}</>\n}\n\nexport default ManagedSlot\n","/**\n * @sonordev/site-kit/slots — On-demand Revalidation\n *\n * Sonor calls this when slot content changes (owner edit or approved Signal\n * proposal) so the change is live in seconds instead of at the next ISR\n * window. Mirrors the llms revalidation handler pattern.\n *\n * @example\n * ```ts\n * // app/api/revalidate-slots/route.ts\n * import { createSlotsRevalidateHandler } from '@sonordev/site-kit/slots'\n *\n * export const POST = createSlotsRevalidateHandler(process.env.REVALIDATION_SECRET!)\n * ```\n */\n\ntype RevalidateTagFn = (tag: string) => void\n\nexport const SLOTS_CACHE_TAG = 'sonor-slots'\n\n/**\n * Create a POST handler that busts the slots data cache.\n * Protected by a shared secret. (POST handlers are never statically\n * prerendered, so reading the request here is fine.)\n */\nexport function createSlotsRevalidateHandler(\n secret: string,\n options?: {\n /** Additional cache tags to revalidate alongside slots. */\n extraTags?: string[]\n /** Custom revalidateTag (for testing or frameworks other than Next.js). */\n revalidateTag?: RevalidateTagFn\n },\n): (request: Request) => Promise<Response> {\n return async function POST(request: Request): Promise<Response> {\n try {\n const authHeader = request.headers.get('authorization')\n const url = new URL(request.url)\n const tokenFromQuery = url.searchParams.get('secret')\n const token = authHeader?.replace(/^Bearer\\s+/i, '') || tokenFromQuery\n\n if (!secret || token !== secret) {\n return new Response(JSON.stringify({ error: 'Invalid secret' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n let revalidateTag: RevalidateTagFn\n if (options?.revalidateTag) {\n revalidateTag = options.revalidateTag\n } else {\n // Dynamic import keeps next/cache out of non-Next consumers.\n const nextCache = await import('next/cache')\n revalidateTag = nextCache.revalidateTag\n }\n\n const tags = [SLOTS_CACHE_TAG, ...(options?.extraTags || [])]\n for (const tag of tags) {\n revalidateTag(tag)\n }\n\n return new Response(JSON.stringify({ revalidated: true, tags }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (err) {\n console.error('[site-kit/slots] Revalidation failed:', err)\n return new Response(JSON.stringify({ error: 'Revalidation failed' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n}\n"]}
@@ -265,4 +265,4 @@ interface GetSitemapEntriesOptions {
265
265
  publishedOnly?: boolean;
266
266
  }
267
267
 
268
- export type { ABTest as A, FAQItem as F, GetManagedMetadataOptions as G, ManagedMetadataResult as M, RedirectResult as R, SonorSEOConfig as S, SEOPageData as a, SchemaMarkup as b, ManagedSchemaProps as c, ManagedFAQData as d, ManagedFAQProps as e, ManagedLink as f, ManagedInternalLinksProps as g, ManagedContentBlock as h, ManagedContentProps as i, SEOEntity as j, ABTestResult as k, GetABVariantOptions as l, ManagedRedirect as m, GetRedirectOptions as n, ManagedScript as o, ManagedScriptsProps as p, RobotsDirective as q, GetRobotsOptions as r, SitemapEntry as s, GetSitemapEntriesOptions as t };
268
+ export type { ABTest as A, FAQItem as F, GetManagedMetadataOptions as G, ManagedMetadataResult as M, RedirectResult as R, SonorSEOConfig as S, ManagedSchemaProps as a, ManagedFAQData as b, ManagedFAQProps as c, ManagedInternalLinksProps as d, ManagedContentProps as e, ManagedContentBlock as f, ManagedScriptsProps as g, SEOPageData as h, SchemaMarkup as i, ManagedLink as j, ABTestResult as k, GetABVariantOptions as l, ManagedRedirect as m, GetRedirectOptions as n, ManagedScript as o, RobotsDirective as p, GetRobotsOptions as q, SitemapEntry as r, GetSitemapEntriesOptions as s, SEOEntity as t };
@@ -265,4 +265,4 @@ interface GetSitemapEntriesOptions {
265
265
  publishedOnly?: boolean;
266
266
  }
267
267
 
268
- export type { ABTest as A, FAQItem as F, GetManagedMetadataOptions as G, ManagedMetadataResult as M, RedirectResult as R, SonorSEOConfig as S, SEOPageData as a, SchemaMarkup as b, ManagedSchemaProps as c, ManagedFAQData as d, ManagedFAQProps as e, ManagedLink as f, ManagedInternalLinksProps as g, ManagedContentBlock as h, ManagedContentProps as i, SEOEntity as j, ABTestResult as k, GetABVariantOptions as l, ManagedRedirect as m, GetRedirectOptions as n, ManagedScript as o, ManagedScriptsProps as p, RobotsDirective as q, GetRobotsOptions as r, SitemapEntry as s, GetSitemapEntriesOptions as t };
268
+ export type { ABTest as A, FAQItem as F, GetManagedMetadataOptions as G, ManagedMetadataResult as M, RedirectResult as R, SonorSEOConfig as S, ManagedSchemaProps as a, ManagedFAQData as b, ManagedFAQProps as c, ManagedInternalLinksProps as d, ManagedContentProps as e, ManagedContentBlock as f, ManagedScriptsProps as g, SEOPageData as h, SchemaMarkup as i, ManagedLink as j, ABTestResult as k, GetABVariantOptions as l, ManagedRedirect as m, GetRedirectOptions as n, ManagedScript as o, RobotsDirective as p, GetRobotsOptions as q, SitemapEntry as r, GetSitemapEntriesOptions as s, SEOEntity as t };
@@ -195,6 +195,23 @@ interface ChatConfig {
195
195
  /** Custom offline message */
196
196
  offlineMessage?: string;
197
197
  }
198
+ /**
199
+ * UI actions the visitor Echo endpoint (POST /echo/public/chat) may return on
200
+ * the JSON response alongside `content` / `conversationId`. The chat widget
201
+ * renders each as an inline card or chip beneath the assistant message.
202
+ *
203
+ * This mirrors the shape emitted by signal-api `echo.service.ts`. It is a LOCAL
204
+ * contract on purpose — never import it from signal-api (separate package).
205
+ *
206
+ * - `show_form` → data.formSlug (+ optional data.prefill) — offer the managed form inline.
207
+ * - `suggest_action` → data.action + data.message — clickable chip (send message or navigate).
208
+ * - `show_portfolio_item` → data.slug — Upforge-specific; the generic widget ignores it (custom Echo chat owns it).
209
+ */
210
+ type UiActionType = 'show_form' | 'suggest_action' | 'show_portfolio_item';
211
+ interface UiAction {
212
+ type: UiActionType | string;
213
+ data: Record<string, unknown>;
214
+ }
198
215
  interface ChatMessage {
199
216
  id: string;
200
217
  conversation_id: string;
@@ -247,4 +264,4 @@ interface EngageAnalytics {
247
264
  created_at: string;
248
265
  }
249
266
 
250
- export { type ActionConfig as A, type ChatConfig as C, DesignRenderer as D, type EngageContext as E, type TargetingRules as T, type WidgetType as W, type DesignDocument as a, type DesignNode as b, type EngageElement as c, type WidgetConfig as d, type TriggerConfig as e, type ChatMessage as f, type ChatConversation as g, type ChatAvailability as h, type EngageConfig as i, type EngageAnalytics as j };
267
+ export { type ActionConfig as A, type ChatConfig as C, DesignRenderer as D, type EngageContext as E, type TargetingRules as T, type UiActionType as U, type WidgetType as W, type DesignDocument as a, type DesignNode as b, type EngageElement as c, type WidgetConfig as d, type TriggerConfig as e, type UiAction as f, type ChatMessage as g, type ChatConversation as h, type ChatAvailability as i, type EngageConfig as j, type EngageAnalytics as k };
@@ -195,6 +195,23 @@ interface ChatConfig {
195
195
  /** Custom offline message */
196
196
  offlineMessage?: string;
197
197
  }
198
+ /**
199
+ * UI actions the visitor Echo endpoint (POST /echo/public/chat) may return on
200
+ * the JSON response alongside `content` / `conversationId`. The chat widget
201
+ * renders each as an inline card or chip beneath the assistant message.
202
+ *
203
+ * This mirrors the shape emitted by signal-api `echo.service.ts`. It is a LOCAL
204
+ * contract on purpose — never import it from signal-api (separate package).
205
+ *
206
+ * - `show_form` → data.formSlug (+ optional data.prefill) — offer the managed form inline.
207
+ * - `suggest_action` → data.action + data.message — clickable chip (send message or navigate).
208
+ * - `show_portfolio_item` → data.slug — Upforge-specific; the generic widget ignores it (custom Echo chat owns it).
209
+ */
210
+ type UiActionType = 'show_form' | 'suggest_action' | 'show_portfolio_item';
211
+ interface UiAction {
212
+ type: UiActionType | string;
213
+ data: Record<string, unknown>;
214
+ }
198
215
  interface ChatMessage {
199
216
  id: string;
200
217
  conversation_id: string;
@@ -247,4 +264,4 @@ interface EngageAnalytics {
247
264
  created_at: string;
248
265
  }
249
266
 
250
- export { type ActionConfig as A, type ChatConfig as C, DesignRenderer as D, type EngageContext as E, type TargetingRules as T, type WidgetType as W, type DesignDocument as a, type DesignNode as b, type EngageElement as c, type WidgetConfig as d, type TriggerConfig as e, type ChatMessage as f, type ChatConversation as g, type ChatAvailability as h, type EngageConfig as i, type EngageAnalytics as j };
267
+ export { type ActionConfig as A, type ChatConfig as C, DesignRenderer as D, type EngageContext as E, type TargetingRules as T, type UiActionType as U, type WidgetType as W, type DesignDocument as a, type DesignNode as b, type EngageElement as c, type WidgetConfig as d, type TriggerConfig as e, type UiAction as f, type ChatMessage as g, type ChatConversation as h, type ChatAvailability as i, type EngageConfig as j, type EngageAnalytics as k };
@@ -86,6 +86,14 @@ interface LLMPortfolioItem {
86
86
  interface GenerateLLMSTxtOptions {
87
87
  /** Project ID for fetching data (optional - uses API key's project) */
88
88
  projectId?: string;
89
+ /**
90
+ * Canonical site origin used to absolutize links (e.g. `https://example.com`),
91
+ * matching the sitemap helper's `baseUrl` convention. When omitted, falls back
92
+ * to Portal/local `business.website`, then `NEXT_PUBLIC_SITE_URL` / `SITE_URL`.
93
+ * Never derived from request headers, so /llms.txt stays statically
94
+ * prerenderable.
95
+ */
96
+ baseUrl?: string;
89
97
  /** When Portal returns empty services/faq/pages, use this to supply local site data */
90
98
  getLocalData?: () => Promise<LLMsDataResponse | null>;
91
99
  /** Include business info section */
@@ -86,6 +86,14 @@ interface LLMPortfolioItem {
86
86
  interface GenerateLLMSTxtOptions {
87
87
  /** Project ID for fetching data (optional - uses API key's project) */
88
88
  projectId?: string;
89
+ /**
90
+ * Canonical site origin used to absolutize links (e.g. `https://example.com`),
91
+ * matching the sitemap helper's `baseUrl` convention. When omitted, falls back
92
+ * to Portal/local `business.website`, then `NEXT_PUBLIC_SITE_URL` / `SITE_URL`.
93
+ * Never derived from request headers, so /llms.txt stays statically
94
+ * prerenderable.
95
+ */
96
+ baseUrl?: string;
89
97
  /** When Portal returns empty services/faq/pages, use this to supply local site data */
90
98
  getLocalData?: () => Promise<LLMsDataResponse | null>;
91
99
  /** Include business info section */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sonordev/site-kit",
3
- "version": "2.8.2",
3
+ "version": "3.0.0",
4
4
  "description": "Complete client-side integration kit for Sonor - SEO, Analytics, Engage, Forms, Blog",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -127,6 +127,16 @@
127
127
  "import": "./dist/llms/contract.mjs",
128
128
  "require": "./dist/llms/contract.js"
129
129
  },
130
+ "./slots": {
131
+ "types": "./dist/slots/index.d.ts",
132
+ "import": "./dist/slots/index.mjs",
133
+ "require": "./dist/slots/index.js"
134
+ },
135
+ "./slots/contract": {
136
+ "types": "./dist/slots/contract.d.ts",
137
+ "import": "./dist/slots/contract.mjs",
138
+ "require": "./dist/slots/contract.js"
139
+ },
130
140
  "./robots": {
131
141
  "types": "./dist/robots/index.d.ts",
132
142
  "import": "./dist/robots/index.mjs",
@@ -1,4 +0,0 @@
1
- export { EngageWidget } from './chunk-IAV32YHL.mjs';
2
- import './chunk-4XPGGLVP.mjs';
3
- //# sourceMappingURL=EngageWidget-4W5EM4UO.mjs.map
4
- //# sourceMappingURL=EngageWidget-4W5EM4UO.mjs.map
@@ -1,13 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkSLJJJWMV_js = require('./chunk-SLJJJWMV.js');
4
- require('./chunk-ZSMWDLMK.js');
5
-
6
-
7
-
8
- Object.defineProperty(exports, "EngageWidget", {
9
- enumerable: true,
10
- get: function () { return chunkSLJJJWMV_js.EngageWidget; }
11
- });
12
- //# sourceMappingURL=EngageWidget-JIXUBG47.js.map
13
- //# sourceMappingURL=EngageWidget-JIXUBG47.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/seo/getManagedMetadata.ts","../src/seo/routing.ts"],"names":[],"mappings":"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","file":"chunk-5SQ4NRPH.mjs","sourcesContent":["import type { Metadata } from 'next'\nimport { getSEOPageData, getABTest, recordABImpression, ensureMetadata } from './server-api'\nimport type { \n GetManagedMetadataOptions, \n ManagedMetadataResult,\n GetABVariantOptions,\n ABTestResult \n} from './types'\n\n/**\n * Get managed metadata for a page\n * \n * Use in generateMetadata() to fetch Portal-managed SEO data\n * \n * @example\n * ```tsx\n * export async function generateMetadata({ params }) {\n * return getManagedMetadata({\n * path: `/services/${params.slug}`,\n * fallback: {\n * title: 'Our Services',\n * description: 'Learn about our services'\n * }\n * })\n * }\n * ```\n */\nexport async function getManagedMetadata(\n options: GetManagedMetadataOptions\n): Promise<ManagedMetadataResult> {\n const { path, fallback = {}, overrides = {}, favicon: faviconMode = 'metadata' } = options\n const omitIcons = faviconMode === 'component'\n\n const result = await getSEOPageData(path)\n const pageData = result?.page\n const projectData = result?.project\n\n // If no managed data, return fallback (still include favicon from project if available, unless using component)\n if (!pageData) {\n const fallbackMeta: ManagedMetadataResult = {\n ...fallback,\n ...overrides,\n _managed: false,\n _source: 'fallback',\n } as ManagedMetadataResult\n\n if (!omitIcons && projectData?.logo_url) {\n fallbackMeta.icons = {\n icon: projectData.logo_url,\n apple: projectData.logo_url,\n }\n }\n\n return fallbackMeta\n }\n\n // Build metadata from managed values, falling back to provided fallbacks\n const metadata: ManagedMetadataResult = {\n _managed: true,\n _source: 'database',\n }\n\n if (!omitIcons && projectData?.logo_url) {\n metadata.icons = {\n icon: projectData.logo_url,\n apple: projectData.logo_url,\n }\n }\n\n // Signal fallback — when page exists but both title & description are empty,\n // trigger Signal AI to generate them (fire-and-forget so it never blocks TTFB;\n // the generated data will be available on the next request)\n if (!pageData.managed_title && !pageData.managed_meta_description && !pageData.managed_description) {\n ensureMetadata(path).catch(() => {})\n }\n\n // Title\n if (pageData.managed_title) {\n metadata.title = pageData.managed_title\n } else if (fallback.title) {\n metadata.title = fallback.title\n }\n\n // Description\n if (pageData.managed_meta_description || pageData.managed_description) {\n metadata.description = pageData.managed_meta_description || pageData.managed_description\n } else if (fallback.description) {\n metadata.description = fallback.description\n }\n\n // Keywords\n if (pageData.managed_keywords?.length) {\n metadata.keywords = pageData.managed_keywords\n } else if (fallback.keywords) {\n metadata.keywords = fallback.keywords\n }\n\n // Robots\n if (pageData.managed_robots) {\n metadata.robots = pageData.managed_robots\n }\n\n // Canonical\n if (pageData.managed_canonical) {\n metadata.alternates = {\n ...metadata.alternates,\n canonical: pageData.managed_canonical,\n }\n }\n\n // Optional hreflang map from Portal (expansion / multilingual sites)\n const langAlts = pageData.language_alternates\n if (langAlts && typeof langAlts === 'object' && !Array.isArray(langAlts)) {\n metadata.alternates = {\n ...metadata.alternates,\n languages: langAlts as Record<string, string>,\n }\n }\n\n // Open Graph\n const ogTitle = pageData.managed_og_title || pageData.managed_title\n const ogDescription = pageData.managed_og_description || pageData.managed_meta_description || pageData.managed_description\n const ogImage = pageData.managed_og_image\n\n if (ogTitle || ogDescription || ogImage) {\n metadata.openGraph = {\n ...(ogTitle && { title: ogTitle }),\n ...(ogDescription && { description: ogDescription }),\n ...(ogImage && { images: [{ url: ogImage }] }),\n }\n }\n\n // Twitter (use OG values as fallback)\n if (ogTitle || ogDescription || ogImage) {\n metadata.twitter = {\n card: 'summary_large_image',\n ...(ogTitle && { title: ogTitle }),\n ...(ogDescription && { description: ogDescription }),\n ...(ogImage && { images: [ogImage] }),\n }\n }\n\n // Apply overrides last\n return {\n ...metadata,\n ...overrides,\n _managed: true,\n _source: 'database',\n }\n}\n\n/**\n * Get A/B test variant for a field\n * \n * @example\n * ```tsx\n * const variant = await getABVariant({\n * path: '/pricing',\n * field: 'title',\n * sessionId: cookies().get('session_id')?.value\n * })\n * \n * if (variant) {\n * // Use variant.value instead of default\n * }\n * ```\n */\nexport async function getABVariant(\n options: GetABVariantOptions\n): Promise<ABTestResult | null> {\n const { path, field, sessionId } = options\n\n const test = await getABTest(path, field)\n\n if (!test) {\n return null\n }\n\n // Determine variant based on session ID or random\n let variant: 'a' | 'b'\n \n if (sessionId) {\n // Consistent variant for same session\n const hash = sessionId.split('').reduce((acc, char) => {\n return ((acc << 5) - acc) + char.charCodeAt(0)\n }, 0)\n variant = (Math.abs(hash) % 100) < (test.traffic_split * 100) ? 'a' : 'b'\n } else {\n // Random variant\n variant = Math.random() < test.traffic_split ? 'a' : 'b'\n }\n\n // Record impression (fire and forget)\n recordABImpression(test.id, variant, sessionId).catch(() => {\n // Silently fail - don't block rendering\n })\n\n return {\n testId: test.id,\n variant,\n value: variant === 'a' ? test.variant_a : test.variant_b,\n }\n}\n\n/**\n * Get managed metadata with A/B test support\n * \n * Automatically applies running A/B test variants to metadata\n */\nexport async function getManagedMetadataWithAB(\n options: GetManagedMetadataOptions & { sessionId?: string }\n): Promise<ManagedMetadataResult> {\n const { sessionId, ...metadataOptions } = options\n \n // Get base metadata\n const metadata = await getManagedMetadata(metadataOptions)\n\n // Check for title A/B test\n const titleTest = await getABVariant({\n path: options.path,\n field: 'title',\n sessionId,\n })\n\n if (titleTest) {\n metadata.title = titleTest.value\n }\n\n // Check for description A/B test\n const descTest = await getABVariant({\n path: options.path,\n field: 'description',\n sessionId,\n })\n\n if (descTest) {\n metadata.description = descTest.value\n }\n\n return metadata\n}\n","import { getRedirectData, getRobotsData, getSitemapEntries } from './server-api'\nimport type { \n GetRedirectOptions, \n RedirectResult, \n GetRobotsOptions, \n RobotsDirective,\n GetSitemapEntriesOptions,\n SitemapEntry \n} from './types'\n\n/**\n * Get redirect for a path if one exists\n * \n * Use in Next.js middleware to handle managed redirects\n * \n * @example\n * ```tsx\n * // middleware.ts\n * import { getRedirect } from '@sonordev/seo'\n * \n * export async function middleware(request) {\n * const redirect = await getRedirect({\n * projectId: process.env.SONOR_PROJECT_ID!,\n * path: request.nextUrl.pathname\n * })\n * \n * if (redirect) {\n * return NextResponse.redirect(redirect.destination, redirect.statusCode)\n * }\n * }\n * ```\n */\nexport async function getRedirect(\n options: GetRedirectOptions\n): Promise<RedirectResult | null> {\n const { path } = options\n\n const redirect = await getRedirectData(path)\n\n if (!redirect) {\n return null\n }\n\n // Check if expired\n if (redirect.expires_at && new Date(redirect.expires_at) < new Date()) {\n return null\n }\n\n // Determine destination\n const destination = redirect.destination_url || redirect.destination_path\n const isExternal = destination.startsWith('http://') || destination.startsWith('https://')\n\n return {\n destination,\n statusCode: redirect.status_code,\n isExternal,\n }\n}\n\n/**\n * Parse robots directive string into structured object\n */\nfunction parseRobotsString(robots: string): RobotsDirective {\n const directive: RobotsDirective = {\n index: true,\n follow: true,\n }\n\n const parts = robots.toLowerCase().split(',').map(p => p.trim())\n\n for (const part of parts) {\n if (part === 'noindex') directive.index = false\n if (part === 'nofollow') directive.follow = false\n if (part === 'noarchive') directive.noarchive = true\n if (part === 'nosnippet') directive.nosnippet = true\n if (part === 'noimageindex') directive.noimageindex = true\n if (part.startsWith('max-snippet:')) {\n directive.max_snippet = parseInt(part.split(':')[1], 10)\n }\n if (part.startsWith('max-image-preview:')) {\n const value = part.split(':')[1] as 'none' | 'standard' | 'large'\n directive.max_image_preview = value\n }\n if (part.startsWith('max-video-preview:')) {\n directive.max_video_preview = parseInt(part.split(':')[1], 10)\n }\n }\n\n return directive\n}\n\n/**\n * Get robots directive for a page\n * \n * @example\n * ```tsx\n * const robots = await getRobotsDirective({\n * projectId: process.env.SONOR_PROJECT_ID!,\n * path: '/private-page'\n * })\n * \n * if (!robots.index) {\n * // Page should not be indexed\n * }\n * ```\n */\nexport async function getRobotsDirective(\n options: GetRobotsOptions\n): Promise<RobotsDirective> {\n const { path } = options\n\n const robotsString = await getRobotsData(path)\n\n if (!robotsString) {\n // Default: index and follow\n return { index: true, follow: true }\n }\n\n return parseRobotsString(robotsString)\n}\n\n/**\n * Get sitemap entries for a project\n * \n * Use in sitemap.ts to generate dynamic sitemap\n * \n * @example\n * ```tsx\n * // app/sitemap.ts\n * import { generateSitemap } from '@sonordev/seo'\n * \n * export default async function sitemap() {\n * return generateSitemap({\n * projectId: process.env.SONOR_PROJECT_ID!,\n * baseUrl: 'https://example.com',\n * publishedOnly: true\n * })\n * }\n * ```\n */\nexport async function generateSitemap(\n options: GetSitemapEntriesOptions\n): Promise<SitemapEntry[]> {\n const { baseUrl, publishedOnly = true } = options\n\n const pages = await getSitemapEntries({ publishedOnly })\n\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl\n\n return pages.map((page: { path: string; lastmod?: string; changefreq?: string; priority?: number }) => ({\n path: page.path,\n url: `${normalizedBase}${page.path}`,\n lastmod: page.lastmod,\n changefreq: (page.changefreq || 'weekly') as SitemapEntry['changefreq'],\n priority: page.priority ?? 0.5,\n }))\n}\n\n/**\n * Register local sitemap entries with Sonor\n * \n * Call this at build time to sync your local routes to seo_pages.\n * This ensures analytics only tracks real pages.\n * \n * After registration, Signal AI will generate optimized meta titles\n * and descriptions for pages that don't have managed meta yet.\n * \n * @example\n * ```ts\n * // scripts/register-sitemap.ts\n * import { registerLocalSitemap } from '@sonordev/seo'\n * \n * // Option 1: Provide entries directly\n * await registerLocalSitemap({\n * entries: [\n * { path: '/', title: 'Home', priority: 1.0 },\n * { path: '/about', title: 'About Us', priority: 0.8 },\n * ]\n * })\n * \n * // Option 2: Auto-discover from Next.js app directory\n * await registerLocalSitemap({ autoDiscover: true })\n * \n * // Option 3: Skip Signal AI meta optimization\n * await registerLocalSitemap({ autoDiscover: true, optimize_meta: false })\n * ```\n */\nexport async function registerLocalSitemap(options: {\n entries?: Array<{\n path: string\n title?: string\n priority?: number\n changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'\n }>\n autoDiscover?: boolean\n /** Trigger Signal AI to generate optimized meta titles/descriptions (default: true) */\n optimize_meta?: boolean\n}): Promise<{ \n success: boolean\n created: number\n updated: number\n removed?: number\n meta_optimization?: {\n triggered: boolean\n pages_queued: number\n } | null\n}> {\n const { registerSitemap } = await import('./server-api')\n \n let entries = options.entries || []\n \n // Auto-discover from Next.js app directory if requested\n if (options.autoDiscover && entries.length === 0) {\n try {\n const fs = await import('fs')\n const path = await import('path')\n \n const appDir = path.join(process.cwd(), 'app')\n if (fs.existsSync(appDir)) {\n entries = discoverNextJsRoutes(appDir, fs, path)\n console.log(`[Sonor] Auto-discovered ${entries.length} routes from app directory`)\n }\n } catch (error) {\n console.error('[Sonor] Auto-discovery failed:', error)\n }\n }\n \n if (entries.length === 0) {\n console.warn('[Sonor] No sitemap entries to register')\n return { success: true, created: 0, updated: 0 }\n }\n \n console.log(`[Sonor] Registering ${entries.length} sitemap entries...`)\n const result = await registerSitemap(entries)\n \n if (result.success) {\n console.log(`[Sonor] Sitemap registered: ${result.created} new, ${result.updated} updated`)\n }\n \n return result\n}\n\n/**\n * Discover routes from Next.js app directory\n */\nfunction discoverNextJsRoutes(\n appDir: string,\n fs: typeof import('fs'),\n path: typeof import('path'),\n basePath: string = ''\n): Array<{ path: string; priority: number }> {\n const entries: Array<{ path: string; priority: number }> = []\n \n const items = fs.readdirSync(appDir, { withFileTypes: true })\n \n for (const item of items) {\n // Skip private folders, api routes, and special files\n if (item.name.startsWith('_') || item.name.startsWith('.')) continue\n if (item.name === 'api') continue\n if (item.name === 'node_modules') continue\n \n const itemPath = path.join(appDir, item.name)\n \n if (item.isDirectory()) {\n // Check for page.tsx/page.js in this directory\n const hasPage = fs.existsSync(path.join(itemPath, 'page.tsx')) ||\n fs.existsSync(path.join(itemPath, 'page.js')) ||\n fs.existsSync(path.join(itemPath, 'page.jsx'))\n \n // Handle route groups (parentheses)\n const isRouteGroup = item.name.startsWith('(') && item.name.endsWith(')')\n \n // Handle dynamic segments [slug]\n const isDynamic = item.name.startsWith('[') && item.name.endsWith(']')\n \n let routePath = basePath\n if (!isRouteGroup && !isDynamic) {\n routePath = `${basePath}/${item.name}`\n }\n \n if (hasPage && !isDynamic) {\n const priority = routePath === '' ? 1.0 : 0.8\n entries.push({ path: routePath || '/', priority })\n }\n \n // Recurse into subdirectories (but not dynamic ones)\n if (!isDynamic) {\n const subEntries = discoverNextJsRoutes(itemPath, fs, path, isRouteGroup ? basePath : routePath)\n entries.push(...subEntries)\n }\n }\n }\n \n // Add root if app/page.tsx exists and we're at root\n if (basePath === '') {\n const hasRootPage = fs.existsSync(path.join(appDir, 'page.tsx')) ||\n fs.existsSync(path.join(appDir, 'page.js')) ||\n fs.existsSync(path.join(appDir, 'page.jsx'))\n if (hasRootPage) {\n entries.unshift({ path: '/', priority: 1.0 })\n }\n }\n \n return entries\n}\n\n/**\n * Check if a path should be indexed\n * \n * Quick helper to check indexability without full directive parsing\n */\nexport async function isIndexable(\n projectId: string,\n path: string\n): Promise<boolean> {\n const directive = await getRobotsDirective({ projectId, path })\n return directive.index\n}\n"]}