@xiwebdesign/xicms-core 7.30.0 → 8.15.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 (463) hide show
  1. package/dist/XiCMSProvider.js +22 -7
  2. package/dist/XiCMSProvider.js.map +1 -1
  3. package/dist/admin/AdminApp.js +175 -93
  4. package/dist/admin/AdminApp.js.map +1 -1
  5. package/dist/admin/AdminHelp.d.ts +1 -1
  6. package/dist/admin/AdminHelp.js +44 -36
  7. package/dist/admin/AdminHelp.js.map +1 -1
  8. package/dist/admin/CoreVersion.d.ts +6 -3
  9. package/dist/admin/CoreVersion.js +60 -24
  10. package/dist/admin/CoreVersion.js.map +1 -1
  11. package/dist/admin/NewsEditor.d.ts +7 -0
  12. package/dist/admin/NewsEditor.js +117 -75
  13. package/dist/admin/NewsEditor.js.map +1 -1
  14. package/dist/admin/SuperadminPanel.js +1 -1
  15. package/dist/admin/SuperadminPanel.js.map +1 -1
  16. package/dist/admin/ZoekPalet.d.ts +15 -0
  17. package/dist/admin/ZoekPalet.js +123 -0
  18. package/dist/admin/ZoekPalet.js.map +1 -0
  19. package/dist/admin/blockEditors/GenericEditor.js +36 -18
  20. package/dist/admin/blockEditors/GenericEditor.js.map +1 -1
  21. package/dist/admin/blockEditors/StyleEditor.js +37 -3
  22. package/dist/admin/blockEditors/StyleEditor.js.map +1 -1
  23. package/dist/admin/blockEditors/SymbolRefEditor.js +44 -2
  24. package/dist/admin/blockEditors/SymbolRefEditor.js.map +1 -1
  25. package/dist/admin/blockEditors/index.js +154 -73
  26. package/dist/admin/blockEditors/index.js.map +1 -1
  27. package/dist/admin/customBlocks/CustomBlocksPanel.js +18 -7
  28. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
  29. package/dist/admin/dashboard/DashboardPanel.js +26 -16
  30. package/dist/admin/dashboard/DashboardPanel.js.map +1 -1
  31. package/dist/admin/editors/RichTextEditor.d.ts +4 -3
  32. package/dist/admin/editors/RichTextEditor.js +19 -56
  33. package/dist/admin/editors/RichTextEditor.js.map +1 -1
  34. package/dist/admin/editors/fontSizeExtension.js +7 -1
  35. package/dist/admin/editors/fontSizeExtension.js.map +1 -1
  36. package/dist/admin/editors/slashMenu.d.ts +18 -0
  37. package/dist/admin/editors/slashMenu.js +34 -0
  38. package/dist/admin/editors/slashMenu.js.map +1 -0
  39. package/dist/admin/editors/useRijkeTekst.d.ts +48 -0
  40. package/dist/admin/editors/useRijkeTekst.js +310 -0
  41. package/dist/admin/editors/useRijkeTekst.js.map +1 -0
  42. package/dist/admin/forms/FormsPanel.d.ts +11 -2
  43. package/dist/admin/forms/FormsPanel.js +218 -65
  44. package/dist/admin/forms/FormsPanel.js.map +1 -1
  45. package/dist/admin/lib/blokInfo.d.ts +17 -0
  46. package/dist/admin/lib/blokInfo.js +80 -0
  47. package/dist/admin/lib/blokInfo.js.map +1 -0
  48. package/dist/admin/lib/foutNaarMens.d.ts +1 -0
  49. package/dist/admin/lib/foutNaarMens.js +22 -0
  50. package/dist/admin/lib/foutNaarMens.js.map +1 -0
  51. package/dist/admin/lib/gewisteVelden.d.ts +13 -0
  52. package/dist/admin/lib/gewisteVelden.js +31 -0
  53. package/dist/admin/lib/gewisteVelden.js.map +1 -0
  54. package/dist/admin/lib/heroHulp.d.ts +11 -0
  55. package/dist/admin/lib/heroHulp.js +18 -0
  56. package/dist/admin/lib/heroHulp.js.map +1 -0
  57. package/dist/admin/lib/kleur.js +2 -3
  58. package/dist/admin/lib/kleur.js.map +1 -1
  59. package/dist/admin/lib/leesbaarheid.d.ts +34 -0
  60. package/dist/admin/lib/leesbaarheid.js +82 -0
  61. package/dist/admin/lib/leesbaarheid.js.map +1 -0
  62. package/dist/admin/lib/lengteAdvies.d.ts +1 -0
  63. package/dist/admin/lib/lengteAdvies.js +16 -0
  64. package/dist/admin/lib/lengteAdvies.js.map +1 -0
  65. package/dist/admin/lib/mediaLijst.d.ts +26 -0
  66. package/dist/admin/lib/mediaLijst.js +42 -0
  67. package/dist/admin/lib/mediaLijst.js.map +1 -0
  68. package/dist/admin/lib/mediaUsage.d.ts +12 -1
  69. package/dist/admin/lib/mediaUsage.js +43 -33
  70. package/dist/admin/lib/mediaUsage.js.map +1 -1
  71. package/dist/admin/lib/menuHulp.d.ts +16 -0
  72. package/dist/admin/lib/menuHulp.js +41 -0
  73. package/dist/admin/lib/menuHulp.js.map +1 -0
  74. package/dist/admin/lib/onopgeslagen.d.ts +10 -0
  75. package/dist/admin/lib/onopgeslagen.js +35 -0
  76. package/dist/admin/lib/onopgeslagen.js.map +1 -0
  77. package/dist/admin/lib/pagesClient.d.ts +55 -2
  78. package/dist/admin/lib/pagesClient.js +126 -28
  79. package/dist/admin/lib/pagesClient.js.map +1 -1
  80. package/dist/admin/lib/paginaGebruik.d.ts +56 -0
  81. package/dist/admin/lib/paginaGebruik.js +109 -0
  82. package/dist/admin/lib/paginaGebruik.js.map +1 -0
  83. package/dist/admin/lib/publicatieCheck.d.ts +25 -0
  84. package/dist/admin/lib/publicatieCheck.js +114 -0
  85. package/dist/admin/lib/publicatieCheck.js.map +1 -0
  86. package/dist/admin/lib/scrollNaarDetail.d.ts +6 -0
  87. package/dist/admin/lib/scrollNaarDetail.js +20 -0
  88. package/dist/admin/lib/scrollNaarDetail.js.map +1 -0
  89. package/dist/admin/lib/symbolsClient.d.ts +10 -0
  90. package/dist/admin/lib/symbolsClient.js +14 -1
  91. package/dist/admin/lib/symbolsClient.js.map +1 -1
  92. package/dist/admin/lib/tekstVeld.d.ts +8 -0
  93. package/dist/admin/lib/tekstVeld.js +10 -0
  94. package/dist/admin/lib/tekstVeld.js.map +1 -0
  95. package/dist/admin/lib/useIsSuperadmin.d.ts +2 -0
  96. package/dist/admin/lib/useIsSuperadmin.js +13 -0
  97. package/dist/admin/lib/useIsSuperadmin.js.map +1 -0
  98. package/dist/admin/lib/veldTaal.d.ts +14 -0
  99. package/dist/admin/lib/veldTaal.js +247 -0
  100. package/dist/admin/lib/veldTaal.js.map +1 -0
  101. package/dist/admin/lib/versieVergelijk.d.ts +17 -0
  102. package/dist/admin/lib/versieVergelijk.js +24 -0
  103. package/dist/admin/lib/versieVergelijk.js.map +1 -0
  104. package/dist/admin/lib/zodFields.js +22 -4
  105. package/dist/admin/lib/zodFields.js.map +1 -1
  106. package/dist/admin/lib/zodFout.d.ts +9 -0
  107. package/dist/admin/lib/zodFout.js +63 -0
  108. package/dist/admin/lib/zodFout.js.map +1 -0
  109. package/dist/admin/media/MediaManager.js +162 -32
  110. package/dist/admin/media/MediaManager.js.map +1 -1
  111. package/dist/admin/media/MediaPicker.d.ts +3 -0
  112. package/dist/admin/media/MediaPicker.js +84 -31
  113. package/dist/admin/media/MediaPicker.js.map +1 -1
  114. package/dist/admin/menu/MenuPanel.js +144 -83
  115. package/dist/admin/menu/MenuPanel.js.map +1 -1
  116. package/dist/admin/navContext.d.ts +3 -0
  117. package/dist/admin/navContext.js +13 -1
  118. package/dist/admin/navContext.js.map +1 -1
  119. package/dist/admin/pages/PageEditor.js +146 -30
  120. package/dist/admin/pages/PageEditor.js.map +1 -1
  121. package/dist/admin/pages/PagesPanel.js +215 -61
  122. package/dist/admin/pages/PagesPanel.js.map +1 -1
  123. package/dist/admin/pages/StarterKiezer.d.ts +12 -0
  124. package/dist/admin/pages/StarterKiezer.js +113 -0
  125. package/dist/admin/pages/StarterKiezer.js.map +1 -0
  126. package/dist/admin/panelRegistry.d.ts +1 -1
  127. package/dist/admin/panelRegistry.js +2 -1
  128. package/dist/admin/panelRegistry.js.map +1 -1
  129. package/dist/admin/patterns/PatternInserter.js +1 -1
  130. package/dist/admin/patterns/PatternInserter.js.map +1 -1
  131. package/dist/admin/settings/BedrijfInstellingen.d.ts +20 -0
  132. package/dist/admin/settings/BedrijfInstellingen.js +144 -0
  133. package/dist/admin/settings/BedrijfInstellingen.js.map +1 -0
  134. package/dist/admin/settings/MailSettings.js +2 -2
  135. package/dist/admin/settings/MailSettings.js.map +1 -1
  136. package/dist/admin/settings/MailTemplates.js +5 -4
  137. package/dist/admin/settings/MailTemplates.js.map +1 -1
  138. package/dist/admin/settings/SettingsPanel.js +120 -78
  139. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  140. package/dist/admin/settings/SnapshotsCard.js +4 -4
  141. package/dist/admin/settings/SnapshotsCard.js.map +1 -1
  142. package/dist/admin/settings/VorigeTerugzetten.d.ts +15 -0
  143. package/dist/admin/settings/VorigeTerugzetten.js +89 -0
  144. package/dist/admin/settings/VorigeTerugzetten.js.map +1 -0
  145. package/dist/admin/shop/CategoriesTab.js +6 -4
  146. package/dist/admin/shop/CategoriesTab.js.map +1 -1
  147. package/dist/admin/shop/DiscountsTab.js +4 -2
  148. package/dist/admin/shop/DiscountsTab.js.map +1 -1
  149. package/dist/admin/shop/OrdersTab.js +57 -19
  150. package/dist/admin/shop/OrdersTab.js.map +1 -1
  151. package/dist/admin/shop/ProductPaneel.js +2 -1
  152. package/dist/admin/shop/ProductPaneel.js.map +1 -1
  153. package/dist/admin/shop/ProductsTab.js +40 -19
  154. package/dist/admin/shop/ProductsTab.js.map +1 -1
  155. package/dist/admin/shop/ShopPanel.js +13 -1
  156. package/dist/admin/shop/ShopPanel.js.map +1 -1
  157. package/dist/admin/shop/ShopSettingsTab.js +27 -1
  158. package/dist/admin/shop/ShopSettingsTab.js.map +1 -1
  159. package/dist/admin/shop/productClient.js +7 -3
  160. package/dist/admin/shop/productClient.js.map +1 -1
  161. package/dist/admin/stats/NietGevonden.d.ts +10 -0
  162. package/dist/admin/stats/NietGevonden.js +78 -0
  163. package/dist/admin/stats/NietGevonden.js.map +1 -0
  164. package/dist/admin/stats/StatsPanel.js +6 -1
  165. package/dist/admin/stats/StatsPanel.js.map +1 -1
  166. package/dist/admin/stats/lib.d.ts +2 -0
  167. package/dist/admin/stats/lib.js.map +1 -1
  168. package/dist/admin/ui/ConfirmDialog.js +10 -1
  169. package/dist/admin/ui/ConfirmDialog.js.map +1 -1
  170. package/dist/admin/ui/Icon.d.ts +34 -0
  171. package/dist/admin/ui/Icon.js +44 -0
  172. package/dist/admin/ui/Icon.js.map +1 -0
  173. package/dist/admin/ui/ImageField.d.ts +15 -0
  174. package/dist/admin/ui/ImageField.js +67 -0
  175. package/dist/admin/ui/ImageField.js.map +1 -0
  176. package/dist/admin/ui/KleurVeld.d.ts +25 -0
  177. package/dist/admin/ui/KleurVeld.js +76 -0
  178. package/dist/admin/ui/KleurVeld.js.map +1 -0
  179. package/dist/admin/ui/LaadStaat.d.ts +21 -0
  180. package/dist/admin/ui/LaadStaat.js +43 -0
  181. package/dist/admin/ui/LaadStaat.js.map +1 -0
  182. package/dist/admin/ui/LeesbaarheidPaneel.d.ts +10 -0
  183. package/dist/admin/ui/LeesbaarheidPaneel.js +68 -0
  184. package/dist/admin/ui/LeesbaarheidPaneel.js.map +1 -0
  185. package/dist/admin/ui/LinkDialog.d.ts +22 -0
  186. package/dist/admin/ui/LinkDialog.js +131 -0
  187. package/dist/admin/ui/LinkDialog.js.map +1 -0
  188. package/dist/admin/ui/LinkVeld.d.ts +14 -0
  189. package/dist/admin/ui/LinkVeld.js +49 -0
  190. package/dist/admin/ui/LinkVeld.js.map +1 -0
  191. package/dist/admin/ui/PreviewLinkResultaat.d.ts +14 -0
  192. package/dist/admin/ui/PreviewLinkResultaat.js +52 -0
  193. package/dist/admin/ui/PreviewLinkResultaat.js.map +1 -0
  194. package/dist/admin/ui/charts.js +5 -5
  195. package/dist/admin/ui/charts.js.map +1 -1
  196. package/dist/admin/ui/primitives.d.ts +16 -6
  197. package/dist/admin/ui/primitives.js +98 -26
  198. package/dist/admin/ui/primitives.js.map +1 -1
  199. package/dist/admin/ui/select.js +5 -2
  200. package/dist/admin/ui/select.js.map +1 -1
  201. package/dist/admin/ui/switch.d.ts +3 -1
  202. package/dist/admin/ui/switch.js +6 -3
  203. package/dist/admin/ui/switch.js.map +1 -1
  204. package/dist/admin/ui/toast.d.ts +2 -1
  205. package/dist/admin/ui/toast.js +17 -11
  206. package/dist/admin/ui/toast.js.map +1 -1
  207. package/dist/admin/visual/BlokKiezer.d.ts +34 -0
  208. package/dist/admin/visual/BlokKiezer.js +225 -0
  209. package/dist/admin/visual/BlokKiezer.js.map +1 -0
  210. package/dist/admin/visual/InlineGereedschap.d.ts +52 -0
  211. package/dist/admin/visual/InlineGereedschap.js +356 -0
  212. package/dist/admin/visual/InlineGereedschap.js.map +1 -0
  213. package/dist/admin/visual/InlineRijkeTekst.d.ts +23 -0
  214. package/dist/admin/visual/InlineRijkeTekst.js +152 -0
  215. package/dist/admin/visual/InlineRijkeTekst.js.map +1 -0
  216. package/dist/admin/visual/VisualEditor.js +273 -125
  217. package/dist/admin/visual/VisualEditor.js.map +1 -1
  218. package/dist/admin.css +1 -1
  219. package/dist/bedrijf/bedrijf.d.ts +36 -0
  220. package/dist/bedrijf/bedrijf.js +110 -0
  221. package/dist/bedrijf/bedrijf.js.map +1 -0
  222. package/dist/blocks/BlockRenderer.js +11 -1
  223. package/dist/blocks/BlockRenderer.js.map +1 -1
  224. package/dist/blocks/blockStyle.d.ts +7 -0
  225. package/dist/blocks/blockStyle.js +16 -3
  226. package/dist/blocks/blockStyle.js.map +1 -1
  227. package/dist/blocks/builtinSchemas.js +6 -0
  228. package/dist/blocks/builtinSchemas.js.map +1 -1
  229. package/dist/blocks/builtins/AgendaRender.d.ts +22 -0
  230. package/dist/blocks/builtins/AgendaRender.js +69 -9
  231. package/dist/blocks/builtins/AgendaRender.js.map +1 -1
  232. package/dist/blocks/builtins/Availability.d.ts +1 -0
  233. package/dist/blocks/builtins/Availability.js +23 -0
  234. package/dist/blocks/builtins/Availability.js.map +1 -0
  235. package/dist/blocks/builtins/AvailabilityRender.d.ts +12 -0
  236. package/dist/blocks/builtins/AvailabilityRender.js +105 -0
  237. package/dist/blocks/builtins/AvailabilityRender.js.map +1 -0
  238. package/dist/blocks/builtins/BeforeAfterRender.js +4 -3
  239. package/dist/blocks/builtins/BeforeAfterRender.js.map +1 -1
  240. package/dist/blocks/builtins/Booking.js +1 -1
  241. package/dist/blocks/builtins/Booking.js.map +1 -1
  242. package/dist/blocks/builtins/BreadcrumbsRender.d.ts +4 -3
  243. package/dist/blocks/builtins/BreadcrumbsRender.js +14 -8
  244. package/dist/blocks/builtins/BreadcrumbsRender.js.map +1 -1
  245. package/dist/blocks/builtins/ColumnsRender.d.ts +1 -1
  246. package/dist/blocks/builtins/ColumnsRender.js +3 -1
  247. package/dist/blocks/builtins/ColumnsRender.js.map +1 -1
  248. package/dist/blocks/builtins/Contact.js +1 -1
  249. package/dist/blocks/builtins/Contact.js.map +1 -1
  250. package/dist/blocks/builtins/ContactRender.d.ts +1 -1
  251. package/dist/blocks/builtins/ContactRender.js +29 -8
  252. package/dist/blocks/builtins/ContactRender.js.map +1 -1
  253. package/dist/blocks/builtins/Cta.js +1 -1
  254. package/dist/blocks/builtins/Cta.js.map +1 -1
  255. package/dist/blocks/builtins/CtaRender.d.ts +1 -1
  256. package/dist/blocks/builtins/CtaRender.js +5 -4
  257. package/dist/blocks/builtins/CtaRender.js.map +1 -1
  258. package/dist/blocks/builtins/Embed.js +1 -1
  259. package/dist/blocks/builtins/Embed.js.map +1 -1
  260. package/dist/blocks/builtins/FormBlock.js +1 -0
  261. package/dist/blocks/builtins/FormBlock.js.map +1 -1
  262. package/dist/blocks/builtins/FormBlockRender.d.ts +6 -0
  263. package/dist/blocks/builtins/FormBlockRender.js +88 -16
  264. package/dist/blocks/builtins/FormBlockRender.js.map +1 -1
  265. package/dist/blocks/builtins/GalleryRender.js +3 -2
  266. package/dist/blocks/builtins/GalleryRender.js.map +1 -1
  267. package/dist/blocks/builtins/Hero.js +1 -1
  268. package/dist/blocks/builtins/Hero.js.map +1 -1
  269. package/dist/blocks/builtins/HeroRender.js +7 -3
  270. package/dist/blocks/builtins/HeroRender.js.map +1 -1
  271. package/dist/blocks/builtins/OpenNowBadge.d.ts +4 -2
  272. package/dist/blocks/builtins/OpenNowBadge.js +6 -6
  273. package/dist/blocks/builtins/OpenNowBadge.js.map +1 -1
  274. package/dist/blocks/builtins/OpeningHoursRender.d.ts +8 -2
  275. package/dist/blocks/builtins/OpeningHoursRender.js +27 -6
  276. package/dist/blocks/builtins/OpeningHoursRender.js.map +1 -1
  277. package/dist/blocks/builtins/PriceList.d.ts +1 -0
  278. package/dist/blocks/builtins/PriceList.js +25 -0
  279. package/dist/blocks/builtins/PriceList.js.map +1 -0
  280. package/dist/blocks/builtins/PriceListRender.d.ts +17 -0
  281. package/dist/blocks/builtins/PriceListRender.js +87 -0
  282. package/dist/blocks/builtins/PriceListRender.js.map +1 -0
  283. package/dist/blocks/builtins/RichTextBlockRender.js +6 -1
  284. package/dist/blocks/builtins/RichTextBlockRender.js.map +1 -1
  285. package/dist/blocks/builtins/SectionHeading.d.ts +1 -0
  286. package/dist/blocks/builtins/SectionHeading.js +14 -0
  287. package/dist/blocks/builtins/SectionHeading.js.map +1 -0
  288. package/dist/blocks/builtins/SectionHeadingRender.d.ts +7 -0
  289. package/dist/blocks/builtins/SectionHeadingRender.js +14 -0
  290. package/dist/blocks/builtins/SectionHeadingRender.js.map +1 -0
  291. package/dist/blocks/builtins/SliderRender.js +3 -2
  292. package/dist/blocks/builtins/SliderRender.js.map +1 -1
  293. package/dist/blocks/builtins/SymbolRef.js +1 -1
  294. package/dist/blocks/builtins/SymbolRef.js.map +1 -1
  295. package/dist/blocks/builtins/TeamRender.js +6 -1
  296. package/dist/blocks/builtins/TeamRender.js.map +1 -1
  297. package/dist/blocks/builtins/Testimonials.js +3 -1
  298. package/dist/blocks/builtins/Testimonials.js.map +1 -1
  299. package/dist/blocks/builtins/TestimonialsRender.js +4 -0
  300. package/dist/blocks/builtins/TestimonialsRender.js.map +1 -1
  301. package/dist/blocks/builtins/WhatsappChatRender.d.ts +1 -1
  302. package/dist/blocks/builtins/WhatsappChatRender.js +1 -1
  303. package/dist/blocks/builtins/WhatsappChatRender.js.map +1 -1
  304. package/dist/blocks/builtins/Widgets.js +2 -2
  305. package/dist/blocks/builtins/Widgets.js.map +1 -1
  306. package/dist/blocks/builtins/WidgetsRender.js +3 -1
  307. package/dist/blocks/builtins/WidgetsRender.js.map +1 -1
  308. package/dist/blocks/builtins/index.d.ts +3 -0
  309. package/dist/blocks/builtins/index.js +6 -0
  310. package/dist/blocks/builtins/index.js.map +1 -1
  311. package/dist/blocks/builtins/lib/autoComplete.d.ts +7 -0
  312. package/dist/blocks/builtins/lib/autoComplete.js +20 -0
  313. package/dist/blocks/builtins/lib/autoComplete.js.map +1 -0
  314. package/dist/blocks/builtins/lib/bookingUrl.d.ts +2 -2
  315. package/dist/blocks/builtins/lib/bookingUrl.js +1 -1
  316. package/dist/blocks/builtins/lib/bookingUrl.js.map +1 -1
  317. package/dist/blocks/builtins/lib/ical.d.ts +24 -0
  318. package/dist/blocks/builtins/lib/ical.js +82 -0
  319. package/dist/blocks/builtins/lib/ical.js.map +1 -0
  320. package/dist/blocks/builtins/lib/vandaag.d.ts +8 -0
  321. package/dist/blocks/builtins/lib/vandaag.js +22 -0
  322. package/dist/blocks/builtins/lib/vandaag.js.map +1 -0
  323. package/dist/blocks/registry.d.ts +6 -0
  324. package/dist/blocks/registry.js.map +1 -1
  325. package/dist/blocks/ui/index.js +1 -0
  326. package/dist/blocks/ui/index.js.map +1 -1
  327. package/dist/components/EditBar.js +2 -4
  328. package/dist/components/EditBar.js.map +1 -1
  329. package/dist/components/News.js +26 -18
  330. package/dist/components/News.js.map +1 -1
  331. package/dist/components/PageView.d.ts +7 -1
  332. package/dist/components/PageView.js +17 -0
  333. package/dist/components/PageView.js.map +1 -1
  334. package/dist/components/ResponsiveImage.js +18 -20
  335. package/dist/components/ResponsiveImage.js.map +1 -1
  336. package/dist/components/site/SiteFooter.d.ts +6 -1
  337. package/dist/components/site/SiteFooter.js +61 -9
  338. package/dist/components/site/SiteFooter.js.map +1 -1
  339. package/dist/components/site/SiteHeader.d.ts +15 -0
  340. package/dist/components/site/SiteHeader.js +70 -0
  341. package/dist/components/site/SiteHeader.js.map +1 -0
  342. package/dist/components/site/SiteNav.d.ts +2 -0
  343. package/dist/components/site/SiteNav.js +29 -5
  344. package/dist/components/site/SiteNav.js.map +1 -1
  345. package/dist/demo/DemoStudio.js +2 -2
  346. package/dist/demo/DemoStudio.js.map +1 -1
  347. package/dist/demo/demoStore.d.ts +13 -0
  348. package/dist/demo/demoStore.js +36 -0
  349. package/dist/demo/demoStore.js.map +1 -1
  350. package/dist/editor/EditorProvider.js +15 -4
  351. package/dist/editor/EditorProvider.js.map +1 -1
  352. package/dist/index.d.ts +4 -1
  353. package/dist/index.js +16 -1
  354. package/dist/index.js.map +1 -1
  355. package/dist/media/ImageContext.d.ts +17 -0
  356. package/dist/media/ImageContext.js +65 -20
  357. package/dist/media/ImageContext.js.map +1 -1
  358. package/dist/media/altInHtml.d.ts +9 -0
  359. package/dist/media/altInHtml.js +17 -0
  360. package/dist/media/altInHtml.js.map +1 -0
  361. package/dist/media/srcset.d.ts +12 -0
  362. package/dist/media/srcset.js +13 -0
  363. package/dist/media/srcset.js.map +1 -0
  364. package/dist/modules/builtins.d.ts +1 -0
  365. package/dist/modules/builtins.js +16 -3
  366. package/dist/modules/builtins.js.map +1 -1
  367. package/dist/modules/shop/blocks/Cart.js +5 -3
  368. package/dist/modules/shop/blocks/Cart.js.map +1 -1
  369. package/dist/modules/shop/blocks/Checkout.js +56 -35
  370. package/dist/modules/shop/blocks/Checkout.js.map +1 -1
  371. package/dist/modules/shop/blocks/Donation.js +4 -4
  372. package/dist/modules/shop/blocks/Donation.js.map +1 -1
  373. package/dist/modules/shop/blocks/ProductDetail.js +19 -3
  374. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
  375. package/dist/modules/shop/blocks/ProductGrid.d.ts +3 -1
  376. package/dist/modules/shop/blocks/ProductGrid.js +8 -4
  377. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
  378. package/dist/modules/shop/blocks/ShopCategories.js +4 -3
  379. package/dist/modules/shop/blocks/ShopCategories.js.map +1 -1
  380. package/dist/modules/shop/lib.d.ts +4 -0
  381. package/dist/modules/shop/lib.js +14 -0
  382. package/dist/modules/shop/lib.js.map +1 -1
  383. package/dist/modules/shop/settings.d.ts +3 -0
  384. package/dist/modules/shop/settings.js +14 -0
  385. package/dist/modules/shop/settings.js.map +1 -1
  386. package/dist/modules/shop/strings.d.ts +4 -0
  387. package/dist/modules/shop/strings.js +8 -0
  388. package/dist/modules/shop/strings.js.map +1 -1
  389. package/dist/next/envCheck.d.ts +42 -0
  390. package/dist/next/envCheck.js +106 -0
  391. package/dist/next/envCheck.js.map +1 -0
  392. package/dist/next/index.cjs +185 -2
  393. package/dist/next/index.d.ts +2 -0
  394. package/dist/next/index.js +8 -1
  395. package/dist/next/index.js.map +1 -1
  396. package/dist/next/securityHeaders.d.ts +36 -0
  397. package/dist/next/securityHeaders.js +74 -0
  398. package/dist/next/securityHeaders.js.map +1 -0
  399. package/dist/patterns/builtins.d.ts +7 -0
  400. package/dist/patterns/builtins.js +138 -3
  401. package/dist/patterns/builtins.js.map +1 -1
  402. package/dist/releaseNotes.js +140 -0
  403. package/dist/releaseNotes.js.map +1 -1
  404. package/dist/render/PublishedPage.d.ts +2 -1
  405. package/dist/render/PublishedPage.js +10 -4
  406. package/dist/render/PublishedPage.js.map +1 -1
  407. package/dist/schema/blocks.d.ts +470 -0
  408. package/dist/schema/blocks.js +102 -4
  409. package/dist/schema/blocks.js.map +1 -1
  410. package/dist/schema/business.d.ts +117 -0
  411. package/dist/schema/business.js +73 -0
  412. package/dist/schema/business.js.map +1 -0
  413. package/dist/schema/fields.d.ts +2 -1
  414. package/dist/schema/fields.js.map +1 -1
  415. package/dist/schema/forms.d.ts +18 -0
  416. package/dist/schema/forms.js +18 -3
  417. package/dist/schema/forms.js.map +1 -1
  418. package/dist/schema/mail.d.ts +10 -0
  419. package/dist/schema/mail.js +7 -0
  420. package/dist/schema/mail.js.map +1 -1
  421. package/dist/schema/settings.d.ts +101 -0
  422. package/dist/schema/settings.js +24 -3
  423. package/dist/schema/settings.js.map +1 -1
  424. package/dist/schema/shop.d.ts +2 -0
  425. package/dist/schema/shop.js +4 -0
  426. package/dist/schema/shop.js.map +1 -1
  427. package/dist/schema/shopGeld.d.ts +1 -0
  428. package/dist/schema/shopGeld.js +1 -0
  429. package/dist/schema/shopGeld.js.map +1 -1
  430. package/dist/schema/siteConfig.d.ts +68 -1
  431. package/dist/schema/siteConfig.js +7 -2
  432. package/dist/schema/siteConfig.js.map +1 -1
  433. package/dist/server/blockData.d.ts +1 -1
  434. package/dist/server/blockData.js +74 -18
  435. package/dist/server/blockData.js.map +1 -1
  436. package/dist/server/cspRapport.d.ts +20 -0
  437. package/dist/server/cspRapport.js +56 -0
  438. package/dist/server/cspRapport.js.map +1 -0
  439. package/dist/server/fonts.d.ts +3 -0
  440. package/dist/server/fonts.js +39 -0
  441. package/dist/server/fonts.js.map +1 -0
  442. package/dist/server/index.d.ts +3 -0
  443. package/dist/server/index.js +8 -0
  444. package/dist/server/index.js.map +1 -1
  445. package/dist/server/sanitize.js +15 -2
  446. package/dist/server/sanitize.js.map +1 -1
  447. package/dist/settings/useSiteSettings.js +4 -1
  448. package/dist/settings/useSiteSettings.js.map +1 -1
  449. package/dist/starters/starters.d.ts +69 -0
  450. package/dist/starters/starters.js +171 -0
  451. package/dist/starters/starters.js.map +1 -0
  452. package/dist/styles.css +1149 -208
  453. package/dist/theme/color.d.ts +10 -0
  454. package/dist/theme/color.js +17 -1
  455. package/dist/theme/color.js.map +1 -1
  456. package/dist/theme/fontFallback.d.ts +23 -0
  457. package/dist/theme/fontFallback.js +58 -0
  458. package/dist/theme/fontFallback.js.map +1 -0
  459. package/dist/theme/fonts.d.ts +11 -0
  460. package/dist/theme/fonts.js +16 -0
  461. package/dist/theme/fonts.js.map +1 -0
  462. package/dist/version.js +1 -1
  463. package/package.json +6 -4
