@xleddyl/nuxt-cms 0.9.4 → 0.10.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 (431) hide show
  1. package/README.md +1 -1
  2. package/dist/module.d.mts +20 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +1037 -148
  5. package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +1 -1
  6. package/dist/runtime/app/blocks/CmsBlocks.vue +57 -9
  7. package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +1 -1
  8. package/dist/runtime/app/blocks/CmsField.d.ts +24 -0
  9. package/dist/runtime/app/blocks/CmsField.js +16 -0
  10. package/dist/runtime/app/blocks/node.d.ts +37 -0
  11. package/dist/runtime/app/blocks/node.js +17 -0
  12. package/dist/runtime/app/blocks/resolve.d.ts +18 -0
  13. package/dist/runtime/app/blocks/resolve.js +34 -0
  14. package/dist/runtime/app/components/cms/BlockPicker.d.vue.ts +22 -0
  15. package/dist/runtime/app/components/cms/BlockPicker.vue +87 -0
  16. package/dist/runtime/app/components/cms/BlockPicker.vue.d.ts +22 -0
  17. package/dist/runtime/app/components/cms/BlockPickerPopover.d.vue.ts +38 -0
  18. package/dist/runtime/app/components/cms/BlockPickerPopover.vue +44 -0
  19. package/dist/runtime/app/components/cms/BlockPickerPopover.vue.d.ts +38 -0
  20. package/dist/runtime/app/components/cms/BlockTree.d.vue.ts +29 -0
  21. package/dist/runtime/app/components/cms/BlockTree.vue +322 -0
  22. package/dist/runtime/app/components/cms/BlockTree.vue.d.ts +29 -0
  23. package/dist/runtime/app/components/cms/BlocksField.vue +569 -218
  24. package/dist/runtime/app/components/cms/BrandMark.vue +7 -13
  25. package/dist/runtime/app/components/cms/ConfirmGroups.vue +15 -13
  26. package/dist/runtime/app/components/cms/{BlockPalette.vue.d.ts → EditorHistory.d.vue.ts} +6 -5
  27. package/dist/runtime/app/components/cms/EditorHistory.vue +34 -0
  28. package/dist/runtime/app/components/cms/{BlockPalette.d.vue.ts → EditorHistory.vue.d.ts} +6 -5
  29. package/dist/runtime/app/components/cms/EntryDrawer.d.vue.ts +11 -5
  30. package/dist/runtime/app/components/cms/EntryDrawer.vue +183 -152
  31. package/dist/runtime/app/components/cms/EntryDrawer.vue.d.ts +11 -5
  32. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +16 -4
  33. package/dist/runtime/app/components/cms/EntryForm.vue +106 -146
  34. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +16 -4
  35. package/dist/runtime/app/components/cms/FieldInput.vue +10 -2
  36. package/dist/runtime/app/components/cms/FormSections.d.vue.ts +22 -0
  37. package/dist/runtime/app/components/cms/FormSections.vue +71 -0
  38. package/dist/runtime/app/components/cms/FormSections.vue.d.ts +22 -0
  39. package/dist/runtime/app/components/cms/ImportModal.vue +46 -58
  40. package/dist/runtime/app/components/cms/LinkField.d.vue.ts +16 -0
  41. package/dist/runtime/app/components/cms/LinkField.vue +250 -0
  42. package/dist/runtime/app/components/cms/LinkField.vue.d.ts +16 -0
  43. package/dist/runtime/app/components/cms/ListFilters.vue +10 -12
  44. package/dist/runtime/app/components/cms/LocaleSwitch.d.vue.ts +2 -1
  45. package/dist/runtime/app/components/cms/LocaleSwitch.vue +29 -5
  46. package/dist/runtime/app/components/cms/LocaleSwitch.vue.d.ts +2 -1
  47. package/dist/runtime/app/components/cms/MediaDetail.d.vue.ts +2 -0
  48. package/dist/runtime/app/components/cms/MediaDetail.vue +112 -83
  49. package/dist/runtime/app/components/cms/MediaDetail.vue.d.ts +2 -0
  50. package/dist/runtime/app/components/cms/MediaField.vue +35 -25
  51. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +26 -71
  52. package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +1 -0
  53. package/dist/runtime/app/components/cms/MediaGallery.vue +442 -419
  54. package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +1 -0
  55. package/dist/runtime/app/components/cms/MediaThumb.vue +9 -25
  56. package/dist/runtime/app/components/cms/MediaUpload.vue +1 -1
  57. package/dist/runtime/app/components/cms/{BlockOutline.d.vue.ts → NestedBlockList.d.vue.ts} +5 -5
  58. package/dist/runtime/app/components/cms/NestedBlockList.vue +87 -0
  59. package/dist/runtime/app/components/cms/{BlockOutline.vue.d.ts → NestedBlockList.vue.d.ts} +5 -5
  60. package/dist/runtime/app/components/cms/PoweredBy.vue +12 -5
  61. package/dist/runtime/app/components/cms/PresetModal.d.vue.ts +18 -0
  62. package/dist/runtime/app/components/cms/PresetModal.vue +70 -0
  63. package/dist/runtime/app/components/cms/PresetModal.vue.d.ts +18 -0
  64. package/dist/runtime/app/components/cms/RelationField.vue +2 -8
  65. package/dist/runtime/app/components/cms/SettingsAccount.vue +60 -71
  66. package/dist/runtime/app/components/cms/SettingsColumns.vue +47 -69
  67. package/dist/runtime/app/components/cms/SettingsEmptyAdd.d.vue.ts +13 -0
  68. package/dist/runtime/app/components/cms/SettingsEmptyAdd.vue +21 -0
  69. package/dist/runtime/app/components/cms/SettingsEmptyAdd.vue.d.ts +13 -0
  70. package/dist/runtime/app/components/cms/SettingsEntries.d.vue.ts +13 -0
  71. package/dist/runtime/app/components/cms/SettingsEntries.vue +50 -0
  72. package/dist/runtime/app/components/cms/SettingsEntries.vue.d.ts +13 -0
  73. package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +1 -10
  74. package/dist/runtime/app/components/cms/SettingsEntry.vue +11 -95
  75. package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +1 -10
  76. package/dist/runtime/app/components/cms/SettingsEntryActions.d.vue.ts +7 -0
  77. package/dist/runtime/app/components/cms/SettingsEntryActions.vue +42 -0
  78. package/dist/runtime/app/components/cms/SettingsEntryActions.vue.d.ts +7 -0
  79. package/dist/runtime/app/components/cms/SettingsEntryTabs.d.vue.ts +7 -0
  80. package/dist/runtime/app/components/cms/SettingsEntryTabs.vue +21 -0
  81. package/dist/runtime/app/components/cms/SettingsEntryTabs.vue.d.ts +7 -0
  82. package/dist/runtime/app/components/cms/SettingsFormAction.d.vue.ts +3 -0
  83. package/dist/runtime/app/components/cms/SettingsFormAction.vue +17 -0
  84. package/dist/runtime/app/components/cms/SettingsFormAction.vue.d.ts +3 -0
  85. package/dist/runtime/app/components/cms/SettingsLayout.vue +18 -14
  86. package/dist/runtime/app/components/cms/SettingsPresetForm.d.vue.ts +11 -0
  87. package/dist/runtime/app/components/cms/SettingsPresetForm.vue +60 -0
  88. package/dist/runtime/app/components/cms/SettingsPresetForm.vue.d.ts +11 -0
  89. package/dist/runtime/app/components/cms/SettingsPresets.d.vue.ts +13 -0
  90. package/dist/runtime/app/components/cms/SettingsPresets.vue +65 -0
  91. package/dist/runtime/app/components/cms/SettingsPresets.vue.d.ts +13 -0
  92. package/dist/runtime/app/components/cms/SettingsRecordActions.d.vue.ts +32 -0
  93. package/dist/runtime/app/components/cms/SettingsRecordActions.vue +45 -0
  94. package/dist/runtime/app/components/cms/SettingsRecordActions.vue.d.ts +32 -0
  95. package/dist/runtime/app/components/cms/SettingsRedirectActions.d.vue.ts +3 -0
  96. package/dist/runtime/app/components/cms/SettingsRedirectActions.vue +59 -0
  97. package/dist/runtime/app/components/cms/SettingsRedirectActions.vue.d.ts +3 -0
  98. package/dist/runtime/app/components/cms/SettingsRedirectForm.d.vue.ts +11 -0
  99. package/dist/runtime/app/components/cms/SettingsRedirectForm.vue +77 -0
  100. package/dist/runtime/app/components/cms/SettingsRedirectForm.vue.d.ts +11 -0
  101. package/dist/runtime/app/components/cms/SettingsRedirects.d.vue.ts +13 -0
  102. package/dist/runtime/app/components/cms/SettingsRedirects.vue +117 -0
  103. package/dist/runtime/app/components/cms/SettingsRedirects.vue.d.ts +13 -0
  104. package/dist/runtime/app/components/cms/SettingsSecretCard.d.vue.ts +17 -0
  105. package/dist/runtime/app/components/cms/SettingsSecretCard.vue +43 -0
  106. package/dist/runtime/app/components/cms/SettingsSecretCard.vue.d.ts +17 -0
  107. package/dist/runtime/app/components/cms/SettingsSite.d.vue.ts +3 -0
  108. package/dist/runtime/app/components/cms/SettingsSite.vue +140 -0
  109. package/dist/runtime/app/components/cms/SettingsSite.vue.d.ts +3 -0
  110. package/dist/runtime/app/components/cms/SettingsUserCreate.d.vue.ts +8 -0
  111. package/dist/runtime/app/components/cms/SettingsUserCreate.vue +82 -0
  112. package/dist/runtime/app/components/cms/SettingsUserCreate.vue.d.ts +8 -0
  113. package/dist/runtime/app/components/cms/SettingsUserDetail.d.vue.ts +11 -0
  114. package/dist/runtime/app/components/cms/SettingsUserDetail.vue +179 -0
  115. package/dist/runtime/app/components/cms/SettingsUserDetail.vue.d.ts +11 -0
  116. package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +9 -6
  117. package/dist/runtime/app/components/cms/SettingsUsers.vue +88 -234
  118. package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +9 -6
  119. package/dist/runtime/app/components/cms/SharePreview.d.vue.ts +18 -0
  120. package/dist/runtime/app/components/cms/SharePreview.vue +219 -0
  121. package/dist/runtime/app/components/cms/SharePreview.vue.d.ts +18 -0
  122. package/dist/runtime/app/components/cms/SlugField.vue +5 -3
  123. package/dist/runtime/app/components/cms/StatusBadge.vue +9 -8
  124. package/dist/runtime/app/components/cms/TranslationBadges.d.vue.ts +7 -0
  125. package/dist/runtime/app/components/cms/TranslationBadges.vue +45 -0
  126. package/dist/runtime/app/components/cms/TranslationBadges.vue.d.ts +7 -0
  127. package/dist/runtime/app/components/cms/TranslationMenu.d.vue.ts +17 -0
  128. package/dist/runtime/app/components/cms/TranslationMenu.vue +28 -0
  129. package/dist/runtime/app/components/cms/TranslationMenu.vue.d.ts +17 -0
  130. package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +22 -4
  131. package/dist/runtime/app/components/cms/VisualEditor.vue +1152 -335
  132. package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +22 -4
  133. package/dist/runtime/app/components/cms/WelcomeModal.vue +32 -50
  134. package/dist/runtime/app/composables/cms-clipboard.d.ts +8 -0
  135. package/dist/runtime/app/composables/cms-clipboard.js +38 -0
  136. package/dist/runtime/app/composables/cms-confirm.d.ts +6 -8
  137. package/dist/runtime/app/composables/cms-confirm.js +5 -7
  138. package/dist/runtime/app/composables/cms-entry-disabled.d.ts +5 -1
  139. package/dist/runtime/app/composables/cms-entry-disabled.js +7 -0
  140. package/dist/runtime/app/composables/cms-entry-form.d.ts +15 -0
  141. package/dist/runtime/app/composables/cms-entry-form.js +40 -0
  142. package/dist/runtime/app/composables/cms-entry-settings.d.ts +30 -0
  143. package/dist/runtime/app/composables/cms-entry-settings.js +122 -0
  144. package/dist/runtime/app/composables/cms-entry.d.ts +5 -1
  145. package/dist/runtime/app/composables/cms-entry.js +36 -11
  146. package/dist/runtime/app/composables/cms-history.d.ts +10 -0
  147. package/dist/runtime/app/composables/cms-history.js +24 -0
  148. package/dist/runtime/app/composables/cms-link-targets.d.ts +10 -0
  149. package/dist/runtime/app/composables/cms-link-targets.js +40 -0
  150. package/dist/runtime/app/composables/cms-link-usage.d.ts +1 -0
  151. package/dist/runtime/app/composables/cms-link-usage.js +45 -0
  152. package/dist/runtime/app/composables/cms-media-library.d.ts +19 -9
  153. package/dist/runtime/app/composables/cms-media-library.js +31 -13
  154. package/dist/runtime/app/composables/cms-media-uploader.js +6 -20
  155. package/dist/runtime/app/composables/cms-permissions.d.ts +18 -0
  156. package/dist/runtime/app/composables/cms-permissions.js +54 -0
  157. package/dist/runtime/app/composables/cms-presets.d.ts +11 -0
  158. package/dist/runtime/app/composables/cms-presets.js +40 -0
  159. package/dist/runtime/app/composables/cms-preview-mode.d.ts +4 -0
  160. package/dist/runtime/app/composables/cms-preview-mode.js +8 -0
  161. package/dist/runtime/app/composables/cms-query.d.ts +3 -0
  162. package/dist/runtime/app/composables/cms-query.js +45 -20
  163. package/dist/runtime/app/composables/cms-record-list.d.ts +31 -0
  164. package/dist/runtime/app/composables/cms-record-list.js +75 -0
  165. package/dist/runtime/app/composables/cms-route.d.ts +6 -0
  166. package/dist/runtime/app/composables/cms-route.js +36 -0
  167. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  168. package/dist/runtime/app/composables/cms-save-handoff.d.ts +4 -0
  169. package/dist/runtime/app/composables/cms-save-handoff.js +19 -0
  170. package/dist/runtime/app/composables/cms-seo.d.ts +12 -0
  171. package/dist/runtime/app/composables/cms-seo.js +69 -0
  172. package/dist/runtime/app/composables/cms-settings-form.d.ts +13 -0
  173. package/dist/runtime/app/composables/cms-settings-form.js +29 -0
  174. package/dist/runtime/app/composables/cms-settings-sections.d.ts +26 -0
  175. package/dist/runtime/app/composables/cms-settings-sections.js +181 -0
  176. package/dist/runtime/app/composables/cms-settings-stores.d.ts +52 -0
  177. package/dist/runtime/app/composables/cms-settings-stores.js +156 -0
  178. package/dist/runtime/app/composables/cms-settings.d.ts +3 -3
  179. package/dist/runtime/app/composables/cms-settings.js +14 -8
  180. package/dist/runtime/app/composables/cms-site-disabled.d.ts +7 -0
  181. package/dist/runtime/app/composables/cms-site-disabled.js +8 -0
  182. package/dist/runtime/app/composables/cms-site.d.ts +7 -0
  183. package/dist/runtime/app/composables/cms-site.js +22 -0
  184. package/dist/runtime/app/composables/cms-toast.d.ts +2 -8
  185. package/dist/runtime/app/composables/cms-toast.js +3 -9
  186. package/dist/runtime/app/composables/cms-translation-actions.d.ts +15 -0
  187. package/dist/runtime/app/composables/cms-translation-actions.js +108 -0
  188. package/dist/runtime/app/css-url.d.ts +4 -0
  189. package/dist/runtime/app/image/CmsImage.d.vue.ts +89 -0
  190. package/dist/runtime/app/image/CmsImage.vue +91 -0
  191. package/dist/runtime/app/image/CmsImage.vue.d.ts +89 -0
  192. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  193. package/dist/runtime/app/layouts/cms-admin.vue +98 -76
  194. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  195. package/dist/runtime/app/layouts/cms-editor.vue +7 -2
  196. package/dist/runtime/app/links/CmsLink.d.vue.ts +18 -0
  197. package/dist/runtime/app/links/CmsLink.vue +18 -0
  198. package/dist/runtime/app/links/CmsLink.vue.d.ts +18 -0
  199. package/dist/runtime/app/middleware/cms-auth.js +13 -1
  200. package/dist/runtime/app/pages/admin-collection.vue +473 -297
  201. package/dist/runtime/app/pages/admin-content.vue +190 -53
  202. package/dist/runtime/app/pages/admin-entry.vue +79 -75
  203. package/dist/runtime/app/pages/admin-index.vue +18 -5
  204. package/dist/runtime/app/pages/admin-login.vue +1 -1
  205. package/dist/runtime/app/pages/admin-logout.vue +1 -1
  206. package/dist/runtime/app/pages/admin-media.vue +3 -3
  207. package/dist/runtime/app/pages/admin-preview.vue +112 -24
  208. package/dist/runtime/app/plugins/cms-field.d.ts +2 -0
  209. package/dist/runtime/app/plugins/cms-field.js +8 -0
  210. package/dist/runtime/app/plugins/cms-preview-banner.client.d.ts +2 -0
  211. package/dist/runtime/app/plugins/cms-preview-banner.client.js +19 -0
  212. package/dist/runtime/app/plugins/cms-preview.d.ts +2 -0
  213. package/dist/runtime/app/plugins/cms-preview.js +27 -0
  214. package/dist/runtime/app/preview/CmsPreviewBanner.d.vue.ts +7 -0
  215. package/dist/runtime/app/preview/CmsPreviewBanner.vue +63 -0
  216. package/dist/runtime/app/preview/CmsPreviewBanner.vue.d.ts +7 -0
  217. package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +17 -3
  218. package/dist/runtime/app/preview/PreviewOverlay.vue +629 -166
  219. package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +17 -3
  220. package/dist/runtime/app/preview/field-marker.d.ts +12 -0
  221. package/dist/runtime/app/preview/field-marker.js +20 -0
  222. package/dist/runtime/app/preview/inline-text.d.ts +15 -0
  223. package/dist/runtime/app/preview/inline-text.js +158 -0
  224. package/dist/runtime/app/utils/block-editor.d.ts +22 -0
  225. package/dist/runtime/app/utils/block-editor.js +12 -0
  226. package/dist/runtime/app/utils/keyboard.d.ts +6 -0
  227. package/dist/runtime/app/utils/keyboard.js +52 -0
  228. package/dist/runtime/app/utils/redirects.d.ts +6 -0
  229. package/dist/runtime/app/utils/redirects.js +7 -0
  230. package/dist/runtime/app/utils/ui.d.ts +8 -0
  231. package/dist/runtime/app/utils/ui.js +15 -1
  232. package/dist/runtime/app/utils/users.d.ts +15 -0
  233. package/dist/runtime/app/utils/users.js +14 -0
  234. package/dist/runtime/assets/cms.css +1 -1
  235. package/dist/runtime/generate.js +16 -2
  236. package/dist/runtime/server/api/collection-export.get.js +3 -2
  237. package/dist/runtime/server/api/collection-import.post.js +39 -11
  238. package/dist/runtime/server/api/collection.get.js +27 -9
  239. package/dist/runtime/server/api/collection.post.js +28 -11
  240. package/dist/runtime/server/api/collection.put.js +35 -13
  241. package/dist/runtime/server/api/item.delete.js +19 -7
  242. package/dist/runtime/server/api/item.get.js +3 -2
  243. package/dist/runtime/server/api/item.put.js +52 -18
  244. package/dist/runtime/server/api/link-usage.post.d.ts +5 -0
  245. package/dist/runtime/server/api/link-usage.post.js +21 -0
  246. package/dist/runtime/server/api/media-delete.post.js +4 -2
  247. package/dist/runtime/server/api/media-download.get.js +2 -2
  248. package/dist/runtime/server/api/media-folder.delete.js +8 -2
  249. package/dist/runtime/server/api/media-folder.patch.js +10 -2
  250. package/dist/runtime/server/api/media-folder.post.js +4 -2
  251. package/dist/runtime/server/api/media-move.post.js +7 -4
  252. package/dist/runtime/server/api/media-presign.post.js +2 -2
  253. package/dist/runtime/server/api/media-upload.put.js +2 -2
  254. package/dist/runtime/server/api/media-usage.post.d.ts +2 -4
  255. package/dist/runtime/server/api/media-usage.post.js +13 -9
  256. package/dist/runtime/server/api/media.delete.js +4 -2
  257. package/dist/runtime/server/api/media.get.js +22 -7
  258. package/dist/runtime/server/api/media.post.js +4 -2
  259. package/dist/runtime/server/api/media.put.js +33 -16
  260. package/dist/runtime/server/api/preset.delete.d.ts +4 -0
  261. package/dist/runtime/server/api/preset.delete.js +9 -0
  262. package/dist/runtime/server/api/preset.put.d.ts +3 -0
  263. package/dist/runtime/server/api/preset.put.js +23 -0
  264. package/dist/runtime/server/api/presets-field.get.d.ts +3 -0
  265. package/dist/runtime/server/api/presets-field.get.js +13 -0
  266. package/dist/runtime/server/api/presets-field.post.d.ts +3 -0
  267. package/dist/runtime/server/api/presets-field.post.js +36 -0
  268. package/dist/runtime/server/api/presets.get.d.ts +3 -0
  269. package/dist/runtime/server/api/presets.get.js +7 -0
  270. package/dist/runtime/server/api/preview-link.delete.d.ts +4 -0
  271. package/dist/runtime/server/api/preview-link.delete.js +15 -0
  272. package/dist/runtime/server/api/preview-links.get.d.ts +3 -0
  273. package/dist/runtime/server/api/preview-links.get.js +10 -0
  274. package/dist/runtime/server/api/preview-links.post.d.ts +3 -0
  275. package/dist/runtime/server/api/preview-links.post.js +17 -0
  276. package/dist/runtime/server/api/preview.get.d.ts +3 -0
  277. package/dist/runtime/server/api/preview.get.js +7 -0
  278. package/dist/runtime/server/api/redirect.delete.d.ts +4 -0
  279. package/dist/runtime/server/api/redirect.delete.js +10 -0
  280. package/dist/runtime/server/api/redirect.put.d.ts +3 -0
  281. package/dist/runtime/server/api/redirect.put.js +8 -0
  282. package/dist/runtime/server/api/redirects-import.post.d.ts +3 -0
  283. package/dist/runtime/server/api/redirects-import.post.js +13 -0
  284. package/dist/runtime/server/api/redirects.get.d.ts +3 -0
  285. package/dist/runtime/server/api/redirects.get.js +7 -0
  286. package/dist/runtime/server/api/redirects.post.d.ts +3 -0
  287. package/dist/runtime/server/api/redirects.post.js +7 -0
  288. package/dist/runtime/server/api/search.get.js +8 -3
  289. package/dist/runtime/server/api/settings.delete.js +2 -2
  290. package/dist/runtime/server/api/settings.get.js +4 -2
  291. package/dist/runtime/server/api/settings.put.js +2 -2
  292. package/dist/runtime/server/api/site.get.d.ts +3 -0
  293. package/dist/runtime/server/api/site.get.js +7 -0
  294. package/dist/runtime/server/api/site.put.d.ts +3 -0
  295. package/dist/runtime/server/api/site.put.js +16 -0
  296. package/dist/runtime/server/api/tags.get.js +3 -2
  297. package/dist/runtime/server/api/user.delete.js +8 -2
  298. package/dist/runtime/server/api/user.put.js +14 -4
  299. package/dist/runtime/server/api/users.get.js +2 -2
  300. package/dist/runtime/server/api/users.post.js +8 -4
  301. package/dist/runtime/server/middleware/hidden-site.d.ts +2 -0
  302. package/dist/runtime/server/middleware/hidden-site.js +8 -0
  303. package/dist/runtime/server/middleware/preview-links.d.ts +2 -0
  304. package/dist/runtime/server/middleware/preview-links.js +6 -0
  305. package/dist/runtime/server/middleware/redirects.d.ts +2 -0
  306. package/dist/runtime/server/middleware/redirects.js +18 -0
  307. package/dist/runtime/server/plugins/session-validate.js +4 -1
  308. package/dist/runtime/server/plugins/webhooks.d.ts +3 -0
  309. package/dist/runtime/server/plugins/webhooks.js +27 -0
  310. package/dist/runtime/server/routes/graphql.js +5 -3
  311. package/dist/runtime/server/routes/robots.d.ts +2 -0
  312. package/dist/runtime/server/routes/robots.js +16 -0
  313. package/dist/runtime/server/routes/sitemap.d.ts +2 -0
  314. package/dist/runtime/server/routes/sitemap.js +67 -0
  315. package/dist/runtime/server/utils/cached-value.d.ts +5 -0
  316. package/dist/runtime/server/utils/cached-value.js +24 -0
  317. package/dist/runtime/server/utils/content-routes.d.ts +5 -0
  318. package/dist/runtime/server/utils/content-routes.js +29 -0
  319. package/dist/runtime/server/utils/field-rules.d.ts +19 -0
  320. package/dist/runtime/server/utils/field-rules.js +72 -0
  321. package/dist/runtime/server/utils/graphql.d.ts +3 -1
  322. package/dist/runtime/server/utils/graphql.js +143 -28
  323. package/dist/runtime/server/utils/hooks.d.ts +14 -0
  324. package/dist/runtime/server/utils/hooks.js +71 -0
  325. package/dist/runtime/server/utils/import.d.ts +7 -2
  326. package/dist/runtime/server/utils/import.js +108 -14
  327. package/dist/runtime/server/utils/link-usage.d.ts +4 -0
  328. package/dist/runtime/server/utils/link-usage.js +28 -0
  329. package/dist/runtime/server/utils/media-references.d.ts +6 -1
  330. package/dist/runtime/server/utils/media-references.js +49 -20
  331. package/dist/runtime/server/utils/media-usage.d.ts +2 -0
  332. package/dist/runtime/server/utils/media-usage.js +5 -3
  333. package/dist/runtime/server/utils/page-rows.d.ts +2 -0
  334. package/dist/runtime/server/utils/page-rows.js +42 -29
  335. package/dist/runtime/server/utils/permissions.d.ts +14 -0
  336. package/dist/runtime/server/utils/permissions.js +43 -0
  337. package/dist/runtime/server/utils/presets.d.ts +18 -0
  338. package/dist/runtime/server/utils/presets.js +68 -0
  339. package/dist/runtime/server/utils/preview-links.d.ts +207 -0
  340. package/dist/runtime/server/utils/preview-links.js +161 -0
  341. package/dist/runtime/server/utils/redirects.d.ts +31 -0
  342. package/dist/runtime/server/utils/redirects.js +240 -0
  343. package/dist/runtime/server/utils/registry.js +4 -1
  344. package/dist/runtime/server/utils/relations.d.ts +8 -0
  345. package/dist/runtime/server/utils/relations.js +25 -6
  346. package/dist/runtime/server/utils/require-admin.d.ts +0 -1
  347. package/dist/runtime/server/utils/require-admin.js +4 -8
  348. package/dist/runtime/server/utils/search.d.ts +1 -0
  349. package/dist/runtime/server/utils/search.js +44 -19
  350. package/dist/runtime/server/utils/site.d.ts +23 -0
  351. package/dist/runtime/server/utils/site.js +79 -0
  352. package/dist/runtime/server/utils/sitemap.d.ts +12 -0
  353. package/dist/runtime/server/utils/sitemap.js +109 -0
  354. package/dist/runtime/server/utils/user-management.d.ts +4 -0
  355. package/dist/runtime/server/utils/user-management.js +20 -0
  356. package/dist/runtime/server/utils/users.d.ts +1 -1
  357. package/dist/runtime/server/utils/users.js +1 -1
  358. package/dist/runtime/server/utils/visibility.d.ts +1 -0
  359. package/dist/runtime/server/utils/visibility.js +6 -1
  360. package/dist/runtime/server/utils/webhooks.d.ts +24 -0
  361. package/dist/runtime/server/utils/webhooks.js +90 -0
  362. package/dist/runtime/shared/block-paths.d.ts +66 -0
  363. package/dist/runtime/shared/block-paths.js +310 -0
  364. package/dist/runtime/shared/block-picker.d.ts +18 -0
  365. package/dist/runtime/shared/block-picker.js +31 -0
  366. package/dist/runtime/shared/block-signature.d.ts +4 -0
  367. package/dist/runtime/shared/block-signature.js +46 -0
  368. package/dist/runtime/shared/clipboard.d.ts +31 -0
  369. package/dist/runtime/shared/clipboard.js +206 -0
  370. package/dist/runtime/shared/conditions.d.ts +36 -0
  371. package/dist/runtime/shared/conditions.js +100 -0
  372. package/dist/runtime/shared/content-routes.d.ts +42 -0
  373. package/dist/runtime/shared/content-routes.js +237 -0
  374. package/dist/runtime/shared/duplicate.js +2 -1
  375. package/dist/runtime/shared/editor-layout.d.ts +18 -0
  376. package/dist/runtime/shared/editor-layout.js +9 -0
  377. package/dist/runtime/shared/field-options.d.ts +21 -0
  378. package/dist/runtime/shared/field-options.js +90 -0
  379. package/dist/runtime/shared/graphql-sdl.d.ts +1 -0
  380. package/dist/runtime/shared/graphql-sdl.js +50 -26
  381. package/dist/runtime/shared/history.d.ts +27 -0
  382. package/dist/runtime/shared/history.js +123 -0
  383. package/dist/runtime/shared/hooks.d.ts +45 -0
  384. package/dist/runtime/shared/hooks.js +28 -0
  385. package/dist/runtime/shared/index.d.ts +90 -21
  386. package/dist/runtime/shared/index.js +121 -37
  387. package/dist/runtime/shared/inline-text.d.ts +38 -0
  388. package/dist/runtime/shared/inline-text.js +85 -0
  389. package/dist/runtime/shared/layout.d.ts +2 -1
  390. package/dist/runtime/shared/layout.js +3 -0
  391. package/dist/runtime/shared/links.d.ts +48 -0
  392. package/dist/runtime/shared/links.js +155 -0
  393. package/dist/runtime/shared/list-filters.d.ts +2 -0
  394. package/dist/runtime/shared/list-filters.js +2 -0
  395. package/dist/runtime/shared/media-focal.d.ts +30 -0
  396. package/dist/runtime/shared/media-focal.js +52 -0
  397. package/dist/runtime/shared/migrations-dir.d.ts +14 -0
  398. package/dist/runtime/shared/migrations-dir.js +48 -2
  399. package/dist/runtime/shared/presets.d.ts +67 -0
  400. package/dist/runtime/shared/presets.js +127 -0
  401. package/dist/runtime/shared/preview-links.d.ts +64 -0
  402. package/dist/runtime/shared/preview-links.js +54 -0
  403. package/dist/runtime/shared/preview.d.ts +124 -21
  404. package/dist/runtime/shared/preview.js +419 -81
  405. package/dist/runtime/shared/redirects.d.ts +75 -0
  406. package/dist/runtime/shared/redirects.js +230 -0
  407. package/dist/runtime/shared/roles.d.ts +57 -0
  408. package/dist/runtime/shared/roles.js +293 -0
  409. package/dist/runtime/shared/save-state.d.ts +8 -0
  410. package/dist/runtime/shared/save-state.js +15 -0
  411. package/dist/runtime/shared/search.d.ts +16 -1
  412. package/dist/runtime/shared/search.js +41 -7
  413. package/dist/runtime/shared/seo.d.ts +167 -0
  414. package/dist/runtime/shared/seo.js +335 -0
  415. package/dist/runtime/shared/transfer.js +2 -1
  416. package/dist/runtime/shared/translations.d.ts +29 -0
  417. package/dist/runtime/shared/translations.js +123 -0
  418. package/dist/runtime/shared/users.d.ts +1 -1
  419. package/dist/runtime/shared/validation.d.ts +23 -1
  420. package/dist/runtime/shared/validation.js +214 -64
  421. package/dist/runtime/webhooks.d.ts +33 -0
  422. package/dist/runtime/webhooks.js +57 -0
  423. package/dist/types.d.mts +5 -1
  424. package/package.json +6 -2
  425. package/dist/runtime/app/components/cms/BlockOutline.vue +0 -88
  426. package/dist/runtime/app/components/cms/BlockPalette.vue +0 -31
  427. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +0 -12
  428. package/dist/runtime/app/components/cms/SettingsModal.vue +0 -126
  429. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +0 -12
  430. package/dist/runtime/app/composables/cms-field-focus.d.ts +0 -4
  431. package/dist/runtime/app/composables/cms-field-focus.js +0 -20
