@actuate-media/cms-admin 0.15.0 → 0.16.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 (298) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +148 -2
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
  5. package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
  6. package/dist/__tests__/components/posts-badges.test.js +68 -0
  7. package/dist/__tests__/components/posts-badges.test.js.map +1 -0
  8. package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
  9. package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
  10. package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
  11. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
  12. package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
  13. package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
  14. package/dist/__tests__/lib/page-editor-service.test.js +137 -0
  15. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
  16. package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
  17. package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
  18. package/dist/__tests__/lib/pages-service.test.js +140 -0
  19. package/dist/__tests__/lib/pages-service.test.js.map +1 -0
  20. package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
  21. package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
  22. package/dist/__tests__/lib/post-editor-service.test.js +185 -0
  23. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
  24. package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
  25. package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
  26. package/dist/__tests__/lib/posts-service.test.js +244 -0
  27. package/dist/__tests__/lib/posts-service.test.js.map +1 -0
  28. package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
  29. package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
  30. package/dist/__tests__/views/posts-list-view.test.js +98 -0
  31. package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
  32. package/dist/actuate-admin.css +1 -1
  33. package/dist/components/pages/tag-badges.d.ts +21 -0
  34. package/dist/components/pages/tag-badges.d.ts.map +1 -0
  35. package/dist/components/pages/tag-badges.js +56 -0
  36. package/dist/components/pages/tag-badges.js.map +1 -0
  37. package/dist/components/posts/badges.d.ts +25 -0
  38. package/dist/components/posts/badges.d.ts.map +1 -0
  39. package/dist/components/posts/badges.js +123 -0
  40. package/dist/components/posts/badges.js.map +1 -0
  41. package/dist/components/posts/icons.d.ts +12 -0
  42. package/dist/components/posts/icons.d.ts.map +1 -0
  43. package/dist/components/posts/icons.js +26 -0
  44. package/dist/components/posts/icons.js.map +1 -0
  45. package/dist/layout/Layout.js +36 -8
  46. package/dist/layout/Layout.js.map +1 -1
  47. package/dist/layout/Sidebar.d.ts +9 -0
  48. package/dist/layout/Sidebar.d.ts.map +1 -1
  49. package/dist/layout/Sidebar.js +100 -48
  50. package/dist/layout/Sidebar.js.map +1 -1
  51. package/dist/lib/page-editor-service.d.ts +45 -0
  52. package/dist/lib/page-editor-service.d.ts.map +1 -0
  53. package/dist/lib/page-editor-service.js +153 -0
  54. package/dist/lib/page-editor-service.js.map +1 -0
  55. package/dist/lib/pages-service.d.ts +143 -0
  56. package/dist/lib/pages-service.d.ts.map +1 -0
  57. package/dist/lib/pages-service.js +444 -0
  58. package/dist/lib/pages-service.js.map +1 -0
  59. package/dist/lib/post-editor-service.d.ts +57 -0
  60. package/dist/lib/post-editor-service.d.ts.map +1 -0
  61. package/dist/lib/post-editor-service.js +234 -0
  62. package/dist/lib/post-editor-service.js.map +1 -0
  63. package/dist/lib/posts-service.d.ts +73 -0
  64. package/dist/lib/posts-service.d.ts.map +1 -0
  65. package/dist/lib/posts-service.js +245 -0
  66. package/dist/lib/posts-service.js.map +1 -0
  67. package/dist/lib/preview-link.d.ts +32 -0
  68. package/dist/lib/preview-link.d.ts.map +1 -0
  69. package/dist/lib/preview-link.js +40 -0
  70. package/dist/lib/preview-link.js.map +1 -0
  71. package/dist/router/index.d.ts +1 -0
  72. package/dist/router/index.d.ts.map +1 -1
  73. package/dist/router/index.js +9 -1
  74. package/dist/router/index.js.map +1 -1
  75. package/dist/views/Dashboard.d.ts.map +1 -1
  76. package/dist/views/Dashboard.js +14 -3
  77. package/dist/views/Dashboard.js.map +1 -1
  78. package/dist/views/Pages/PageTagManager.d.ts +14 -0
  79. package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
  80. package/dist/views/Pages/PageTagManager.js +145 -0
  81. package/dist/views/Pages/PageTagManager.js.map +1 -0
  82. package/dist/views/Pages/PagesListView.d.ts +18 -0
  83. package/dist/views/Pages/PagesListView.d.ts.map +1 -0
  84. package/dist/views/Pages/PagesListView.js +287 -0
  85. package/dist/views/Pages/PagesListView.js.map +1 -0
  86. package/dist/views/Posts/NewPostPicker.d.ts +16 -0
  87. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
  88. package/dist/views/Posts/NewPostPicker.js +59 -0
  89. package/dist/views/Posts/NewPostPicker.js.map +1 -0
  90. package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
  91. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
  92. package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
  93. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
  94. package/dist/views/Posts/PostTypesView.d.ts +26 -0
  95. package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
  96. package/dist/views/Posts/PostTypesView.js +52 -0
  97. package/dist/views/Posts/PostTypesView.js.map +1 -0
  98. package/dist/views/Posts/PostsListView.d.ts +25 -0
  99. package/dist/views/Posts/PostsListView.d.ts.map +1 -0
  100. package/dist/views/Posts/PostsListView.js +239 -0
  101. package/dist/views/Posts/PostsListView.js.map +1 -0
  102. package/dist/views/Posts/post-type-presets.d.ts +32 -0
  103. package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
  104. package/dist/views/Posts/post-type-presets.js +72 -0
  105. package/dist/views/Posts/post-type-presets.js.map +1 -0
  106. package/dist/views/page-builder/Widgets.d.ts +13 -0
  107. package/dist/views/page-builder/Widgets.d.ts.map +1 -0
  108. package/dist/views/page-builder/Widgets.js +15 -0
  109. package/dist/views/page-builder/Widgets.js.map +1 -0
  110. package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
  111. package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
  112. package/dist/views/page-editor/AddSectionModal.js +22 -0
  113. package/dist/views/page-editor/AddSectionModal.js.map +1 -0
  114. package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
  115. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
  116. package/dist/views/page-editor/EditorCanvas.js +64 -0
  117. package/dist/views/page-editor/EditorCanvas.js.map +1 -0
  118. package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
  119. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
  120. package/dist/views/page-editor/EditorTopBar.js +42 -0
  121. package/dist/views/page-editor/EditorTopBar.js.map +1 -0
  122. package/dist/views/page-editor/PagePreview.d.ts +13 -0
  123. package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
  124. package/dist/views/page-editor/PagePreview.js +37 -0
  125. package/dist/views/page-editor/PagePreview.js.map +1 -0
  126. package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
  127. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
  128. package/dist/views/page-editor/PageSectionEditor.js +272 -0
  129. package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
  130. package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
  131. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
  132. package/dist/views/page-editor/PageSettingsModal.js +52 -0
  133. package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
  134. package/dist/views/page-editor/SectionInspector.d.ts +17 -0
  135. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
  136. package/dist/views/page-editor/SectionInspector.js +98 -0
  137. package/dist/views/page-editor/SectionInspector.js.map +1 -0
  138. package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
  139. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
  140. package/dist/views/page-editor/SectionsPanel.js +43 -0
  141. package/dist/views/page-editor/SectionsPanel.js.map +1 -0
  142. package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
  143. package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
  144. package/dist/views/page-editor/ValidationSummary.js +10 -0
  145. package/dist/views/page-editor/ValidationSummary.js.map +1 -0
  146. package/dist/views/page-editor/section-icons.d.ts +4 -0
  147. package/dist/views/page-editor/section-icons.d.ts.map +1 -0
  148. package/dist/views/page-editor/section-icons.js +20 -0
  149. package/dist/views/page-editor/section-icons.js.map +1 -0
  150. package/dist/views/page-editor/section-types.d.ts +11 -0
  151. package/dist/views/page-editor/section-types.d.ts.map +1 -0
  152. package/dist/views/page-editor/section-types.js +10 -0
  153. package/dist/views/page-editor/section-types.js.map +1 -0
  154. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
  155. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
  156. package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
  157. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
  158. package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
  159. package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
  160. package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
  161. package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
  162. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
  163. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
  164. package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
  165. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
  166. package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
  167. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
  168. package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
  169. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
  170. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
  171. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
  172. package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
  173. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
  174. package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
  175. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
  176. package/dist/views/page-editor/sections/GallerySection.js +17 -0
  177. package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
  178. package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
  179. package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
  180. package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
  181. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
  182. package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
  183. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
  184. package/dist/views/page-editor/sections/MissionSection.js +12 -0
  185. package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
  186. package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
  187. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
  188. package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
  189. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
  190. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
  191. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
  192. package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
  193. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
  194. package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
  195. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
  196. package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
  197. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
  198. package/dist/views/page-editor/sections/index.d.ts +14 -0
  199. package/dist/views/page-editor/sections/index.d.ts.map +1 -0
  200. package/dist/views/page-editor/sections/index.js +12 -0
  201. package/dist/views/page-editor/sections/index.js.map +1 -0
  202. package/dist/views/page-editor/sections/parts.d.ts +55 -0
  203. package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
  204. package/dist/views/page-editor/sections/parts.js +60 -0
  205. package/dist/views/page-editor/sections/parts.js.map +1 -0
  206. package/dist/views/page-editor/viewports.d.ts +14 -0
  207. package/dist/views/page-editor/viewports.d.ts.map +1 -0
  208. package/dist/views/page-editor/viewports.js +12 -0
  209. package/dist/views/page-editor/viewports.js.map +1 -0
  210. package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
  211. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
  212. package/dist/views/post-editor/PostEditorCanvas.js +58 -0
  213. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
  214. package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
  215. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
  216. package/dist/views/post-editor/PostFieldsModal.js +53 -0
  217. package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
  218. package/dist/views/post-editor/PostHeader.d.ts +15 -0
  219. package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
  220. package/dist/views/post-editor/PostHeader.js +27 -0
  221. package/dist/views/post-editor/PostHeader.js.map +1 -0
  222. package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
  223. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
  224. package/dist/views/post-editor/PostHeaderPanel.js +21 -0
  225. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
  226. package/dist/views/post-editor/PostPreview.d.ts +14 -0
  227. package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
  228. package/dist/views/post-editor/PostPreview.js +53 -0
  229. package/dist/views/post-editor/PostPreview.js.map +1 -0
  230. package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
  231. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
  232. package/dist/views/post-editor/PostSectionEditor.js +290 -0
  233. package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
  234. package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
  235. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
  236. package/dist/views/post-editor/PostTemplateEditor.js +144 -0
  237. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
  238. package/package.json +2 -2
  239. package/src/AdminRoot.tsx +213 -2
  240. package/src/__tests__/components/posts-badges.test.tsx +77 -0
  241. package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
  242. package/src/__tests__/lib/page-editor-service.test.ts +173 -0
  243. package/src/__tests__/lib/pages-service.test.ts +157 -0
  244. package/src/__tests__/lib/post-editor-service.test.ts +215 -0
  245. package/src/__tests__/lib/posts-service.test.ts +279 -0
  246. package/src/__tests__/views/posts-list-view.test.ts +110 -0
  247. package/src/components/pages/tag-badges.tsx +104 -0
  248. package/src/components/posts/badges.tsx +170 -0
  249. package/src/components/posts/icons.ts +40 -0
  250. package/src/layout/Layout.tsx +66 -10
  251. package/src/layout/Sidebar.tsx +116 -50
  252. package/src/lib/page-editor-service.ts +233 -0
  253. package/src/lib/pages-service.ts +599 -0
  254. package/src/lib/post-editor-service.ts +342 -0
  255. package/src/lib/posts-service.ts +366 -0
  256. package/src/lib/preview-link.ts +63 -0
  257. package/src/router/index.ts +10 -1
  258. package/src/views/Dashboard.tsx +15 -3
  259. package/src/views/Pages/PageTagManager.tsx +436 -0
  260. package/src/views/Pages/PagesListView.tsx +944 -0
  261. package/src/views/Posts/NewPostPicker.tsx +147 -0
  262. package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
  263. package/src/views/Posts/PostTypesView.tsx +194 -0
  264. package/src/views/Posts/PostsListView.tsx +869 -0
  265. package/src/views/Posts/post-type-presets.ts +105 -0
  266. package/src/views/page-builder/Widgets.tsx +44 -0
  267. package/src/views/page-editor/AddSectionModal.tsx +56 -0
  268. package/src/views/page-editor/EditorCanvas.tsx +127 -0
  269. package/src/views/page-editor/EditorTopBar.tsx +233 -0
  270. package/src/views/page-editor/PagePreview.tsx +96 -0
  271. package/src/views/page-editor/PageSectionEditor.tsx +476 -0
  272. package/src/views/page-editor/PageSettingsModal.tsx +154 -0
  273. package/src/views/page-editor/SectionInspector.tsx +599 -0
  274. package/src/views/page-editor/SectionsPanel.tsx +270 -0
  275. package/src/views/page-editor/ValidationSummary.tsx +53 -0
  276. package/src/views/page-editor/section-icons.ts +33 -0
  277. package/src/views/page-editor/section-types.ts +27 -0
  278. package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
  279. package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
  280. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
  281. package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
  282. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
  283. package/src/views/page-editor/sections/GallerySection.tsx +54 -0
  284. package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
  285. package/src/views/page-editor/sections/MissionSection.tsx +22 -0
  286. package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
  287. package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
  288. package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
  289. package/src/views/page-editor/sections/index.ts +13 -0
  290. package/src/views/page-editor/sections/parts.tsx +113 -0
  291. package/src/views/page-editor/viewports.ts +25 -0
  292. package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
  293. package/src/views/post-editor/PostFieldsModal.tsx +196 -0
  294. package/src/views/post-editor/PostHeader.tsx +87 -0
  295. package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
  296. package/src/views/post-editor/PostPreview.tsx +114 -0
  297. package/src/views/post-editor/PostSectionEditor.tsx +506 -0
  298. package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