@@ -1,6 +1,7 @@
1
1
  import "server-only";
2
2
  import { listPublishedPages, listPublishedNews, listFormDefinitions, listSiteMedia } from "./pages.js";
3
3
  import { blocksToText } from "../blocks/searchText.js";
4
+ import { parseIcalBezet, veiligeIcalUrl } from "../blocks/builtins/lib/ical.js";
4
5
  const SHOP_TYPES = /* @__PURE__ */ new Set(["productGrid", "shopCategories", "productDetail", "cart", "checkout", "donation", "orderStatus"]);
5
6
  const NEWS_TYPES = /* @__PURE__ */ new Set(["newsList", "newsWidget"]);
6
7
  function collectTypes(v, out, depth) {
@@ -23,6 +24,9 @@ function collectFormIds(v, out, depth) {
23
24
  if (o._type === "form" && typeof o.formId === "string" && o.formId) out.add(o.formId);
24
25
  for (const val of Object.values(o)) collectFormIds(val, out, depth + 1);
25
26
  }
27
+ function heeftOpslagUrl(tekst) {
28
+ return tekst.includes("firebasestorage") || tekst.includes("storage.googleapis");
29
+ }
26
30
  function searchWantsNews(v, depth) {
27
31
  if (depth > 6 || v == null || typeof v !== "object") return false;
28
32
  if (Array.isArray(v)) return v.some((item) => searchWantsNews(item, depth + 1));
@@ -30,15 +34,45 @@ function searchWantsNews(v, depth) {
30
34
  if (o._type === "search" && o.includeNews === true) return true;
31
35
  return Object.values(o).some((val) => searchWantsNews(val, depth + 1));
32
36
  }
37
+ function collectIcalUrls(v, out, depth) {
38
+ if (depth > 6 || v == null || typeof v !== "object") return;
39
+ if (Array.isArray(v)) return v.forEach((x) => collectIcalUrls(x, out, depth + 1));
40
+ const o = v;
41
+ if (o._type === "availability" && Array.isArray(o.units)) {
42
+ for (const u of o.units) if (typeof u?.icalUrl === "string" && u.icalUrl.trim()) out.add(u.icalUrl.trim());
43
+ }
44
+ Object.values(o).forEach((x) => collectIcalUrls(x, out, depth + 1));
45
+ }
46
+ async function haalBezetting(url) {
47
+ if (!veiligeIcalUrl(url)) return null;
48
+ try {
49
+ const res = await fetch(url.replace(/^webcal:\/\//i, "https://"), {
50
+ signal: AbortSignal.timeout(6e3),
51
+ headers: { accept: "text/calendar, text/plain;q=0.8" },
52
+ next: { revalidate: 3600 }
53
+ });
54
+ if (!res.ok) return null;
55
+ const tekst = await res.text();
56
+ if (tekst.length > 2e6 || !tekst.includes("BEGIN:VCALENDAR")) return null;
57
+ return parseIcalBezet(tekst);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
33
62
  async function getPageBlockData(siteId, page, opts) {
34
63
  const types = /* @__PURE__ */ new Set();
35
64
  collectTypes(page.blocks ?? [], types, 0);
36
65
  const wilShop = [...SHOP_TYPES].some((t) => types.has(t));
37
66
  const wilNews = [...NEWS_TYPES].some((t) => types.has(t));
38
67
  const wilSearch = types.has("search");
68
+ const wilKruimels = types.has("breadcrumbs") && typeof page.path === "string";
39
69
  const formIds = /* @__PURE__ */ new Set();
40
70
  collectFormIds(page.blocks ?? [], formIds, 0);
41
- if (!wilShop && !wilNews && !wilSearch && formIds.size === 0) return void 0;
71
+ const icalUrls = /* @__PURE__ */ new Set();
72
+ collectIcalUrls(page.blocks ?? [], icalUrls, 0);
73
+ const blokTekst = JSON.stringify(page.blocks ?? []);
74
+ const wilMedia = heeftOpslagUrl(blokTekst) || wilShop || wilNews;
75
+ if (!wilShop && !wilNews && !wilSearch && formIds.size === 0 && !wilMedia && !wilKruimels && icalUrls.size === 0) return void 0;
42
76
  const data = {};
43
77
  const zacht = async (naam, fn) => {
44
78
  try {
@@ -53,6 +87,14 @@ async function getPageBlockData(siteId, page, opts) {
53
87
  const { getShopPageData } = await import("./shop.js");
54
88
  data.shop = await getShopPageData(siteId, opts?.searchParams);
55
89
  }),
90
+ zacht("beschikbaarheid", async () => {
91
+ if (icalUrls.size === 0) return;
92
+ const map = {};
93
+ await Promise.all([...icalUrls].slice(0, 10).map(async (u) => {
94
+ map[u] = await haalBezetting(u);
95
+ }));
96
+ data.beschikbaarheid = map;
97
+ }),
56
98
  zacht("news", async () => {
57
99
  if (!wilNews) return;
58
100
  data.news = await listPublishedNews(siteId);
@@ -61,25 +103,18 @@ async function getPageBlockData(siteId, page, opts) {
61
103
  if (formIds.size === 0) return;
62
104
  const alle = await listFormDefinitions(siteId);
63
105
  const map = {};
106
+ for (const id of formIds) map[id] = null;
64
107
  for (const f of alle) if (formIds.has(f.id)) map[f.id] = f;
65
108
  data.forms = map;
66
109
  }),
67
- zacht("media", async () => {
68
- const blokTekst = JSON.stringify(page.blocks ?? []);
69
- if (!blokTekst.includes("firebasestorage") && !blokTekst.includes("storage.googleapis")) return;
70
- const alle = await listSiteMedia(siteId);
71
- const map = {};
72
- for (const m of alle) {
73
- const url = typeof m.url === "string" ? m.url : "";
74
- const varianten = m.variants;
75
- const focal = m.focal;
76
- const heeftFocal = typeof focal?.x === "number" && typeof focal?.y === "number";
77
- if (!url || m.status === "processing" || !varianten?.sizes?.length && !heeftFocal) continue;
78
- if (blokTekst.includes(url)) {
79
- map[url] = { variants: m.variants, width: m.width, height: m.height, ...heeftFocal ? { focal } : {} };
80
- }
81
- }
82
- if (Object.keys(map).length > 0) data.media = map;
110
+ // 8.3: het kruimelpad server-side, met paginatitels. Eerst rendeerde het pas na mount (niet in
111
+ // de HTML, layout-verschuiving, JSON-LD alleen client-side) en met labels uit het pad.
112
+ zacht("breadcrumbs", async () => {
113
+ if (!wilKruimels) return;
114
+ const pages = await listPublishedPages(siteId);
115
+ const titels = {};
116
+ for (const p of pages) if (typeof p.path === "string" && p.title) titels[p.path] = String(p.title);
117
+ data.breadcrumbs = { path: page.path, titels };
83
118
  }),
84
119
  zacht("search", async () => {
85
120
  if (!wilSearch) return;
@@ -102,7 +137,28 @@ async function getPageBlockData(siteId, page, opts) {
102
137
  data.searchIndex = index;
103
138
  })
104
139
  ]);
105
- return data;
140
+ await zacht("media", async () => {
141
+ const corpus = `${blokTekst}
142
+ ${data.shop ? JSON.stringify(data.shop) : ""}
143
+ ${data.news ? JSON.stringify(data.news) : ""}`;
144
+ if (!heeftOpslagUrl(corpus)) return;
145
+ const alle = await listSiteMedia(siteId);
146
+ const map = {};
147
+ for (const m of alle) {
148
+ const url = typeof m.url === "string" ? m.url : "";
149
+ const varianten = m.variants;
150
+ const focal = m.focal;
151
+ const heeftFocal = typeof focal?.x === "number" && typeof focal?.y === "number";
152
+ const heeftAlt = typeof m.alt === "string" && m.alt.trim() !== "";
153
+ if (!url || m.status === "processing" || !varianten?.sizes?.length && !heeftFocal && !heeftAlt) continue;
154
+ if (corpus.includes(url) || corpus.includes(url.replace(/&/g, "&"))) {
155
+ const alt = typeof m.alt === "string" && m.alt.trim() ? m.alt.trim() : void 0;
156
+ map[url] = { variants: m.variants, width: m.width, height: m.height, ...heeftFocal ? { focal } : {}, ...alt ? { alt } : {} };
157
+ }
158
+ }
159
+ if (Object.keys(map).length > 0) data.media = map;
160
+ });
161
+ return Object.keys(data).length > 0 ? data : void 0;
106
162
  }
107
163
  export {
108
164
  getPageBlockData
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/blockData.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Page } from \"../schema/pages\";\nimport { listPublishedPages, listPublishedNews, listFormDefinitions, listSiteMedia } from \"./pages\";\nimport { blocksToText, type SearchEntry } from \"../blocks/searchText\";\n\n/**\n * SSR-seeds voor blokken die data buiten de pagina zelf nodig hebben (7.12).\n *\n * Het duurste patroon in XiCMS was niet een gemiste servercache maar de BROWSER: blokken als de\n * productgrid, het zoekblok en het formulier haalden hun data client-side uit Firestore — per\n * bezoeker, buiten elke cache om. Eén winkelpagina kostte zo honderden reads per bezoeker, en het\n * zoekblok las de complete pagina-collectie. De server-readers bestonden (server/shop.ts), maar\n * elke site moest er zelf aan denken ze aan te roepen — en geen enkele deed dat. SITE-AUDIT noemt\n * ontbrekende SSR-seeds \"de #1 prestatieval\"; dit bestand maakt die val structureel onmogelijk.\n *\n * `getPageBlockData` kijkt naar de blóktypes van de pagina en verzamelt precies de data die deze\n * pagina nodig heeft — niets meer. Een pagina zonder shop-blokken doet nul product-reads; een\n * pagina zonder zoekblok bouwt geen index. Alle onderliggende readers zitten in de tagged\n * servercache (server/cached.ts), dus dit kost Firestore éénmaal per interval, niet per bezoeker.\n *\n * Gebruik in de catch-all van de klantsite (ná symbol-expansie, zodat blokken ín symbolen meetellen):\n *\n * const page = await getPublishedPageByPathExpanded(siteId, path, locale);\n * const blockData = page ? await getPageBlockData(siteId, page, { searchParams }) : undefined;\n * <PublishedPage page={page} siteId={siteId} blockData={blockData} />\n *\n * De blokken zelf houden hun client-fallback: een site die dit (nog) niet doorgeeft blijft werken,\n * alleen duurder en trager — precies het oude gedrag.\n */\n\n/** Bloktypes die de shop-seed nodig hebben (productDetail leest daarnaast `?p=` voor het product). */\nconst SHOP_TYPES = new Set([\"productGrid\", \"shopCategories\", \"productDetail\", \"cart\", \"checkout\", \"donation\", \"orderStatus\"]);\n/** Bloktypes die de nieuws-seed nodig hebben. */\nconst NEWS_TYPES = new Set([\"newsList\", \"newsWidget\"]);\n\n/** Verzamel alle `_type`-waarden, recursief — kolommen/tabs/secties kunnen blokken bevatten. */\nfunction collectTypes(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectTypes(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (typeof o._type === \"string\") out.add(o._type);\n for (const val of Object.values(o)) collectTypes(val, out, depth + 1);\n}\n\n/** Verzamel de `formId`s van alle formulierblokken (ook genest). */\nfunction collectFormIds(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectFormIds(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (o._type === \"form\" && typeof o.formId === \"string\" && o.formId) out.add(o.formId);\n for (const val of Object.values(o)) collectFormIds(val, out, depth + 1);\n}\n\n/** Wil één van de zoekblokken op de pagina ook nieuws doorzoeken? */\nfunction searchWantsNews(v: unknown, depth: number): boolean {\n if (depth > 6 || v == null || typeof v !== \"object\") return false;\n if (Array.isArray(v)) return v.some((item) => searchWantsNews(item, depth + 1));\n const o = v as Record<string, unknown>;\n if (o._type === \"search\" && o.includeNews === true) return true;\n return Object.values(o).some((val) => searchWantsNews(val, depth + 1));\n}\n\nexport interface PageBlockDataOptions {\n /** `searchParams` van de route — alleen nodig voor productdetailpagina's (`?p=<slug>`). */\n searchParams?: { p?: string | string[]; [k: string]: unknown };\n /** De taal van de render — de zoekindex indexeert dan de vertaalde paden. (Nog ongebruikt.) */\n locale?: string;\n}\n\n/**\n * Server-side blockData voor één pagina, op basis van zijn bloktypes.\n * Geef het resultaat door als `<PublishedPage blockData={...}>`. `undefined`-namespaces blijven\n * weg zodat de blokken hun eigen fallback houden waar deze functie niets voor verzamelde.\n */\nexport async function getPageBlockData(\n siteId: string,\n page: Pick<Page, \"blocks\">,\n opts?: PageBlockDataOptions,\n): Promise<Record<string, unknown> | undefined> {\n const types = new Set<string>();\n collectTypes(page.blocks ?? [], types, 0);\n\n const wilShop = [...SHOP_TYPES].some((t) => types.has(t));\n const wilNews = [...NEWS_TYPES].some((t) => types.has(t));\n const wilSearch = types.has(\"search\");\n const formIds = new Set<string>();\n collectFormIds(page.blocks ?? [], formIds, 0);\n\n if (!wilShop && !wilNews && !wilSearch && formIds.size === 0) return undefined;\n\n const data: Record<string, unknown> = {};\n\n // Elke seed faalt ZACHT: bij een fout blijft die namespace weg en neemt de client-fallback van\n // het blok het over — hetzelfde gedrag als een site die (nog) geen seeds doorgeeft. De readers\n // gooien bewust bij een hik (uitkomsten worden gecached, fouten niet — zie server/cached.ts),\n // dus hier opvangen is wat \"één mislukte read\" scheidt van \"één mislukte pagina\".\n const zacht = async (naam: string, fn: () => Promise<void>) => {\n try {\n await fn();\n } catch (e) {\n console.warn(`[xicms] blockData-seed \"${naam}\" overgeslagen:`, (e as Error)?.message ?? e);\n }\n };\n\n // Parallel: elke seed leest uit de tagged servercache, dus dit zijn meestal nul echte reads.\n await Promise.all([\n zacht(\"shop\", async () => {\n if (!wilShop) return;\n // Dynamische import: shop-servercode alleen in de graph van pagina's/sites die hem gebruiken.\n const { getShopPageData } = await import(\"./shop\");\n data.shop = await getShopPageData(siteId, opts?.searchParams);\n }),\n zacht(\"news\", async () => {\n if (!wilNews) return;\n data.news = await listPublishedNews(siteId);\n }),\n zacht(\"forms\", async () => {\n if (formIds.size === 0) return;\n const alle = await listFormDefinitions(siteId);\n const map: Record<string, unknown> = {};\n for (const f of alle) if (formIds.has(f.id)) map[f.id] = f;\n data.forms = map;\n }),\n zacht(\"media\", async () => {\n // Beeld-ladder-seed: alleen de media-docs waarvan de URL werkelijk op deze pagina staat,\n // en alleen als de pipeline varianten heeft gemaakt. Eén stringify-scan is goedkoper en\n // robuuster dan per bloktype weten welke velden afbeeldingen zijn.\n const blokTekst = JSON.stringify(page.blocks ?? []);\n if (!blokTekst.includes(\"firebasestorage\") && !blokTekst.includes(\"storage.googleapis\")) return;\n const alle = await listSiteMedia(siteId);\n const map: Record<string, unknown> = {};\n for (const m of alle) {\n const url = typeof m.url === \"string\" ? m.url : \"\";\n const varianten = m.variants as { sizes?: unknown[] } | undefined;\n // 7.24: ook een foto zónder ladder maar mét focuspunt gaat mee — anders zou een focuspunt\n // op een oudere upload (van vóór de pipeline) nooit aankomen.\n const focal = m.focal as { x?: unknown; y?: unknown } | undefined;\n const heeftFocal = typeof focal?.x === \"number\" && typeof focal?.y === \"number\";\n if (!url || m.status === \"processing\" || (!varianten?.sizes?.length && !heeftFocal)) continue;\n if (blokTekst.includes(url)) {\n map[url] = { variants: m.variants, width: m.width, height: m.height, ...(heeftFocal ? { focal } : {}) };\n }\n }\n if (Object.keys(map).length > 0) data.media = map;\n }),\n zacht(\"search\", async () => {\n if (!wilSearch) return;\n const [pages, news] = await Promise.all([\n listPublishedPages(siteId),\n searchWantsNews(page.blocks ?? [], 0) ? listPublishedNews(siteId) : Promise.resolve([]),\n ]);\n const index: SearchEntry[] = pages.map((p) => ({\n title: String(p.title ?? \"\"),\n path: String(p.path ?? \"/\"),\n text: blocksToText(p.blocks),\n }));\n for (const n of news) {\n index.push({\n title: n.title,\n path: `/nieuws/${n.slug || n.id}`,\n text: `${n.excerpt ?? \"\"} ${(n.bodyHtml ?? n.body ?? \"\").replace(/<[^>]*>/g, \" \")}`,\n });\n }\n data.searchIndex = index;\n }),\n ]);\n\n return data;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/server/blockData.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { Page } from \"../schema/pages\";\nimport { listPublishedPages, listPublishedNews, listFormDefinitions, listSiteMedia } from \"./pages\";\nimport { blocksToText, type SearchEntry } from \"../blocks/searchText\";\nimport { parseIcalBezet, veiligeIcalUrl, type Bezet } from \"../blocks/builtins/lib/ical\";\n\n/**\n * SSR-seeds voor blokken die data buiten de pagina zelf nodig hebben (7.12).\n *\n * Het duurste patroon in XiCMS was niet een gemiste servercache maar de BROWSER: blokken als de\n * productgrid, het zoekblok en het formulier haalden hun data client-side uit Firestore — per\n * bezoeker, buiten elke cache om. Eén winkelpagina kostte zo honderden reads per bezoeker, en het\n * zoekblok las de complete pagina-collectie. De server-readers bestonden (server/shop.ts), maar\n * elke site moest er zelf aan denken ze aan te roepen — en geen enkele deed dat. SITE-AUDIT noemt\n * ontbrekende SSR-seeds \"de #1 prestatieval\"; dit bestand maakt die val structureel onmogelijk.\n *\n * `getPageBlockData` kijkt naar de blóktypes van de pagina en verzamelt precies de data die deze\n * pagina nodig heeft — niets meer. Een pagina zonder shop-blokken doet nul product-reads; een\n * pagina zonder zoekblok bouwt geen index. Alle onderliggende readers zitten in de tagged\n * servercache (server/cached.ts), dus dit kost Firestore éénmaal per interval, niet per bezoeker.\n *\n * Gebruik in de catch-all van de klantsite (ná symbol-expansie, zodat blokken ín symbolen meetellen):\n *\n * const page = await getPublishedPageByPathExpanded(siteId, path, locale);\n * const blockData = page ? await getPageBlockData(siteId, page, { searchParams }) : undefined;\n * <PublishedPage page={page} siteId={siteId} blockData={blockData} />\n *\n * De blokken zelf houden hun client-fallback: een site die dit (nog) niet doorgeeft blijft werken,\n * alleen duurder en trager — precies het oude gedrag.\n */\n\n/** Bloktypes die de shop-seed nodig hebben (productDetail leest daarnaast `?p=` voor het product). */\nconst SHOP_TYPES = new Set([\"productGrid\", \"shopCategories\", \"productDetail\", \"cart\", \"checkout\", \"donation\", \"orderStatus\"]);\n/** Bloktypes die de nieuws-seed nodig hebben. */\nconst NEWS_TYPES = new Set([\"newsList\", \"newsWidget\"]);\n\n/** Verzamel alle `_type`-waarden, recursief — kolommen/tabs/secties kunnen blokken bevatten. */\nfunction collectTypes(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectTypes(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (typeof o._type === \"string\") out.add(o._type);\n for (const val of Object.values(o)) collectTypes(val, out, depth + 1);\n}\n\n/** Verzamel de `formId`s van alle formulierblokken (ook genest). */\nfunction collectFormIds(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) {\n for (const item of v) collectFormIds(item, out, depth + 1);\n return;\n }\n const o = v as Record<string, unknown>;\n if (o._type === \"form\" && typeof o.formId === \"string\" && o.formId) out.add(o.formId);\n for (const val of Object.values(o)) collectFormIds(val, out, depth + 1);\n}\n\n/** Staat er een eigen (Firebase Storage-)beeld in deze tekst? */\nfunction heeftOpslagUrl(tekst: string): boolean {\n return tekst.includes(\"firebasestorage\") || tekst.includes(\"storage.googleapis\");\n}\n\n/** Wil één van de zoekblokken op de pagina ook nieuws doorzoeken? */\nfunction searchWantsNews(v: unknown, depth: number): boolean {\n if (depth > 6 || v == null || typeof v !== \"object\") return false;\n if (Array.isArray(v)) return v.some((item) => searchWantsNews(item, depth + 1));\n const o = v as Record<string, unknown>;\n if (o._type === \"search\" && o.includeNews === true) return true;\n return Object.values(o).some((val) => searchWantsNews(val, depth + 1));\n}\n\n/** iCal-links van beschikbaarheidsblokken op de pagina (8.15). */\nfunction collectIcalUrls(v: unknown, out: Set<string>, depth: number): void {\n if (depth > 6 || v == null || typeof v !== \"object\") return;\n if (Array.isArray(v)) return v.forEach((x) => collectIcalUrls(x, out, depth + 1));\n const o = v as Record<string, unknown>;\n if (o._type === \"availability\" && Array.isArray(o.units)) {\n for (const u of o.units as { icalUrl?: unknown }[]) if (typeof u?.icalUrl === \"string\" && u.icalUrl.trim()) out.add(u.icalUrl.trim());\n }\n Object.values(o).forEach((x) => collectIcalUrls(x, out, depth + 1));\n}\n\n/**\n * Eén iCal-agenda ophalen (8.15): alleen https naar een gewone hostnaam, max 6 s en 2 MB, en een\n * uur gecachet (Next-fetchcache). Een externe agenda die hapert mag de pagina niet laten hangen;\n * bij een fout geeft de seed `null` en toont de kalender een eerlijke melding.\n */\nasync function haalBezetting(url: string): Promise<Bezet[] | null> {\n if (!veiligeIcalUrl(url)) return null;\n try {\n const res = await fetch(url.replace(/^webcal:\\/\\//i, \"https://\"), {\n signal: AbortSignal.timeout(6000),\n headers: { accept: \"text/calendar, text/plain;q=0.8\" },\n next: { revalidate: 3600 },\n } as RequestInit);\n if (!res.ok) return null;\n const tekst = await res.text();\n if (tekst.length > 2_000_000 || !tekst.includes(\"BEGIN:VCALENDAR\")) return null;\n return parseIcalBezet(tekst);\n } catch {\n return null;\n }\n}\n\nexport interface PageBlockDataOptions {\n /** `searchParams` van de route — alleen nodig voor productdetailpagina's (`?p=<slug>`). */\n searchParams?: { p?: string | string[]; [k: string]: unknown };\n /** De taal van de render — de zoekindex indexeert dan de vertaalde paden. (Nog ongebruikt.) */\n locale?: string;\n}\n\n/**\n * Server-side blockData voor één pagina, op basis van zijn bloktypes.\n * Geef het resultaat door als `<PublishedPage blockData={...}>`. `undefined`-namespaces blijven\n * weg zodat de blokken hun eigen fallback houden waar deze functie niets voor verzamelde.\n */\nexport async function getPageBlockData(\n siteId: string,\n page: Pick<Page, \"blocks\"> & Partial<Pick<Page, \"path\">>,\n opts?: PageBlockDataOptions,\n): Promise<Record<string, unknown> | undefined> {\n const types = new Set<string>();\n collectTypes(page.blocks ?? [], types, 0);\n\n const wilShop = [...SHOP_TYPES].some((t) => types.has(t));\n const wilNews = [...NEWS_TYPES].some((t) => types.has(t));\n const wilSearch = types.has(\"search\");\n const wilKruimels = types.has(\"breadcrumbs\") && typeof page.path === \"string\";\n const formIds = new Set<string>();\n collectFormIds(page.blocks ?? [], formIds, 0);\n const icalUrls = new Set<string>();\n collectIcalUrls(page.blocks ?? [], icalUrls, 0);\n\n // 8.3: ook pagina's met alleen eigen foto's krijgen een seed (de beeld-ladder). Eerst stond\n // \"media\" niet in deze voorwaarde, waardoor een gewone pagina met een hero of galerij hier al\n // stopte en de eigen srcset-ladder van 7.13 daar nooit werd gebruikt.\n const blokTekst = JSON.stringify(page.blocks ?? []);\n const wilMedia = heeftOpslagUrl(blokTekst) || wilShop || wilNews;\n if (!wilShop && !wilNews && !wilSearch && formIds.size === 0 && !wilMedia && !wilKruimels && icalUrls.size === 0) return undefined;\n\n const data: Record<string, unknown> = {};\n\n // Elke seed faalt ZACHT: bij een fout blijft die namespace weg en neemt de client-fallback van\n // het blok het over — hetzelfde gedrag als een site die (nog) geen seeds doorgeeft. De readers\n // gooien bewust bij een hik (uitkomsten worden gecached, fouten niet — zie server/cached.ts),\n // dus hier opvangen is wat \"één mislukte read\" scheidt van \"één mislukte pagina\".\n const zacht = async (naam: string, fn: () => Promise<void>) => {\n try {\n await fn();\n } catch (e) {\n console.warn(`[xicms] blockData-seed \"${naam}\" overgeslagen:`, (e as Error)?.message ?? e);\n }\n };\n\n // Parallel: elke seed leest uit de tagged servercache, dus dit zijn meestal nul echte reads.\n await Promise.all([\n zacht(\"shop\", async () => {\n if (!wilShop) return;\n // Dynamische import: shop-servercode alleen in de graph van pagina's/sites die hem gebruiken.\n const { getShopPageData } = await import(\"./shop\");\n data.shop = await getShopPageData(siteId, opts?.searchParams);\n }),\n zacht(\"beschikbaarheid\", async () => {\n if (icalUrls.size === 0) return;\n const map: Record<string, Bezet[] | null> = {};\n await Promise.all([...icalUrls].slice(0, 10).map(async (u) => { map[u] = await haalBezetting(u); }));\n data.beschikbaarheid = map;\n }),\n zacht(\"news\", async () => {\n if (!wilNews) return;\n data.news = await listPublishedNews(siteId);\n }),\n zacht(\"forms\", async () => {\n if (formIds.size === 0) return;\n const alle = await listFormDefinitions(siteId);\n // 8.9: een formulier dat niet bestaat expliciet als `null` meegeven. Zonder die markering\n // haalde de browser de hele Firestore-SDK (±170 kB gzip) binnen om hetzelfde vast te stellen.\n const map: Record<string, unknown> = {};\n for (const id of formIds) map[id] = null;\n for (const f of alle) if (formIds.has(f.id)) map[f.id] = f;\n data.forms = map;\n }),\n // 8.3: het kruimelpad server-side, met paginatitels. Eerst rendeerde het pas na mount (niet in\n // de HTML, layout-verschuiving, JSON-LD alleen client-side) en met labels uit het pad.\n zacht(\"breadcrumbs\", async () => {\n if (!wilKruimels) return;\n const pages = await listPublishedPages(siteId);\n const titels: Record<string, string> = {};\n for (const p of pages) if (typeof p.path === \"string\" && p.title) titels[p.path] = String(p.title);\n data.breadcrumbs = { path: page.path, titels };\n }),\n zacht(\"search\", async () => {\n if (!wilSearch) return;\n const [pages, news] = await Promise.all([\n listPublishedPages(siteId),\n searchWantsNews(page.blocks ?? [], 0) ? listPublishedNews(siteId) : Promise.resolve([]),\n ]);\n const index: SearchEntry[] = pages.map((p) => ({\n title: String(p.title ?? \"\"),\n path: String(p.path ?? \"/\"),\n text: blocksToText(p.blocks),\n }));\n for (const n of news) {\n index.push({\n title: n.title,\n path: `/nieuws/${n.slug || n.id}`,\n text: `${n.excerpt ?? \"\"} ${(n.bodyHtml ?? n.body ?? \"\").replace(/<[^>]*>/g, \" \")}`,\n });\n }\n data.searchIndex = index;\n }),\n ]);\n\n // Na de andere seeds (8.3): productfoto's en nieuwscovers staan niet in de blokken maar in de\n // winkel- en nieuwsdata, en verdienen dezelfde ladder.\n await zacht(\"media\", async () => {\n const corpus = `${blokTekst}\\n${data.shop ? JSON.stringify(data.shop) : \"\"}\\n${data.news ? JSON.stringify(data.news) : \"\"}`;\n if (!heeftOpslagUrl(corpus)) return;\n const alle = await listSiteMedia(siteId);\n const map: Record<string, unknown> = {};\n for (const m of alle) {\n const url = typeof m.url === \"string\" ? m.url : \"\";\n const varianten = m.variants as { sizes?: unknown[] } | undefined;\n // 7.24: ook een foto zónder ladder maar mét focuspunt gaat mee — anders zou een focuspunt\n // op een oudere upload (van vóór de pipeline) nooit aankomen.\n const focal = m.focal as { x?: unknown; y?: unknown } | undefined;\n const heeftFocal = typeof focal?.x === \"number\" && typeof focal?.y === \"number\";\n const heeftAlt = typeof m.alt === \"string\" && m.alt.trim() !== \"\";\n if (!url || m.status === \"processing\" || (!varianten?.sizes?.length && !heeftFocal && !heeftAlt)) continue;\n // In rich text staat de url HTML-ge-escaped (`&amp;token=`) — die telt ook (8.10).\n if (corpus.includes(url) || corpus.includes(url.replace(/&/g, \"&amp;\"))) {\n // 8.4: ook de alt-tekst uit de bibliotheek — een blok met een leeg alt valt daarop terug.\n const alt = typeof m.alt === \"string\" && m.alt.trim() ? m.alt.trim() : undefined;\n map[url] = { variants: m.variants, width: m.width, height: m.height, ...(heeftFocal ? { focal } : {}), ...(alt ? { alt } : {}) };\n }\n }\n if (Object.keys(map).length > 0) data.media = map;\n });\n\n return Object.keys(data).length > 0 ? data : undefined;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Ontvangt CSP-overtredingen van browsers (8.7) en zet ze als één leesbare regel in de serverlog
3
+ * (Vercel → Logs). Zo weet je, voordat je de policy afdwingt, wat er op de echte site geladen wordt.
4
+ *
5
+ * Twee formaten: het oude `report-uri` (`{"csp-report": {...}}`, application/csp-report) en de
6
+ * Reporting API (`[{type:"csp-violation", body:{...}}]`, application/reports+json).
7
+ *
8
+ * Bewust zuinig: hoogstens 64 kB per verzoek, en elke unieke overtreding (directive + bron + pad)
9
+ * maar één keer per serverinstantie gelogd — één kapotte embed op een drukke pagina mag de log
10
+ * niet vullen.
11
+ */
12
+ export interface CspOvertreding {
13
+ directive: string;
14
+ bron: string;
15
+ pagina: string;
16
+ }
17
+ /** Puur: haal de overtredingen uit een rapport (beide formaten). Onbekend = lege lijst. */
18
+ export declare function leesCspRapport(data: unknown): CspOvertreding[];
19
+ /** Route-handler voor `app/api/csp-rapport/route.ts`. */
20
+ export declare function cspRapportPOST(req: Request): Promise<Response>;
@@ -0,0 +1,56 @@
1
+ const MAX_BYTES = 64 * 1024;
2
+ const MAX_GEZIEN = 500;
3
+ const gezien = /* @__PURE__ */ new Set();
4
+ function leesCspRapport(data) {
5
+ const lijst = [];
6
+ if (Array.isArray(data)) {
7
+ for (const r of data) {
8
+ const rr = r;
9
+ if (rr?.type === "csp-violation" && rr.body) lijst.push(rr.body);
10
+ }
11
+ } else if (data && typeof data === "object" && "csp-report" in data) {
12
+ const r = data["csp-report"];
13
+ if (r && typeof r === "object") lijst.push(r);
14
+ }
15
+ const tekst = (v) => typeof v === "string" ? v : "";
16
+ return lijst.map((r) => {
17
+ const directive = tekst(r.effectiveDirective) || tekst(r["effective-directive"]) || tekst(r["violated-directive"]) || "?";
18
+ const bron = tekst(r.blockedURL) || tekst(r["blocked-uri"]) || "?";
19
+ let pagina = tekst(r.documentURL) || tekst(r["document-uri"]);
20
+ try {
21
+ pagina = new URL(pagina).pathname;
22
+ } catch {
23
+ }
24
+ let b = bron;
25
+ try {
26
+ b = new URL(bron).origin;
27
+ } catch {
28
+ }
29
+ return { directive: directive.split(" ")[0], bron: b.slice(0, 200), pagina: pagina.slice(0, 200) };
30
+ });
31
+ }
32
+ async function cspRapportPOST(req) {
33
+ const lengte = Number(req.headers.get("content-length") ?? "0");
34
+ if (lengte > MAX_BYTES) return new Response(null, { status: 413 });
35
+ let data;
36
+ try {
37
+ const tekst = await req.text();
38
+ if (tekst.length > MAX_BYTES) return new Response(null, { status: 413 });
39
+ data = JSON.parse(tekst);
40
+ } catch {
41
+ return new Response(null, { status: 400 });
42
+ }
43
+ for (const o of leesCspRapport(data)) {
44
+ const sleutel = `${o.directive}|${o.bron}|${o.pagina}`;
45
+ if (gezien.has(sleutel)) continue;
46
+ if (gezien.size >= MAX_GEZIEN) gezien.clear();
47
+ gezien.add(sleutel);
48
+ console.warn(`[xicms-csp] ${o.directive} weigert ${o.bron} op ${o.pagina || "?"}`);
49
+ }
50
+ return new Response(null, { status: 204 });
51
+ }
52
+ export {
53
+ cspRapportPOST,
54
+ leesCspRapport
55
+ };
56
+ //# sourceMappingURL=cspRapport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/cspRapport.ts"],"sourcesContent":["/**\n * Ontvangt CSP-overtredingen van browsers (8.7) en zet ze als één leesbare regel in de serverlog\n * (Vercel → Logs). Zo weet je, voordat je de policy afdwingt, wat er op de echte site geladen wordt.\n *\n * Twee formaten: het oude `report-uri` (`{\"csp-report\": {...}}`, application/csp-report) en de\n * Reporting API (`[{type:\"csp-violation\", body:{...}}]`, application/reports+json).\n *\n * Bewust zuinig: hoogstens 64 kB per verzoek, en elke unieke overtreding (directive + bron + pad)\n * maar één keer per serverinstantie gelogd — één kapotte embed op een drukke pagina mag de log\n * niet vullen.\n */\n\nconst MAX_BYTES = 64 * 1024;\nconst MAX_GEZIEN = 500;\nconst gezien = new Set<string>();\n\nexport interface CspOvertreding {\n directive: string;\n bron: string;\n pagina: string;\n}\n\n/** Puur: haal de overtredingen uit een rapport (beide formaten). Onbekend = lege lijst. */\nexport function leesCspRapport(data: unknown): CspOvertreding[] {\n const lijst: Record<string, unknown>[] = [];\n if (Array.isArray(data)) {\n for (const r of data) {\n const rr = r as { type?: string; body?: Record<string, unknown> };\n if (rr?.type === \"csp-violation\" && rr.body) lijst.push(rr.body);\n }\n } else if (data && typeof data === \"object\" && \"csp-report\" in data) {\n const r = (data as Record<string, unknown>)[\"csp-report\"];\n if (r && typeof r === \"object\") lijst.push(r as Record<string, unknown>);\n }\n const tekst = (v: unknown) => (typeof v === \"string\" ? v : \"\");\n return lijst.map((r) => {\n const directive = tekst(r.effectiveDirective) || tekst(r[\"effective-directive\"]) || tekst(r[\"violated-directive\"]) || \"?\";\n const bron = tekst(r.blockedURL) || tekst(r[\"blocked-uri\"]) || \"?\";\n let pagina = tekst(r.documentURL) || tekst(r[\"document-uri\"]);\n try {\n pagina = new URL(pagina).pathname;\n } catch {\n /* geen geldige URL: laat staan */\n }\n // Alleen de origin van de geblokkeerde bron: query's kunnen tokens bevatten.\n let b = bron;\n try {\n b = new URL(bron).origin;\n } catch {\n /* \"inline\", \"eval\", \"data\" … */\n }\n return { directive: directive.split(\" \")[0], bron: b.slice(0, 200), pagina: pagina.slice(0, 200) };\n });\n}\n\n/** Route-handler voor `app/api/csp-rapport/route.ts`. */\nexport async function cspRapportPOST(req: Request): Promise<Response> {\n const lengte = Number(req.headers.get(\"content-length\") ?? \"0\");\n if (lengte > MAX_BYTES) return new Response(null, { status: 413 });\n let data: unknown;\n try {\n const tekst = await req.text();\n if (tekst.length > MAX_BYTES) return new Response(null, { status: 413 });\n data = JSON.parse(tekst);\n } catch {\n return new Response(null, { status: 400 });\n }\n for (const o of leesCspRapport(data)) {\n const sleutel = `${o.directive}|${o.bron}|${o.pagina}`;\n if (gezien.has(sleutel)) continue;\n if (gezien.size >= MAX_GEZIEN) gezien.clear();\n gezien.add(sleutel);\n console.warn(`[xicms-csp] ${o.directive} weigert ${o.bron} op ${o.pagina || \"?\"}`);\n }\n return new Response(null, { status: 204 });\n}\n"],"mappings":"AAYA,MAAM,YAAY,KAAK;AACvB,MAAM,aAAa;AACnB,MAAM,SAAS,oBAAI,IAAY;AASxB,SAAS,eAAe,MAAiC;AAC9D,QAAM,QAAmC,CAAC;AAC1C,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,eAAW,KAAK,MAAM;AACpB,YAAM,KAAK;AACX,UAAI,IAAI,SAAS,mBAAmB,GAAG,KAAM,OAAM,KAAK,GAAG,IAAI;AAAA,IACjE;AAAA,EACF,WAAW,QAAQ,OAAO,SAAS,YAAY,gBAAgB,MAAM;AACnE,UAAM,IAAK,KAAiC,YAAY;AACxD,QAAI,KAAK,OAAO,MAAM,SAAU,OAAM,KAAK,CAA4B;AAAA,EACzE;AACA,QAAM,QAAQ,CAAC,MAAgB,OAAO,MAAM,WAAW,IAAI;AAC3D,SAAO,MAAM,IAAI,CAAC,MAAM;AACtB,UAAM,YAAY,MAAM,EAAE,kBAAkB,KAAK,MAAM,EAAE,qBAAqB,CAAC,KAAK,MAAM,EAAE,oBAAoB,CAAC,KAAK;AACtH,UAAM,OAAO,MAAM,EAAE,UAAU,KAAK,MAAM,EAAE,aAAa,CAAC,KAAK;AAC/D,QAAI,SAAS,MAAM,EAAE,WAAW,KAAK,MAAM,EAAE,cAAc,CAAC;AAC5D,QAAI;AACF,eAAS,IAAI,IAAI,MAAM,EAAE;AAAA,IAC3B,QAAQ;AAAA,IAER;AAEA,QAAI,IAAI;AACR,QAAI;AACF,UAAI,IAAI,IAAI,IAAI,EAAE;AAAA,IACpB,QAAQ;AAAA,IAER;AACA,WAAO,EAAE,WAAW,UAAU,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,EAAE,MAAM,GAAG,GAAG,GAAG,QAAQ,OAAO,MAAM,GAAG,GAAG,EAAE;AAAA,EACnG,CAAC;AACH;AAGA,eAAsB,eAAe,KAAiC;AACpE,QAAM,SAAS,OAAO,IAAI,QAAQ,IAAI,gBAAgB,KAAK,GAAG;AAC9D,MAAI,SAAS,UAAW,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AACjE,MAAI;AACJ,MAAI;AACF,UAAM,QAAQ,MAAM,IAAI,KAAK;AAC7B,QAAI,MAAM,SAAS,UAAW,QAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AACvE,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB,QAAQ;AACN,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3C;AACA,aAAW,KAAK,eAAe,IAAI,GAAG;AACpC,UAAM,UAAU,GAAG,EAAE,SAAS,IAAI,EAAE,IAAI,IAAI,EAAE,MAAM;AACpD,QAAI,OAAO,IAAI,OAAO,EAAG;AACzB,QAAI,OAAO,QAAQ,WAAY,QAAO,MAAM;AAC5C,WAAO,IAAI,OAAO;AAClB,YAAQ,KAAK,eAAe,EAAE,SAAS,YAAY,EAAE,IAAI,OAAO,EAAE,UAAU,GAAG,EAAE;AAAA,EACnF;AACA,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAC3C;","names":[]}
@@ -0,0 +1,3 @@
1
+ import "server-only";
2
+ export declare function herschrijfFontCss(css: string, route?: string): string;
3
+ export declare function fontsGET(req: Request): Promise<Response>;
@@ -0,0 +1,39 @@
1
+ import "server-only";
2
+ import { FONT_ROUTE, isGoogleFontUrl } from "../theme/fonts.js";
3
+ const GSTATIC = "https://fonts.gstatic.com/";
4
+ const MODERNE_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36";
5
+ const BESTAND = /^[a-z0-9][a-z0-9/_.-]*\.(woff2|woff|ttf|otf)$/i;
6
+ function herschrijfFontCss(css, route = FONT_ROUTE) {
7
+ return css.replace(/https:\/\/fonts\.gstatic\.com\/([^)\s'"]+)/g, (_, pad) => `${route}?f=${encodeURIComponent(pad)}`);
8
+ }
9
+ async function fontsGET(req) {
10
+ const url = new URL(req.url);
11
+ const bestand = url.searchParams.get("f");
12
+ if (bestand !== null) {
13
+ if (!BESTAND.test(bestand) || bestand.includes("..")) return new Response("Ongeldig lettertype", { status: 400 });
14
+ const res2 = await fetch(GSTATIC + bestand);
15
+ if (!res2.ok || !res2.body) return new Response("Niet gevonden", { status: 404 });
16
+ return new Response(res2.body, {
17
+ headers: {
18
+ "content-type": res2.headers.get("content-type") ?? "font/woff2",
19
+ "cache-control": "public, max-age=31536000, immutable",
20
+ "access-control-allow-origin": "*"
21
+ }
22
+ });
23
+ }
24
+ const css = url.searchParams.get("css") ?? "";
25
+ if (!isGoogleFontUrl(css)) return new Response("Alleen Google-Fonts-CSS", { status: 400 });
26
+ const res = await fetch(css, { headers: { "user-agent": MODERNE_UA } });
27
+ if (!res.ok) return new Response("Lettertype-CSS niet beschikbaar", { status: 502 });
28
+ return new Response(herschrijfFontCss(await res.text()), {
29
+ headers: {
30
+ "content-type": "text/css; charset=utf-8",
31
+ "cache-control": "public, max-age=86400, s-maxage=604800, stale-while-revalidate=2592000"
32
+ }
33
+ });
34
+ }
35
+ export {
36
+ fontsGET,
37
+ herschrijfFontCss
38
+ };
39
+ //# sourceMappingURL=fonts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/fonts.ts"],"sourcesContent":["import \"server-only\";\nimport { FONT_ROUTE, isGoogleFontUrl } from \"../theme/fonts\";\n\n/**\n * Route-handler voor `app/api/fonts/route.ts` (8.3): `export { fontsGET as GET } from\n * \"@xiwebdesign/xicms-core/server\"`. Haalt de Google-Fonts-CSS server-side op, herschrijft de\n * bestandsadressen naar deze route, en serveert de bestanden zelf met een jaar cache. De bezoeker\n * praat zo alleen met de eigen site.\n *\n * Bewust smal (geen open proxy): alleen fonts.googleapis.com-CSS en fonts.gstatic.com-bestanden\n * met een lettertype-extensie.\n */\nconst GSTATIC = \"https://fonts.gstatic.com/\";\n// Google kiest het formaat op de user-agent; met een moderne browser-UA krijgen we woff2.\nconst MODERNE_UA =\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36\";\nconst BESTAND = /^[a-z0-9][a-z0-9/_.-]*\\.(woff2|woff|ttf|otf)$/i;\n\nexport function herschrijfFontCss(css: string, route = FONT_ROUTE): string {\n return css.replace(/https:\\/\\/fonts\\.gstatic\\.com\\/([^)\\s'\"]+)/g, (_, pad: string) => `${route}?f=${encodeURIComponent(pad)}`);\n}\n\nexport async function fontsGET(req: Request): Promise<Response> {\n const url = new URL(req.url);\n const bestand = url.searchParams.get(\"f\");\n if (bestand !== null) {\n if (!BESTAND.test(bestand) || bestand.includes(\"..\")) return new Response(\"Ongeldig lettertype\", { status: 400 });\n const res = await fetch(GSTATIC + bestand);\n if (!res.ok || !res.body) return new Response(\"Niet gevonden\", { status: 404 });\n return new Response(res.body, {\n headers: {\n \"content-type\": res.headers.get(\"content-type\") ?? \"font/woff2\",\n \"cache-control\": \"public, max-age=31536000, immutable\",\n \"access-control-allow-origin\": \"*\",\n },\n });\n }\n const css = url.searchParams.get(\"css\") ?? \"\";\n if (!isGoogleFontUrl(css)) return new Response(\"Alleen Google-Fonts-CSS\", { status: 400 });\n const res = await fetch(css, { headers: { \"user-agent\": MODERNE_UA } });\n if (!res.ok) return new Response(\"Lettertype-CSS niet beschikbaar\", { status: 502 });\n return new Response(herschrijfFontCss(await res.text()), {\n headers: {\n \"content-type\": \"text/css; charset=utf-8\",\n \"cache-control\": \"public, max-age=86400, s-maxage=604800, stale-while-revalidate=2592000\",\n },\n });\n}\n"],"mappings":"AAAA,OAAO;AACP,SAAS,YAAY,uBAAuB;AAW5C,MAAM,UAAU;AAEhB,MAAM,aACJ;AACF,MAAM,UAAU;AAET,SAAS,kBAAkB,KAAa,QAAQ,YAAoB;AACzE,SAAO,IAAI,QAAQ,+CAA+C,CAAC,GAAG,QAAgB,GAAG,KAAK,MAAM,mBAAmB,GAAG,CAAC,EAAE;AAC/H;AAEA,eAAsB,SAAS,KAAiC;AAC9D,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,UAAU,IAAI,aAAa,IAAI,GAAG;AACxC,MAAI,YAAY,MAAM;AACpB,QAAI,CAAC,QAAQ,KAAK,OAAO,KAAK,QAAQ,SAAS,IAAI,EAAG,QAAO,IAAI,SAAS,uBAAuB,EAAE,QAAQ,IAAI,CAAC;AAChH,UAAMA,OAAM,MAAM,MAAM,UAAU,OAAO;AACzC,QAAI,CAACA,KAAI,MAAM,CAACA,KAAI,KAAM,QAAO,IAAI,SAAS,iBAAiB,EAAE,QAAQ,IAAI,CAAC;AAC9E,WAAO,IAAI,SAASA,KAAI,MAAM;AAAA,MAC5B,SAAS;AAAA,QACP,gBAAgBA,KAAI,QAAQ,IAAI,cAAc,KAAK;AAAA,QACnD,iBAAiB;AAAA,QACjB,+BAA+B;AAAA,MACjC;AAAA,IACF,CAAC;AAAA,EACH;AACA,QAAM,MAAM,IAAI,aAAa,IAAI,KAAK,KAAK;AAC3C,MAAI,CAAC,gBAAgB,GAAG,EAAG,QAAO,IAAI,SAAS,2BAA2B,EAAE,QAAQ,IAAI,CAAC;AACzF,QAAM,MAAM,MAAM,MAAM,KAAK,EAAE,SAAS,EAAE,cAAc,WAAW,EAAE,CAAC;AACtE,MAAI,CAAC,IAAI,GAAI,QAAO,IAAI,SAAS,mCAAmC,EAAE,QAAQ,IAAI,CAAC;AACnF,SAAO,IAAI,SAAS,kBAAkB,MAAM,IAAI,KAAK,CAAC,GAAG;AAAA,IACvD,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,IACnB;AAAA,EACF,CAAC;AACH;","names":["res"]}
@@ -15,6 +15,9 @@ export { draftPOST, maakDraftGET } from "./draft.js";
15
15
  export { signPreviewToken, verifyPreviewToken, type PreviewClaims } from "./previewToken.js";
16
16
  export { resolveRedirect } from "./redirects.js";
17
17
  export { versionGET } from "./version.js";
18
+ export { fontsGET, herschrijfFontCss } from "./fonts.js";
19
+ export { cspRapportPOST, leesCspRapport, type CspOvertreding } from "./cspRapport.js";
18
20
  export { pathFromSlugs, listPublishedPages, listPublishedNews, getPublishedPageByPath, getPublishedPageByPathExpanded, getSiteSymbols, expandSymbols, getSiteCustomBlocks, getPublicSiteSettings, getDraftPageById, getDraftPageByPath, type StoredPage, } from "./pages.js";
19
21
  export { getBundle, getBundlePage, getBundleNews, getNewsPageData, type PublishedBundle, type BundleItem, } from "./bundle.js";
20
22
  export { siteLocales, parseLocaleSlugs, localizedPath, localizePage, localizeBlocks, pruneTranslations, TranslationsSchema, type Translations, type LocaleConfig, } from "../i18n/overlay.js";
23
+ export { bedrijfJsonLd } from "../bedrijf/bedrijf.js";
@@ -24,6 +24,8 @@ import { draftPOST, maakDraftGET } from "./draft.js";
24
24
  import { signPreviewToken, verifyPreviewToken } from "./previewToken.js";
25
25
  import { resolveRedirect } from "./redirects.js";
26
26
  import { versionGET } from "./version.js";
27
+ import { fontsGET, herschrijfFontCss } from "./fonts.js";
28
+ import { cspRapportPOST, leesCspRapport } from "./cspRapport.js";
27
29
  import {
28
30
  pathFromSlugs,
29
31
  listPublishedPages,
@@ -52,18 +54,22 @@ import {
52
54
  pruneTranslations,
53
55
  TranslationsSchema
54
56
  } from "../i18n/overlay.js";
57
+ import { bedrijfJsonLd } from "../bedrijf/bedrijf.js";
55
58
  export {
56
59
  TranslationsSchema,
57
60
  articleJsonLd,
58
61
  assertEnv,
62
+ bedrijfJsonLd,
59
63
  breadcrumbListJsonLd,
60
64
  buildJsonLd,
61
65
  buildMetadata,
62
66
  buildRobots,
63
67
  buildSitemap,
64
68
  businessJsonLd,
69
+ cspRapportPOST,
65
70
  draftPOST,
66
71
  expandSymbols,
72
+ fontsGET,
67
73
  galleryJsonLd,
68
74
  getAdminApp,
69
75
  getAdminDb,
@@ -79,7 +85,9 @@ export {
79
85
  getPublishedPageByPathExpanded,
80
86
  getSiteCustomBlocks,
81
87
  getSiteSymbols,
88
+ herschrijfFontCss,
82
89
  jsonLdHtml,
90
+ leesCspRapport,
83
91
  listFormDefinitions,
84
92
  listPublishedNews,
85
93
  listPublishedPages,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\n// `getAdminAuth` zit BEWUST niet hier maar achter de subpath `@xiwebdesign/xicms-core/server/auth`\n// (zie ./auth.ts): firebase-admin/auth sloopt via jwks-rsa/jose élke server-render op Vercel, en\n// hoort dus niet in de bundel van sites die alleen pagina's lezen.\nexport { getAdminApp, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\n// Veilige JSON-LD-serialisatie voor <script type=\"application/ld+json\"> in consumer-routes\n// (escapet `<` zodat klant-tekst met </script> de pagina niet kan breken).\nexport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\n// On-demand revalidation (6.5): POST-handler voor `app/api/revalidate/route.ts` in de klantsite.\n// De backend roept die route na een publicatie aan → wijziging binnen seconden live, zonder rebuild.\n// SSR-seeds (7.12): verzamelt server-side de blockData die de blokken van déze pagina nodig\n// hebben (shop, nieuws, formulieren, zoekindex) — één aanroep in de catch-all, en geen enkel blok\n// hoeft meer per bezoeker uit Firestore te lezen.\nexport { getPageBlockData, type PageBlockDataOptions } from \"./blockData\";\nexport { listFormDefinitions, listSiteMedia } from \"./pages\";\nexport { revalidatePOST } from \"./revalidate\";\n// Preview (Draft Mode): POST-handler voor `app/api/draft/route.ts` in de klantsite. Zet de\n// bypass-cookie zodat een ingelogde editor de content-bundel + ISR-cache overslaat, terwijl de\n// pagina-route voor publiek statisch blijft.\nexport { draftPOST, maakDraftGET } from \"./draft\";\nexport { signPreviewToken, verifyPreviewToken, type PreviewClaims } from \"./previewToken\";\n// Doorverwijzingen (7.24): `resolveRedirect(siteId, path)` in de catch-all vlak vóór notFound().\nexport { resolveRedirect } from \"./redirects\";\n// Versie-endpoint: GET-handler voor `app/api/version/route.ts`, zodat het superbeheer kan zien op\n// welke engine een DEPLOYMENT draait (en niet alleen wanneer er voor het laatst is ingelogd).\nexport { versionGET } from \"./version\";\n// Shop-SSR (6.0): opt-in via subpath `@xiwebdesign/xicms-core/server/shop` (readers + product-SEO).\n// Niet meer onvoorwaardelijk her-geëxporteerd, zodat shop-servercode niet in elke site landt.\nexport {\n pathFromSlugs,\n listPublishedPages,\n listPublishedNews,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getPublicSiteSettings,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Content-bundle (5.7): build-time reader van `content/published.bundle.json`. De pagina-/settings-/\n// symbol-/customBlock-readers hierboven zijn al bundle-first; deze exports zijn voor directe\n// consumptie (bv. nieuws-blockData in de klant-routes).\nexport {\n getBundle,\n getBundlePage,\n getBundleNews,\n getNewsPageData,\n type PublishedBundle,\n type BundleItem,\n} from \"./bundle\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n"],"mappings":"AAIA,OAAO;AAKP,SAAS,aAAa,kBAAkB;AACxC,SAAS,wBAAwB;AAGjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,aAAa,iBAAiC;AAMvD,SAAS,wBAAmD;AAC5D,SAAS,qBAAqB,qBAAqB;AACnD,SAAS,sBAAsB;AAI/B,SAAS,WAAW,oBAAoB;AACxC,SAAS,kBAAkB,0BAA8C;AAEzE,SAAS,uBAAuB;AAGhC,SAAS,kBAAkB;AAG3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
1
+ {"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\n// `getAdminAuth` zit BEWUST niet hier maar achter de subpath `@xiwebdesign/xicms-core/server/auth`\n// (zie ./auth.ts): firebase-admin/auth sloopt via jwks-rsa/jose élke server-render op Vercel, en\n// hoort dus niet in de bundel van sites die alleen pagina's lezen.\nexport { getAdminApp, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\n// Veilige JSON-LD-serialisatie voor <script type=\"application/ld+json\"> in consumer-routes\n// (escapet `<` zodat klant-tekst met </script> de pagina niet kan breken).\nexport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\n// On-demand revalidation (6.5): POST-handler voor `app/api/revalidate/route.ts` in de klantsite.\n// De backend roept die route na een publicatie aan → wijziging binnen seconden live, zonder rebuild.\n// SSR-seeds (7.12): verzamelt server-side de blockData die de blokken van déze pagina nodig\n// hebben (shop, nieuws, formulieren, zoekindex) — één aanroep in de catch-all, en geen enkel blok\n// hoeft meer per bezoeker uit Firestore te lezen.\nexport { getPageBlockData, type PageBlockDataOptions } from \"./blockData\";\nexport { listFormDefinitions, listSiteMedia } from \"./pages\";\nexport { revalidatePOST } from \"./revalidate\";\n// Preview (Draft Mode): POST-handler voor `app/api/draft/route.ts` in de klantsite. Zet de\n// bypass-cookie zodat een ingelogde editor de content-bundel + ISR-cache overslaat, terwijl de\n// pagina-route voor publiek statisch blijft.\nexport { draftPOST, maakDraftGET } from \"./draft\";\nexport { signPreviewToken, verifyPreviewToken, type PreviewClaims } from \"./previewToken\";\n// Doorverwijzingen (7.24): `resolveRedirect(siteId, path)` in de catch-all vlak vóór notFound().\nexport { resolveRedirect } from \"./redirects\";\n// Versie-endpoint: GET-handler voor `app/api/version/route.ts`, zodat het superbeheer kan zien op\n// welke engine een DEPLOYMENT draait (en niet alleen wanneer er voor het laatst is ingelogd).\nexport { versionGET } from \"./version\";\n// 8.3: Google Fonts via de eigen site (geen verbinding met Google vanaf de bezoeker).\nexport { fontsGET, herschrijfFontCss } from \"./fonts\";\n// 8.7: CSP-overtredingen van browsers naar de serverlog (zie xicmsSecurityHeaders in /next).\nexport { cspRapportPOST, leesCspRapport, type CspOvertreding } from \"./cspRapport\";\n// Shop-SSR (6.0): opt-in via subpath `@xiwebdesign/xicms-core/server/shop` (readers + product-SEO).\n// Niet meer onvoorwaardelijk her-geëxporteerd, zodat shop-servercode niet in elke site landt.\nexport {\n pathFromSlugs,\n listPublishedPages,\n listPublishedNews,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getPublicSiteSettings,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Content-bundle (5.7): build-time reader van `content/published.bundle.json`. De pagina-/settings-/\n// symbol-/customBlock-readers hierboven zijn al bundle-first; deze exports zijn voor directe\n// consumptie (bv. nieuws-blockData in de klant-routes).\nexport {\n getBundle,\n getBundlePage,\n getBundleNews,\n getNewsPageData,\n type PublishedBundle,\n type BundleItem,\n} from \"./bundle\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n\n// 8.13: LocalBusiness-JSON-LD uit het bedrijfsprofiel (Instellingen → Bedrijf).\nexport { bedrijfJsonLd } from \"../bedrijf/bedrijf\";\n"],"mappings":"AAIA,OAAO;AAKP,SAAS,aAAa,kBAAkB;AACxC,SAAS,wBAAwB;AAGjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,aAAa,iBAAiC;AAMvD,SAAS,wBAAmD;AAC5D,SAAS,qBAAqB,qBAAqB;AACnD,SAAS,sBAAsB;AAI/B,SAAS,WAAW,oBAAoB;AACxC,SAAS,kBAAkB,0BAA8C;AAEzE,SAAS,uBAAuB;AAGhC,SAAS,kBAAkB;AAE3B,SAAS,UAAU,yBAAyB;AAE5C,SAAS,gBAAgB,sBAA2C;AAGpE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAGP,SAAS,qBAAqB;","names":[]}
@@ -23,7 +23,15 @@ const OPTIONS = {
23
23
  "h4",
24
24
  "code",
25
25
  "pre",
26
- "hr"
26
+ "hr",
27
+ "table",
28
+ "thead",
29
+ "tbody",
30
+ "tfoot",
31
+ "tr",
32
+ "th",
33
+ "td",
34
+ "caption"
27
35
  ],
28
36
  allowedAttributes: {
29
37
  a: ["href", "target", "rel"],
@@ -31,7 +39,10 @@ const OPTIONS = {
31
39
  img: ["src", "alt", "title", "width", "height", "loading", "decoding"],
32
40
  // `span` mag uitsluitend `style` dragen — en zelfs dat alleen voor de font-size-preset (zie
33
41
  // `allowedStyles` hieronder). Geen andere attributen (geen class/id/data-*).
34
- span: ["style"]
42
+ span: ["style"],
43
+ // Tabellen: alleen celspanning, geen style/class (opmaak komt uit .xicms-richtext in styles.css).
44
+ th: ["colspan", "rowspan"],
45
+ td: ["colspan", "rowspan"]
35
46
  },
36
47
  allowedStyles: {
37
48
  span: { "font-size": [ALLOWED_FONT_SIZES] }
@@ -52,6 +63,8 @@ const OPTIONS = {
52
63
  // Afbeeldingen lazy + async laten laden voor performance.
53
64
  img: (tagName, attribs) => ({ tagName, attribs: { ...attribs, loading: "lazy", decoding: "async" } })
54
65
  },
66
+ // 8.10: lege koppen weg (zie de spiegel in functions/src/lib/sanitize.ts).
67
+ exclusiveFilter: (frame) => /^h[2-4]$/.test(frame.tag) && !frame.text.trim() && !frame.mediaChildren?.length,
55
68
  disallowedTagsMode: "discard"
56
69
  };
57
70
  function sanitizeRichText(html) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/sanitize.ts"],"sourcesContent":["import \"server-only\";\n\nimport sanitizeHtml from \"sanitize-html\";\n\n/**\n * Server-side sanitization van rich text (TipTap-output) met een strikte allowlist.\n * Dit draait bij elke write/publish, zodat opgeslagen HTML gegarandeerd veilig te renderen is\n * (de RichText-block rendert met dangerouslySetInnerHTML). Nooit op vertrouwen van de client.\n */\n// Lettergrootte (RichTextEditor.tsx, FONT_SIZES): vaste preset-waarden, geen vrij invoerveld — de\n// regex staat UITSLUITEND deze exacte strings toe (geen url()/expression()/willekeurige CSS).\nconst ALLOWED_FONT_SIZES = /^(0\\.85|1\\.25|1\\.5)rem$/;\n\nconst OPTIONS: sanitizeHtml.IOptions = {\n allowedTags: [\n \"p\", \"br\", \"strong\", \"b\", \"em\", \"i\", \"u\", \"s\", \"a\", \"img\", \"span\",\n \"ul\", \"ol\", \"li\", \"blockquote\", \"h2\", \"h3\", \"h4\", \"code\", \"pre\", \"hr\",\n ],\n allowedAttributes: {\n a: [\"href\", \"target\", \"rel\"],\n // Afbeeldingen in rich text (WYSIWYG): alleen veilige presentatie-attributen, geen on*-handlers.\n img: [\"src\", \"alt\", \"title\", \"width\", \"height\", \"loading\", \"decoding\"],\n // `span` mag uitsluitend `style` dragen — en zelfs dat alleen voor de font-size-preset (zie\n // `allowedStyles` hieronder). Geen andere attributen (geen class/id/data-*).\n span: [\"style\"],\n },\n allowedStyles: {\n span: { \"font-size\": [ALLOWED_FONT_SIZES] },\n },\n // Geen data:-URI's voor afbeeldingen (alleen http/https + relatieve paden naar onze eigen Storage).\n allowedSchemesByTag: { img: [\"http\", \"https\"] },\n allowedSchemes: [\"http\", \"https\", \"mailto\", \"tel\"],\n transformTags: {\n // Externe links altijd veilig openen.\n a: (tagName, attribs) => ({\n tagName,\n attribs: {\n ...attribs,\n rel: \"noopener noreferrer nofollow\",\n ...(attribs.target === \"_blank\" ? { target: \"_blank\" } : {}),\n },\n }),\n // Afbeeldingen lazy + async laten laden voor performance.\n img: (tagName, attribs) => ({ tagName, attribs: { ...attribs, loading: \"lazy\", decoding: \"async\" } }),\n },\n disallowedTagsMode: \"discard\",\n};\n\nexport function sanitizeRichText(html: string): string {\n return sanitizeHtml(html ?? \"\", OPTIONS);\n}\n"],"mappings":"AAAA,OAAO;AAEP,OAAO,kBAAkB;AASzB,MAAM,qBAAqB;AAE3B,MAAM,UAAiC;AAAA,EACrC,aAAa;AAAA,IACX;AAAA,IAAK;AAAA,IAAM;AAAA,IAAU;AAAA,IAAK;AAAA,IAAM;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAO;AAAA,IAC3D;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAc;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAQ;AAAA,IAAO;AAAA,EACnE;AAAA,EACA,mBAAmB;AAAA,IACjB,GAAG,CAAC,QAAQ,UAAU,KAAK;AAAA;AAAA,IAE3B,KAAK,CAAC,OAAO,OAAO,SAAS,SAAS,UAAU,WAAW,UAAU;AAAA;AAAA;AAAA,IAGrE,MAAM,CAAC,OAAO;AAAA,EAChB;AAAA,EACA,eAAe;AAAA,IACb,MAAM,EAAE,aAAa,CAAC,kBAAkB,EAAE;AAAA,EAC5C;AAAA;AAAA,EAEA,qBAAqB,EAAE,KAAK,CAAC,QAAQ,OAAO,EAAE;AAAA,EAC9C,gBAAgB,CAAC,QAAQ,SAAS,UAAU,KAAK;AAAA,EACjD,eAAe;AAAA;AAAA,IAEb,GAAG,CAAC,SAAS,aAAa;AAAA,MACxB;AAAA,MACA,SAAS;AAAA,QACP,GAAG;AAAA,QACH,KAAK;AAAA,QACL,GAAI,QAAQ,WAAW,WAAW,EAAE,QAAQ,SAAS,IAAI,CAAC;AAAA,MAC5D;AAAA,IACF;AAAA;AAAA,IAEA,KAAK,CAAC,SAAS,aAAa,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,SAAS,QAAQ,UAAU,QAAQ,EAAE;AAAA,EACrG;AAAA,EACA,oBAAoB;AACtB;AAEO,SAAS,iBAAiB,MAAsB;AACrD,SAAO,aAAa,QAAQ,IAAI,OAAO;AACzC;","names":[]}
1
+ {"version":3,"sources":["../../src/server/sanitize.ts"],"sourcesContent":["import \"server-only\";\n\nimport sanitizeHtml from \"sanitize-html\";\n\n/**\n * Server-side sanitization van rich text (TipTap-output) met een strikte allowlist.\n * Dit draait bij elke write/publish, zodat opgeslagen HTML gegarandeerd veilig te renderen is\n * (de RichText-block rendert met dangerouslySetInnerHTML). Nooit op vertrouwen van de client.\n */\n// Lettergrootte (RichTextEditor.tsx, FONT_SIZES): vaste preset-waarden, geen vrij invoerveld — de\n// regex staat UITSLUITEND deze exacte strings toe (geen url()/expression()/willekeurige CSS).\nconst ALLOWED_FONT_SIZES = /^(0\\.85|1\\.25|1\\.5)rem$/;\n\nconst OPTIONS: sanitizeHtml.IOptions = {\n allowedTags: [\n \"p\", \"br\", \"strong\", \"b\", \"em\", \"i\", \"u\", \"s\", \"a\", \"img\", \"span\",\n \"ul\", \"ol\", \"li\", \"blockquote\", \"h2\", \"h3\", \"h4\", \"code\", \"pre\", \"hr\",\n \"table\", \"thead\", \"tbody\", \"tfoot\", \"tr\", \"th\", \"td\", \"caption\",\n ],\n allowedAttributes: {\n a: [\"href\", \"target\", \"rel\"],\n // Afbeeldingen in rich text (WYSIWYG): alleen veilige presentatie-attributen, geen on*-handlers.\n img: [\"src\", \"alt\", \"title\", \"width\", \"height\", \"loading\", \"decoding\"],\n // `span` mag uitsluitend `style` dragen — en zelfs dat alleen voor de font-size-preset (zie\n // `allowedStyles` hieronder). Geen andere attributen (geen class/id/data-*).\n span: [\"style\"],\n // Tabellen: alleen celspanning, geen style/class (opmaak komt uit .xicms-richtext in styles.css).\n th: [\"colspan\", \"rowspan\"],\n td: [\"colspan\", \"rowspan\"],\n },\n allowedStyles: {\n span: { \"font-size\": [ALLOWED_FONT_SIZES] },\n },\n // Geen data:-URI's voor afbeeldingen (alleen http/https + relatieve paden naar onze eigen Storage).\n allowedSchemesByTag: { img: [\"http\", \"https\"] },\n allowedSchemes: [\"http\", \"https\", \"mailto\", \"tel\"],\n transformTags: {\n // Externe links altijd veilig openen.\n a: (tagName, attribs) => ({\n tagName,\n attribs: {\n ...attribs,\n rel: \"noopener noreferrer nofollow\",\n ...(attribs.target === \"_blank\" ? { target: \"_blank\" } : {}),\n },\n }),\n // Afbeeldingen lazy + async laten laden voor performance.\n img: (tagName, attribs) => ({ tagName, attribs: { ...attribs, loading: \"lazy\", decoding: \"async\" } }),\n },\n // 8.10: lege koppen weg (zie de spiegel in functions/src/lib/sanitize.ts).\n exclusiveFilter: (frame) => /^h[2-4]$/.test(frame.tag) && !frame.text.trim() && !frame.mediaChildren?.length,\n disallowedTagsMode: \"discard\",\n};\n\nexport function sanitizeRichText(html: string): string {\n return sanitizeHtml(html ?? \"\", OPTIONS);\n}\n"],"mappings":"AAAA,OAAO;AAEP,OAAO,kBAAkB;AASzB,MAAM,qBAAqB;AAE3B,MAAM,UAAiC;AAAA,EACrC,aAAa;AAAA,IACX;AAAA,IAAK;AAAA,IAAM;AAAA,IAAU;AAAA,IAAK;AAAA,IAAM;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAK;AAAA,IAAO;AAAA,IAC3D;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAc;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAQ;AAAA,IAAO;AAAA,IACjE;AAAA,IAAS;AAAA,IAAS;AAAA,IAAS;AAAA,IAAS;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,EACxD;AAAA,EACA,mBAAmB;AAAA,IACjB,GAAG,CAAC,QAAQ,UAAU,KAAK;AAAA;AAAA,IAE3B,KAAK,CAAC,OAAO,OAAO,SAAS,SAAS,UAAU,WAAW,UAAU;AAAA;AAAA;AAAA,IAGrE,MAAM,CAAC,OAAO;AAAA;AAAA,IAEd,IAAI,CAAC,WAAW,SAAS;AAAA,IACzB,IAAI,CAAC,WAAW,SAAS;AAAA,EAC3B;AAAA,EACA,eAAe;AAAA,IACb,MAAM,EAAE,aAAa,CAAC,kBAAkB,EAAE;AAAA,EAC5C;AAAA;AAAA,EAEA,qBAAqB,EAAE,KAAK,CAAC,QAAQ,OAAO,EAAE;AAAA,EAC9C,gBAAgB,CAAC,QAAQ,SAAS,UAAU,KAAK;AAAA,EACjD,eAAe;AAAA;AAAA,IAEb,GAAG,CAAC,SAAS,aAAa;AAAA,MACxB;AAAA,MACA,SAAS;AAAA,QACP,GAAG;AAAA,QACH,KAAK;AAAA,QACL,GAAI,QAAQ,WAAW,WAAW,EAAE,QAAQ,SAAS,IAAI,CAAC;AAAA,MAC5D;AAAA,IACF;AAAA;AAAA,IAEA,KAAK,CAAC,SAAS,aAAa,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,SAAS,QAAQ,UAAU,QAAQ,EAAE;AAAA,EACrG;AAAA;AAAA,EAEA,iBAAiB,CAAC,UAAU,WAAW,KAAK,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,KAAK,KAAK,CAAC,MAAM,eAAe;AAAA,EACtG,oBAAoB;AACtB;AAEO,SAAS,iBAAiB,MAAsB;AACrD,SAAO,aAAa,QAAQ,IAAI,OAAO;AACzC;","names":[]}
@@ -96,7 +96,10 @@ function useMergedSiteConfig() {
96
96
  name: settings.name ?? base.name,
97
97
  nav,
98
98
  theme: mergeThemeTokens(base.theme, settings.theme),
99
- seoDefaults: settings.seoDefaults ?? base.seoDefaults
99
+ seoDefaults: settings.seoDefaults ?? base.seoDefaults,
100
+ // 8.13: het profiel uit /beheer wint; de site-config is de startwaarde.
101
+ business: settings.business ?? base.business,
102
+ header: settings.header ?? base.header
100
103
  };
101
104
  }, [base, settings]);
102
105
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/settings/useSiteSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { useSiteConfig } from \"../config\";\nimport {\n PublicSiteSettingsSchema,\n mergeThemeTokens,\n type PublicSiteSettings,\n} from \"../schema/settings\";\nimport type { SiteConfig } from \"../schema/siteConfig\";\n\n/**\n * Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de\n * klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.\n *\n * - Met SSR-seed (`initial`, via XiCMSProvider `settingsData`): géén client-read voor publiek —\n * de settings zitten al in de server-HTML (geen theme-flits) en verversen via ISR.\n * - Zonder seed (publiek): één `getDoc` (geen per-bezoeker listener).\n * - Editors: `useSiteSettingsLiveSync` (onSnapshot) zodat /beheer-wijzigingen meteen zichtbaar zijn.\n * Zonder Firebase of zonder doc valt alles terug op de statische `SiteConfig` (identiek aan vroeger).\n */\ninterface Ctx {\n settings: PublicSiteSettings;\n loaded: boolean;\n /** Interne setter voor de editor-live-sync (useSiteSettingsLiveSync). */\n _set?: (s: PublicSiteSettings) => void;\n}\nconst SettingsContext = createContext<Ctx>({ settings: {}, loaded: false });\n\nexport function SiteSettingsProvider({\n siteId,\n live = false,\n initial,\n children,\n}: {\n siteId: string;\n live?: boolean;\n /** SSR-startset (server-side gelezen via `getPublicSiteSettings`); slaat de client-read over. */\n initial?: PublicSiteSettings;\n children: ReactNode;\n}) {\n const hasInitial = initial !== undefined;\n const [settings, setSettings] = useState<PublicSiteSettings>(initial ?? {});\n const [loaded, setLoaded] = useState(hasInitial);\n\n useEffect(() => {\n // Met een SSR-seed doet de publieke site géén client-read meer: de layout-route is ISR-gecached\n // en levert verse settings per revalidate; editors krijgen apart een live sync.\n if (hasInitial && !live) return;\n const apply = (data: unknown) => {\n const res = PublicSiteSettingsSchema.safeParse(data ?? {});\n if (res.success) setSettings(res.data);\n setLoaded(true);\n };\n // Firestore DYNAMISCH: met een SSR-seed komt de publieke site hier niet eens, en een bezoeker\n // hoort de SDK nooit te downloaden. Statisch importeren zette hem in de bundel van elke pagina.\n let cancelled = false;\n let unsub: (() => void) | undefined;\n void (async () => {\n const [{ getDb }, { doc, getDoc, onSnapshot }] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || cancelled) {\n setLoaded(true);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"settings\", \"public\");\n if (live) {\n unsub = onSnapshot(ref, (snap) => apply(snap.exists() ? snap.data() : {}), () => setLoaded(true));\n return;\n }\n try {\n const snap = await getDoc(ref);\n if (!cancelled) apply(snap.exists() ? snap.data() : {});\n } catch {\n if (!cancelled) setLoaded(true);\n }\n })();\n return () => {\n cancelled = true;\n unsub?.();\n };\n }, [siteId, live, hasInitial]);\n\n const value = useMemo<Ctx>(() => ({ settings, loaded, _set: setSettings }), [settings, loaded]);\n return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;\n}\n\n/** Ruwe publieke settings + laadstatus. */\nexport function useSiteSettings(): Ctx {\n return useContext(SettingsContext);\n}\n\n/**\n * Editor-live-sync: onSnapshot op `settings/public` zolang `enabled` (ingelogde editor). Aparte\n * hook (i.p.v. `live` op de provider) zodat publiek nooit een listener krijgt — zelfde patroon\n * als CustomBlocksSync in XiCMSProvider. Een leesfout laat de bestaande settings staan.\n */\nexport function useSiteSettingsLiveSync(siteId: string, enabled: boolean) {\n const { _set } = useContext(SettingsContext);\n useEffect(() => {\n // Alleen editors (enabled) krijgen een listener, en pas dán laadt de SDK.\n if (!enabled || !_set) return;\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { doc, onSnapshot }] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) return;\n unsub = onSnapshot(\n doc(db, \"sites\", siteId, \"settings\", \"public\"),\n (snap) => {\n const res = PublicSiteSettingsSchema.safeParse(snap.exists() ? snap.data() : {});\n if (res.success) _set(res.data);\n },\n () => undefined,\n );\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [enabled, siteId, _set]);\n}\n\n/** SiteConfig met de live Firestore-settings eroverheen (Firestore wint waar gezet). */\nexport function useMergedSiteConfig(): SiteConfig {\n const base = useSiteConfig();\n const { settings } = useSiteSettings();\n return useMemo(() => {\n // AANWEZIG wint, ook als de lijst leeg is. Dat was `length > 0`, en dan kon je het menu niet\n // leegmaken: sla je een leeg menu op, dan kwam de nav uit `site.config.ts` gewoon terug. Het\n // verschil dat telt is \"heeft iemand hier een keuze gemaakt\", niet \"staat er iets in\".\n const nav = settings.nav ?? base.nav;\n return {\n ...base,\n name: settings.name ?? base.name,\n nav,\n theme: mergeThemeTokens(base.theme, settings.theme),\n seoDefaults: settings.seoDefaults ?? base.seoDefaults,\n };\n }, [base, settings]);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/settings/useSiteSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { useSiteConfig } from \"../config\";\nimport {\n PublicSiteSettingsSchema,\n mergeThemeTokens,\n type PublicSiteSettings,\n} from \"../schema/settings\";\nimport type { SiteConfig } from \"../schema/siteConfig\";\n\n/**\n * Live, publiek-veilige site-instellingen (`sites/{siteId}/settings/public`) zodat dingen die de\n * klant in /beheer wijzigt (footer, nav, thema, module-instellingen) direct op de site verschijnen.\n *\n * - Met SSR-seed (`initial`, via XiCMSProvider `settingsData`): géén client-read voor publiek —\n * de settings zitten al in de server-HTML (geen theme-flits) en verversen via ISR.\n * - Zonder seed (publiek): één `getDoc` (geen per-bezoeker listener).\n * - Editors: `useSiteSettingsLiveSync` (onSnapshot) zodat /beheer-wijzigingen meteen zichtbaar zijn.\n * Zonder Firebase of zonder doc valt alles terug op de statische `SiteConfig` (identiek aan vroeger).\n */\ninterface Ctx {\n settings: PublicSiteSettings;\n loaded: boolean;\n /** Interne setter voor de editor-live-sync (useSiteSettingsLiveSync). */\n _set?: (s: PublicSiteSettings) => void;\n}\nconst SettingsContext = createContext<Ctx>({ settings: {}, loaded: false });\n\nexport function SiteSettingsProvider({\n siteId,\n live = false,\n initial,\n children,\n}: {\n siteId: string;\n live?: boolean;\n /** SSR-startset (server-side gelezen via `getPublicSiteSettings`); slaat de client-read over. */\n initial?: PublicSiteSettings;\n children: ReactNode;\n}) {\n const hasInitial = initial !== undefined;\n const [settings, setSettings] = useState<PublicSiteSettings>(initial ?? {});\n const [loaded, setLoaded] = useState(hasInitial);\n\n useEffect(() => {\n // Met een SSR-seed doet de publieke site géén client-read meer: de layout-route is ISR-gecached\n // en levert verse settings per revalidate; editors krijgen apart een live sync.\n if (hasInitial && !live) return;\n const apply = (data: unknown) => {\n const res = PublicSiteSettingsSchema.safeParse(data ?? {});\n if (res.success) setSettings(res.data);\n setLoaded(true);\n };\n // Firestore DYNAMISCH: met een SSR-seed komt de publieke site hier niet eens, en een bezoeker\n // hoort de SDK nooit te downloaden. Statisch importeren zette hem in de bundel van elke pagina.\n let cancelled = false;\n let unsub: (() => void) | undefined;\n void (async () => {\n const [{ getDb }, { doc, getDoc, onSnapshot }] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || cancelled) {\n setLoaded(true);\n return;\n }\n const ref = doc(db, \"sites\", siteId, \"settings\", \"public\");\n if (live) {\n unsub = onSnapshot(ref, (snap) => apply(snap.exists() ? snap.data() : {}), () => setLoaded(true));\n return;\n }\n try {\n const snap = await getDoc(ref);\n if (!cancelled) apply(snap.exists() ? snap.data() : {});\n } catch {\n if (!cancelled) setLoaded(true);\n }\n })();\n return () => {\n cancelled = true;\n unsub?.();\n };\n }, [siteId, live, hasInitial]);\n\n const value = useMemo<Ctx>(() => ({ settings, loaded, _set: setSettings }), [settings, loaded]);\n return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;\n}\n\n/** Ruwe publieke settings + laadstatus. */\nexport function useSiteSettings(): Ctx {\n return useContext(SettingsContext);\n}\n\n/**\n * Editor-live-sync: onSnapshot op `settings/public` zolang `enabled` (ingelogde editor). Aparte\n * hook (i.p.v. `live` op de provider) zodat publiek nooit een listener krijgt — zelfde patroon\n * als CustomBlocksSync in XiCMSProvider. Een leesfout laat de bestaande settings staan.\n */\nexport function useSiteSettingsLiveSync(siteId: string, enabled: boolean) {\n const { _set } = useContext(SettingsContext);\n useEffect(() => {\n // Alleen editors (enabled) krijgen een listener, en pas dán laadt de SDK.\n if (!enabled || !_set) return;\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const [{ getDb }, { doc, onSnapshot }] = await Promise.all([\n import(\"../firebase/client\"),\n import(\"firebase/firestore\"),\n ]);\n const db = getDb();\n if (!db || afgebroken) return;\n unsub = onSnapshot(\n doc(db, \"sites\", siteId, \"settings\", \"public\"),\n (snap) => {\n const res = PublicSiteSettingsSchema.safeParse(snap.exists() ? snap.data() : {});\n if (res.success) _set(res.data);\n },\n () => undefined,\n );\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [enabled, siteId, _set]);\n}\n\n/** SiteConfig met de live Firestore-settings eroverheen (Firestore wint waar gezet). */\nexport function useMergedSiteConfig(): SiteConfig {\n const base = useSiteConfig();\n const { settings } = useSiteSettings();\n return useMemo(() => {\n // AANWEZIG wint, ook als de lijst leeg is. Dat was `length > 0`, en dan kon je het menu niet\n // leegmaken: sla je een leeg menu op, dan kwam de nav uit `site.config.ts` gewoon terug. Het\n // verschil dat telt is \"heeft iemand hier een keuze gemaakt\", niet \"staat er iets in\".\n const nav = settings.nav ?? base.nav;\n return {\n ...base,\n name: settings.name ?? base.name,\n nav,\n theme: mergeThemeTokens(base.theme, settings.theme),\n seoDefaults: settings.seoDefaults ?? base.seoDefaults,\n // 8.13: het profiel uit /beheer wint; de site-config is de startwaarde.\n business: settings.business ?? base.business,\n header: settings.header ?? base.header,\n };\n }, [base, settings]);\n}\n"],"mappings":"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","names":[]}