package/dist/module.mjs CHANGED
@@ -3,18 +3,23 @@ import { existsSync, readFileSync } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import { extname, resolve, isAbsolute, join, relative, dirname } from 'node:path';
5
5
  import { pathToFileURL, fileURLToPath } from 'node:url';
6
- import { defineNuxtModule, createResolver, useLogger, addImports, addTemplate, addServerPlugin, addTypeTemplate, addComponentsDir, tryResolveModule, addRouteMiddleware, extendPages, addServerHandler, resolvePath, addComponent } from '@nuxt/kit';
6
+ import { defineNuxtModule, createResolver, useLogger, addImports, addTemplate, addPlugin, addServerPlugin, addTypeTemplate, addComponentsDir, tryResolveModule, addRouteMiddleware, extendPages, addServerHandler, resolvePath, addComponent, hasNuxtModule } from '@nuxt/kit';
7
7
  import { introspectionFromSchema, buildSchema } from 'graphql';
8
8
  import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal';
9
9
  import { createJiti } from 'jiti';
10
- import { typeName, blockTypeName as blockTypeName$1, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
11
- import { migrationsDirFor } from '../dist/runtime/shared/migrations-dir.js';
12
- import { entryFieldsFor, isPrivateField, blockTypeName, pageColumnFields, isTranslatableField, isTranslatableMediaField, isStringListField, pageAllFields, normalizeContentEntries, PAGE_PATH_FIELD, pageRoutes, isCollectionKind, isMultiSelect, layoutErrors, listErrors, mobileMediaKey, fieldConditions, selectOptionValues, pageFieldsTableName, pageMediaTableName, CONTENT_SYSTEM_FIELDS, isRequiredField, pageFields, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, normalizeCmsConfig } from '../dist/runtime/shared/index.js';
10
+ import { typeName, blocksFieldTypeName, blockTypeName as blockTypeName$1, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
11
+ import { migrationsDirFor, MIGRATION_FIXES_FILE, migrationSqlFiles, fixNewMigrations } from '../dist/runtime/shared/migrations-dir.js';
12
+ import { blocksFieldPaths, entryFieldsFor, isPrivateField, blockTypeName, pageColumnFields, isTranslatableField, isTranslatableMediaField, isStringListField, pageAllFields, isRequiredField, normalizeContentEntries, contentRouteConfigErrors, PAGE_PATH_FIELD, pageRoutes, isCollectionKind, isMultiSelect, layoutErrors, listErrors, mobileMediaKey, relationTarget, pageFieldsTableName, pageMediaTableName, contentRouteClashErrors, CONTENT_SYSTEM_FIELDS, CONTENT_ROUTE_FIELDS, declaredLinkTargets, isLinkTargetEntry, MAX_BLOCKS_DEPTH, CONDITION_GROUPS, normalizeTags, selectOptionValues, CONDITION_OPERATORS, routedContentNames, pageFields, isRoutedContent, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, normalizeCmsConfig } from '../dist/runtime/shared/index.js';
13
13
  import { previewPathErrors, DEFAULT_CMS_PREVIEW_PATH } from '../dist/runtime/shared/preview.js';
14
+ import { previewBannerErrors } from '../dist/runtime/shared/preview-links.js';
15
+ import { seoOptionErrors, normalizeSiteUrl, SITEMAP_PATH, resolveSeoSwitches, ROBOTS_PATH, missingSiteUrlWarning, SITEMAP_CONFLICT_MODULES, ROBOTS_CONFLICT_MODULES } from '../dist/runtime/shared/seo.js';
14
16
  import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
15
17
  import { createHash } from 'node:crypto';
16
18
  import { readFile } from 'node:fs/promises';
19
+ import { isUniqueValueField, UNIQUE_FIELD_TYPES, IMMUTABLE_FIELD_TYPES, DEFAULT_FIELD_TYPES, DEFAULT_NOW, DEFAULT_TODAY } from '../dist/runtime/shared/field-options.js';
17
20
  import { routePathsFromDir, resolvePageRoutes } from '../dist/runtime/shared/page-routes.js';
21
+ import { CMS_ENTRY_ACTIONS, CMS_MEDIA_ACTIONS } from '../dist/runtime/shared/hooks.js';
22
+ import { validateRoles } from '../dist/runtime/shared/roles.js';
18
23
 
19
24
  const DEFAULT_BRAND_TITLE = "nuxt-cms";
20
25
  const DEFAULT_BRAND_SUBTITLE = "Content studio";
@@ -101,22 +106,42 @@ function renderBrandFile(brand) {
101
106
  ].join("\n");
102
107
  }
103
108
 
109
+ function resolveCmsImageSetup(installed, options) {
110
+ const provider = options.image?.provider;
111
+ return {
112
+ nuxtImage: installed,
113
+ provider: installed && typeof provider === "string" && provider ? provider : null
114
+ };
115
+ }
116
+ function renderImageFile(setup) {
117
+ const lines = [`import type { Component } from 'vue'`];
118
+ if (setup.nuxtImage) lines.push(`import { NuxtImg, NuxtPicture } from '#components'`);
119
+ lines.push(
120
+ ``,
121
+ `export const cmsNuxtImg: Component | null = ${setup.nuxtImage ? "NuxtImg" : "null"}`,
122
+ `export const cmsNuxtPicture: Component | null = ${setup.nuxtImage ? "NuxtPicture" : "null"}`,
123
+ `export const cmsImageProvider: string | null = ${JSON.stringify(setup.provider)}`,
124
+ ``
125
+ );
126
+ return lines.join("\n");
127
+ }
128
+
104
129
  function componentPascalName(name) {
105
130
  return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
106
131
  }
107
132
  function collectBlockComponents(config) {
108
133
  const refs = [];
109
134
  for (const [entryName, entry] of Object.entries(config)) {
110
- for (const [fieldKey, field] of Object.entries(entryFieldsFor(entry))) {
111
- if (field.type !== "blocks" || isPrivateField(field)) continue;
135
+ for (const { field, schemaPath } of blocksFieldPaths(entryFieldsFor(entry))) {
136
+ if (isPrivateField(field)) continue;
112
137
  for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
113
138
  if (typeof block.component !== "string" || !block.component.trim()) continue;
114
139
  refs.push({
115
140
  entry: entryName,
116
- field: fieldKey,
141
+ field: schemaPath.join("."),
117
142
  block: blockName,
118
143
  component: componentPascalName(block.component.trim()),
119
- typeName: blockTypeName(entryName, fieldKey, blockName)
144
+ typeName: blockTypeName(entryName, schemaPath, blockName)
120
145
  });
121
146
  }
122
147
  }
@@ -238,13 +263,16 @@ const RESERVED_ENTRY_KEYS = [
238
263
  "graphql",
239
264
  "settings",
240
265
  "cms_media",
266
+ "cms_preview_links",
267
+ "cms_redirects",
241
268
  "cms_settings",
242
269
  "cms_users"
243
270
  ];
244
271
  const RESERVED_COLUMNS = ["id", "status", "created_at", "updated_at"];
245
272
  const TITLE_FIELD_TYPES = ["text", "slug", "email", "number", "date", "datetime", "select"];
246
273
  const TRANSLATABLE_FIELD_TYPES = ["text", "richtext", "media"];
247
- const CONDITION_FIELD_TYPES = [
274
+ const TRANSLATABLE_ERROR = "translatable is only supported on text, richtext, media and link fields";
275
+ const CONDITION_VALUE_TYPES = [
248
276
  "select",
249
277
  "boolean",
250
278
  "text",
@@ -254,12 +282,19 @@ const CONDITION_FIELD_TYPES = [
254
282
  "email",
255
283
  "slug"
256
284
  ];
285
+ const CONDITION_ORDER_TYPES = ["number", "date", "datetime"];
286
+ const CONDITION_LIST_TYPES = ["tags", "select (multiple)", "relation (many-to-many)"];
287
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
288
+ const DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
257
289
  const COMPONENT_RESERVED_KEYS = ["key", "ref", "class", "style"];
258
290
  const RESERVED_TYPE_NAMES = [
259
291
  "Query",
260
292
  "Mutation",
261
293
  "Subscription",
262
294
  "CmsMedia",
295
+ "CmsLink",
296
+ "CmsLocalePath",
297
+ "CmsSite",
263
298
  "JSON",
264
299
  "SortDirection",
265
300
  "IntFilter",
@@ -309,6 +344,11 @@ function validateConfig(config, i18n) {
309
344
  errors.push(`${at}: kind must be 'collection', 'single', 'page' or 'content'`);
310
345
  if (entry.kind === "content") errors.push(...contentErrors(at, entry));
311
346
  else if (entry.preview !== void 0) errors.push(`${at}: preview needs kind 'content'`);
347
+ if (entry.route !== void 0) {
348
+ if (entry.kind === "content")
349
+ errors.push(...contentRouteConfigErrors(at, entry.route, { locales }));
350
+ else errors.push(`${at}: route needs kind 'content'`);
351
+ }
312
352
  if (entry.kind === "page") {
313
353
  if (pageEntry) {
314
354
  errors.push(
@@ -420,13 +460,12 @@ function validateConfig(config, i18n) {
420
460
  columnNames.add(column);
421
461
  }
422
462
  if (field.translatable) {
423
- if (!TRANSLATABLE_FIELD_TYPES.includes(field.type))
424
- errors.push(
425
- `${fat}: translatable is only supported on text, richtext and media fields`
426
- );
463
+ if (field.type !== "link" && !TRANSLATABLE_FIELD_TYPES.includes(field.type))
464
+ errors.push(`${fat}: ${TRANSLATABLE_ERROR}`);
427
465
  if (!locales.length)
428
466
  errors.push(`${fat}: translatable requires cms.i18n.locales in nuxt.config`);
429
467
  }
468
+ errors.push(...linkFieldErrors(fat, field, config));
430
469
  if (field.mobile) {
431
470
  const mobileKey = mobileMediaKey(key);
432
471
  if (field.type !== "media")
@@ -435,48 +474,10 @@ function validateConfig(config, i18n) {
435
474
  errors.push(`${fat}: mobile needs the key '${mobileKey}', which is already declared`);
436
475
  }
437
476
  if (field.type === "select") errors.push(...selectOptionErrors(fat, field));
438
- if (field.showIf) {
477
+ if (field.showIf !== void 0) {
439
478
  if (entry.titleField === key)
440
479
  errors.push(`${fat}: the titleField cannot be conditional`);
441
- for (const condition of fieldConditions(field)) {
442
- const cat = `${fat}, showIf on '${condition.field}'`;
443
- const target = allFields[condition.field];
444
- if (!condition.field || !target) {
445
- errors.push(`${cat}: '${condition.field}' is not a declared field`);
446
- continue;
447
- }
448
- if (condition.field === key) {
449
- errors.push(`${cat}: a field cannot depend on itself`);
450
- continue;
451
- }
452
- if (!CONDITION_FIELD_TYPES.includes(target.type) || isMultiSelect(target)) {
453
- errors.push(
454
- `${cat}: showIf can only depend on ${CONDITION_FIELD_TYPES.join(
455
- ", "
456
- )} fields (got '${target.type}')`
457
- );
458
- continue;
459
- }
460
- if (isTranslatableField(target)) {
461
- errors.push(`${cat}: showIf cannot depend on a translatable field`);
462
- continue;
463
- }
464
- const values = condition.in ?? (condition.eq === void 0 ? null : [condition.eq]);
465
- if (!values || !values.length) {
466
- errors.push(`${cat}: showIf requires 'eq' or a non-empty 'in'`);
467
- continue;
468
- }
469
- if (condition.in && condition.eq !== void 0)
470
- errors.push(`${cat}: showIf accepts either 'eq' or 'in', not both`);
471
- if (target.type === "select") {
472
- const options = selectOptionValues(target);
473
- const unknown = values.filter((value) => !options.includes(String(value)));
474
- if (unknown.length)
475
- errors.push(
476
- `${cat}: ${unknown.map((value) => `'${String(value)}'`).join(", ")} not in the options of '${condition.field}'`
477
- );
478
- }
479
- }
480
+ errors.push(...showIfErrors(fat, key, field.showIf, allFields, "a declared field"));
480
481
  }
481
482
  if (field.type === "slug" && field.from) {
482
483
  const source = entry.fields?.[field.from];
@@ -489,59 +490,16 @@ function validateConfig(config, i18n) {
489
490
  } else if (field.suggestions !== void 0) {
490
491
  errors.push(`${fat}: suggestions is only supported on tags fields`);
491
492
  }
492
- if (field.type !== "blocks" && field.type !== "tags" && (field.min !== void 0 || field.max !== void 0))
493
- errors.push(`${fat}: min and max are only supported on blocks and tags fields`);
493
+ errors.push(...valueBoundsErrors(fat, field));
494
+ errors.push(...fieldOptionErrors(fat, field, entry, null));
494
495
  if (field.type === "blocks") {
495
496
  if (field.translatable) errors.push(`${fat}: blocks fields cannot be translatable`);
496
- errors.push(...blocksCountErrors(fat, field));
497
- if (!field.blocks || !Object.keys(field.blocks).length) {
498
- errors.push(`${fat}: blocks requires a non-empty blocks map`);
499
- }
500
- for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
501
- const bat = `${fat}, block '${blockName}'`;
502
- if (!IDENTIFIER.test(blockName))
503
- errors.push(`${bat}: key must be a valid identifier`);
504
- if (!block.fields || !Object.keys(block.fields).length)
505
- errors.push(`${bat}: fields must not be empty`);
506
- errors.push(...blockMetaErrors(bat, block));
507
- for (const [blockFieldKey, blockField] of Object.entries(block.fields ?? {})) {
508
- const bfat = `${bat}, field '${blockFieldKey}'`;
509
- if (!IDENTIFIER.test(blockFieldKey))
510
- errors.push(`${bfat}: key must be a valid identifier`);
511
- if (blockFieldKey === "type")
512
- errors.push(`${bfat}: 'type' is reserved inside blocks`);
513
- if (["relation", "blocks", "slug", "tags"].includes(blockField.type)) {
514
- errors.push(
515
- `${bfat}: ${blockField.type} fields are not supported inside blocks`
516
- );
517
- }
518
- if (blockField.translatable) {
519
- if (!TRANSLATABLE_FIELD_TYPES.includes(blockField.type))
520
- errors.push(
521
- `${bfat}: translatable is only supported on text, richtext and media fields`
522
- );
523
- if (!locales.length)
524
- errors.push(
525
- `${bfat}: translatable requires cms.i18n.locales in nuxt.config`
526
- );
527
- }
528
- if (blockField.private)
529
- errors.push(`${bfat}: private fields are not supported inside blocks`);
530
- if (blockField.showIf)
531
- errors.push(`${bfat}: showIf is not supported inside blocks`);
532
- if (blockField.mobile)
533
- errors.push(`${bfat}: mobile is not supported inside blocks`);
534
- if (blockField.type === "select")
535
- errors.push(...selectOptionErrors(bfat, blockField));
536
- if (blockField.type === "select" && blockField.multiple) {
537
- errors.push(`${bfat}: multiple select is not supported inside blocks`);
538
- }
539
- }
540
- }
497
+ errors.push(...blocksFieldErrors(fat, field, 1, { config, entry, locales }));
541
498
  }
542
499
  if (field.type === "relation") {
543
- const target = field.to ? config[field.to] : void 0;
544
- if (!field.to || !target) {
500
+ const to = relationTarget(field);
501
+ const target = to ? config[to] : void 0;
502
+ if (!to || !target) {
545
503
  errors.push(`${fat}: relation target '${field.to}' is not in the registry`);
546
504
  } else {
547
505
  if (!isCollectionKind(target))
@@ -571,6 +529,8 @@ function validateConfig(config, i18n) {
571
529
  }
572
530
  const sqlTableNames = /* @__PURE__ */ new Map([
573
531
  ["cms_media", "the built-in media table"],
532
+ ["cms_preview_links", "the built-in preview links table"],
533
+ ["cms_redirects", "the built-in redirects table"],
574
534
  ["cms_settings", "the built-in settings table"],
575
535
  ["cms_users", "the built-in users table"]
576
536
  ]);
@@ -596,6 +556,95 @@ function validateConfig(config, i18n) {
596
556
  registerTable(`${name}_${snakeCase(key)}`, `the join table of '${name}.${key}'`);
597
557
  }
598
558
  }
559
+ errors.push(...contentRouteClashErrors(config));
560
+ errors.push(...blockTypeNameErrors(config));
561
+ return errors;
562
+ }
563
+ function blocksFieldErrors(fat, field, depth, scope) {
564
+ const { config, entry, locales } = scope;
565
+ const errors = [];
566
+ errors.push(...blocksCountErrors(fat, field));
567
+ if (!field.blocks || !Object.keys(field.blocks).length) {
568
+ errors.push(`${fat}: blocks requires a non-empty blocks map`);
569
+ }
570
+ for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
571
+ const bat = `${fat}, block '${blockName}'`;
572
+ if (!IDENTIFIER.test(blockName)) errors.push(`${bat}: key must be a valid identifier`);
573
+ if (!block.fields || !Object.keys(block.fields).length)
574
+ errors.push(`${bat}: fields must not be empty`);
575
+ errors.push(...blockMetaErrors(bat, block));
576
+ errors.push(...layoutErrors(bat, block.fields ?? {}, block.layout));
577
+ for (const [blockFieldKey, blockField] of Object.entries(block.fields ?? {})) {
578
+ const bfat = `${bat}, field '${blockFieldKey}'`;
579
+ if (!IDENTIFIER.test(blockFieldKey)) errors.push(`${bfat}: key must be a valid identifier`);
580
+ if (blockFieldKey === "type") errors.push(`${bfat}: 'type' is reserved inside blocks`);
581
+ if (["relation", "slug", "tags"].includes(blockField.type)) {
582
+ errors.push(`${bfat}: ${blockField.type} fields are not supported inside blocks`);
583
+ }
584
+ errors.push(...linkFieldErrors(bfat, blockField, config));
585
+ if (blockField.translatable) {
586
+ if (blockField.type === "blocks")
587
+ errors.push(`${bfat}: blocks fields cannot be translatable`);
588
+ else if (blockField.type !== "link" && !TRANSLATABLE_FIELD_TYPES.includes(blockField.type))
589
+ errors.push(`${bfat}: ${TRANSLATABLE_ERROR}`);
590
+ if (!locales.length)
591
+ errors.push(`${bfat}: translatable requires cms.i18n.locales in nuxt.config`);
592
+ }
593
+ if (blockField.private)
594
+ errors.push(`${bfat}: private fields are not supported inside blocks`);
595
+ if (blockField.showIf !== void 0)
596
+ errors.push(
597
+ ...showIfErrors(
598
+ bfat,
599
+ blockFieldKey,
600
+ blockField.showIf,
601
+ block.fields ?? {},
602
+ "a field of this block"
603
+ )
604
+ );
605
+ if (blockField.mobile)
606
+ errors.push(`${bfat}: mobile is not supported inside blocks`);
607
+ if (blockField.type === "blocks") {
608
+ if (depth >= MAX_BLOCKS_DEPTH)
609
+ errors.push(
610
+ `${bfat}: blocks nest at most ${MAX_BLOCKS_DEPTH} levels deep (a blocks field in a block in a blocks field counts as 2)`
611
+ );
612
+ else errors.push(...blocksFieldErrors(bfat, blockField, depth + 1, scope));
613
+ errors.push(...fieldOptionErrors(bfat, blockField, entry, "blocks"));
614
+ continue;
615
+ }
616
+ errors.push(...valueBoundsErrors(bfat, blockField));
617
+ errors.push(...fieldOptionErrors(bfat, blockField, entry, "blocks"));
618
+ if (blockField.type === "select") errors.push(...selectOptionErrors(bfat, blockField));
619
+ if (blockField.type === "select" && blockField.multiple) {
620
+ errors.push(`${bfat}: multiple select is not supported inside blocks`);
621
+ }
622
+ }
623
+ }
624
+ return errors;
625
+ }
626
+ function blockTypeNameErrors(config) {
627
+ const errors = [];
628
+ const seen = /* @__PURE__ */ new Map();
629
+ for (const [name, entry] of Object.entries(config)) seen.set(typeName(name), `entry '${name}'`);
630
+ const claim = (type, owner) => {
631
+ const existing = seen.get(type);
632
+ if (existing && existing !== owner)
633
+ errors.push(
634
+ `cms.config: GraphQL type name '${type}' of ${owner} clashes with ${existing}, rename a block or field`
635
+ );
636
+ else seen.set(type, owner);
637
+ };
638
+ for (const [name, entry] of Object.entries(config)) {
639
+ const fields = entry.kind === "page" ? pageAllFields(entry) : entry.fields ?? {};
640
+ for (const { field, schemaPath } of blocksFieldPaths(fields)) {
641
+ if (field.private) continue;
642
+ const where = `'${name}.${schemaPath.join(".")}'`;
643
+ claim(blocksFieldTypeName(name, schemaPath), `the blocks of ${where}`);
644
+ for (const blockName of Object.keys(field.blocks ?? {}))
645
+ claim(blockTypeName$1(name, schemaPath, blockName), `block '${blockName}' of ${where}`);
646
+ }
647
+ }
599
648
  return errors;
600
649
  }
601
650
  function contentErrors(at, entry) {
@@ -604,6 +653,8 @@ function contentErrors(at, entry) {
604
653
  for (const key of entry.declared?.fields ?? []) {
605
654
  if (system.includes(key))
606
655
  errors.push(`${at}: '${key}' is a system field of content entries, remove it from fields`);
656
+ else if (entry.route !== void 0 && CONTENT_ROUTE_FIELDS.includes(key))
657
+ errors.push(`${at}: '${key}' is reserved on content entries with a route`);
607
658
  }
608
659
  if (entry.declared?.titleField !== void 0)
609
660
  errors.push(`${at}: content entries use 'title' as titleField, remove titleField`);
@@ -625,6 +676,43 @@ function contentErrors(at, entry) {
625
676
  }
626
677
  return errors;
627
678
  }
679
+ function linkFieldErrors(at, field, config) {
680
+ const errors = [];
681
+ if (field.type !== "link") {
682
+ if (field.external !== void 0)
683
+ errors.push(`${at}: external is only supported on link fields`);
684
+ if (field.withLabel !== void 0)
685
+ errors.push(`${at}: withLabel is only supported on link fields`);
686
+ return errors;
687
+ }
688
+ if (field.external !== void 0 && typeof field.external !== "boolean")
689
+ errors.push(`${at}: external must be a boolean`);
690
+ if (field.withLabel !== void 0 && typeof field.withLabel !== "boolean")
691
+ errors.push(`${at}: withLabel must be a boolean`);
692
+ if (field.translatable && !field.withLabel)
693
+ errors.push(`${at}: translatable needs withLabel (only the link text is translated)`);
694
+ const to = field.to;
695
+ const declared = declaredLinkTargets(field);
696
+ if (to !== void 0 && (!declared || declared.length !== to.length)) {
697
+ errors.push(`${at}: to must be a list of entry names`);
698
+ return errors;
699
+ }
700
+ const seen = /* @__PURE__ */ new Set();
701
+ for (const name of declared ?? []) {
702
+ if (seen.has(name)) errors.push(`${at}: link target '${name}' is listed twice`);
703
+ seen.add(name);
704
+ const target = config[name];
705
+ if (!target) errors.push(`${at}: link target '${name}' is not in the registry`);
706
+ else if (!isLinkTargetEntry(target))
707
+ errors.push(
708
+ `${at}: link target '${name}' must be the page entry or a content entry with a route`
709
+ );
710
+ }
711
+ const targets = declared ?? Object.keys(config).filter((name) => isLinkTargetEntry(config[name]));
712
+ if (field.external === false && !targets.length)
713
+ errors.push(`${at}: the link allows no target: list entries in 'to' or allow external URLs`);
714
+ return errors;
715
+ }
628
716
  function selectOptionErrors(at, field) {
629
717
  const errors = [];
630
718
  if (!Array.isArray(field.options) || !field.options.length)
@@ -664,6 +752,282 @@ function blocksCountErrors(at, field) {
664
752
  errors.push(`${at}: min (${field.min}) cannot be greater than max (${field.max})`);
665
753
  return errors;
666
754
  }
755
+ function conditionTypeName(field) {
756
+ if (isMultiSelect(field)) return "select (multiple)";
757
+ if (field.type === "relation") return `relation (${field.cardinality ?? "many-to-one"})`;
758
+ return field.type;
759
+ }
760
+ function isConditionListTarget(field) {
761
+ return isStringListField(field) || isManyToMany(field);
762
+ }
763
+ function isDateValue(value) {
764
+ return typeof value === "string" && DATE_PATTERN.test(value) && !Number.isNaN((/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).getTime());
765
+ }
766
+ function isDateTimeValue(value) {
767
+ return typeof value === "string" && DATETIME_PATTERN.test(value) && !Number.isNaN(new Date(value).getTime());
768
+ }
769
+ function conditionValueExpectation(field) {
770
+ switch (field.type) {
771
+ case "boolean":
772
+ return { label: "a boolean", test: (value) => typeof value === "boolean" };
773
+ case "number":
774
+ return {
775
+ label: "a number",
776
+ test: (value) => typeof value === "number" && Number.isFinite(value)
777
+ };
778
+ case "date":
779
+ return { label: "a yyyy-mm-dd date", test: isDateValue };
780
+ case "datetime":
781
+ return {
782
+ label: "an ISO 8601 date and time with a time zone",
783
+ test: isDateTimeValue
784
+ };
785
+ default:
786
+ return { label: "a string", test: (value) => typeof value === "string" };
787
+ }
788
+ }
789
+ function conditionOptionErrors(at, name, target, values) {
790
+ const options = selectOptionValues(target);
791
+ const unknown = values.filter((value) => !options.includes(String(value)));
792
+ if (!unknown.length) return [];
793
+ return [
794
+ `${at}: ${unknown.map((value) => `'${String(value)}'`).join(", ")} not in the options of '${name}'`
795
+ ];
796
+ }
797
+ const ORDER_OPERATORS = ["gt", "gte", "lt", "lte"];
798
+ function conditionLeafErrors(at, key, condition, scope, scopeLabel) {
799
+ const name = condition.field;
800
+ const cat = `${at}, showIf on '${String(name)}'`;
801
+ if (typeof name !== "string" || !name || !Object.hasOwn(scope, name))
802
+ return [`${cat}: '${String(name)}' is not ${scopeLabel}`];
803
+ if (name === key) return [`${cat}: a field cannot depend on itself`];
804
+ const errors = [];
805
+ const target = scope[name];
806
+ const operators = CONDITION_OPERATORS;
807
+ const unknown = Object.keys(condition).filter((k) => k !== "field" && !operators.includes(k));
808
+ if (unknown.length)
809
+ errors.push(`${cat}: unknown showIf key ${unknown.map((k) => `'${k}'`).join(", ")}`);
810
+ const used = CONDITION_OPERATORS.filter((operator) => condition[operator] !== void 0);
811
+ if (!used.length) {
812
+ errors.push(`${cat}: showIf needs an operator (${CONDITION_OPERATORS.join(", ")})`);
813
+ return errors;
814
+ }
815
+ const type = conditionTypeName(target);
816
+ for (const operator of used) {
817
+ const value = condition[operator];
818
+ if (operator === "empty") {
819
+ if (target.type === "boolean")
820
+ errors.push(`${cat}: 'empty' does not apply to boolean fields, use 'eq'`);
821
+ else if (typeof value !== "boolean") errors.push(`${cat}: 'empty' must be true or false`);
822
+ continue;
823
+ }
824
+ if (isTranslatableField(target)) {
825
+ errors.push(
826
+ `${cat}: showIf cannot depend on a translatable field with '${operator}', only with 'empty'`
827
+ );
828
+ continue;
829
+ }
830
+ if (operator === "has") {
831
+ if (!isConditionListTarget(target))
832
+ errors.push(
833
+ `${cat}: showIf can only depend on ${CONDITION_LIST_TYPES.join(
834
+ ", "
835
+ )} fields with 'has' (got '${type}')`
836
+ );
837
+ else if (typeof value !== "string" || !value)
838
+ errors.push(`${cat}: 'has' needs a non-empty string`);
839
+ else if (target.type === "select")
840
+ errors.push(...conditionOptionErrors(cat, name, target, [value]));
841
+ continue;
842
+ }
843
+ const allowed = ORDER_OPERATORS.includes(operator) ? CONDITION_ORDER_TYPES : CONDITION_VALUE_TYPES;
844
+ if (!allowed.includes(target.type) || isMultiSelect(target)) {
845
+ errors.push(
846
+ `${cat}: showIf can only depend on ${allowed.join(
847
+ ", "
848
+ )} fields with '${operator}' (got '${type}')`
849
+ );
850
+ continue;
851
+ }
852
+ const isList = operator === "in" || operator === "notIn";
853
+ if (isList && (!Array.isArray(value) || !value.length)) {
854
+ errors.push(`${cat}: '${operator}' needs a non-empty list`);
855
+ continue;
856
+ }
857
+ const values = isList ? value : [value];
858
+ const expectation = conditionValueExpectation(target);
859
+ const wrong = values.filter((item) => !expectation.test(item));
860
+ if (wrong.length)
861
+ errors.push(
862
+ `${cat}: '${operator}' needs ${expectation.label} for a ${type} field (got ${wrong.map((item) => JSON.stringify(item)).join(", ")})`
863
+ );
864
+ else if (target.type === "select")
865
+ errors.push(...conditionOptionErrors(cat, name, target, values));
866
+ }
867
+ return errors;
868
+ }
869
+ function showIfErrors(at, key, showIf, scope, scopeLabel) {
870
+ const errors = [];
871
+ const visit = (node) => {
872
+ if (Array.isArray(node)) {
873
+ if (!node.length) errors.push(`${at}: showIf needs at least one condition`);
874
+ for (const child of node) visit(child);
875
+ return;
876
+ }
877
+ if (!node || typeof node !== "object") {
878
+ errors.push(`${at}: every showIf condition must be an object`);
879
+ return;
880
+ }
881
+ const record = node;
882
+ const group = CONDITION_GROUPS.find((name) => Object.hasOwn(record, name));
883
+ if (!group) {
884
+ errors.push(...conditionLeafErrors(at, key, record, scope, scopeLabel));
885
+ return;
886
+ }
887
+ if (Object.keys(record).length !== 1) {
888
+ errors.push(`${at}: a showIf group holds only one of 'all', 'any' or 'not'`);
889
+ return;
890
+ }
891
+ const inner = record[group];
892
+ if (group === "not") {
893
+ if (!inner || typeof inner !== "object" || Array.isArray(inner))
894
+ errors.push(`${at}: showIf 'not' takes one condition`);
895
+ else visit(inner);
896
+ return;
897
+ }
898
+ if (!Array.isArray(inner) || !inner.length) {
899
+ errors.push(`${at}: showIf '${group}' takes a non-empty list of conditions`);
900
+ return;
901
+ }
902
+ for (const child of inner) visit(child);
903
+ };
904
+ visit(showIf);
905
+ return errors;
906
+ }
907
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
908
+ function valueBoundsErrors(at, field) {
909
+ if (field.type === "blocks" || field.type === "tags") return [];
910
+ const errors = [];
911
+ const raw = field;
912
+ for (const name of ["minLength", "maxLength"])
913
+ if (raw[name] !== void 0)
914
+ errors.push(
915
+ `${at}: ${name} is not an option, use ${name.slice(0, 3)} (characters on text fields)`
916
+ );
917
+ if (field.min === void 0 && field.max === void 0) return errors;
918
+ if (field.type === "number") {
919
+ const isBound = (value) => typeof value === "number" && Number.isFinite(value);
920
+ if (field.min !== void 0 && !isBound(field.min)) errors.push(`${at}: min must be a number`);
921
+ if (field.max !== void 0 && !isBound(field.max)) errors.push(`${at}: max must be a number`);
922
+ if (isBound(field.min) && isBound(field.max) && field.min > field.max)
923
+ errors.push(`${at}: min (${field.min}) cannot be greater than max (${field.max})`);
924
+ return errors;
925
+ }
926
+ if (field.type === "text") {
927
+ const isLength = (value, least) => typeof value === "number" && Number.isInteger(value) && value >= least;
928
+ if (field.min !== void 0 && !isLength(field.min, 0))
929
+ errors.push(`${at}: min must be an integer of 0 or more (characters)`);
930
+ if (field.max !== void 0 && !isLength(field.max, 1))
931
+ errors.push(`${at}: max must be an integer of 1 or more (characters)`);
932
+ if (isLength(field.min, 0) && isLength(field.max, 1) && field.min > field.max)
933
+ errors.push(`${at}: min (${field.min}) cannot be greater than max (${field.max})`);
934
+ return errors;
935
+ }
936
+ errors.push(`${at}: min and max are only supported on number, text, blocks and tags fields`);
937
+ return errors;
938
+ }
939
+ function defaultValueError(field) {
940
+ const value = field.default;
941
+ switch (field.type) {
942
+ case "text":
943
+ case "richtext": {
944
+ if (typeof value !== "string") return "default must be a string";
945
+ if (field.type === "text" && value.trim()) {
946
+ if (field.min !== void 0 && value.length < field.min)
947
+ return `default is shorter than min (${field.min})`;
948
+ if (field.max !== void 0 && value.length > field.max)
949
+ return `default is longer than max (${field.max})`;
950
+ }
951
+ return null;
952
+ }
953
+ case "number":
954
+ if (typeof value !== "number" || !Number.isFinite(value)) return "default must be a number";
955
+ if (field.integer && !Number.isInteger(value)) return "default must be an integer";
956
+ if (field.min !== void 0 && value < field.min)
957
+ return `default is lower than min (${field.min})`;
958
+ if (field.max !== void 0 && value > field.max)
959
+ return `default is greater than max (${field.max})`;
960
+ return null;
961
+ case "boolean":
962
+ return typeof value === "boolean" ? null : "default must be true or false";
963
+ case "date":
964
+ return value === DEFAULT_TODAY || isDateValue(value) ? null : `default must be '${DEFAULT_TODAY}' or a yyyy-mm-dd date`;
965
+ case "datetime":
966
+ return value === DEFAULT_NOW || isDateTimeValue(value) ? null : `default must be '${DEFAULT_NOW}' or an ISO 8601 date and time with a time zone`;
967
+ case "email":
968
+ return typeof value === "string" && EMAIL_PATTERN.test(value) ? null : "default must be a valid email";
969
+ case "select": {
970
+ const options = selectOptionValues(field);
971
+ if (!field.multiple)
972
+ return typeof value === "string" && options.includes(value) ? null : `default must be one of the options (${options.join(", ")})`;
973
+ if (!Array.isArray(value)) return "default of a multiple select must be a list of options";
974
+ const unknown = value.filter((item) => typeof item !== "string" || !options.includes(item));
975
+ if (unknown.length)
976
+ return `default holds values that are not options: ${unknown.map((item) => JSON.stringify(item)).join(", ")}`;
977
+ return new Set(value).size === value.length ? null : "default repeats an option";
978
+ }
979
+ case "tags": {
980
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim()))
981
+ return "default must be a list of non-empty strings";
982
+ const count = normalizeTags(value).length;
983
+ return field.max !== void 0 && count > field.max ? `default holds more than max (${field.max}) tags` : null;
984
+ }
985
+ default:
986
+ return `default is only supported on ${DEFAULT_FIELD_TYPES.join(", ")} fields`;
987
+ }
988
+ }
989
+ function fieldOptionErrors(at, field, entry, scope) {
990
+ const errors = [];
991
+ const place = scope === "blocks" ? "inside blocks" : entry.kind === "page" ? "on pages" : null;
992
+ if (field.default !== void 0) {
993
+ if (place) errors.push(`${at}: default is not supported ${place}`);
994
+ else {
995
+ const problem = defaultValueError(field);
996
+ if (problem) errors.push(`${at}: ${problem}`);
997
+ }
998
+ }
999
+ if (field.unique !== void 0 && typeof field.unique !== "boolean")
1000
+ errors.push(`${at}: unique must be a boolean`);
1001
+ if (field.unique === true) {
1002
+ if (!UNIQUE_FIELD_TYPES.includes(field.type))
1003
+ errors.push(
1004
+ `${at}: unique is only supported on ${UNIQUE_FIELD_TYPES.join(", ")} fields${field.type === "slug" ? " (a slug is always unique)" : ""}`
1005
+ );
1006
+ else if (place) errors.push(`${at}: unique is not supported ${place}`);
1007
+ else if (entry.kind === "single")
1008
+ errors.push(`${at}: unique is not supported on singles, which hold one item`);
1009
+ else if (field.translatable)
1010
+ errors.push(`${at}: unique is not supported on translatable fields`);
1011
+ else if (field.default !== void 0)
1012
+ errors.push(
1013
+ `${at}: default is not supported on unique fields (every new item would get the same value)`
1014
+ );
1015
+ }
1016
+ if (field.immutable !== void 0 && typeof field.immutable !== "boolean")
1017
+ errors.push(`${at}: immutable must be a boolean`);
1018
+ if (field.immutable === true) {
1019
+ if (!IMMUTABLE_FIELD_TYPES.includes(field.type) || isManyToMany(field))
1020
+ errors.push(
1021
+ `${at}: immutable is only supported on ${IMMUTABLE_FIELD_TYPES.join(
1022
+ ", "
1023
+ )} fields (not many-to-many)`
1024
+ );
1025
+ else if (place) errors.push(`${at}: immutable is not supported ${place}`);
1026
+ else if (field.translatable)
1027
+ errors.push(`${at}: immutable is not supported on translatable fields`);
1028
+ }
1029
+ return errors;
1030
+ }
667
1031
  function tagsOptionErrors(at, field) {
668
1032
  const errors = [];
669
1033
  if (field.min !== void 0) errors.push(`${at}: min is not supported on tags fields`);
@@ -691,6 +1055,10 @@ function blockMetaErrors(at, block) {
691
1055
  errors.push(`${at}: icon must be a non-empty string`);
692
1056
  if (block.description !== void 0 && typeof block.description !== "string")
693
1057
  errors.push(`${at}: description must be a string`);
1058
+ if (block.group !== void 0 && (typeof block.group !== "string" || !block.group.trim()))
1059
+ errors.push(`${at}: group must be a non-empty string`);
1060
+ if (block.tags !== void 0 && !(Array.isArray(block.tags) && block.tags.every((tag) => typeof tag === "string" && !!tag.trim())))
1061
+ errors.push(`${at}: tags must be an array of non-empty strings`);
694
1062
  return errors;
695
1063
  }
696
1064
  function pageColumnsErrors(at, entry) {
@@ -722,6 +1090,13 @@ function pageColumnsErrors(at, entry) {
722
1090
  }
723
1091
  return errors;
724
1092
  }
1093
+ function uniqueTextColumns(config) {
1094
+ return Object.entries(config).filter(([, entry]) => !entry.table && entry.kind !== "page").flatMap(
1095
+ ([name, entry]) => Object.entries(entry.fields).filter(
1096
+ ([, field]) => isUniqueValueField(field) && field.type !== "number" && !isRequiredField(field)
1097
+ ).map(([key]) => ({ table: name, column: snakeCase(key) }))
1098
+ );
1099
+ }
725
1100
  function snakeCase(key) {
726
1101
  return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
727
1102
  }
@@ -748,6 +1123,7 @@ function columnExpr(key, field, dialect, table) {
748
1123
  break;
749
1124
  case "json":
750
1125
  case "blocks":
1126
+ case "link":
751
1127
  expr = jsonExpr(col, dialect);
752
1128
  break;
753
1129
  case "select":
@@ -768,6 +1144,7 @@ function columnExpr(key, field, dialect, table) {
768
1144
  default:
769
1145
  expr = `text('${col}')`;
770
1146
  }
1147
+ if (isUniqueValueField(field)) expr += ".unique()";
771
1148
  if (isRequiredField(field)) expr += ".notNull()";
772
1149
  return ` ${key}: ${expr},`;
773
1150
  }
@@ -863,6 +1240,35 @@ function usersTableExpr(dialect) {
863
1240
  updatedAt: ${nowExpr(pg, "updated_at")},
864
1241
  })`;
865
1242
  }
1243
+ function redirectsTableExpr(dialect) {
1244
+ const pg = dialect === "postgres";
1245
+ const tableFn = pg ? "pgTable" : "sqliteTable";
1246
+ return `export const cms_redirects = ${tableFn}('cms_redirects', {
1247
+ id: text('id').primaryKey(),
1248
+ from: text('from_path').notNull().unique(),
1249
+ to: text('to_path').notNull(),
1250
+ status: integer('status').notNull().default(301),
1251
+ keepQuery: ${pg ? `boolean('keep_query')` : `integer('keep_query', { mode: 'boolean' })`}.notNull().default(true),
1252
+ source: text('source').notNull().default('manual'),
1253
+ createdAt: ${nowExpr(pg, "created_at")},
1254
+ updatedAt: ${nowExpr(pg, "updated_at")},
1255
+ })`;
1256
+ }
1257
+ function previewLinksTableExpr(dialect) {
1258
+ const pg = dialect === "postgres";
1259
+ const tableFn = pg ? "pgTable" : "sqliteTable";
1260
+ return `export const cms_preview_links = ${tableFn}('cms_preview_links', {
1261
+ id: text('id').primaryKey(),
1262
+ tokenHash: text('token_hash').notNull().unique(),
1263
+ entry: text('entry').notNull(),
1264
+ itemId: text('item_id').notNull(),
1265
+ locale: text('locale'),
1266
+ createdBy: text('created_by'),
1267
+ createdAt: ${nowExpr(pg, "created_at")},
1268
+ expiresAt: text('expires_at').notNull(),
1269
+ revokedAt: text('revoked_at'),
1270
+ })`;
1271
+ }
866
1272
  function mediaTableExpr(dialect) {
867
1273
  const pg = dialect === "postgres";
868
1274
  const tableFn = pg ? "pgTable" : "sqliteTable";
@@ -875,6 +1281,8 @@ function mediaTableExpr(dialect) {
875
1281
  size: integer('size'),
876
1282
  width: integer('width'),
877
1283
  height: integer('height'),
1284
+ focalX: ${pg ? `doublePrecision('focal_x')` : `real('focal_x')`},
1285
+ focalY: ${pg ? `doublePrecision('focal_y')` : `real('focal_y')`},
878
1286
  createdAt: ${nowExpr(pg, "created_at")},
879
1287
  })`;
880
1288
  }
@@ -886,12 +1294,11 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
886
1294
  const rowsEntries = derived.filter(([, entry]) => entry.kind === "page");
887
1295
  const pg = dialect === "postgres";
888
1296
  const core = /* @__PURE__ */ new Set(["integer", "text", pg ? "pgTable" : "sqliteTable"]);
889
- if (fields.some((f) => f.type === "number" && !f.integer))
890
- core.add(pg ? "doublePrecision" : "real");
1297
+ core.add(pg ? "doublePrecision" : "real");
891
1298
  if (pg && fields.some((f) => f.type === "date")) core.add("date");
892
1299
  if (pg) core.add("boolean");
893
1300
  if (pg && fields.some(
894
- (f) => f.type === "json" || f.type === "blocks" || isTranslatableField(f) && !isTranslatableMediaField(f) || isStringListField(f)
1301
+ (f) => f.type === "json" || f.type === "blocks" || f.type === "link" || isTranslatableField(f) && !isTranslatableMediaField(f) || isStringListField(f)
895
1302
  ))
896
1303
  core.add("jsonb");
897
1304
  if (pg) core.add("timestamp");
@@ -919,6 +1326,8 @@ ${[
919
1326
  mediaTableExpr(dialect),
920
1327
  settingsTableExpr(dialect),
921
1328
  usersTableExpr(dialect),
1329
+ redirectsTableExpr(dialect),
1330
+ previewLinksTableExpr(dialect),
922
1331
  ...tables,
923
1332
  ...joins,
924
1333
  ...rows
@@ -926,38 +1335,47 @@ ${[
926
1335
  `;
927
1336
  }
928
1337
 
929
- const MEDIA_SELECTION = "key url type alt folder mime size width height";
930
- function blocksSelection(entryName, key, field) {
1338
+ const MEDIA_SELECTION = "key url type alt folder mime size width height focalX focalY";
1339
+ const LINK_SELECTION = "href label newTab external entry id";
1340
+ function fieldSelection(key, field) {
1341
+ if (field.type === "media") return `${key} { ${MEDIA_SELECTION} }`;
1342
+ if (field.type === "link") return `${key} { ${LINK_SELECTION} }`;
1343
+ return key;
1344
+ }
1345
+ function blocksSelection(entryName, schemaPath, field) {
931
1346
  const parts = ["__typename type"];
932
1347
  for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
933
1348
  const fields = Object.entries(block.fields).map(
934
- ([blockFieldKey, blockField]) => blockField.type === "media" ? `${blockFieldKey} { ${MEDIA_SELECTION} }` : blockFieldKey
1349
+ ([blockFieldKey, blockField]) => blockField.type === "blocks" ? `${blockFieldKey} { ${blocksSelection(
1350
+ entryName,
1351
+ [...schemaPath, blockName, blockFieldKey],
1352
+ blockField
1353
+ )} }` : fieldSelection(blockFieldKey, blockField)
935
1354
  );
936
1355
  if (!fields.length) continue;
937
- parts.push(`... on ${blockTypeName$1(entryName, key, blockName)} { ${fields.join(" ")} }`);
1356
+ parts.push(
1357
+ `... on ${blockTypeName$1(entryName, schemaPath, blockName)} { ${fields.join(" ")} }`
1358
+ );
938
1359
  }
939
1360
  return parts.join(" ");
940
1361
  }
941
1362
  function entrySelection(config, name, entry, withRelations, fields = entryFieldsFor(entry)) {
942
1363
  const parts = ["id"];
943
1364
  if (entry.kind === "page") parts.push("path");
1365
+ if (isRoutedContent(entry)) parts.push("path", "paths { locale path }");
944
1366
  for (const [key, field] of Object.entries(fields)) {
945
1367
  if (isPrivateField(field)) continue;
946
1368
  if (field.type === "relation") {
947
- const target = config[field.to];
1369
+ const target = config[relationTarget(field)];
948
1370
  if (!withRelations || !target) continue;
949
- parts.push(`${key} { ${entrySelection(config, field.to, target, false)} }`);
950
- continue;
951
- }
952
- if (field.type === "media") {
953
- parts.push(`${key} { ${MEDIA_SELECTION} }`);
1371
+ parts.push(`${key} { ${entrySelection(config, relationTarget(field), target, false)} }`);
954
1372
  continue;
955
1373
  }
956
1374
  if (field.type === "blocks") {
957
- parts.push(`${key} { ${blocksSelection(name, key, field)} }`);
1375
+ parts.push(`${key} { ${blocksSelection(name, [key], field)} }`);
958
1376
  continue;
959
1377
  }
960
- parts.push(key);
1378
+ parts.push(fieldSelection(key, field));
961
1379
  }
962
1380
  if (isCollectionKind(entry)) parts.push("createdAt");
963
1381
  parts.push("updatedAt");
@@ -986,6 +1404,19 @@ function contentItemQuery(config, name, entry) {
986
1404
  const selection = entrySelection(config, name, entry, true);
987
1405
  return `query CmsContent($slug: String!, $locale: String) { item: ${name}BySlug(slug: $slug, locale: $locale) { ${selection} } }`;
988
1406
  }
1407
+ function contentRouteQuery(config) {
1408
+ const names = routedContentNames(config);
1409
+ if (!names.length) return null;
1410
+ const fields = names.map(
1411
+ (name) => `${name}: ${name}ByPath(path: $path, locale: $locale) { ${entrySelection(
1412
+ config,
1413
+ name,
1414
+ config[name],
1415
+ true
1416
+ )} }`
1417
+ );
1418
+ return `query CmsRoute($path: String!, $locale: String) { ${fields.join(" ")} }`;
1419
+ }
989
1420
  function pageQuery(config, name, entry, path) {
990
1421
  const selection = entrySelection(config, name, entry, true, pageFields(entry, path));
991
1422
  return `query CmsPage($path: String!, $locale: String) { page: ${name}ByPath(path: $path, locale: $locale) { ${selection} } }`;
@@ -1042,6 +1473,10 @@ function renderQueriesFile(config) {
1042
1473
  `export const cmsContentItemQueries: Record<string, string> = {`,
1043
1474
  ...contentItems,
1044
1475
  `}`,
1476
+ ``,
1477
+ `export const cmsRouteEntries: string[] = ${JSON.stringify(routedContentNames(config))}`,
1478
+ ``,
1479
+ `export const cmsRouteQuery: string | null = ${JSON.stringify(contentRouteQuery(config))}`,
1045
1480
  ``
1046
1481
  ].join("\n");
1047
1482
  }
@@ -1066,11 +1501,12 @@ function scalarTsType(field) {
1066
1501
  }
1067
1502
  function fieldTsType(config, entryName, key, field) {
1068
1503
  if (field.type === "relation") {
1069
- const target = typeName(field.to);
1504
+ const target = typeName(relationTarget(field));
1070
1505
  if (field.cardinality === "many-to-many") return `${target}[]`;
1071
- return isRequiredField(field) && !config[field.to]?.drafts ? target : `${target} | null`;
1506
+ return isRequiredField(field) && !config[relationTarget(field)]?.drafts ? target : `${target} | null`;
1072
1507
  }
1073
1508
  if (field.type === "media") return `${mediaTsType(field)} | null`;
1509
+ if (field.type === "link") return "CmsLink | null";
1074
1510
  if (field.type === "tags") return "string[]";
1075
1511
  if (field.type === "select" && field.multiple) {
1076
1512
  const union = selectOptionValues(field).map((o) => JSON.stringify(o)).join(" | ");
@@ -1084,20 +1520,32 @@ function fieldTsType(config, entryName, key, field) {
1084
1520
  const base = scalarTsType(field);
1085
1521
  return isRequiredField(field) ? base : `${base} | null`;
1086
1522
  }
1087
- function blockPropsTypeName(entryName, fieldKey, blockName) {
1088
- return `${blockTypeName$1(entryName, fieldKey, blockName)}Props`;
1523
+ function blockPropsTypeName(entryName, schemaPath, blockName) {
1524
+ return `${blockTypeName$1(entryName, schemaPath, blockName)}Props`;
1525
+ }
1526
+ function blockFieldTs(entryName, schemaPath, blockName, key, field) {
1527
+ if (field.type === "media") return mediaTsType(field);
1528
+ if (field.type === "link") return "CmsLink";
1529
+ if (field.type === "blocks")
1530
+ return `${blocksFieldTypeName(entryName, [...schemaPath, blockName, key])}[]`;
1531
+ return scalarTsType(field);
1089
1532
  }
1090
- function blockTypesTs(entryName, key, field) {
1533
+ function blockTypesTs(entryName, schemaPath, field) {
1091
1534
  const defs = [];
1535
+ const nested = [];
1092
1536
  const members = [];
1093
1537
  for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
1094
- const name = blockTypeName$1(entryName, key, blockName);
1095
- const props = blockPropsTypeName(entryName, key, blockName);
1538
+ const name = blockTypeName$1(entryName, schemaPath, blockName);
1539
+ const props = blockPropsTypeName(entryName, schemaPath, blockName);
1096
1540
  members.push(name);
1097
1541
  const lines = [];
1098
1542
  for (const [blockFieldKey, blockField] of Object.entries(block.fields)) {
1099
- const base = blockField.type === "media" ? mediaTsType(blockField) : scalarTsType(blockField);
1543
+ const base = blockFieldTs(entryName, schemaPath, blockName, blockFieldKey, blockField);
1100
1544
  lines.push(` ${blockFieldKey}: ${base} | null`);
1545
+ if (blockField.type === "blocks")
1546
+ nested.push(
1547
+ ...blockTypesTs(entryName, [...schemaPath, blockName, blockFieldKey], blockField)
1548
+ );
1101
1549
  }
1102
1550
  defs.push(`export interface ${props} {
1103
1551
  ${lines.join("\n")}
@@ -1110,12 +1558,15 @@ ${lines.join("\n")}
1110
1558
  );
1111
1559
  }
1112
1560
  if (members.length)
1113
- defs.push(`export type ${blocksFieldTypeName(entryName, key)} = ${members.join(" | ")}`);
1114
- return defs;
1561
+ defs.push(
1562
+ `export type ${blocksFieldTypeName(entryName, schemaPath)} = ${members.join(" | ")}`
1563
+ );
1564
+ return [...defs, ...nested];
1115
1565
  }
1116
1566
  function entryTs(config, name, entry) {
1117
1567
  const lines = [" id: string"];
1118
1568
  if (entry.kind === "page") lines.push(" path: string");
1569
+ if (isRoutedContent(entry)) lines.push(" path: string", " paths: CmsLocalePath[]");
1119
1570
  for (const [key, field] of Object.entries(entryFieldsFor(entry))) {
1120
1571
  if (isPrivateField(field)) continue;
1121
1572
  lines.push(` ${key}: ${fieldTsType(config, name, key, field)}`);
@@ -1144,9 +1595,9 @@ function autoTypeTs(config, name, entry) {
1144
1595
  if (!keys.length) return `export type ${auto} = ${typeName(name)}`;
1145
1596
  const lines = keys.map((key) => {
1146
1597
  const field = entry.fields[key];
1147
- const shallow = shallowTypeTs(config, field.to);
1598
+ const shallow = shallowTypeTs(config, relationTarget(field));
1148
1599
  if (field.cardinality === "many-to-many") return ` ${key}: ${shallow}[]`;
1149
- const nonNull = isRequiredField(field) && !config[field.to]?.drafts;
1600
+ const nonNull = isRequiredField(field) && !config[relationTarget(field)]?.drafts;
1150
1601
  return ` ${key}: ${shallow}${nonNull ? "" : " | null"}`;
1151
1602
  });
1152
1603
  return `export type ${auto} = Omit<${typeName(name)}, ${quotedKeys(keys)}> & {
@@ -1185,6 +1636,7 @@ function entryMapsTs(config) {
1185
1636
  else if (entry.kind === "content") contents.push(line);
1186
1637
  else collections.push(line);
1187
1638
  }
1639
+ const routed = routedContentNames(config);
1188
1640
  if (!pages.length) {
1189
1641
  pages.push(
1190
1642
  `export interface CmsPageTypes {
@@ -1206,6 +1658,12 @@ ${contents.join("\n")}
1206
1658
  `export type CmsSingleName = keyof CmsSingleTypes`,
1207
1659
  `export type CmsCollectionName = keyof CmsCollectionTypes`,
1208
1660
  `export type CmsContentName = keyof CmsContentTypes`,
1661
+ `export type CmsRoutedContentName = ${routed.length ? quotedKeys(routed) : "never"}`,
1662
+ [
1663
+ `export type CmsContentRouteMatch = {`,
1664
+ ` [K in CmsRoutedContentName]: { entry: K; item: CmsContentTypes[K] }`,
1665
+ `}[CmsRoutedContentName]`
1666
+ ].join("\n"),
1209
1667
  ...pages
1210
1668
  ];
1211
1669
  }
@@ -1213,15 +1671,21 @@ function blockPropsMapTs(config) {
1213
1671
  const entries = [];
1214
1672
  for (const [name, entry] of Object.entries(config)) {
1215
1673
  const fields = [];
1216
- for (const [key, field] of Object.entries(entryFieldsFor(entry))) {
1217
- if (field.type !== "blocks" || isPrivateField(field)) continue;
1674
+ for (const { field, schemaPath } of blocksFieldPaths(entryFieldsFor(entry))) {
1675
+ if (isPrivateField(field)) continue;
1218
1676
  const blocks = Object.keys(field.blocks ?? {}).map(
1219
- (blockName) => ` ${JSON.stringify(blockName)}: ${blockPropsTypeName(name, key, blockName)}`
1677
+ (blockName) => ` ${JSON.stringify(blockName)}: ${blockPropsTypeName(
1678
+ name,
1679
+ schemaPath,
1680
+ blockName
1681
+ )}`
1220
1682
  );
1221
1683
  if (blocks.length)
1222
- fields.push(` ${JSON.stringify(key)}: {
1684
+ fields.push(
1685
+ ` ${JSON.stringify(schemaPath.join("."))}: {
1223
1686
  ${blocks.join("\n")}
1224
- }`);
1687
+ }`
1688
+ );
1225
1689
  }
1226
1690
  if (fields.length) entries.push(` ${JSON.stringify(name)}: {
1227
1691
  ${fields.join("\n")}
@@ -1245,7 +1709,19 @@ ${entries.join("\n")}
1245
1709
  }
1246
1710
  function renderTypesFile(config) {
1247
1711
  const parts = [
1712
+ `export interface CmsLocalePath {
1713
+ locale: string
1714
+ path: string
1715
+ }`,
1248
1716
  `export type CmsMediaType = 'image' | 'video' | 'file'`,
1717
+ `export interface CmsLink {
1718
+ href: string | null
1719
+ label: string | null
1720
+ newTab: boolean
1721
+ external: boolean
1722
+ entry: string | null
1723
+ id: string | null
1724
+ }`,
1249
1725
  `export interface CmsMedia<T extends CmsMediaType = CmsMediaType> {
1250
1726
  key: string
1251
1727
  url: string | null
@@ -1256,12 +1732,14 @@ function renderTypesFile(config) {
1256
1732
  size: number | null
1257
1733
  width: number | null
1258
1734
  height: number | null
1735
+ focalX: number | null
1736
+ focalY: number | null
1259
1737
  }`
1260
1738
  ];
1261
1739
  for (const [name, entry] of Object.entries(config)) {
1262
1740
  for (const [key, field] of Object.entries(entryFieldsFor(entry))) {
1263
1741
  if (field.type === "blocks" && !isPrivateField(field))
1264
- parts.push(...blockTypesTs(name, key, field));
1742
+ parts.push(...blockTypesTs(name, [key], field));
1265
1743
  }
1266
1744
  parts.push(entryTs(config, name, entry));
1267
1745
  }
@@ -1282,6 +1760,107 @@ function resolveCmsEnabled(explicit, envValue) {
1282
1760
  return true;
1283
1761
  }
1284
1762
 
1763
+ const DEFAULT_WEBHOOK_TIMEOUT = 5e3;
1764
+ const MAX_WEBHOOK_TIMEOUT = 3e4;
1765
+ const MIN_WEBHOOK_SECRET_LENGTH = 16;
1766
+ const WEBHOOK_KEYS = /* @__PURE__ */ new Set(["name", "url", "secret", "events", "timeout"]);
1767
+ const WEBHOOK_NAME_PATTERN = /^[a-z]+(?:[A-Z][a-z]+)*[0-9]*$/;
1768
+ const WEBHOOK_EVENT_FILTERS = [
1769
+ "entry",
1770
+ "media",
1771
+ ...CMS_ENTRY_ACTIONS.map((action) => `entry:${action}`),
1772
+ ...CMS_MEDIA_ACTIONS.map((action) => `media:${action}`)
1773
+ ];
1774
+ function webhookEnvName(name, key) {
1775
+ return `NUXT_CMS_WEBHOOKS_${name.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase()}_${key.toUpperCase()}`;
1776
+ }
1777
+ function isRecord(value) {
1778
+ return !!value && typeof value === "object" && !Array.isArray(value);
1779
+ }
1780
+ function urlError(label, url) {
1781
+ try {
1782
+ const parsed = new URL(url);
1783
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") return null;
1784
+ } catch {
1785
+ return `${label}.url '${url}' is not a valid URL`;
1786
+ }
1787
+ return `${label}.url '${url}' must use http or https`;
1788
+ }
1789
+ function resolveWebhooks(input, env = {}) {
1790
+ const webhooks = {};
1791
+ const errors = [];
1792
+ const warnings = [];
1793
+ if (input === void 0) return { webhooks, errors, warnings };
1794
+ if (!Array.isArray(input)) {
1795
+ return {
1796
+ webhooks,
1797
+ errors: ["webhooks must be an array of { name, url, secret, events }"],
1798
+ warnings
1799
+ };
1800
+ }
1801
+ input.forEach((item, index) => {
1802
+ const label = `webhooks[${index}]`;
1803
+ if (!isRecord(item)) {
1804
+ errors.push(`${label} must be an object`);
1805
+ return;
1806
+ }
1807
+ for (const key of Object.keys(item)) {
1808
+ if (!WEBHOOK_KEYS.has(key)) errors.push(`${label} has an unknown key '${key}'`);
1809
+ }
1810
+ const { name, url, secret, events, timeout } = item;
1811
+ if (typeof name !== "string" || !WEBHOOK_NAME_PATTERN.test(name)) {
1812
+ errors.push(
1813
+ `${label}.name must be a camelCase identifier like 'deploy' or 'searchIndex', got ${JSON.stringify(
1814
+ name
1815
+ )}`
1816
+ );
1817
+ return;
1818
+ }
1819
+ const named = `webhooks '${name}'`;
1820
+ if (Object.hasOwn(webhooks, name)) {
1821
+ errors.push(`${named} is declared twice`);
1822
+ return;
1823
+ }
1824
+ if (url !== void 0 && typeof url !== "string") errors.push(`${named}.url must be a string`);
1825
+ else if (url) {
1826
+ const error = urlError(named, url);
1827
+ if (error) errors.push(error);
1828
+ } else if (!env[webhookEnvName(name, "url")]) {
1829
+ warnings.push(
1830
+ `${named} has no url: set ${webhookEnvName(name, "url")} at runtime or it never fires`
1831
+ );
1832
+ }
1833
+ if (secret !== void 0 && typeof secret !== "string")
1834
+ errors.push(`${named}.secret must be a string`);
1835
+ else if (secret && secret.length < MIN_WEBHOOK_SECRET_LENGTH)
1836
+ errors.push(`${named}.secret must be at least ${MIN_WEBHOOK_SECRET_LENGTH} characters`);
1837
+ if (events !== void 0) {
1838
+ if (!Array.isArray(events) || !events.length) {
1839
+ errors.push(`${named}.events must be a non-empty array (omit it for every event)`);
1840
+ } else {
1841
+ for (const value of events) {
1842
+ if (!WEBHOOK_EVENT_FILTERS.includes(value))
1843
+ errors.push(
1844
+ `${named}.events has an unknown event ${JSON.stringify(
1845
+ value
1846
+ )}: use 'entry', 'media', 'entry:<action>' or 'media:<action>'`
1847
+ );
1848
+ }
1849
+ }
1850
+ }
1851
+ if (timeout !== void 0 && (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout < 1 || timeout > MAX_WEBHOOK_TIMEOUT)) {
1852
+ errors.push(`${named}.timeout must be an integer between 1 and ${MAX_WEBHOOK_TIMEOUT} ms`);
1853
+ }
1854
+ webhooks[name] = {
1855
+ url: typeof url === "string" ? url : "",
1856
+ secret: typeof secret === "string" ? secret : "",
1857
+ events: Array.isArray(events) ? [...new Set(events)] : [],
1858
+ timeout: typeof timeout === "number" ? timeout : DEFAULT_WEBHOOK_TIMEOUT
1859
+ };
1860
+ });
1861
+ return { webhooks, errors, warnings };
1862
+ }
1863
+
1285
1864
  function resolveDatabaseOptions(database) {
1286
1865
  const shared = {
1287
1866
  migrateOnBoot: database?.migrateOnBoot ?? true,
@@ -1312,6 +1891,9 @@ function resolveDatabaseOptions(database) {
1312
1891
  }
1313
1892
  return { ...shared, driver: "sqlite", path: database?.path ?? "data/cms.db" };
1314
1893
  }
1894
+ const MODULE_VERSION = JSON.parse(
1895
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
1896
+ ).version;
1315
1897
  const DEFAULT_FILESYSTEM_MEDIA_DIR = "data/media";
1316
1898
  const DEFAULT_FILESYSTEM_MEDIA_BASE_URL = "/media";
1317
1899
  function resolveMediaOptions(media) {
@@ -1377,11 +1959,69 @@ function resolveModuleOptions(options) {
1377
1959
  },
1378
1960
  preview: {
1379
1961
  path: options.preview?.path ?? DEFAULT_CMS_PREVIEW_PATH,
1380
- component: options.preview?.component?.trim() ?? ""
1962
+ component: options.preview?.component?.trim() ?? "",
1963
+ banner: options.preview?.banner ?? true
1381
1964
  }
1382
1965
  };
1383
1966
  }
1384
1967
  const moduleRequire = createRequire(import.meta.url);
1968
+ function resolveSiteUrlOption(nuxt, options) {
1969
+ const existing = nuxt.options.runtimeConfig.public.siteUrl;
1970
+ const nuxtSite = nuxt.options.site?.url;
1971
+ const candidates = [
1972
+ options.site?.url,
1973
+ typeof existing === "string" ? existing : void 0,
1974
+ typeof nuxtSite === "string" ? nuxtSite : void 0,
1975
+ process.env.NUXT_PUBLIC_SITE_URL
1976
+ ];
1977
+ return normalizeSiteUrl(candidates.find((candidate) => candidate?.trim()));
1978
+ }
1979
+ function setupSiteUrl(nuxt, options, logger) {
1980
+ const errors = seoOptionErrors(options.seo, options.site);
1981
+ if (errors.length) throw new Error(`[nuxt-cms] ${errors.join("\n")}`);
1982
+ const existing = nuxt.options.runtimeConfig.public.siteUrl;
1983
+ const siteUrl = resolveSiteUrlOption(nuxt, options);
1984
+ if (options.site?.url && typeof existing === "string" && existing.trim() && normalizeSiteUrl(existing) !== siteUrl) {
1985
+ logger.warn(
1986
+ `[nuxt-cms] cms.site.url (${siteUrl}) replaces runtimeConfig.public.siteUrl (${existing}).`
1987
+ );
1988
+ }
1989
+ nuxt.options.runtimeConfig.public.siteUrl = siteUrl;
1990
+ nuxt.options.runtimeConfig.public.cmsTrustForwardedHost = options.site?.trustForwardedHost === true;
1991
+ }
1992
+ function setupSeoRoutes(nuxt, resolver, seo, publicDir, logger) {
1993
+ nuxt.hook("modules:done", () => {
1994
+ const installed = (names) => names.filter((name) => hasNuxtModule(name, nuxt));
1995
+ const publicFile = (file) => existsSync(join(publicDir, file)) ? [`public/${file}`] : [];
1996
+ const sitemapConflicts = [
1997
+ ...installed(SITEMAP_CONFLICT_MODULES),
1998
+ ...publicFile(SITEMAP_PATH.slice(1))
1999
+ ];
2000
+ const switches = resolveSeoSwitches(seo, {
2001
+ sitemap: sitemapConflicts,
2002
+ robots: [...installed(ROBOTS_CONFLICT_MODULES), ...publicFile(ROBOTS_PATH.slice(1))]
2003
+ });
2004
+ for (const warning of switches.warnings) logger.warn(warning);
2005
+ const siteUrlWarning = nuxt.options.runtimeConfig.public.siteUrl ? null : missingSiteUrlWarning(switches);
2006
+ if (siteUrlWarning && !nuxt.options.dev) logger.warn(siteUrlWarning);
2007
+ const runtime = nuxt.options.runtimeConfig.cms;
2008
+ runtime.seo = {
2009
+ sitemap: switches.sitemap,
2010
+ robots: switches.robots,
2011
+ robotsSitemap: switches.sitemap || sitemapConflicts.length > 0
2012
+ };
2013
+ if (switches.sitemap)
2014
+ addServerHandler({
2015
+ route: SITEMAP_PATH,
2016
+ handler: resolver.resolve("./runtime/server/routes/sitemap")
2017
+ });
2018
+ if (switches.robots)
2019
+ addServerHandler({
2020
+ route: ROBOTS_PATH,
2021
+ handler: resolver.resolve("./runtime/server/routes/robots")
2022
+ });
2023
+ });
2024
+ }
1385
2025
  function resolveImport(specifier) {
1386
2026
  try {
1387
2027
  return fileURLToPath(import.meta.resolve(specifier)).replace(/\\/g, "/");
@@ -1528,6 +2168,30 @@ function addCmsBlocks(nuxt, resolver, cmsConfig, previewComponent) {
1528
2168
  name: "CmsBlocks",
1529
2169
  filePath: resolver.resolve("./runtime/app/blocks/CmsBlocks.vue")
1530
2170
  });
2171
+ addComponent({
2172
+ name: "CmsLink",
2173
+ filePath: resolver.resolve("./runtime/app/links/CmsLink.vue")
2174
+ });
2175
+ addComponent({
2176
+ name: "CmsField",
2177
+ filePath: resolver.resolve("./runtime/app/blocks/CmsField")
2178
+ });
2179
+ addPlugin(resolver.resolve("./runtime/app/plugins/cms-field"));
2180
+ addTypeTemplate({
2181
+ filename: "types/nuxt-cms-field.d.ts",
2182
+ getContents: () => [
2183
+ "import type { Directive } from 'vue'",
2184
+ "",
2185
+ "declare module 'vue' {",
2186
+ " interface GlobalDirectives {",
2187
+ " vCmsField: Directive<HTMLElement, string>",
2188
+ " }",
2189
+ "}",
2190
+ "",
2191
+ "export {}",
2192
+ ""
2193
+ ].join("\n")
2194
+ });
1531
2195
  if (!refs.length && !previews.length) return;
1532
2196
  nuxt.hook("components:extend", (components) => {
1533
2197
  const names = components.map((component) => component.pascalName);
@@ -1540,6 +2204,23 @@ function addCmsBlocks(nuxt, resolver, cmsConfig, previewComponent) {
1540
2204
  ${errors.join("\n")}`);
1541
2205
  });
1542
2206
  }
2207
+ function addCmsImage(nuxt, resolver) {
2208
+ const imageTemplate = addTemplate({
2209
+ filename: "cms/image.ts",
2210
+ write: true,
2211
+ getContents: () => renderImageFile(
2212
+ resolveCmsImageSetup(
2213
+ hasNuxtModule("@nuxt/image", nuxt),
2214
+ nuxt.options
2215
+ )
2216
+ )
2217
+ });
2218
+ nuxt.options.alias["#cms-image"] = imageTemplate.dst;
2219
+ addComponent({
2220
+ name: "CmsImage",
2221
+ filePath: resolver.resolve("./runtime/app/image/CmsImage.vue")
2222
+ });
2223
+ }
1543
2224
  const module$1 = defineNuxtModule({
1544
2225
  meta: {
1545
2226
  name: "@xleddyl/nuxt-cms",
@@ -1602,6 +2283,13 @@ const module$1 = defineNuxtModule({
1602
2283
  const resolver = createResolver(import.meta.url);
1603
2284
  const logger = useLogger("nuxt-cms");
1604
2285
  const resolved = resolveModuleOptions(options);
2286
+ const webhooks = resolveWebhooks(options.webhooks, process.env);
2287
+ if (webhooks.errors.length) {
2288
+ throw new Error(`[nuxt-cms] Invalid webhooks:
2289
+ ${webhooks.errors.join("\n")}`);
2290
+ }
2291
+ for (const warning of webhooks.warnings) logger.warn(`[nuxt-cms] ${warning}`);
2292
+ setupSiteUrl(nuxt, options, logger);
1605
2293
  if (!resolveCmsEnabled(options.enabled, process.env[CMS_ENABLED_ENV])) {
1606
2294
  const queryStub = resolver.resolve("./runtime/app/composables/cms-query-disabled");
1607
2295
  const entryStub = resolver.resolve("./runtime/app/composables/cms-entry-disabled");
@@ -1612,7 +2300,17 @@ const module$1 = defineNuxtModule({
1612
2300
  { name: "useCmsCollection", from: entryStub },
1613
2301
  { name: "useCmsPage", from: entryStub },
1614
2302
  { name: "useCmsContents", from: entryStub },
1615
- { name: "useCmsContent", from: entryStub }
2303
+ { name: "useCmsContent", from: entryStub },
2304
+ { name: "useCmsRoute", from: entryStub },
2305
+ {
2306
+ name: "useCmsSite",
2307
+ from: resolver.resolve("./runtime/app/composables/cms-site-disabled")
2308
+ },
2309
+ { name: "useCmsSeo", from: resolver.resolve("./runtime/app/composables/cms-seo") },
2310
+ {
2311
+ name: "useCmsPreviewMode",
2312
+ from: resolver.resolve("./runtime/app/composables/cms-preview-mode")
2313
+ }
1616
2314
  ]);
1617
2315
  const disabledConfig = await loadCmsConfig(
1618
2316
  nuxt,
@@ -1623,12 +2321,14 @@ const module$1 = defineNuxtModule({
1623
2321
  );
1624
2322
  addCmsTypeTemplates(nuxt, disabledConfig);
1625
2323
  addCmsBlocks(nuxt, resolver, disabledConfig, resolved.preview.component);
2324
+ addCmsImage(nuxt, resolver);
1626
2325
  nuxt.options.runtimeConfig.public.cms = {
1627
2326
  mediaBaseUrl: resolved.media.publicBaseUrl,
1628
2327
  mediaStorage: resolved.media.storage,
1629
2328
  mediaMaxFileSize: resolved.media.maxFileSize,
1630
2329
  i18n: resolved.i18n,
1631
- previewPath: resolved.preview.path
2330
+ previewPath: resolved.preview.path,
2331
+ version: MODULE_VERSION
1632
2332
  };
1633
2333
  logger.info(
1634
2334
  "[nuxt-cms] disabled: registering no-op query composables and generated types, skipping admin, server and database setup"
@@ -1674,19 +2374,61 @@ const module$1 = defineNuxtModule({
1674
2374
  });
1675
2375
  nuxt.options.alias["#cms-tables"] = schemaTemplate.dst;
1676
2376
  addCmsTypeTemplates(nuxt, cmsConfig);
2377
+ const roles = validateRoles(options.roles, cmsConfig);
2378
+ if (roles.errors.length) {
2379
+ for (const error of roles.errors) logger.error(error);
2380
+ throw new Error(
2381
+ `[nuxt-cms] Invalid cms.roles (${roles.errors.length} error${roles.errors.length > 1 ? "s" : ""})`
2382
+ );
2383
+ }
2384
+ for (const warning of roles.warnings) logger.warn(`[nuxt-cms] ${warning}`);
2385
+ const rolesTypes = resolver.resolve("./runtime/shared/roles").replace(/\\/g, "/");
2386
+ addTemplate({
2387
+ filename: "cms/roles.d.ts",
2388
+ write: true,
2389
+ getContents: () => [
2390
+ `import type { CmsRoles } from '${rolesTypes}'`,
2391
+ ``,
2392
+ `declare const roles: CmsRoles`,
2393
+ ``,
2394
+ `export default roles`,
2395
+ ``
2396
+ ].join("\n")
2397
+ });
2398
+ const rolesTemplate = addTemplate({
2399
+ filename: "cms/roles.js",
2400
+ write: true,
2401
+ getContents: () => `export default ${JSON.stringify(roles.roles, null, 3)}
2402
+ `
2403
+ });
2404
+ nuxt.options.alias["#cms-roles"] = rolesTemplate.dst;
1677
2405
  const queryComposables = resolver.resolve("./runtime/app/composables/cms-query");
1678
2406
  const entryComposables = resolver.resolve("./runtime/app/composables/cms-entry");
1679
2407
  addImports([
2408
+ { name: "useCmsSite", from: resolver.resolve("./runtime/app/composables/cms-site") },
2409
+ { name: "useCmsSeo", from: resolver.resolve("./runtime/app/composables/cms-seo") },
1680
2410
  { name: "useCms", from: queryComposables },
1681
2411
  { name: "$cmsQuery", from: queryComposables },
1682
2412
  { name: "useCmsSingle", from: entryComposables },
1683
2413
  { name: "useCmsCollection", from: entryComposables },
1684
2414
  { name: "useCmsPage", from: entryComposables },
1685
2415
  { name: "useCmsContents", from: entryComposables },
1686
- { name: "useCmsContent", from: entryComposables }
2416
+ { name: "useCmsContent", from: entryComposables },
2417
+ { name: "useCmsRoute", from: entryComposables },
2418
+ {
2419
+ name: "useCmsPreviewMode",
2420
+ from: resolver.resolve("./runtime/app/composables/cms-preview-mode")
2421
+ }
1687
2422
  ]);
1688
2423
  addCmsBlocks(nuxt, resolver, cmsConfig, resolved.preview.component);
1689
- const previewErrors = previewPathErrors(resolved.preview.path, Object.keys(cmsConfig));
2424
+ addCmsImage(nuxt, resolver);
2425
+ addPlugin(resolver.resolve("./runtime/app/plugins/cms-preview"));
2426
+ if (resolved.preview.banner)
2427
+ addPlugin(resolver.resolve("./runtime/app/plugins/cms-preview-banner.client"));
2428
+ const previewErrors = [
2429
+ ...previewPathErrors(resolved.preview.path, Object.keys(cmsConfig)),
2430
+ ...previewBannerErrors(options.preview?.banner)
2431
+ ];
1690
2432
  if (previewErrors.length) throw new Error(`[nuxt-cms] ${previewErrors.join("\n")}`);
1691
2433
  const previewPath = resolved.preview.path;
1692
2434
  const previewRule = nuxt.options.routeRules?.[previewPath] ?? {};
@@ -1729,6 +2471,16 @@ const module$1 = defineNuxtModule({
1729
2471
  const relativeSchemaPath = relative(nuxt.options.rootDir, schemaTemplate.dst);
1730
2472
  const relativeMigrationsDir = relative(nuxt.options.rootDir, migrationsDir);
1731
2473
  const toPosix = (path) => path.replace(/\\/g, "/");
2474
+ const migrationFixes = {
2475
+ dialect,
2476
+ out: toPosix(migrationsDir),
2477
+ uniqueTextColumns: uniqueTextColumns(cmsConfig)
2478
+ };
2479
+ addTemplate({
2480
+ filename: `cms/${MIGRATION_FIXES_FILE}`,
2481
+ write: true,
2482
+ getContents: () => JSON.stringify(migrationFixes, null, 2)
2483
+ });
1732
2484
  addTemplate({
1733
2485
  filename: "cms/drizzle.config.ts",
1734
2486
  write: true,
@@ -1803,6 +2555,9 @@ const module$1 = defineNuxtModule({
1803
2555
  )
1804
2556
  );
1805
2557
  addServerPlugin(resolver.resolve("./runtime/server/plugins/session-validate"));
2558
+ if (Object.keys(webhooks.webhooks).length) {
2559
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/webhooks"));
2560
+ }
1806
2561
  if (!nuxt.options.dev) {
1807
2562
  addServerPlugin(resolver.resolve("./runtime/server/plugins/session-check"));
1808
2563
  }
@@ -1819,11 +2574,22 @@ const module$1 = defineNuxtModule({
1819
2574
  `--schema=${relativeSchemaPath}`,
1820
2575
  `--out=${relativeMigrationsDir}`
1821
2576
  ];
2577
+ const before = migrationSqlFiles(migrationsDir);
1822
2578
  const code = await new Promise((done) => {
1823
2579
  spawn(process.execPath, args, { cwd: nuxt.options.rootDir, stdio: "inherit" }).on("close", (exitCode) => done(exitCode ?? 1)).on("error", () => done(1));
1824
2580
  });
1825
- if (code !== 0)
2581
+ if (code !== 0) {
1826
2582
  logger.error(`[nuxt-cms] drizzle-kit generate failed (exit code ${code})`);
2583
+ return;
2584
+ }
2585
+ const fixed = fixNewMigrations(
2586
+ migrationsDir,
2587
+ before,
2588
+ migrationFixes.uniqueTextColumns,
2589
+ dialect
2590
+ );
2591
+ for (const file of fixed)
2592
+ logger.info(`[nuxt-cms] ${file}: blank values of new unique fields become NULL`);
1827
2593
  });
1828
2594
  }
1829
2595
  const mediaFilesystemBase = resolved.media.storage === "filesystem" ? `/${resolved.media.publicBaseUrl.split("/").filter(Boolean).join("/")}` : "";
@@ -1867,14 +2633,20 @@ const module$1 = defineNuxtModule({
1867
2633
  localRoot: mediaLocalRoot,
1868
2634
  dir: mediaDir,
1869
2635
  ...existingConfig.media ?? {}
1870
- }
2636
+ },
2637
+ webhooks: {
2638
+ ...webhooks.webhooks,
2639
+ ...existingConfig.webhooks ?? {}
2640
+ },
2641
+ seo: { sitemap: false, robots: false, robotsSitemap: false }
1871
2642
  };
1872
2643
  nuxt.options.runtimeConfig.public.cms = {
1873
2644
  mediaBaseUrl: resolved.media.publicBaseUrl,
1874
2645
  mediaStorage: resolved.media.storage,
1875
2646
  mediaMaxFileSize: resolved.media.maxFileSize,
1876
2647
  i18n: resolved.i18n,
1877
- previewPath
2648
+ previewPath,
2649
+ version: MODULE_VERSION
1878
2650
  };
1879
2651
  addTypeTemplate(
1880
2652
  {
@@ -1887,7 +2659,7 @@ const module$1 = defineNuxtModule({
1887
2659
  " id?: string",
1888
2660
  " email: string",
1889
2661
  " name?: string | null",
1890
- " role?: 'superadmin' | 'admin'",
2662
+ " role?: string",
1891
2663
  " firstLogin?: boolean",
1892
2664
  " passwordStamp?: string",
1893
2665
  " }",
@@ -1913,6 +2685,25 @@ const module$1 = defineNuxtModule({
1913
2685
  },
1914
2686
  { nuxt: true, nitro: true }
1915
2687
  );
2688
+ const hooksTypes = toPosix(resolver.resolve("./runtime/shared/hooks"));
2689
+ addTypeTemplate(
2690
+ {
2691
+ filename: "types/nuxt-cms-hooks.d.ts",
2692
+ getContents: () => [
2693
+ `import type { CmsRuntimeHooks } from '${hooksTypes}'`,
2694
+ "",
2695
+ ...["nitropack", "nitropack/types"].flatMap((name) => [
2696
+ `declare module '${name}' {`,
2697
+ " interface NitroRuntimeHooks extends CmsRuntimeHooks {}",
2698
+ "}",
2699
+ ""
2700
+ ]),
2701
+ "export {}",
2702
+ ""
2703
+ ].join("\n")
2704
+ },
2705
+ { nuxt: true, nitro: true }
2706
+ );
1916
2707
  const brandPaths = {
1917
2708
  rootDir: nuxt.options.rootDir,
1918
2709
  srcDir: nuxt.options.srcDir ?? nuxt.options.rootDir
@@ -1944,12 +2735,12 @@ const module$1 = defineNuxtModule({
1944
2735
  );
1945
2736
  });
1946
2737
  nuxt.hook("modules:done", async () => {
1947
- const include = nuxt.options.vite.optimizeDeps?.include;
1948
- if (!include) return;
2738
+ const optimizeDeps = nuxt.options.vite.optimizeDeps ??= {};
2739
+ const include = [.../* @__PURE__ */ new Set([...optimizeDeps.include ?? [], "zod"])];
1949
2740
  const rootUrl = pathToFileURL(`${nuxt.options.rootDir}/`).href;
1950
- nuxt.options.vite.optimizeDeps.include = await Promise.all(
2741
+ optimizeDeps.include = await Promise.all(
1951
2742
  include.map(
1952
- async (entry) => entry.startsWith("@tiptap/") && !await tryResolveModule(entry, rootUrl) ? `@xleddyl/nuxt-cms > ${entry}` : entry
2743
+ async (entry) => (entry.startsWith("@tiptap/") || entry === "zod") && !await tryResolveModule(entry, rootUrl) ? `@xleddyl/nuxt-cms > ${entry}` : entry
1953
2744
  )
1954
2745
  );
1955
2746
  });
@@ -2092,6 +2883,11 @@ const module$1 = defineNuxtModule({
2092
2883
  handler: resolver.resolve("./runtime/server/routes/media-file")
2093
2884
  });
2094
2885
  }
2886
+ addServerHandler({
2887
+ route: "/api/cms/link-usage",
2888
+ method: "post",
2889
+ handler: resolver.resolve("./runtime/server/api/link-usage.post")
2890
+ });
2095
2891
  addServerHandler({
2096
2892
  route: "/api/cms/search",
2097
2893
  method: "get",
@@ -2132,6 +2928,74 @@ const module$1 = defineNuxtModule({
2132
2928
  method: "post",
2133
2929
  handler: resolver.resolve("./runtime/server/api/account-welcome.post")
2134
2930
  });
2931
+ addServerHandler({
2932
+ route: "/api/cms/site",
2933
+ method: "get",
2934
+ handler: resolver.resolve("./runtime/server/api/site.get")
2935
+ });
2936
+ addServerHandler({
2937
+ route: "/api/cms/site",
2938
+ method: "put",
2939
+ handler: resolver.resolve("./runtime/server/api/site.put")
2940
+ });
2941
+ addServerHandler({
2942
+ middleware: true,
2943
+ handler: resolver.resolve("./runtime/server/middleware/hidden-site")
2944
+ });
2945
+ addServerHandler({
2946
+ middleware: true,
2947
+ handler: resolver.resolve("./runtime/server/middleware/redirects")
2948
+ });
2949
+ addServerHandler({
2950
+ middleware: true,
2951
+ handler: resolver.resolve("./runtime/server/middleware/preview-links")
2952
+ });
2953
+ addServerHandler({
2954
+ route: "/api/cms/preview",
2955
+ method: "get",
2956
+ handler: resolver.resolve("./runtime/server/api/preview.get")
2957
+ });
2958
+ addServerHandler({
2959
+ route: "/api/cms/preview-links/:collection/:id",
2960
+ method: "get",
2961
+ handler: resolver.resolve("./runtime/server/api/preview-links.get")
2962
+ });
2963
+ addServerHandler({
2964
+ route: "/api/cms/preview-links/:collection/:id",
2965
+ method: "post",
2966
+ handler: resolver.resolve("./runtime/server/api/preview-links.post")
2967
+ });
2968
+ addServerHandler({
2969
+ route: "/api/cms/preview-links/:collection/:id/:linkId",
2970
+ method: "delete",
2971
+ handler: resolver.resolve("./runtime/server/api/preview-link.delete")
2972
+ });
2973
+ addServerHandler({
2974
+ route: "/api/cms/redirects",
2975
+ method: "get",
2976
+ handler: resolver.resolve("./runtime/server/api/redirects.get")
2977
+ });
2978
+ addServerHandler({
2979
+ route: "/api/cms/redirects",
2980
+ method: "post",
2981
+ handler: resolver.resolve("./runtime/server/api/redirects.post")
2982
+ });
2983
+ addServerHandler({
2984
+ route: "/api/cms/redirects/import",
2985
+ method: "post",
2986
+ handler: resolver.resolve("./runtime/server/api/redirects-import.post")
2987
+ });
2988
+ addServerHandler({
2989
+ route: "/api/cms/redirects/:id",
2990
+ method: "put",
2991
+ handler: resolver.resolve("./runtime/server/api/redirect.put")
2992
+ });
2993
+ addServerHandler({
2994
+ route: "/api/cms/redirects/:id",
2995
+ method: "delete",
2996
+ handler: resolver.resolve("./runtime/server/api/redirect.delete")
2997
+ });
2998
+ setupSeoRoutes(nuxt, resolver, options.seo, publicDir, logger);
2135
2999
  addServerHandler({
2136
3000
  route: "/api/cms/admin/settings",
2137
3001
  method: "get",
@@ -2147,6 +3011,31 @@ const module$1 = defineNuxtModule({
2147
3011
  method: "delete",
2148
3012
  handler: resolver.resolve("./runtime/server/api/settings.delete")
2149
3013
  });
3014
+ addServerHandler({
3015
+ route: "/api/cms/presets",
3016
+ method: "get",
3017
+ handler: resolver.resolve("./runtime/server/api/presets.get")
3018
+ });
3019
+ addServerHandler({
3020
+ route: "/api/cms/presets/:id",
3021
+ method: "put",
3022
+ handler: resolver.resolve("./runtime/server/api/preset.put")
3023
+ });
3024
+ addServerHandler({
3025
+ route: "/api/cms/presets/:id",
3026
+ method: "delete",
3027
+ handler: resolver.resolve("./runtime/server/api/preset.delete")
3028
+ });
3029
+ addServerHandler({
3030
+ route: "/api/cms/presets/:collection/:field",
3031
+ method: "get",
3032
+ handler: resolver.resolve("./runtime/server/api/presets-field.get")
3033
+ });
3034
+ addServerHandler({
3035
+ route: "/api/cms/presets/:collection/:field",
3036
+ method: "post",
3037
+ handler: resolver.resolve("./runtime/server/api/presets-field.post")
3038
+ });
2150
3039
  addServerHandler({
2151
3040
  route: "/api/cms/tags/:collection/:field",
2152
3041
  method: "get",