@sonordev/agency-site-kit 0.3.1 → 0.4.1

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 (374) hide show
  1. package/README.md +302 -0
  2. package/bin/setup.mjs +79 -0
  3. package/dist/{chunk-RXBK2EOE.js → chunk-4IES23GV.js} +3 -3
  4. package/dist/{chunk-RXBK2EOE.js.map → chunk-4IES23GV.js.map} +1 -1
  5. package/dist/{chunk-WNR4KNG4.cjs → chunk-5MEZU3OG.cjs} +4 -4
  6. package/dist/{chunk-WNR4KNG4.cjs.map → chunk-5MEZU3OG.cjs.map} +1 -1
  7. package/dist/chunk-7D4H36UF.js +13 -0
  8. package/dist/chunk-7D4H36UF.js.map +1 -0
  9. package/dist/chunk-7ZHRMWJU.cjs +306 -0
  10. package/dist/chunk-7ZHRMWJU.cjs.map +1 -0
  11. package/dist/chunk-CVT7I5BQ.js +71 -0
  12. package/dist/chunk-CVT7I5BQ.js.map +1 -0
  13. package/dist/chunk-HQGAX2LI.cjs +8 -0
  14. package/dist/chunk-HQGAX2LI.cjs.map +1 -0
  15. package/dist/chunk-K2BGTDSN.js +45 -0
  16. package/dist/chunk-K2BGTDSN.js.map +1 -0
  17. package/dist/chunk-KXLROAO5.js +291 -0
  18. package/dist/chunk-KXLROAO5.js.map +1 -0
  19. package/dist/chunk-L4EOQVRJ.js +6 -0
  20. package/dist/chunk-L4EOQVRJ.js.map +1 -0
  21. package/dist/chunk-NI4FVCLS.cjs +32 -0
  22. package/dist/chunk-NI4FVCLS.cjs.map +1 -0
  23. package/dist/chunk-NLGUPJZA.js +507 -0
  24. package/dist/chunk-NLGUPJZA.js.map +1 -0
  25. package/dist/chunk-QDXZ27R7.js +33 -0
  26. package/dist/chunk-QDXZ27R7.js.map +1 -0
  27. package/dist/chunk-QGSW43TM.cjs +538 -0
  28. package/dist/chunk-QGSW43TM.cjs.map +1 -0
  29. package/dist/{chunk-TVFXN6W6.js → chunk-QL6KVIFR.js} +8 -4
  30. package/dist/chunk-QL6KVIFR.js.map +1 -0
  31. package/dist/chunk-RWQ675N6.js +165 -0
  32. package/dist/chunk-RWQ675N6.js.map +1 -0
  33. package/dist/chunk-UXUU7FVT.cjs +171 -0
  34. package/dist/chunk-UXUU7FVT.cjs.map +1 -0
  35. package/dist/chunk-XJ434N7I.js +30 -0
  36. package/dist/chunk-XJ434N7I.js.map +1 -0
  37. package/dist/{chunk-EE75WRCM.cjs → chunk-XOWSXZYW.cjs} +8 -4
  38. package/dist/chunk-XOWSXZYW.cjs.map +1 -0
  39. package/dist/chunk-ZBPF7TLZ.cjs +16 -0
  40. package/dist/chunk-ZBPF7TLZ.cjs.map +1 -0
  41. package/dist/chunk-ZINKVJQU.js +67 -0
  42. package/dist/chunk-ZINKVJQU.js.map +1 -0
  43. package/dist/index.cjs +53 -30
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +17 -9
  46. package/dist/index.d.ts +17 -9
  47. package/dist/index.js +8 -9
  48. package/dist/index.js.map +1 -1
  49. package/dist/layout/index.cjs +3 -3
  50. package/dist/layout/index.d.cts +1 -1
  51. package/dist/layout/index.d.ts +1 -1
  52. package/dist/layout/index.js +2 -2
  53. package/dist/portfolio/client.cjs +376 -12
  54. package/dist/portfolio/client.cjs.map +1 -1
  55. package/dist/portfolio/client.d.cts +27 -85
  56. package/dist/portfolio/client.d.ts +27 -85
  57. package/dist/portfolio/client.js +182 -4
  58. package/dist/portfolio/client.js.map +1 -1
  59. package/dist/portfolio/components/PortfolioAnalytics.cjs +87 -0
  60. package/dist/portfolio/components/PortfolioAnalytics.cjs.map +1 -0
  61. package/dist/portfolio/components/PortfolioAnalytics.d.cts +5 -0
  62. package/dist/portfolio/components/PortfolioAnalytics.d.ts +5 -0
  63. package/dist/portfolio/components/PortfolioAnalytics.js +9 -0
  64. package/dist/portfolio/components/PortfolioAnalytics.js.map +1 -0
  65. package/dist/portfolio/components/PortfolioErrorFallback.cjs +65 -0
  66. package/dist/portfolio/components/PortfolioErrorFallback.cjs.map +1 -0
  67. package/dist/portfolio/components/PortfolioErrorFallback.d.cts +22 -0
  68. package/dist/portfolio/components/PortfolioErrorFallback.d.ts +22 -0
  69. package/dist/portfolio/components/PortfolioErrorFallback.js +9 -0
  70. package/dist/portfolio/components/PortfolioErrorFallback.js.map +1 -0
  71. package/dist/portfolio/components/PortfolioLeadCTA.cjs +91 -0
  72. package/dist/portfolio/components/PortfolioLeadCTA.cjs.map +1 -0
  73. package/dist/portfolio/components/PortfolioLeadCTA.d.cts +34 -0
  74. package/dist/portfolio/components/PortfolioLeadCTA.d.ts +34 -0
  75. package/dist/portfolio/components/PortfolioLeadCTA.js +9 -0
  76. package/dist/portfolio/components/PortfolioLeadCTA.js.map +1 -0
  77. package/dist/portfolio/index.cjs +43 -19
  78. package/dist/portfolio/index.d.cts +210 -7
  79. package/dist/portfolio/index.d.ts +210 -7
  80. package/dist/portfolio/index.js +7 -7
  81. package/dist/portfolio/primitives/AnimatedCounter.cjs +88 -0
  82. package/dist/portfolio/primitives/AnimatedCounter.cjs.map +1 -0
  83. package/dist/portfolio/primitives/AnimatedCounter.d.cts +22 -0
  84. package/dist/portfolio/primitives/AnimatedCounter.d.ts +22 -0
  85. package/dist/portfolio/primitives/AnimatedCounter.js +68 -0
  86. package/dist/portfolio/primitives/AnimatedCounter.js.map +1 -0
  87. package/dist/portfolio/primitives/AnnotatedImage.cjs +320 -0
  88. package/dist/portfolio/primitives/AnnotatedImage.cjs.map +1 -0
  89. package/dist/portfolio/primitives/AnnotatedImage.d.cts +18 -0
  90. package/dist/portfolio/primitives/AnnotatedImage.d.ts +18 -0
  91. package/dist/portfolio/primitives/AnnotatedImage.js +264 -0
  92. package/dist/portfolio/primitives/AnnotatedImage.js.map +1 -0
  93. package/dist/portfolio/primitives/BeforeAfterSlider.cjs +348 -0
  94. package/dist/portfolio/primitives/BeforeAfterSlider.cjs.map +1 -0
  95. package/dist/portfolio/primitives/BeforeAfterSlider.d.cts +13 -0
  96. package/dist/portfolio/primitives/BeforeAfterSlider.d.ts +13 -0
  97. package/dist/portfolio/primitives/BeforeAfterSlider.js +292 -0
  98. package/dist/portfolio/primitives/BeforeAfterSlider.js.map +1 -0
  99. package/dist/portfolio/primitives/DeviceTrifolio.cjs +298 -0
  100. package/dist/portfolio/primitives/DeviceTrifolio.cjs.map +1 -0
  101. package/dist/portfolio/primitives/DeviceTrifolio.d.cts +14 -0
  102. package/dist/portfolio/primitives/DeviceTrifolio.d.ts +14 -0
  103. package/dist/portfolio/primitives/DeviceTrifolio.js +242 -0
  104. package/dist/portfolio/primitives/DeviceTrifolio.js.map +1 -0
  105. package/dist/portfolio/primitives/FrequencyBarSkeleton.cjs +88 -0
  106. package/dist/portfolio/primitives/FrequencyBarSkeleton.cjs.map +1 -0
  107. package/dist/portfolio/primitives/FrequencyBarSkeleton.d.cts +10 -0
  108. package/dist/portfolio/primitives/FrequencyBarSkeleton.d.ts +10 -0
  109. package/dist/portfolio/primitives/FrequencyBarSkeleton.js +68 -0
  110. package/dist/portfolio/primitives/FrequencyBarSkeleton.js.map +1 -0
  111. package/dist/portfolio/primitives/GlassCard.cjs +45 -0
  112. package/dist/portfolio/primitives/GlassCard.cjs.map +1 -0
  113. package/dist/portfolio/primitives/GlassCard.d.cts +11 -0
  114. package/dist/portfolio/primitives/GlassCard.d.ts +11 -0
  115. package/dist/portfolio/primitives/GlassCard.js +43 -0
  116. package/dist/portfolio/primitives/GlassCard.js.map +1 -0
  117. package/dist/portfolio/primitives/MetricBadge.cjs +74 -0
  118. package/dist/portfolio/primitives/MetricBadge.cjs.map +1 -0
  119. package/dist/portfolio/primitives/MetricBadge.d.cts +9 -0
  120. package/dist/portfolio/primitives/MetricBadge.d.ts +9 -0
  121. package/dist/portfolio/primitives/MetricBadge.js +54 -0
  122. package/dist/portfolio/primitives/MetricBadge.js.map +1 -0
  123. package/dist/portfolio/primitives/SVGChart.cjs +316 -0
  124. package/dist/portfolio/primitives/SVGChart.cjs.map +1 -0
  125. package/dist/portfolio/primitives/SVGChart.d.cts +18 -0
  126. package/dist/portfolio/primitives/SVGChart.d.ts +18 -0
  127. package/dist/portfolio/primitives/SVGChart.js +296 -0
  128. package/dist/portfolio/primitives/SVGChart.js.map +1 -0
  129. package/dist/portfolio/primitives/ScrollReveal.cjs +97 -0
  130. package/dist/portfolio/primitives/ScrollReveal.cjs.map +1 -0
  131. package/dist/portfolio/primitives/ScrollReveal.d.cts +27 -0
  132. package/dist/portfolio/primitives/ScrollReveal.d.ts +27 -0
  133. package/dist/portfolio/primitives/ScrollReveal.js +77 -0
  134. package/dist/portfolio/primitives/ScrollReveal.js.map +1 -0
  135. package/dist/portfolio/primitives/SplitHeadline.cjs +107 -0
  136. package/dist/portfolio/primitives/SplitHeadline.cjs.map +1 -0
  137. package/dist/portfolio/primitives/SplitHeadline.d.cts +24 -0
  138. package/dist/portfolio/primitives/SplitHeadline.d.ts +24 -0
  139. package/dist/portfolio/primitives/SplitHeadline.js +87 -0
  140. package/dist/portfolio/primitives/SplitHeadline.js.map +1 -0
  141. package/dist/portfolio/sections/BeforeAfterSection.cjs +271 -0
  142. package/dist/portfolio/sections/BeforeAfterSection.cjs.map +1 -0
  143. package/dist/portfolio/sections/BeforeAfterSection.d.cts +9 -0
  144. package/dist/portfolio/sections/BeforeAfterSection.d.ts +9 -0
  145. package/dist/{BeforeAfterSection-6BHFLY4Y.js → portfolio/sections/BeforeAfterSection.js} +66 -27
  146. package/dist/portfolio/sections/BeforeAfterSection.js.map +1 -0
  147. package/dist/portfolio/sections/CTASection.cjs +123 -0
  148. package/dist/portfolio/sections/CTASection.cjs.map +1 -0
  149. package/dist/portfolio/sections/CTASection.d.cts +9 -0
  150. package/dist/portfolio/sections/CTASection.d.ts +9 -0
  151. package/dist/portfolio/sections/CTASection.js +93 -0
  152. package/dist/portfolio/sections/CTASection.js.map +1 -0
  153. package/dist/{ChallengesSection-IZ3DHECS.cjs → portfolio/sections/ChallengesSection.cjs} +13 -7
  154. package/dist/portfolio/sections/ChallengesSection.cjs.map +1 -0
  155. package/dist/portfolio/sections/ChallengesSection.d.cts +9 -0
  156. package/dist/portfolio/sections/ChallengesSection.d.ts +9 -0
  157. package/dist/{ChallengesSection-GEQGVSJN.js → portfolio/sections/ChallengesSection.js} +6 -5
  158. package/dist/portfolio/sections/ChallengesSection.js.map +1 -0
  159. package/dist/portfolio/sections/ConversionFunnelSection.cjs +263 -0
  160. package/dist/portfolio/sections/ConversionFunnelSection.cjs.map +1 -0
  161. package/dist/portfolio/sections/ConversionFunnelSection.d.cts +9 -0
  162. package/dist/portfolio/sections/ConversionFunnelSection.d.ts +9 -0
  163. package/dist/{ConversionFunnelSection-D3GE4NKE.js → portfolio/sections/ConversionFunnelSection.js} +60 -56
  164. package/dist/portfolio/sections/ConversionFunnelSection.js.map +1 -0
  165. package/dist/{DesignSystemSection-KXIQXITF.cjs → portfolio/sections/DesignSystemSection.cjs} +13 -7
  166. package/dist/portfolio/sections/DesignSystemSection.cjs.map +1 -0
  167. package/dist/portfolio/sections/DesignSystemSection.d.cts +32 -0
  168. package/dist/portfolio/sections/DesignSystemSection.d.ts +32 -0
  169. package/dist/{DesignSystemSection-2R5BRBGO.js → portfolio/sections/DesignSystemSection.js} +5 -4
  170. package/dist/portfolio/sections/DesignSystemSection.js.map +1 -0
  171. package/dist/{DetailsSection-TTUZAPZZ.cjs → portfolio/sections/DetailsSection.cjs} +12 -6
  172. package/dist/portfolio/sections/DetailsSection.cjs.map +1 -0
  173. package/dist/portfolio/sections/DetailsSection.d.cts +9 -0
  174. package/dist/portfolio/sections/DetailsSection.d.ts +9 -0
  175. package/dist/{DetailsSection-A6PZQUQL.js → portfolio/sections/DetailsSection.js} +5 -4
  176. package/dist/portfolio/sections/DetailsSection.js.map +1 -0
  177. package/dist/portfolio/sections/FeatureSpotlightSection.cjs +267 -0
  178. package/dist/portfolio/sections/FeatureSpotlightSection.cjs.map +1 -0
  179. package/dist/portfolio/sections/FeatureSpotlightSection.d.cts +9 -0
  180. package/dist/portfolio/sections/FeatureSpotlightSection.d.ts +9 -0
  181. package/dist/{FeatureSpotlightSection-B7P3JGNL.js → portfolio/sections/FeatureSpotlightSection.js} +39 -33
  182. package/dist/portfolio/sections/FeatureSpotlightSection.js.map +1 -0
  183. package/dist/portfolio/sections/GallerySection.cjs +321 -0
  184. package/dist/portfolio/sections/GallerySection.cjs.map +1 -0
  185. package/dist/portfolio/sections/GallerySection.d.cts +9 -0
  186. package/dist/portfolio/sections/GallerySection.d.ts +9 -0
  187. package/dist/{GallerySection-VMKORC47.js → portfolio/sections/GallerySection.js} +65 -18
  188. package/dist/portfolio/sections/GallerySection.js.map +1 -0
  189. package/dist/portfolio/sections/HeroSection.cjs +180 -0
  190. package/dist/portfolio/sections/HeroSection.cjs.map +1 -0
  191. package/dist/portfolio/sections/HeroSection.d.cts +15 -0
  192. package/dist/portfolio/sections/HeroSection.d.ts +15 -0
  193. package/dist/portfolio/sections/HeroSection.js +171 -0
  194. package/dist/portfolio/sections/HeroSection.js.map +1 -0
  195. package/dist/{MetricsTimelineSection-4L6DUHJ5.cjs → portfolio/sections/MetricsTimelineSection.cjs} +102 -61
  196. package/dist/portfolio/sections/MetricsTimelineSection.cjs.map +1 -0
  197. package/dist/portfolio/sections/MetricsTimelineSection.d.cts +9 -0
  198. package/dist/portfolio/sections/MetricsTimelineSection.d.ts +9 -0
  199. package/dist/{MetricsTimelineSection-6BT5GNFV.js → portfolio/sections/MetricsTimelineSection.js} +45 -29
  200. package/dist/portfolio/sections/MetricsTimelineSection.js.map +1 -0
  201. package/dist/portfolio/sections/PerformanceSection.cjs +336 -0
  202. package/dist/portfolio/sections/PerformanceSection.cjs.map +1 -0
  203. package/dist/portfolio/sections/PerformanceSection.d.cts +22 -0
  204. package/dist/portfolio/sections/PerformanceSection.d.ts +22 -0
  205. package/dist/{PerformanceSection-MGCEIXDX.js → portfolio/sections/PerformanceSection.js} +59 -104
  206. package/dist/portfolio/sections/PerformanceSection.js.map +1 -0
  207. package/dist/{ResultsSection-XLGMMQKY.cjs → portfolio/sections/ResultsSection.cjs} +23 -10
  208. package/dist/portfolio/sections/ResultsSection.cjs.map +1 -0
  209. package/dist/portfolio/sections/ResultsSection.d.cts +13 -0
  210. package/dist/portfolio/sections/ResultsSection.d.ts +13 -0
  211. package/dist/{ResultsSection-DFUJ5U6M.js → portfolio/sections/ResultsSection.js} +14 -7
  212. package/dist/portfolio/sections/ResultsSection.js.map +1 -0
  213. package/dist/portfolio/sections/ServicesSection.cjs +129 -0
  214. package/dist/portfolio/sections/ServicesSection.cjs.map +1 -0
  215. package/dist/portfolio/sections/ServicesSection.d.cts +9 -0
  216. package/dist/portfolio/sections/ServicesSection.d.ts +9 -0
  217. package/dist/portfolio/sections/ServicesSection.js +122 -0
  218. package/dist/portfolio/sections/ServicesSection.js.map +1 -0
  219. package/dist/{SiteArchitectureSection-J7BPBERH.cjs → portfolio/sections/SiteArchitectureSection.cjs} +117 -93
  220. package/dist/portfolio/sections/SiteArchitectureSection.cjs.map +1 -0
  221. package/dist/portfolio/sections/SiteArchitectureSection.d.cts +21 -0
  222. package/dist/portfolio/sections/SiteArchitectureSection.d.ts +21 -0
  223. package/dist/{SiteArchitectureSection-TE22OAWV.js → portfolio/sections/SiteArchitectureSection.js} +47 -48
  224. package/dist/portfolio/sections/SiteArchitectureSection.js.map +1 -0
  225. package/dist/{SpeedComparisonSection-EZKFQVGW.cjs → portfolio/sections/SpeedComparisonSection.cjs} +13 -7
  226. package/dist/portfolio/sections/SpeedComparisonSection.cjs.map +1 -0
  227. package/dist/portfolio/sections/SpeedComparisonSection.d.cts +20 -0
  228. package/dist/portfolio/sections/SpeedComparisonSection.d.ts +20 -0
  229. package/dist/{SpeedComparisonSection-Y3K7OFZQ.js → portfolio/sections/SpeedComparisonSection.js} +6 -5
  230. package/dist/portfolio/sections/SpeedComparisonSection.js.map +1 -0
  231. package/dist/portfolio/sections/StrategySection.cjs +233 -0
  232. package/dist/portfolio/sections/StrategySection.cjs.map +1 -0
  233. package/dist/portfolio/sections/StrategySection.d.cts +9 -0
  234. package/dist/portfolio/sections/StrategySection.d.ts +9 -0
  235. package/dist/{StrategySection-DI5RSCJU.js → portfolio/sections/StrategySection.js} +60 -26
  236. package/dist/portfolio/sections/StrategySection.js.map +1 -0
  237. package/dist/portfolio/sections/TeamSection.cjs +162 -0
  238. package/dist/portfolio/sections/TeamSection.cjs.map +1 -0
  239. package/dist/portfolio/sections/TeamSection.d.cts +9 -0
  240. package/dist/portfolio/sections/TeamSection.d.ts +9 -0
  241. package/dist/portfolio/sections/TeamSection.js +106 -0
  242. package/dist/portfolio/sections/TeamSection.js.map +1 -0
  243. package/dist/portfolio/sections/TechStackSection.cjs +106 -0
  244. package/dist/portfolio/sections/TechStackSection.cjs.map +1 -0
  245. package/dist/portfolio/sections/TechStackSection.d.cts +9 -0
  246. package/dist/portfolio/sections/TechStackSection.d.ts +9 -0
  247. package/dist/portfolio/sections/TechStackSection.js +99 -0
  248. package/dist/portfolio/sections/TechStackSection.js.map +1 -0
  249. package/dist/{TestimonialSection-XPTFUQIN.cjs → portfolio/sections/TestimonialSection.cjs} +47 -28
  250. package/dist/portfolio/sections/TestimonialSection.cjs.map +1 -0
  251. package/dist/portfolio/sections/TestimonialSection.d.cts +9 -0
  252. package/dist/portfolio/sections/TestimonialSection.d.ts +9 -0
  253. package/dist/{TestimonialSection-6RGSMXQB.js → portfolio/sections/TestimonialSection.js} +40 -27
  254. package/dist/portfolio/sections/TestimonialSection.js.map +1 -0
  255. package/dist/portfolio/sections/VideoSection.cjs +181 -0
  256. package/dist/portfolio/sections/VideoSection.cjs.map +1 -0
  257. package/dist/portfolio/sections/VideoSection.d.cts +9 -0
  258. package/dist/portfolio/sections/VideoSection.d.ts +9 -0
  259. package/dist/{VideoSection-4A2HC6K6.js → portfolio/sections/VideoSection.js} +21 -13
  260. package/dist/portfolio/sections/VideoSection.js.map +1 -0
  261. package/dist/portfolio/server.cjs +56 -144
  262. package/dist/portfolio/server.cjs.map +1 -1
  263. package/dist/portfolio/server.d.cts +157 -7
  264. package/dist/portfolio/server.d.ts +157 -7
  265. package/dist/portfolio/server.js +3 -142
  266. package/dist/portfolio/server.js.map +1 -1
  267. package/dist/preview.cjs +56 -0
  268. package/dist/preview.cjs.map +1 -0
  269. package/dist/preview.d.cts +31 -0
  270. package/dist/preview.d.ts +31 -0
  271. package/dist/preview.js +54 -0
  272. package/dist/preview.js.map +1 -0
  273. package/dist/{types-DL4t_Cfa.d.ts → types-0tVPy4pU.d.cts} +7 -1
  274. package/dist/{types-DL4t_Cfa.d.cts → types-0tVPy4pU.d.ts} +7 -1
  275. package/dist/types-D0nb6KAx.d.cts +299 -0
  276. package/dist/types-D0nb6KAx.d.ts +299 -0
  277. package/package.json +38 -14
  278. package/templates/api-portfolio-preview.ts +6 -0
  279. package/templates/api-revalidate.ts +5 -0
  280. package/templates/work-category-page.tsx +40 -0
  281. package/templates/work-error.tsx +3 -0
  282. package/templates/work-loading.tsx +1 -0
  283. package/templates/work-page.tsx +37 -0
  284. package/templates/work-slug-loading.tsx +1 -0
  285. package/templates/work-slug-page.tsx +41 -0
  286. package/dist/BeforeAfterSection-6BHFLY4Y.js.map +0 -1
  287. package/dist/BeforeAfterSection-JTORBR3A.cjs +0 -181
  288. package/dist/BeforeAfterSection-JTORBR3A.cjs.map +0 -1
  289. package/dist/CTASection-4JKLXEUF.cjs +0 -111
  290. package/dist/CTASection-4JKLXEUF.cjs.map +0 -1
  291. package/dist/CTASection-BJA72XIL.js +0 -106
  292. package/dist/CTASection-BJA72XIL.js.map +0 -1
  293. package/dist/ChallengesSection-GEQGVSJN.js.map +0 -1
  294. package/dist/ChallengesSection-IZ3DHECS.cjs.map +0 -1
  295. package/dist/ConversionFunnelSection-AUUSJ5HQ.cjs +0 -209
  296. package/dist/ConversionFunnelSection-AUUSJ5HQ.cjs.map +0 -1
  297. package/dist/ConversionFunnelSection-D3GE4NKE.js.map +0 -1
  298. package/dist/DesignSystemSection-2R5BRBGO.js.map +0 -1
  299. package/dist/DesignSystemSection-KXIQXITF.cjs.map +0 -1
  300. package/dist/DetailsSection-A6PZQUQL.js.map +0 -1
  301. package/dist/DetailsSection-TTUZAPZZ.cjs.map +0 -1
  302. package/dist/FeatureSpotlightSection-B7P3JGNL.js.map +0 -1
  303. package/dist/FeatureSpotlightSection-WRHXS7TU.cjs +0 -210
  304. package/dist/FeatureSpotlightSection-WRHXS7TU.cjs.map +0 -1
  305. package/dist/GallerySection-VMKORC47.js.map +0 -1
  306. package/dist/GallerySection-WJ4PQDBI.cjs +0 -219
  307. package/dist/GallerySection-WJ4PQDBI.cjs.map +0 -1
  308. package/dist/MetricsTimelineSection-4L6DUHJ5.cjs.map +0 -1
  309. package/dist/MetricsTimelineSection-6BT5GNFV.js.map +0 -1
  310. package/dist/PerformanceSection-24TVVFZA.cjs +0 -356
  311. package/dist/PerformanceSection-24TVVFZA.cjs.map +0 -1
  312. package/dist/PerformanceSection-MGCEIXDX.js.map +0 -1
  313. package/dist/ResultsSection-DFUJ5U6M.js.map +0 -1
  314. package/dist/ResultsSection-XLGMMQKY.cjs.map +0 -1
  315. package/dist/ServicesSection-D5V3Q4GR.js +0 -118
  316. package/dist/ServicesSection-D5V3Q4GR.js.map +0 -1
  317. package/dist/ServicesSection-WJMGK2MF.cjs +0 -120
  318. package/dist/ServicesSection-WJMGK2MF.cjs.map +0 -1
  319. package/dist/SiteArchitectureSection-J7BPBERH.cjs.map +0 -1
  320. package/dist/SiteArchitectureSection-TE22OAWV.js.map +0 -1
  321. package/dist/SpeedComparisonSection-EZKFQVGW.cjs.map +0 -1
  322. package/dist/SpeedComparisonSection-Y3K7OFZQ.js.map +0 -1
  323. package/dist/StrategySection-CJ7Y6OFQ.cjs +0 -174
  324. package/dist/StrategySection-CJ7Y6OFQ.cjs.map +0 -1
  325. package/dist/StrategySection-DI5RSCJU.js.map +0 -1
  326. package/dist/TeamSection-DZVSNZE6.cjs +0 -112
  327. package/dist/TeamSection-DZVSNZE6.cjs.map +0 -1
  328. package/dist/TeamSection-HGKFW6PQ.js +0 -107
  329. package/dist/TeamSection-HGKFW6PQ.js.map +0 -1
  330. package/dist/TechStackSection-2AQ7RGY3.js +0 -93
  331. package/dist/TechStackSection-2AQ7RGY3.js.map +0 -1
  332. package/dist/TechStackSection-VTNNZR5V.cjs +0 -95
  333. package/dist/TechStackSection-VTNNZR5V.cjs.map +0 -1
  334. package/dist/TestimonialSection-6RGSMXQB.js.map +0 -1
  335. package/dist/TestimonialSection-XPTFUQIN.cjs.map +0 -1
  336. package/dist/VideoSection-4A2HC6K6.js.map +0 -1
  337. package/dist/VideoSection-G3DFS7UH.cjs +0 -118
  338. package/dist/VideoSection-G3DFS7UH.cjs.map +0 -1
  339. package/dist/chunk-2Y4O3LWM.js +0 -53
  340. package/dist/chunk-2Y4O3LWM.js.map +0 -1
  341. package/dist/chunk-633UMQQH.cjs +0 -31
  342. package/dist/chunk-633UMQQH.cjs.map +0 -1
  343. package/dist/chunk-7CFFAKDM.js +0 -74
  344. package/dist/chunk-7CFFAKDM.js.map +0 -1
  345. package/dist/chunk-APG2QSMB.js +0 -131
  346. package/dist/chunk-APG2QSMB.js.map +0 -1
  347. package/dist/chunk-AVCH4LMG.cjs +0 -620
  348. package/dist/chunk-AVCH4LMG.cjs.map +0 -1
  349. package/dist/chunk-AYBUHRFS.js +0 -613
  350. package/dist/chunk-AYBUHRFS.js.map +0 -1
  351. package/dist/chunk-EE75WRCM.cjs.map +0 -1
  352. package/dist/chunk-IKBK7HYX.cjs +0 -79
  353. package/dist/chunk-IKBK7HYX.cjs.map +0 -1
  354. package/dist/chunk-KEOHORIH.cjs +0 -79
  355. package/dist/chunk-KEOHORIH.cjs.map +0 -1
  356. package/dist/chunk-OA5ZM4OA.cjs +0 -139
  357. package/dist/chunk-OA5ZM4OA.cjs.map +0 -1
  358. package/dist/chunk-TVFXN6W6.js.map +0 -1
  359. package/dist/chunk-VVTK4OL6.js +0 -28
  360. package/dist/chunk-VVTK4OL6.js.map +0 -1
  361. package/dist/chunk-XCKXHK44.js +0 -15
  362. package/dist/chunk-XCKXHK44.js.map +0 -1
  363. package/dist/chunk-XONXEFJY.cjs +0 -58
  364. package/dist/chunk-XONXEFJY.cjs.map +0 -1
  365. package/dist/chunk-XQNJED46.cjs +0 -19
  366. package/dist/chunk-XQNJED46.cjs.map +0 -1
  367. package/dist/chunk-YB4B3OMC.js +0 -74
  368. package/dist/chunk-YB4B3OMC.js.map +0 -1
  369. package/dist/portfolio/sections.cjs +0 -20
  370. package/dist/portfolio/sections.cjs.map +0 -1
  371. package/dist/portfolio/sections.d.cts +0 -42
  372. package/dist/portfolio/sections.d.ts +0 -42
  373. package/dist/portfolio/sections.js +0 -4
  374. package/dist/portfolio/sections.js.map +0 -1