@@ -102,29 +102,26 @@ function computePageActions({
102
102
  config,
103
103
  onNavigate,
104
104
  }: PageActionsContext): ReactNode | null {
105
- // Resolve the "primary" content collection (used for both the dashboard
106
- // and any explicit /posts route). Prefer a real `posts` or `post`-type
107
- // collection over the hard-coded slug so admin-managed CMS configs work.
108
105
  const collections = collectionList(config)
109
- const postsCol =
110
- collections.find((c) => c.slug === 'posts' || c.type === 'post') ??
111
- collections.find((c) => c.type === 'post')
112
- const postsSlug = postsCol?.slug ?? 'posts'
113
- const postsLabel = postsCol?.labels?.singular ?? 'Post'
106
+ const postCollections = collections.filter((c) => c.type === 'post')
107
+ const newPostPath = resolveNewPostPath(postCollections)
114
108
  const siteUrl: string | null = config?.site?.url ?? config?.seo?.siteUrl ?? null
115
109
 
116
110
  const isDashboard = currentPath === '/' || currentPath === '/dashboard'
117
111
 
112
+ // Dashboard: keep the existing "+ New Post" / "View Site" pair. With
113
+ // a single Post Type we route straight to its editor; with several we
114
+ // route to the picker so the user can choose the type first.
118
115
  if (isDashboard) {
119
116
  return (
120
117
  <>
121
118
  <button
122
119
  type="button"
123
- onClick={() => onNavigate(`/${postsSlug}/new`)}
120
+ onClick={() => onNavigate(newPostPath)}
124
121
  className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
125
122
  >
126
123
  <Plus className="h-3.5 w-3.5" aria-hidden />
127
- New {postsLabel}
124
+ New Post
128
125
  </button>
129
126
  {siteUrl ? (
130
127
  <a
@@ -141,6 +138,52 @@ function computePageActions({
141
138
  )
142
139
  }
143
140
 
141
+ // Posts area — "+ New Post" + "Manage types" on the list page;
142
+ // "+ New Post Type" on the types page. The buttons live in the top
143
+ // bar so the page header stays clean. See the dashboard rule for
144
+ // why these come from a single config-aware helper.
145
+ if (currentPath === '/posts') {
146
+ return (
147
+ <button
148
+ type="button"
149
+ onClick={() => onNavigate(newPostPath)}
150
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
151
+ >
152
+ <Plus className="h-3.5 w-3.5" aria-hidden />
153
+ New Post
154
+ </button>
155
+ )
156
+ }
157
+
158
+ if (currentPath === '/posts/types') {
159
+ return (
160
+ <button
161
+ type="button"
162
+ onClick={() => onNavigate('/posts/types/new')}
163
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
164
+ >
165
+ <Plus className="h-3.5 w-3.5" aria-hidden />
166
+ New Post Type
167
+ </button>
168
+ )
169
+ }
170
+
171
+ // Pages list — "+ New page" routes to the page creation flow. Handled
172
+ // explicitly (not via the collection loop) so the label/route stay
173
+ // exact even though `pages` is a reserved route.
174
+ if (currentPath === '/pages') {
175
+ return (
176
+ <button
177
+ type="button"
178
+ onClick={() => onNavigate('/pages/new')}
179
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
180
+ >
181
+ <Plus className="h-3.5 w-3.5" aria-hidden />
182
+ New page
183
+ </button>
184
+ )
185
+ }
186
+
144
187
  // List views — surface "+ New X" so authors can keep creating without
145
188
  // hunting for a per-list button. Skip on editor views; those have their
146
189
  // own toolbar.
@@ -169,6 +212,19 @@ interface CollectionMeta {
169
212
  admin?: { hidden?: boolean }
170
213
  }
171
214
 
215
+ /**
216
+ * Where "+ New Post" should land, given the configured post-type
217
+ * collections:
218
+ * - exactly one → straight to that type's editor (`/<slug>/new`)
219
+ * - more than one → the type picker (`/posts/new`)
220
+ * - none → the Post Type setup instructions
221
+ */
222
+ function resolveNewPostPath(postCollections: CollectionMeta[]): string {
223
+ if (postCollections.length === 1) return `/${postCollections[0]!.slug}/new`
224
+ if (postCollections.length > 1) return '/posts/new'
225
+ return '/posts/types/new'
226
+ }
227
+
172
228
  function collectionList(config: any): CollectionMeta[] {
173
229
  if (!config?.collections) return []
174
230
  const raw = config.collections
@@ -19,9 +19,12 @@ import {
19
19
  PanelTop,
20
20
  PanelBottom,
21
21
  Layers,
22
+ LayoutGrid,
22
23
  Code2,
23
24
  Library,
24
25
  KeyRound,
26
+ ListTree,
27
+ Menu as MenuIcon,
25
28
  PanelLeftClose,
26
29
  PanelLeftOpen,
27
30
  } from 'lucide-react'
@@ -61,6 +64,9 @@ const ICON_MAP: Record<string, LucideIcon> = {
61
64
  book: BookOpen,
62
65
  help: HelpCircle,
63
66
  newspaper: Newspaper,
67
+ navigation: ListTree,
68
+ menu: MenuIcon,
69
+ 'list-tree': ListTree,
64
70
  PanelTop: PanelTop,
65
71
  PanelBottom: PanelBottom,
66
72
  Layers: Layers,
@@ -106,24 +112,45 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
106
112
  }
107
113
 
108
114
  /**
109
- * Nav tree for the default (config-less) install. Mirrors the reference
110
- * dashboard exactly: Dashboard / Posts / Pages (with Saved Sections +
111
- * Templates as nested children), then a CONTENT group (Media / Forms /
112
- * SEO / Script Tags), then a SETTINGS group (Users / API Keys /
113
- * Settings). The group ordering is meaningful — the renderer emits a
114
- * section header the first time it sees a new group.
115
+ * Children of the Posts parent. Same shape regardless of how many
116
+ * post-type collections are registered Post Types are accessed via
117
+ * the `/posts/types` cards page, not as individual sidebar entries.
118
+ */
119
+ const POSTS_CHILDREN: NavItem[] = [
120
+ { path: '/posts', label: 'All Posts', icon: FileText },
121
+ { path: '/posts/types', label: 'Post Types', icon: Layers },
122
+ ]
123
+
124
+ /**
125
+ * Children of the Templates parent. Templates is its own top-level item
126
+ * (page building tools) — Saved Sections are reusable content blocks and
127
+ * Widgets are embeddable components. These are deliberately NOT children
128
+ * of Pages: Pages is content, Templates/Sections/Widgets are structure.
129
+ */
130
+ const TEMPLATES_CHILDREN: NavItem[] = [
131
+ { path: '/saved-sections', label: 'Saved Sections', icon: Library },
132
+ { path: '/widgets', label: 'Widgets', icon: LayoutGrid },
133
+ ]
134
+
135
+ /**
136
+ * Nav tree for the default (config-less) install. Dashboard sits on its
137
+ * own at the top, then a CONTENT group holding Pages (no children) above
138
+ * Posts (with All Posts + Post Types) and Templates (with Saved Sections
139
+ * + Widgets), followed by Media / Forms / SEO / Script Tags, then a
140
+ * SETTINGS group (Users / API Keys / Settings). The group ordering is
141
+ * meaningful — the renderer emits a section header the first time it
142
+ * sees a new group, so "CONTENT" renders once, above Pages.
115
143
  */
116
144
  const defaultNavItems: NavItem[] = [
117
145
  { path: '/', label: 'Dashboard', icon: LayoutDashboard },
118
- { path: '/posts', label: 'Posts', icon: FileText },
146
+ { path: '/pages', label: 'Pages', icon: File, group: 'Content' },
147
+ { path: '/posts', label: 'Posts', icon: FileText, group: 'Content', children: POSTS_CHILDREN },
119
148
  {
120
- path: '/pages',
121
- label: 'Pages',
122
- icon: File,
123
- children: [
124
- { path: '/saved-sections', label: 'Saved Sections', icon: Library },
125
- { path: '/page-templates', label: 'Templates', icon: Layers },
126
- ],
149
+ path: '/page-templates',
150
+ label: 'Templates',
151
+ icon: Layers,
152
+ group: 'Content',
153
+ children: TEMPLATES_CHILDREN,
127
154
  },
128
155
  { path: '/media', label: 'Media', icon: Image, group: 'Content' },
129
156
  { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
@@ -207,6 +234,15 @@ interface NavRenderContext {
207
234
 
208
235
  function isPathActive(currentPath: string, itemPath: string): boolean {
209
236
  if (itemPath === '/') return currentPath === '/'
237
+
238
+ // Special case: /posts is a prefix of /posts/types — without this
239
+ // guard, "All Posts" (/posts) would stay highlighted while the user
240
+ // is on /posts/types or /posts/types/new. Treat /posts as exact-only
241
+ // when a sibling /posts/* route exists.
242
+ if (itemPath === '/posts') {
243
+ return currentPath === '/posts' || currentPath === '/posts/new'
244
+ }
245
+
210
246
  // Use boundary match so /pages doesn't match /pages-something. Exact, or
211
247
  // followed by a `/` separator.
212
248
  return currentPath === itemPath || currentPath.startsWith(itemPath + '/')
@@ -236,10 +272,16 @@ function renderNavTree(items: NavItem[], ctx: NavRenderContext): ReactNode {
236
272
  lastGroup = undefined
237
273
  }
238
274
 
239
- nodes.push(<NavLink key={item.path} item={item} ctx={ctx} />)
275
+ // Keys are namespaced because a parent and its first child can share
276
+ // the same path (e.g. the Posts parent and its "All Posts" child are
277
+ // both `/posts`). Using the raw path as the key collided and made
278
+ // React warn about — and risk dropping — duplicate siblings.
279
+ nodes.push(<NavLink key={`item:${item.path}`} item={item} ctx={ctx} />)
240
280
  if (item.children && item.children.length > 0 && !ctx.collapsed) {
241
281
  for (const child of item.children) {
242
- nodes.push(<NavLink key={child.path} item={child} ctx={ctx} nested />)
282
+ nodes.push(
283
+ <NavLink key={`child:${item.path}:${child.path}`} item={child} ctx={ctx} nested />,
284
+ )
243
285
  }
244
286
  }
245
287
  }
@@ -315,24 +357,32 @@ export interface NavItem {
315
357
  * destination (Media, Forms, SEO, Script Tags, Users, API Keys,
316
358
  * Settings) is anchored under the right Content / Settings group.
317
359
  */
318
- function buildNavItems(config: any): NavItem[] {
360
+ export function buildNavItems(config: any): NavItem[] {
319
361
  if (!config?.collections) return defaultNavItems
320
362
 
321
363
  const raw = config.collections
322
364
  const collectionsList: any[] = Array.isArray(raw) ? raw : Object.values(raw)
323
365
  const visible = collectionsList.filter((c) => !c.admin?.hidden)
324
366
 
367
+ // Treat `admin.group: 'Posts'` (case-insensitive) as a synonym for
368
+ // `type: 'post'` so consumers can tag arbitrary collections into
369
+ // the Posts area without changing the discriminator they already
370
+ // use elsewhere (sitemaps, schema.org, etc.).
371
+ const isPostsGroup = (c: any) =>
372
+ typeof c.admin?.group === 'string' && c.admin.group.toLowerCase() === 'posts'
325
373
  const pages = visible.filter((c) => c.type === 'page' && !c.admin?.group)
326
- const posts = visible.filter((c) => c.type === 'post' && !c.admin?.group)
374
+ const posts = visible.filter((c) => c.type === 'post' || isPostsGroup(c))
327
375
  const otherUngrouped = visible.filter(
328
376
  (c) => c.type !== 'page' && c.type !== 'post' && !c.admin?.group,
329
377
  )
330
- const grouped = visible.filter((c) => c.admin?.group)
378
+ // Anything explicitly grouped that isn't a posts collection.
379
+ const grouped = visible.filter((c) => c.admin?.group && !isPostsGroup(c) && c.type !== 'post')
331
380
 
332
381
  const items: NavItem[] = [{ path: '/', label: 'Dashboard', icon: LayoutDashboard }]
333
382
 
334
- // Posts-style collections come first (mirrors the reference layout).
335
- for (const collection of posts) {
383
+ // Ungrouped collections that are neither page- nor post-type stay at
384
+ // the top level, directly under Dashboard, before the CONTENT group.
385
+ for (const collection of otherUngrouped) {
336
386
  items.push({
337
387
  label: collection.labels?.plural ?? collection.slug,
338
388
  path: `/${collection.slug}`,
@@ -340,37 +390,59 @@ function buildNavItems(config: any): NavItem[] {
340
390
  })
341
391
  }
342
392
 
343
- // Pages-style collections get the nested Saved Sections + Templates
344
- // tree. We attach the children to the *first* page-type collection
345
- // so we don't end up with three "Saved Sections" links if a project
346
- // configures multiple page collections — admins editing custom
347
- // page-style content still navigate to those secondary surfaces via
348
- // the primary Pages parent.
349
- pages.forEach((collection, idx) => {
350
- const item: NavItem = {
393
+ // CONTENT group. Everything below shares `group: 'Content'`, so the
394
+ // renderer emits the "CONTENT" header once above Pages. Order:
395
+ // Pages Posts static content destinations (Media / Forms / SEO /
396
+ // Script Tags).
397
+
398
+ // Pages-style collections render as plain Content entries (no nested
399
+ // children). Saved Sections / Widgets now live under the dedicated
400
+ // Templates parent below, not under Pages.
401
+ pages.forEach((collection) => {
402
+ items.push({
351
403
  label: collection.labels?.plural ?? collection.slug,
352
404
  path: `/${collection.slug}`,
353
405
  icon: resolveIcon(collection),
354
- }
355
- if (idx === 0) {
356
- item.children = [
357
- { path: '/saved-sections', label: 'Saved Sections', icon: Library },
358
- { path: '/page-templates', label: 'Templates', icon: Layers },
359
- ]
360
- }
361
- items.push(item)
406
+ group: 'Content',
407
+ })
362
408
  })
363
409
 
364
- for (const collection of otherUngrouped) {
410
+ // Posts area — collapses every post-type collection under a single
411
+ // "Posts" parent. The aggregated table at /posts and the type cards
412
+ // at /posts/types are the entry points; individual post-type
413
+ // collections are not enumerated in the sidebar to avoid the
414
+ // duplicate-nav problem when a project has many types.
415
+ if (posts.length > 0) {
365
416
  items.push({
366
- label: collection.labels?.plural ?? collection.slug,
367
- path: `/${collection.slug}`,
368
- icon: resolveIcon(collection),
417
+ path: '/posts',
418
+ label: 'Posts',
419
+ icon: FileText,
420
+ group: 'Content',
421
+ children: POSTS_CHILDREN,
369
422
  })
370
423
  }
371
424
 
372
- // Config-supplied groups land before our standard Content/Settings
373
- // groups so admin-defined sections take precedence.
425
+ // Templates its own parent holding Saved Sections + Widgets (page
426
+ // building structure, distinct from Pages content).
427
+ items.push({
428
+ path: '/page-templates',
429
+ label: 'Templates',
430
+ icon: Layers,
431
+ group: 'Content',
432
+ children: TEMPLATES_CHILDREN,
433
+ })
434
+
435
+ // Standard Content destinations (always present — they map to CMS
436
+ // features, not collections).
437
+ items.push(
438
+ { path: '/media', label: 'Media', icon: Image, group: 'Content' },
439
+ { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
440
+ { path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
441
+ { path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
442
+ )
443
+
444
+ // Config-supplied custom groups land after the standard Content block
445
+ // and before Settings. Each renders under its own header.
374
446
  const groupBuckets = new Map<string, NavItem[]>()
375
447
  for (const col of grouped) {
376
448
  const group = col.admin.group as string
@@ -385,14 +457,8 @@ function buildNavItems(config: any): NavItem[] {
385
457
  }
386
458
  for (const bucket of groupBuckets.values()) items.push(...bucket)
387
459
 
388
- // Standard Content + Settings destinations. These are always
389
- // present, regardless of which collections the admin configures,
390
- // because they map to CMS features (not collections).
460
+ // Settings destinations always last.
391
461
  items.push(
392
- { path: '/media', label: 'Media', icon: Image, group: 'Content' },
393
- { path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
394
- { path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
395
- { path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
396
462
  { path: '/users', label: 'Users', icon: Users, group: 'Settings' },
397
463
  { path: '/api-keys', label: 'API Keys', icon: KeyRound, group: 'Settings' },
398
464
  { path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Page Editor service — data layer for the flat, section-based page
3
+ * editor (`/pages/:id/edit`).
4
+ *
5
+ * A page is a document in the `pages` collection. The editor reads and
6
+ * writes a single JSON field, `data.sections`, holding an ordered list of
7
+ * `PageSection`s. Save draft = `PUT` the sections (+ title/slug/SEO) with
8
+ * no status change; publish = the same payload plus `status: 'PUBLISHED'`.
9
+ *
10
+ * Persistence reuses the existing collection document API, so there is no
11
+ * new endpoint and no Prisma migration — `sections` is declared as a
12
+ * `type: 'json'` field on the `pages` collection so the field-access layer
13
+ * doesn't strip it on write.
14
+ *
15
+ * The section mutation helpers (`addSection`, `moveSection`, …) are pure
16
+ * functions over `PageSection[]` so the editor state and the unit tests
17
+ * share one implementation.
18
+ */
19
+ import { cmsApi } from './api.js'
20
+ import {
21
+ getSectionType,
22
+ createSection,
23
+ addSection,
24
+ removeSection,
25
+ duplicateSection,
26
+ toggleSectionVisibility,
27
+ moveSection,
28
+ reorderSections,
29
+ updateSectionContent,
30
+ updateSectionSettings,
31
+ updateSectionMeta,
32
+ coerceSections as coerceSectionsCore,
33
+ validateSectionContent,
34
+ type PageSection,
35
+ } from '@actuate-media/cms-core/page-sections'
36
+
37
+ export type { PageSection } from '@actuate-media/cms-core/page-sections'
38
+
39
+ // Re-export the pure section helpers so existing editor imports
40
+ // (`../../lib/page-editor-service.js`) keep working after the move to core.
41
+ export {
42
+ createSection,
43
+ addSection,
44
+ removeSection,
45
+ duplicateSection,
46
+ toggleSectionVisibility,
47
+ moveSection,
48
+ reorderSections,
49
+ updateSectionContent,
50
+ updateSectionSettings,
51
+ updateSectionMeta,
52
+ }
53
+
54
+ export type PageStatus = 'DRAFT' | 'PUBLISHED' | 'SCHEDULED' | 'ARCHIVED'
55
+
56
+ export const PAGES_COLLECTION = 'pages'
57
+
58
+ export interface EditorPage {
59
+ id: string | null
60
+ title: string
61
+ slug: string
62
+ path: string
63
+ status: PageStatus
64
+ sections: PageSection[]
65
+ seoTitle: string
66
+ seoDescription: string
67
+ publishedAt: string | null
68
+ updatedAt: string | null
69
+ }
70
+
71
+ export interface ValidationResult {
72
+ /** blocking issues — publish is disabled while any exist */
73
+ errors: string[]
74
+ /** non-blocking advisories (e.g. SEO) */
75
+ warnings: string[]
76
+ }
77
+
78
+ // ─── Validation ──────────────────────────────────────────────────────
79
+
80
+ function isBlank(v: unknown): boolean {
81
+ return v === undefined || v === null || (typeof v === 'string' && v.trim() === '')
82
+ }
83
+
84
+ /**
85
+ * Validate a page for save/publish. `forPublish` turns on the stricter
86
+ * rules (at least one visible section, required section fields, valid
87
+ * button URLs, image alt text). SEO gaps are surfaced as warnings.
88
+ *
89
+ * Per-section rules are delegated to the shared `validateSectionContent`
90
+ * in `@actuate-media/cms-core/page-sections`, so the editor and the
91
+ * server/MCP enforce identical content rules. Page-level rules
92
+ * (title/path, "at least one visible section", SEO) stay here because
93
+ * they operate on the whole `EditorPage`.
94
+ */
95
+ export function validatePage(page: EditorPage, forPublish = false): ValidationResult {
96
+ const errors: string[] = []
97
+ const warnings: string[] = []
98
+
99
+ if (isBlank(page.title)) errors.push('Page title is required.')
100
+ if (isBlank(page.path)) errors.push('Page path is required.')
101
+
102
+ if (forPublish) {
103
+ const visible = page.sections.filter((s) => s.visible)
104
+ if (visible.length === 0) {
105
+ errors.push('Add at least one visible section before publishing.')
106
+ }
107
+
108
+ for (const section of visible) {
109
+ const def = getSectionType(section.sectionType)
110
+ const label = section.name || def?.name || 'Section'
111
+ const result = validateSectionContent(section.sectionType, section.content, section.settings)
112
+ for (const issue of result.errors) {
113
+ errors.push(`${label}: ${issue.message}`)
114
+ }
115
+ }
116
+
117
+ if (isBlank(page.seoTitle)) warnings.push('Add an SEO title to improve search results.')
118
+ if (isBlank(page.seoDescription)) {
119
+ warnings.push('Add an SEO description to improve search results.')
120
+ }
121
+ }
122
+
123
+ return { errors, warnings }
124
+ }
125
+
126
+ // ─── API row mapping ─────────────────────────────────────────────────
127
+
128
+ interface DocumentApiRow {
129
+ id: string
130
+ title: string | null
131
+ slug: string | null
132
+ status: PageStatus
133
+ publishedAt: string | null
134
+ updatedAt: string | null
135
+ data: Record<string, unknown> | null
136
+ }
137
+
138
+ function rowToEditorPage(doc: DocumentApiRow): EditorPage {
139
+ const data = doc.data ?? {}
140
+ const slug = doc.slug ?? ''
141
+ return {
142
+ id: doc.id,
143
+ title: doc.title ?? '',
144
+ slug,
145
+ path: typeof data.path === 'string' && data.path ? data.path : slug ? `/${slug}` : '/',
146
+ status: doc.status ?? 'DRAFT',
147
+ sections: coerceSectionsCore(data.sections),
148
+ seoTitle: typeof data.metaTitle === 'string' ? data.metaTitle : '',
149
+ seoDescription: typeof data.metaDescription === 'string' ? data.metaDescription : '',
150
+ publishedAt: doc.publishedAt ?? null,
151
+ updatedAt: doc.updatedAt ?? null,
152
+ }
153
+ }
154
+
155
+ // ─── API calls ───────────────────────────────────────────────────────
156
+
157
+ export async function fetchPageForEditor(pageId: string): Promise<EditorPage> {
158
+ const res = await cmsApi<DocumentApiRow>(`/collections/${PAGES_COLLECTION}/${pageId}`)
159
+ if (res.error || !res.data) throw new Error(res.error ?? 'Failed to load page')
160
+ return rowToEditorPage(res.data)
161
+ }
162
+
163
+ interface PageWritePayload {
164
+ title: string
165
+ slug: string
166
+ path: string
167
+ sections: PageSection[]
168
+ metaTitle: string
169
+ metaDescription: string
170
+ }
171
+
172
+ function toWriteBody(page: EditorPage, extra?: Record<string, unknown>): string {
173
+ const payload: PageWritePayload & Record<string, unknown> = {
174
+ title: page.title,
175
+ slug: page.slug,
176
+ path: page.path,
177
+ sections: page.sections,
178
+ metaTitle: page.seoTitle,
179
+ metaDescription: page.seoDescription,
180
+ ...extra,
181
+ }
182
+ return JSON.stringify(payload)
183
+ }
184
+
185
+ /** Create a new page document, returning the new id. */
186
+ export async function createPage(page: EditorPage): Promise<EditorPage> {
187
+ const res = await cmsApi<DocumentApiRow>(`/collections/${PAGES_COLLECTION}`, {
188
+ method: 'POST',
189
+ body: toWriteBody(page),
190
+ })
191
+ if (res.error || !res.data) throw new Error(res.error ?? 'Failed to create page')
192
+ return rowToEditorPage(res.data)
193
+ }
194
+
195
+ /** Save the current draft (no status change). */
196
+ export async function savePageDraft(page: EditorPage): Promise<EditorPage> {
197
+ if (!page.id) return createPage(page)
198
+ const res = await cmsApi<DocumentApiRow>(`/collections/${PAGES_COLLECTION}/${page.id}`, {
199
+ method: 'PUT',
200
+ body: toWriteBody(page),
201
+ })
202
+ if (res.error || !res.data) throw new Error(res.error ?? 'Failed to save draft')
203
+ return rowToEditorPage(res.data)
204
+ }
205
+
206
+ /** Publish: persist the latest content and flip status to PUBLISHED. */
207
+ export async function publishPage(page: EditorPage): Promise<EditorPage> {
208
+ const endpoint = page.id
209
+ ? `/collections/${PAGES_COLLECTION}/${page.id}`
210
+ : `/collections/${PAGES_COLLECTION}`
211
+ const res = await cmsApi<DocumentApiRow>(endpoint, {
212
+ method: page.id ? 'PUT' : 'POST',
213
+ body: toWriteBody(page, { status: 'PUBLISHED' }),
214
+ })
215
+ if (res.error || !res.data) throw new Error(res.error ?? 'Failed to publish page')
216
+ return rowToEditorPage(res.data)
217
+ }
218
+
219
+ /** A blank page scaffold for `/pages/new`. */
220
+ export function emptyPage(): EditorPage {
221
+ return {
222
+ id: null,
223
+ title: '',
224
+ slug: '',
225
+ path: '',
226
+ status: 'DRAFT',
227
+ sections: [],
228
+ seoTitle: '',
229
+ seoDescription: '',
230
+ publishedAt: null,
231
+ updatedAt: null,
232
+ }
233
+ }