@@ -0,0 +1,291 @@
1
+ import { PORTFOLIO_CACHE_TAG } from './chunk-N7TVKUYQ.js';
2
+ import { PORTFOLIO_PREVIEW_COOKIE } from './chunk-L4EOQVRJ.js';
3
+ import { apiFetch } from './chunk-QL6KVIFR.js';
4
+
5
+ // src/portfolio/sanitize.ts
6
+ function humanizeArchPageEntry(page) {
7
+ const title = (page?.title || "").trim();
8
+ if (title && !title.startsWith("/")) return { title };
9
+ const source = title || (page?.path || "").trim();
10
+ const lastSegment = source.split(/[?#]/)[0].split("/").filter(Boolean).pop() || "";
11
+ const label = lastSegment.replace(/\.(html?|php|aspx?)$/i, "").replace(/[-_]+/g, " ").trim().replace(/\b\w/g, (c) => c.toUpperCase());
12
+ return { title: label || "Home" };
13
+ }
14
+ function sanitizeSiteArchitectureData(data) {
15
+ if (!data || !Array.isArray(data.groups)) return data;
16
+ return {
17
+ ...data,
18
+ groups: data.groups.map((group) => ({
19
+ ...group,
20
+ pages: Array.isArray(group.pages) ? group.pages.map(humanizeArchPageEntry) : []
21
+ }))
22
+ };
23
+ }
24
+ function sanitizePortfolioSections(sections) {
25
+ return sections.map(
26
+ (section) => section?.sectionType === "portfolioSiteArchitecture" ? { ...section, data: sanitizeSiteArchitectureData(section.data) } : section
27
+ );
28
+ }
29
+
30
+ // src/portfolio/server.ts
31
+ var LOG_PREFIX = "[@sonordev/agency-site-kit/portfolio]";
32
+ var PortfolioApiError = class extends Error {
33
+ constructor(path, message) {
34
+ super(
35
+ message ?? `${LOG_PREFIX} Portfolio API request failed: ${path}. This is an infrastructure/config failure (network, 5xx, or missing SONOR_API_KEY), not a missing item \u2014 failing loudly so it cannot be mistaken for a 404 or an empty portfolio.`
36
+ );
37
+ this.name = "PortfolioApiError";
38
+ this.path = path;
39
+ }
40
+ };
41
+ var PortfolioContractError = class extends PortfolioApiError {
42
+ constructor(path, field, detail) {
43
+ super(
44
+ path,
45
+ `${LOG_PREFIX} Portfolio API contract violation at "${field}" (${path}): ${detail}. Refusing to render partial or corrupt data.`
46
+ );
47
+ this.name = "PortfolioContractError";
48
+ this.field = field;
49
+ }
50
+ };
51
+ function assertPortfolioItemShape(item, path) {
52
+ const fail = (field, detail) => {
53
+ throw new PortfolioContractError(path, field, detail);
54
+ };
55
+ if (typeof item !== "object" || item === null) fail("item", "not an object");
56
+ const record = item;
57
+ for (const field of ["id", "slug", "title"]) {
58
+ const v = record[field];
59
+ if (typeof v !== "string" || v.length === 0) {
60
+ fail(field, `expected a non-empty string, got ${JSON.stringify(v)}`);
61
+ }
62
+ }
63
+ for (const field of ["sections", "services", "kpis", "metricsDelta"]) {
64
+ const v = record[field];
65
+ if (v != null && !Array.isArray(v)) {
66
+ fail(field, `expected an array when present, got ${typeof v}`);
67
+ }
68
+ }
69
+ const sections = record.sections ?? [];
70
+ sections.forEach((section, i) => {
71
+ if (typeof section !== "object" || section === null) {
72
+ fail(`sections[${i}]`, "not an object");
73
+ }
74
+ const s = section;
75
+ if (typeof s.sectionType !== "string" || s.sectionType.length === 0) {
76
+ fail(`sections[${i}].sectionType`, "expected a non-empty string");
77
+ }
78
+ if (typeof s.data !== "object" || s.data === null) {
79
+ fail(`sections[${i}].data`, "expected an object");
80
+ }
81
+ });
82
+ }
83
+ async function portfolioGet(path, opts = {}) {
84
+ const data = await apiFetch(path, {
85
+ method: "GET",
86
+ tags: [PORTFOLIO_CACHE_TAG],
87
+ retry: true,
88
+ maxAttempts: 3,
89
+ requireKey: true,
90
+ ...opts.revalidate !== void 0 ? { revalidate: opts.revalidate } : {}
91
+ });
92
+ if (data === null) throw new PortfolioApiError(path);
93
+ return data;
94
+ }
95
+ async function getDraftPreviewToken() {
96
+ try {
97
+ const { draftMode, cookies } = await import('next/headers');
98
+ const dm = await draftMode();
99
+ if (!dm.isEnabled) return null;
100
+ const jar = await cookies();
101
+ return jar.get(PORTFOLIO_PREVIEW_COOKIE)?.value ?? null;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+ function buildQuery(params) {
107
+ const entries = Object.entries(params).filter(
108
+ (entry) => entry[1] != null
109
+ );
110
+ if (entries.length === 0) return "";
111
+ const qs = new URLSearchParams(
112
+ entries.map(([k, v]) => [k, String(v)])
113
+ ).toString();
114
+ return `?${qs}`;
115
+ }
116
+ async function getPortfolioItems(options) {
117
+ const query = buildQuery({
118
+ category: options?.category,
119
+ featured: options?.featured,
120
+ limit: options?.limit,
121
+ offset: options?.offset
122
+ });
123
+ return portfolioGet(
124
+ `/api/public/portfolio/items${query}`
125
+ );
126
+ }
127
+ async function getPortfolioItem(slug) {
128
+ if (!slug) {
129
+ console.warn(`${LOG_PREFIX} getPortfolioItem called with empty slug.`);
130
+ return null;
131
+ }
132
+ const previewToken = await getDraftPreviewToken();
133
+ const path = previewToken ? `/api/public/portfolio/items/${encodeURIComponent(slug)}?preview=${encodeURIComponent(previewToken)}` : `/api/public/portfolio/items/${encodeURIComponent(slug)}`;
134
+ const data = await portfolioGet(
135
+ path,
136
+ previewToken ? { revalidate: 0 } : {}
137
+ );
138
+ if (!data.found || !data.item) {
139
+ console.warn(`${LOG_PREFIX} Portfolio item not found for slug: "${slug}".`);
140
+ return null;
141
+ }
142
+ assertPortfolioItemShape(data.item, `/api/public/portfolio/items/${slug}`);
143
+ const { item } = data;
144
+ return {
145
+ id: item.id,
146
+ slug: item.slug,
147
+ title: item.title,
148
+ subtitle: item.subtitle,
149
+ category: item.category,
150
+ services: item.services,
151
+ description: item.description,
152
+ hero_image: item.hero_image,
153
+ hero_image_alt: item.hero_image_alt,
154
+ live_url: item.live_url,
155
+ project_id: item.project_id ?? null,
156
+ kpis: item.kpis,
157
+ details: item.details,
158
+ seo: item.seo,
159
+ featured: item.featured,
160
+ order: item.order,
161
+ published_at: item.published_at,
162
+ hero_screenshots: item.hero_screenshots,
163
+ baseline_metrics: item.baseline_metrics,
164
+ current_metrics: item.current_metrics,
165
+ metrics_last_refreshed_at: item.metrics_last_refreshed_at,
166
+ // Sanitized HERE — the enforcing layer. Even with PortfolioPage as a
167
+ // genuine RSC, each rendered section's data still crosses the client
168
+ // boundary into the flight payload, and any future bundling regression
169
+ // would leak the whole item again (the 0.3.1 GSC 404-flood vector).
170
+ // Sanitize at the last server-side hop, always.
171
+ sections: sanitizePortfolioSections(item.sections ?? []),
172
+ metricsDelta: item.metricsDelta ?? [],
173
+ sectionsOrdered: item.sectionsOrdered ?? false
174
+ };
175
+ }
176
+ async function getPortfolioCategories() {
177
+ return portfolioGet("/api/public/portfolio/categories");
178
+ }
179
+ async function getPortfolioBrandConfig() {
180
+ const fallback = {
181
+ primary: "#6366f1",
182
+ secondary: "#8b5cf6",
183
+ background: "#0a0a0f",
184
+ backgroundElevated: "#12121a",
185
+ surface: "#1a1a2e",
186
+ surfaceHover: "#22223a",
187
+ surfaceBorder: "#2a2a3e",
188
+ textPrimary: "#f0f0f5",
189
+ textSecondary: "#a0a0b5",
190
+ textTertiary: "#6b6b80",
191
+ fontHeading: "Inter, system-ui, sans-serif",
192
+ fontBody: "Inter, system-ui, sans-serif"
193
+ };
194
+ try {
195
+ const data = await portfolioGet(
196
+ "/api/public/portfolio/config"
197
+ );
198
+ if (!data.brand) {
199
+ console.warn(`${LOG_PREFIX} Brand config response empty, using defaults.`);
200
+ return fallback;
201
+ }
202
+ return data.brand;
203
+ } catch {
204
+ console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);
205
+ return fallback;
206
+ }
207
+ }
208
+ async function generatePortfolioMetadata(slug) {
209
+ const item = await getPortfolioItem(slug);
210
+ if (!item) {
211
+ return {};
212
+ }
213
+ const seo = item.seo;
214
+ const title = seo?.metaTitle || item.title;
215
+ const description = seo?.metaDescription || item.description;
216
+ const keywords = seo?.keywords ?? [];
217
+ const ogImages = [];
218
+ if (item.hero_image) {
219
+ ogImages.push(item.hero_image);
220
+ }
221
+ return {
222
+ title,
223
+ description,
224
+ keywords: keywords.length > 0 ? keywords : void 0,
225
+ openGraph: {
226
+ title,
227
+ description,
228
+ images: ogImages.length > 0 ? ogImages : void 0,
229
+ type: "article"
230
+ }
231
+ };
232
+ }
233
+ async function generatePortfolioStaticParams() {
234
+ const data = await getPortfolioItems({ limit: 500, offset: 0 });
235
+ return data.items.map((item) => ({ slug: item.slug }));
236
+ }
237
+ function slugifyCategory(category) {
238
+ return category.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
239
+ }
240
+ async function generatePortfolioCategoryStaticParams() {
241
+ const categories = await getPortfolioCategories();
242
+ return categories.map((c) => ({ category: slugifyCategory(c) }));
243
+ }
244
+ async function generatePortfolioIndexMetadata(options) {
245
+ const {
246
+ baseUrl,
247
+ basePath = "/work",
248
+ category = null,
249
+ page = 1,
250
+ titleBase = "Our Work"
251
+ } = options;
252
+ const origin = baseUrl.replace(/\/+$/, "");
253
+ const prefix = basePath.startsWith("/") ? basePath : `/${basePath}`;
254
+ let title = titleBase;
255
+ let path = prefix;
256
+ if (category) {
257
+ const categories = await getPortfolioCategories();
258
+ const match = categories.find((c) => slugifyCategory(c) === category);
259
+ if (match) title = `${match} | ${titleBase}`;
260
+ path = `${prefix}/category/${category}`;
261
+ }
262
+ if (page > 1) {
263
+ title = `${title} \u2014 Page ${page}`;
264
+ path = `${path}?page=${page}`;
265
+ }
266
+ return { title, alternates: { canonical: `${origin}${path}` } };
267
+ }
268
+ async function generatePortfolioSitemapEntries(options) {
269
+ const {
270
+ baseUrl,
271
+ basePath = "/work",
272
+ changeFrequency = "monthly",
273
+ priority = 0.7
274
+ } = options;
275
+ const origin = baseUrl.replace(/\/+$/, "");
276
+ const prefix = basePath.startsWith("/") ? basePath : `/${basePath}`;
277
+ const data = await getPortfolioItems({ limit: 500, offset: 0 });
278
+ return data.items.map((item) => {
279
+ const lastModified = item.metrics_last_refreshed_at || item.published_at || void 0;
280
+ return {
281
+ url: `${origin}${prefix}/${item.slug}`,
282
+ ...lastModified ? { lastModified } : {},
283
+ changeFrequency,
284
+ priority
285
+ };
286
+ });
287
+ }
288
+
289
+ export { PortfolioApiError, PortfolioContractError, assertPortfolioItemShape, generatePortfolioCategoryStaticParams, generatePortfolioIndexMetadata, generatePortfolioMetadata, generatePortfolioSitemapEntries, generatePortfolioStaticParams, getPortfolioBrandConfig, getPortfolioCategories, getPortfolioItem, getPortfolioItems, sanitizeSiteArchitectureData, slugifyCategory };
290
+ //# sourceMappingURL=chunk-KXLROAO5.js.map
291
+ //# sourceMappingURL=chunk-KXLROAO5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/portfolio/sanitize.ts","../src/portfolio/server.ts"],"names":[],"mappings":"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","file":"chunk-KXLROAO5.js","sourcesContent":["/**\n * Portfolio section sanitization — single source of truth.\n *\n * Site-architecture entries arrive from generation with raw CLIENT-site paths\n * (often title === path, e.g. \"/wheels/amp-tires\"). Those strings must never\n * cross a server→client props boundary: Googlebot extracts URL-like strings\n * from the RSC flight payload and crawls them against the AGENCY site's\n * origin, flooding Search Console with 404s (seen on upforge.io, June 2026 —\n * ~100 404'd client paths, all traced to /work/* case-study payloads).\n *\n * The enforcement point is `getPortfolioItem` (portfolio/server.ts) — the\n * last hop that is guaranteed server-side no matter how the build behaves.\n * PortfolioPage is a genuine RSC since 0.4.0 (and `pnpm verify` enforces\n * that), but rendered section data still crosses the client boundary as\n * props, and any future bundling regression would leak the whole item again.\n * Sanitize at the fetch, always. The PortfolioPage call is defense-in-depth.\n */\n\ninterface ArchPageEntry {\n path?: string;\n title?: string;\n}\n\n/** Convert a raw path/title entry into a display-only { title } label. */\nexport function humanizeArchPageEntry(page: ArchPageEntry): { title: string } {\n const title = (page?.title || '').trim();\n if (title && !title.startsWith('/')) return { title };\n const source = title || (page?.path || '').trim();\n const lastSegment = source.split(/[?#]/)[0].split('/').filter(Boolean).pop() || '';\n const label = lastSegment\n .replace(/\\.(html?|php|aspx?)$/i, '')\n .replace(/[-_]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n return { title: label || 'Home' };\n}\n\n/** Strip raw page paths from a portfolioSiteArchitecture data payload. */\nexport function sanitizeSiteArchitectureData(data: any): any {\n if (!data || !Array.isArray(data.groups)) return data;\n return {\n ...data,\n groups: data.groups.map((group: any) => ({\n ...group,\n pages: Array.isArray(group.pages) ? group.pages.map(humanizeArchPageEntry) : [],\n })),\n };\n}\n\n/** Sanitize every section that can carry raw client-site paths. */\nexport function sanitizePortfolioSections<T extends { sectionType?: string; data?: any }>(\n sections: T[],\n): T[] {\n return sections.map((section) =>\n section?.sectionType === 'portfolioSiteArchitecture'\n ? { ...section, data: sanitizeSiteArchitectureData(section.data) }\n : section,\n );\n}\n","/**\n * @sonordev/agency-site-kit — Portfolio server-side data fetching\n *\n * RSC (React Server Component) data layer for portfolio pages.\n * All functions are async and designed for use in Next.js App Router\n * server components, generateMetadata(), and generateStaticParams().\n *\n * This file is server-only — no 'use client' directive.\n */\n\nimport { apiFetch } from '../shared/api';\nimport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\nimport { PORTFOLIO_PREVIEW_COOKIE } from '../shared/preview';\nimport { sanitizePortfolioSections } from './sanitize';\nimport type {\n BrandConfig,\n MetricsDelta,\n PortfolioConfigResponse,\n PortfolioItemFull,\n PortfolioListResponse,\n PortfolioSeoData,\n PortfolioSection,\n} from '../types';\n\nconst LOG_PREFIX = '[@sonordev/agency-site-kit/portfolio]';\n\n// Re-export so consumers and route handlers can reference the same tag.\nexport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\n\n/**\n * Error contract (single source of truth for portfolio content fetching):\n *\n * - \"Not found\" is a DATA answer: the API returns 200 with `found: false`,\n * and `getPortfolioItem` returns null. Consumers map null → notFound().\n * - Infrastructure failure (network error, 5xx, rate-limit exhaustion,\n * missing SONOR_API_KEY) THROWS PortfolioApiError. It must never be\n * conflated with \"not found\": treating an outage as a 404 turns indexed\n * /work URLs into real 404s during ISR regeneration (the GSC 404-flood\n * class of incident). A thrown error makes Next.js keep serving the last\n * good page during background revalidation, and fails builds loudly\n * instead of silently generating an empty portfolio.\n *\n * The one exception is getPortfolioBrandConfig, which degrades to default\n * branding on failure — cosmetic fallback is safe; content fallback is not.\n */\nexport class PortfolioApiError extends Error {\n readonly path: string;\n\n constructor(path: string, message?: string) {\n super(\n message ??\n `${LOG_PREFIX} Portfolio API request failed: ${path}. ` +\n 'This is an infrastructure/config failure (network, 5xx, or missing ' +\n 'SONOR_API_KEY), not a missing item — failing loudly so it cannot be ' +\n 'mistaken for a 404 or an empty portfolio.',\n );\n this.name = 'PortfolioApiError';\n this.path = path;\n }\n}\n\n/**\n * A 200 response whose body violates the expected shape. Thrown before any\n * mapping so drift between the API and this package surfaces as a loud,\n * named error instead of `undefined` text on live pages.\n */\nexport class PortfolioContractError extends PortfolioApiError {\n readonly field: string;\n\n constructor(path: string, field: string, detail: string) {\n super(\n path,\n `${LOG_PREFIX} Portfolio API contract violation at \"${field}\" (${path}): ${detail}. ` +\n 'Refusing to render partial or corrupt data.',\n );\n this.name = 'PortfolioContractError';\n this.field = field;\n }\n}\n\n/**\n * Minimal runtime shape check for the single-item response. Deliberately\n * lenient about unknown extra fields (the API may grow) and strict about\n * the fields this package renders from.\n */\nexport function assertPortfolioItemShape(\n item: unknown,\n path: string,\n): asserts item is PortfolioItemApiResponse['item'] {\n const fail = (field: string, detail: string): never => {\n throw new PortfolioContractError(path, field, detail);\n };\n\n if (typeof item !== 'object' || item === null) fail('item', 'not an object');\n const record = item as Record<string, unknown>;\n\n for (const field of ['id', 'slug', 'title'] as const) {\n const v = record[field];\n if (typeof v !== 'string' || v.length === 0) {\n fail(field, `expected a non-empty string, got ${JSON.stringify(v)}`);\n }\n }\n\n for (const field of ['sections', 'services', 'kpis', 'metricsDelta'] as const) {\n const v = record[field];\n if (v != null && !Array.isArray(v)) {\n fail(field, `expected an array when present, got ${typeof v}`);\n }\n }\n\n const sections = (record.sections ?? []) as unknown[];\n sections.forEach((section, i) => {\n if (typeof section !== 'object' || section === null) {\n fail(`sections[${i}]`, 'not an object');\n }\n const s = section as Record<string, unknown>;\n if (typeof s.sectionType !== 'string' || s.sectionType.length === 0) {\n fail(`sections[${i}].sectionType`, 'expected a non-empty string');\n }\n if (typeof s.data !== 'object' || s.data === null) {\n fail(`sections[${i}].data`, 'expected an object');\n }\n });\n}\n\n/**\n * GET against the public portfolio API with the shared portfolio cache tag,\n * so the Portal's revalidation webhook can invalidate every portfolio fetch\n * (list, detail, categories, config, sitemap data) via one revalidateTag.\n *\n * Retries transient failures (429/502/503 + network) up to 3 attempts, then\n * throws PortfolioApiError per the error contract above. All portfolio\n * endpoints answer \"unknown slug\" with 200 + `found: false`, so a null from\n * the fetcher can only mean infrastructure failure.\n */\nasync function portfolioGet<T>(\n path: string,\n opts: { revalidate?: number } = {},\n): Promise<T> {\n const data = await apiFetch<T>(path, {\n method: 'GET',\n tags: [PORTFOLIO_CACHE_TAG],\n retry: true,\n maxAttempts: 3,\n requireKey: true,\n ...(opts.revalidate !== undefined ? { revalidate: opts.revalidate } : {}),\n });\n\n if (data === null) throw new PortfolioApiError(path);\n return data;\n}\n\n/**\n * Draft preview: when Next draft mode is active AND the preview cookie is\n * set (both done by createPortfolioPreviewHandler), returns the raw token\n * so getPortfolioItem can fetch the unpublished item uncached. Fails soft —\n * outside a request scope (build, tests) this is simply not a draft.\n */\nasync function getDraftPreviewToken(): Promise<string | null> {\n try {\n const { draftMode, cookies } = await import('next/headers');\n const dm = await draftMode();\n if (!dm.isEnabled) return null;\n const jar = await cookies();\n return jar.get(PORTFOLIO_PREVIEW_COOKIE)?.value ?? null;\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Builds a query string from an object, omitting undefined/null values.\n */\nfunction buildQuery(params: Record<string, string | number | boolean | undefined | null>): string {\n const entries = Object.entries(params).filter(\n (entry): entry is [string, string | number | boolean] => entry[1] != null,\n );\n if (entries.length === 0) return '';\n const qs = new URLSearchParams(\n entries.map(([k, v]) => [k, String(v)]),\n ).toString();\n return `?${qs}`;\n}\n\n// ---------------------------------------------------------------------------\n// Public API — List items\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches published portfolio items from the public API.\n * Cached via Next.js ISR (revalidate: 3600).\n *\n * An empty portfolio is `{ items: [] }` from a healthy API. Infrastructure\n * failure throws PortfolioApiError (see the error contract above) — it never\n * silently renders an empty portfolio or an empty sitemap.\n */\nexport async function getPortfolioItems(options?: {\n category?: string;\n featured?: boolean;\n limit?: number;\n offset?: number;\n}): Promise<PortfolioListResponse> {\n const query = buildQuery({\n category: options?.category,\n featured: options?.featured,\n limit: options?.limit,\n offset: options?.offset,\n });\n\n return portfolioGet<PortfolioListResponse>(\n `/api/public/portfolio/items${query}`,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Single item\n// ---------------------------------------------------------------------------\n\n/** Shape returned by the single-item API endpoint. */\ninterface PortfolioItemApiResponse {\n found: boolean;\n item: {\n id: string;\n slug: string;\n title: string;\n subtitle: string;\n category: string;\n services: string[];\n description: string;\n hero_image: string;\n hero_image_alt: string;\n live_url: string | null;\n project_id: string | null;\n kpis: PortfolioItemFull['kpis'];\n details: PortfolioItemFull['details'];\n seo: PortfolioItemFull['seo'];\n featured: boolean;\n order?: number;\n published_at: string | null;\n hero_screenshots: PortfolioItemFull['hero_screenshots'];\n baseline_metrics: PortfolioItemFull['baseline_metrics'];\n current_metrics: PortfolioItemFull['current_metrics'];\n metrics_last_refreshed_at: string | null;\n sections: PortfolioSection[];\n metricsDelta: MetricsDelta[];\n sectionsOrdered?: boolean;\n };\n}\n\n/**\n * Fetches a single portfolio item by slug, including full Sanity sections\n * and live metrics delta. This is the main data function for portfolio detail pages.\n *\n * Returns null ONLY when the item genuinely does not exist (API answered\n * `found: false`) — safe to map to notFound(). Infrastructure failures throw\n * PortfolioApiError instead, so an API outage during ISR regeneration keeps\n * serving the last good page rather than minting a real 404.\n */\nexport async function getPortfolioItem(slug: string): Promise<PortfolioItemFull | null> {\n if (!slug) {\n console.warn(`${LOG_PREFIX} getPortfolioItem called with empty slug.`);\n return null;\n }\n\n // Draft preview (dashboard Preview button): fetch with the token, uncached.\n const previewToken = await getDraftPreviewToken();\n const path = previewToken\n ? `/api/public/portfolio/items/${encodeURIComponent(slug)}?preview=${encodeURIComponent(previewToken)}`\n : `/api/public/portfolio/items/${encodeURIComponent(slug)}`;\n\n const data = await portfolioGet<PortfolioItemApiResponse>(\n path,\n previewToken ? { revalidate: 0 } : {},\n );\n\n if (!data.found || !data.item) {\n console.warn(`${LOG_PREFIX} Portfolio item not found for slug: \"${slug}\".`);\n return null;\n }\n\n // Contract guard — a malformed 200 must not reach the renderer.\n assertPortfolioItemShape(data.item, `/api/public/portfolio/items/${slug}`);\n\n // Map the API response to the PortfolioItemFull type\n const { item } = data;\n return {\n id: item.id,\n slug: item.slug,\n title: item.title,\n subtitle: item.subtitle,\n category: item.category,\n services: item.services,\n description: item.description,\n hero_image: item.hero_image,\n hero_image_alt: item.hero_image_alt,\n live_url: item.live_url,\n project_id: item.project_id ?? null,\n kpis: item.kpis,\n details: item.details,\n seo: item.seo,\n featured: item.featured,\n order: item.order,\n published_at: item.published_at,\n hero_screenshots: item.hero_screenshots,\n baseline_metrics: item.baseline_metrics,\n current_metrics: item.current_metrics,\n metrics_last_refreshed_at: item.metrics_last_refreshed_at,\n // Sanitized HERE — the enforcing layer. Even with PortfolioPage as a\n // genuine RSC, each rendered section's data still crosses the client\n // boundary into the flight payload, and any future bundling regression\n // would leak the whole item again (the 0.3.1 GSC 404-flood vector).\n // Sanitize at the last server-side hop, always.\n sections: sanitizePortfolioSections(item.sections ?? []),\n metricsDelta: item.metricsDelta ?? [],\n sectionsOrdered: item.sectionsOrdered ?? false,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Categories\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches available portfolio categories.\n * Infrastructure failure throws PortfolioApiError (see error contract).\n */\nexport async function getPortfolioCategories(): Promise<string[]> {\n return portfolioGet<string[]>('/api/public/portfolio/categories');\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Brand config\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches brand configuration for the agency (colors, fonts, logo).\n * Returns sensible defaults when the API call fails — the ONE deliberate\n * exception to the throw-on-failure contract: brand config is cosmetic, and\n * an API blip must not take down the whole layout that wraps every page.\n */\nexport async function getPortfolioBrandConfig(): Promise<BrandConfig> {\n const fallback: BrandConfig = {\n primary: '#6366f1',\n secondary: '#8b5cf6',\n background: '#0a0a0f',\n backgroundElevated: '#12121a',\n surface: '#1a1a2e',\n surfaceHover: '#22223a',\n surfaceBorder: '#2a2a3e',\n textPrimary: '#f0f0f5',\n textSecondary: '#a0a0b5',\n textTertiary: '#6b6b80',\n fontHeading: 'Inter, system-ui, sans-serif',\n fontBody: 'Inter, system-ui, sans-serif',\n };\n\n try {\n const data = await portfolioGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n );\n if (!data.brand) {\n console.warn(`${LOG_PREFIX} Brand config response empty, using defaults.`);\n return fallback;\n }\n return data.brand;\n } catch {\n console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);\n return fallback;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Metadata helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates Next.js Metadata for a portfolio item page.\n * For use in generateMetadata() in the App Router.\n *\n * Returns an empty object when the item does not exist (safe to spread into\n * the metadata return value); infrastructure failures throw so a build never\n * silently ships pages with missing metadata.\n */\nexport async function generatePortfolioMetadata(slug: string): Promise<{\n title?: string;\n description?: string;\n keywords?: string[];\n openGraph?: {\n title?: string;\n description?: string;\n images?: string[];\n type?: string;\n };\n}> {\n const item = await getPortfolioItem(slug);\n\n if (!item) {\n return {};\n }\n\n // Prefer dedicated SEO data if available, fall back to item fields\n const seo: PortfolioSeoData | null = item.seo;\n const title = seo?.metaTitle || item.title;\n const description = seo?.metaDescription || item.description;\n const keywords = seo?.keywords ?? [];\n\n // Collect OG images: prefer SEO og image, then hero image\n const ogImages: string[] = [];\n if (item.hero_image) {\n ogImages.push(item.hero_image);\n }\n\n return {\n title,\n description,\n keywords: keywords.length > 0 ? keywords : undefined,\n openGraph: {\n title,\n description,\n images: ogImages.length > 0 ? ogImages : undefined,\n type: 'article',\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Static Params helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates static params for portfolio pages.\n * For use in generateStaticParams() in the App Router.\n *\n * Fetches all published portfolio items and returns their slugs.\n * Infrastructure failure throws, failing the build loudly instead of\n * silently building a site with zero portfolio pages.\n */\nexport async function generatePortfolioStaticParams(): Promise<Array<{ slug: string }>> {\n // Fetch a large batch to cover all published items\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => ({ slug: item.slug }));\n}\n\n// ---------------------------------------------------------------------------\n// Category helpers (route-based, crawlable filtering)\n// ---------------------------------------------------------------------------\n\n/**\n * URL slug for a category display value (\"Web Development\" → \"web-development\").\n * Single source of truth for category URLs — PortfolioIndex resolves slugs\n * back to the original values by matching against getPortfolioCategories().\n */\nexport function slugifyCategory(category: string): string {\n return category\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/**\n * Static params for the category filter routes.\n * For use in app/work/category/[category]/page.tsx:\n *\n * ```ts\n * export async function generateStaticParams() {\n * return generatePortfolioCategoryStaticParams();\n * }\n * ```\n */\nexport async function generatePortfolioCategoryStaticParams(): Promise<\n Array<{ category: string }>\n> {\n const categories = await getPortfolioCategories();\n return categories.map((c) => ({ category: slugifyCategory(c) }));\n}\n\n/**\n * Metadata for index + category + paginated portfolio routes: a stable\n * title/canonical so filtered and paginated variants don't compete in\n * search. Spread into generateMetadata()'s return value.\n */\nexport async function generatePortfolioIndexMetadata(options: {\n /** Site origin (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix. Defaults to \"/work\". */\n basePath?: string;\n /** Active category SLUG (from the route param), if any. */\n category?: string | null;\n /** 1-based page number. */\n page?: number;\n /** Title base, e.g. \"Our Work\". Category/page qualifiers are appended. */\n titleBase?: string;\n}): Promise<{ title: string; alternates: { canonical: string } }> {\n const {\n baseUrl,\n basePath = '/work',\n category = null,\n page = 1,\n titleBase = 'Our Work',\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n let title = titleBase;\n let path = prefix;\n\n if (category) {\n const categories = await getPortfolioCategories();\n const match = categories.find((c) => slugifyCategory(c) === category);\n if (match) title = `${match} | ${titleBase}`;\n path = `${prefix}/category/${category}`;\n }\n if (page > 1) {\n title = `${title} — Page ${page}`;\n path = `${path}?page=${page}`;\n }\n\n return { title, alternates: { canonical: `${origin}${path}` } };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Sitemap helper\n// ---------------------------------------------------------------------------\n\n/** One entry in the shape Next.js expects from app/sitemap.(js|ts). */\nexport interface PortfolioSitemapEntry {\n url: string;\n lastModified?: string;\n changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';\n priority?: number;\n}\n\n/**\n * Builds sitemap entries for every published portfolio item, in the shape\n * Next.js `app/sitemap.(js|ts)` expects — spread the result into the array\n * you return:\n *\n * ```ts\n * const portfolio = await generatePortfolioSitemapEntries({\n * baseUrl: 'https://youragency.com',\n * });\n * return [...staticEntries, ...portfolio];\n * ```\n *\n * `lastModified` prefers the metrics refresh timestamp (case studies change\n * when their live metrics update) and falls back to the publish date.\n * Infrastructure failure throws — a sitemap silently missing every case\n * study is how portfolio URLs fall out of the index.\n */\nexport async function generatePortfolioSitemapEntries(options: {\n /** Site origin, no trailing slash needed (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix for case studies. Defaults to \"/work\". */\n basePath?: string;\n changeFrequency?: PortfolioSitemapEntry['changeFrequency'];\n priority?: number;\n}): Promise<PortfolioSitemapEntry[]> {\n const {\n baseUrl,\n basePath = '/work',\n changeFrequency = 'monthly',\n priority = 0.7,\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => {\n const lastModified =\n item.metrics_last_refreshed_at || item.published_at || undefined;\n return {\n url: `${origin}${prefix}/${item.slug}`,\n ...(lastModified ? { lastModified } : {}),\n changeFrequency,\n priority,\n };\n });\n}\n"]}
@@ -0,0 +1,6 @@
1
+ // src/shared/preview.ts
2
+ var PORTFOLIO_PREVIEW_COOKIE = "__sonor_portfolio_preview";
3
+
4
+ export { PORTFOLIO_PREVIEW_COOKIE };
5
+ //# sourceMappingURL=chunk-L4EOQVRJ.js.map
6
+ //# sourceMappingURL=chunk-L4EOQVRJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/preview.ts"],"names":[],"mappings":";AAIO,IAAM,wBAAA,GAA2B","file":"chunk-L4EOQVRJ.js","sourcesContent":["/**\n * Draft-preview constants shared by the preview route handler (sets the\n * cookie) and the portfolio data layer (reads it). One name, one place.\n */\nexport const PORTFOLIO_PREVIEW_COOKIE = '__sonor_portfolio_preview';\n"]}
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ // src/shared/images.ts
4
+ var SANITY_ASSET_REF = /^image-[A-Za-z0-9]+-\d+x\d+-[a-z0-9]+$/;
5
+ var FILENAME_DIMS = /-(\d{2,5})x(\d{2,5})\.[a-z0-9]+(?:\?.*)?$/i;
6
+ var warnedAboutRefs = false;
7
+ function resolvePortfolioImage(input) {
8
+ const raw = typeof input === "string" ? input : input?.asset?._ref ?? "";
9
+ const value = (raw || "").trim();
10
+ if (!value) return null;
11
+ if (SANITY_ASSET_REF.test(value)) {
12
+ if (!warnedAboutRefs && typeof console !== "undefined") {
13
+ warnedAboutRefs = true;
14
+ console.warn(
15
+ "[@sonordev/agency-site-kit] Received a raw Sanity asset ref instead of an image URL \u2014 skipping render. The Sonor API should resolve image refs to cdn.sanity.io URLs before returning portfolio sections."
16
+ );
17
+ }
18
+ return null;
19
+ }
20
+ if (!/^https?:\/\//i.test(value) && !value.startsWith("/")) return null;
21
+ const dims = value.match(FILENAME_DIMS);
22
+ if (dims) {
23
+ const width = parseInt(dims[1], 10);
24
+ const height = parseInt(dims[2], 10);
25
+ if (width > 0 && height > 0) return { url: value, width, height };
26
+ }
27
+ return { url: value };
28
+ }
29
+
30
+ exports.resolvePortfolioImage = resolvePortfolioImage;
31
+ //# sourceMappingURL=chunk-NI4FVCLS.cjs.map
32
+ //# sourceMappingURL=chunk-NI4FVCLS.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/images.ts"],"names":[],"mappings":";;;AAsBA,IAAM,gBAAA,GAAmB,wCAAA;AAGzB,IAAM,aAAA,GAAgB,4CAAA;AAMtB,IAAI,eAAA,GAAkB,KAAA;AAOf,SAAS,sBACd,KAAA,EAC+B;AAC/B,EAAA,MAAM,MACJ,OAAO,KAAA,KAAU,WAAW,KAAA,GAAQ,KAAA,EAAO,OAAO,IAAA,IAAQ,EAAA;AAC5D,EAAA,MAAM,KAAA,GAAA,CAAS,GAAA,IAAO,EAAA,EAAI,IAAA,EAAK;AAC/B,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AAEnB,EAAA,IAAI,gBAAA,CAAiB,IAAA,CAAK,KAAK,CAAA,EAAG;AAChC,IAAA,IAAI,CAAC,eAAA,IAAmB,OAAO,OAAA,KAAY,WAAA,EAAa;AACtD,MAAA,eAAA,GAAkB,IAAA;AAClB,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN;AAAA,OAGF;AAAA,IACF;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,IAAI,CAAC,eAAA,CAAgB,IAAA,CAAK,KAAK,CAAA,IAAK,CAAC,KAAA,CAAM,UAAA,CAAW,GAAG,CAAA,EAAG,OAAO,IAAA;AAEnE,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,aAAa,CAAA;AACtC,EAAA,IAAI,IAAA,EAAM;AACR,IAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,IAAA,CAAK,CAAC,GAAG,EAAE,CAAA;AAClC,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,CAAK,CAAC,GAAG,EAAE,CAAA;AACnC,IAAA,IAAI,KAAA,GAAQ,KAAK,MAAA,GAAS,CAAA,SAAU,EAAE,GAAA,EAAK,KAAA,EAAO,KAAA,EAAO,MAAA,EAAO;AAAA,EAClE;AAEA,EAAA,OAAO,EAAE,KAAK,KAAA,EAAM;AACtB","file":"chunk-NI4FVCLS.cjs","sourcesContent":["/**\n * Portfolio image resolution — single source of truth for turning the mixed\n * image shapes the API sends (plain URL strings, Supabase Storage URLs,\n * Sanity image objects) into something safe to render.\n *\n * IMPORTANT: a raw Sanity asset _ref (`image-{id}-{W}x{H}-{fmt}`) is an asset\n * ID, NOT a URL. Rendering it as `src` issues a garbage relative request\n * against the agency site's origin. Resolving refs to cdn.sanity.io URLs is\n * the API's job (it owns the Sanity project config); this helper refuses refs\n * so a broken image can never render, and warns once so the gap is visible.\n *\n * When a URL carries `-{W}x{H}.{ext}` dimensions (Sanity CDN URLs always do),\n * they're parsed out so callers can give next/image exact intrinsic sizes.\n */\n\nexport interface ResolvedPortfolioImage {\n url: string;\n width?: number;\n height?: number;\n}\n\n/** Raw Sanity asset reference — an ID, not a URL. */\nconst SANITY_ASSET_REF = /^image-[A-Za-z0-9]+-\\d+x\\d+-[a-z0-9]+$/;\n\n/** `…-1200x800.png` (optionally with a query string) → intrinsic dimensions. */\nconst FILENAME_DIMS = /-(\\d{2,5})x(\\d{2,5})\\.[a-z0-9]+(?:\\?.*)?$/i;\n\ninterface SanityImageLike {\n asset?: { _ref?: string };\n}\n\nlet warnedAboutRefs = false;\n\n/**\n * Resolve a portfolio image input into a renderable URL (+ intrinsic\n * dimensions when they can be derived). Returns null for empty values and\n * for unresolved Sanity refs — callers skip rendering in that case.\n */\nexport function resolvePortfolioImage(\n input: string | SanityImageLike | null | undefined,\n): ResolvedPortfolioImage | null {\n const raw =\n typeof input === 'string' ? input : input?.asset?._ref ?? '';\n const value = (raw || '').trim();\n if (!value) return null;\n\n if (SANITY_ASSET_REF.test(value)) {\n if (!warnedAboutRefs && typeof console !== 'undefined') {\n warnedAboutRefs = true;\n console.warn(\n '[@sonordev/agency-site-kit] Received a raw Sanity asset ref instead of an ' +\n 'image URL — skipping render. The Sonor API should resolve image refs to ' +\n 'cdn.sanity.io URLs before returning portfolio sections.',\n );\n }\n return null;\n }\n\n // Only absolute http(s) URLs or site-rooted paths are renderable.\n if (!/^https?:\\/\\//i.test(value) && !value.startsWith('/')) return null;\n\n const dims = value.match(FILENAME_DIMS);\n if (dims) {\n const width = parseInt(dims[1], 10);\n const height = parseInt(dims[2], 10);\n if (width > 0 && height > 0) return { url: value, width, height };\n }\n\n return { url: value };\n}\n"]}