domma-cms 0.44.0 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CLAUDE.md +53 -53
  2. package/README.md +27 -27
  3. package/admin/css/admin.css +1 -1
  4. package/admin/dist/domma/domma-tools.css +3 -3
  5. package/admin/dist/domma/domma-tools.min.js +3 -3
  6. package/admin/index.html +2 -2
  7. package/admin/js/app.js +1 -1
  8. package/admin/js/lib/card-builder.js +2 -2
  9. package/admin/js/lib/crud-tutorial.js +1 -1
  10. package/admin/js/lib/effect-defs.js +1 -1
  11. package/admin/js/lib/effects-builder.js +1 -1
  12. package/admin/js/lib/image-editor.js +1 -1
  13. package/admin/js/lib/markdown-toolbar.js +2 -2
  14. package/admin/js/lib/project-context.js +1 -1
  15. package/admin/js/lib/project-quick-create.js +1 -1
  16. package/admin/js/lib/scribe-composer.js +1 -1
  17. package/admin/js/lib/server-restart.js +1 -1
  18. package/admin/js/lib/shortcode-context-menu.js +2 -2
  19. package/admin/js/lib/simple-editor.js +1 -1
  20. package/admin/js/lib/themes.js +1 -1
  21. package/admin/js/lib/timeline-builder.js +1 -1
  22. package/admin/js/templates/action-editor.html +8 -8
  23. package/admin/js/templates/api-endpoint-editor.html +7 -7
  24. package/admin/js/templates/api-endpoints.html +1 -1
  25. package/admin/js/templates/api-reference.html +1 -1
  26. package/admin/js/templates/block-editor.html +6 -6
  27. package/admin/js/templates/collection-editor.html +1 -1
  28. package/admin/js/templates/component-editor.html +2 -2
  29. package/admin/js/templates/dashboard/cache.html +3 -3
  30. package/admin/js/templates/dashboard/journeys.html +3 -3
  31. package/admin/js/templates/dashboard/kpi-strip.html +3 -3
  32. package/admin/js/templates/dashboard/top-pages.html +1 -1
  33. package/admin/js/templates/dashboard/traffic-chart.html +1 -1
  34. package/admin/js/templates/docs/api-actions.html +1 -1
  35. package/admin/js/templates/docs/api-collections.html +4 -4
  36. package/admin/js/templates/docs/api-pages.html +2 -2
  37. package/admin/js/templates/docs/api-plugins.html +1 -1
  38. package/admin/js/templates/docs/api-settings.html +2 -2
  39. package/admin/js/templates/docs/api-users.html +1 -1
  40. package/admin/js/templates/docs/components-howto.html +4 -4
  41. package/admin/js/templates/docs/components-reference.html +21 -21
  42. package/admin/js/templates/docs/components-rules.html +14 -14
  43. package/admin/js/templates/docs/components-walkthrough.html +13 -13
  44. package/admin/js/templates/docs/tutorial-crud.html +49 -49
  45. package/admin/js/templates/docs/tutorial-forms.html +4 -4
  46. package/admin/js/templates/docs/tutorial-plugin.html +10 -10
  47. package/admin/js/templates/docs/usage-actions.html +6 -6
  48. package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
  49. package/admin/js/templates/docs/usage-dconfig.html +4 -4
  50. package/admin/js/templates/docs/usage-navigation.html +1 -1
  51. package/admin/js/templates/docs/usage-pages.html +1 -1
  52. package/admin/js/templates/docs/usage-shortcodes.html +24 -24
  53. package/admin/js/templates/docs/usage-users-roles.html +3 -3
  54. package/admin/js/templates/docs/usage-views.html +4 -4
  55. package/admin/js/templates/documentation.html +1 -1
  56. package/admin/js/templates/effects.html +752 -752
  57. package/admin/js/templates/form-editor.html +5 -5
  58. package/admin/js/templates/forms.html +17 -17
  59. package/admin/js/templates/login.html +5 -5
  60. package/admin/js/templates/menu-editor.html +10 -10
  61. package/admin/js/templates/my-profile.html +17 -17
  62. package/admin/js/templates/navigation.html +11 -11
  63. package/admin/js/templates/notifications.html +1 -1
  64. package/admin/js/templates/page-editor.html +4 -4
  65. package/admin/js/templates/pro-docs.html +14 -14
  66. package/admin/js/templates/role-editor.html +70 -70
  67. package/admin/js/templates/roles.html +10 -10
  68. package/admin/js/templates/settings.html +10 -10
  69. package/admin/js/templates/tutorials.html +3 -3
  70. package/admin/js/templates/view-editor.html +15 -15
  71. package/admin/js/views/action-editor.js +1 -1
  72. package/admin/js/views/actions-list.js +1 -1
  73. package/admin/js/views/api-endpoint-editor.js +2 -2
  74. package/admin/js/views/api-endpoints.js +2 -2
  75. package/admin/js/views/api-tokens.js +3 -3
  76. package/admin/js/views/blocks.js +2 -2
  77. package/admin/js/views/collection-editor.js +1 -1
  78. package/admin/js/views/collection-entries.js +1 -1
  79. package/admin/js/views/collections.js +1 -1
  80. package/admin/js/views/component-editor.js +1 -1
  81. package/admin/js/views/components.js +4 -4
  82. package/admin/js/views/dashboard/widgets/cache.js +1 -1
  83. package/admin/js/views/dashboard/widgets/journeys.js +1 -1
  84. package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
  85. package/admin/js/views/form-editor.js +7 -7
  86. package/admin/js/views/form-submissions.js +1 -1
  87. package/admin/js/views/forms.js +1 -1
  88. package/admin/js/views/layouts.js +1 -1
  89. package/admin/js/views/login.js +2 -2
  90. package/admin/js/views/menu-editor.js +3 -3
  91. package/admin/js/views/menu-locations.js +1 -1
  92. package/admin/js/views/menus.js +3 -3
  93. package/admin/js/views/navigation.js +8 -8
  94. package/admin/js/views/page-editor.js +40 -40
  95. package/admin/js/views/pages.js +3 -3
  96. package/admin/js/views/plugin-code.js +2 -2
  97. package/admin/js/views/plugins.js +1 -1
  98. package/admin/js/views/project-settings.js +1 -1
  99. package/admin/js/views/projects.js +1 -1
  100. package/admin/js/views/roles.js +1 -1
  101. package/admin/js/views/user-editor.js +1 -1
  102. package/admin/js/views/users.js +3 -3
  103. package/admin/js/views/view-editor.js +1 -1
  104. package/admin/js/views/view-preview.js +1 -1
  105. package/admin/js/views/views-list.js +1 -1
  106. package/bin/cli.js +8 -8
  107. package/bin/lib/config-merge.js +44 -44
  108. package/bin/update.js +16 -16
  109. package/config/plugins.json +1 -1
  110. package/package.json +2 -2
  111. package/plugins/_template/admin/views/index.js +1 -1
  112. package/plugins/_template/config.js +1 -1
  113. package/plugins/_template/plugin.js +1 -1
  114. package/plugins/analytics/admin/templates/analytics.html +3 -3
  115. package/plugins/analytics/admin/views/analytics.js +1 -1
  116. package/plugins/analytics/config.js +1 -1
  117. package/plugins/analytics/plugin.js +4 -4
  118. package/plugins/analytics/plugin.json +1 -1
  119. package/plugins/analytics/public/inject-body.html +2 -2
  120. package/plugins/analytics/public/inject-head.html +1 -1
  121. package/plugins/blog/admin/templates/blog.html +4 -4
  122. package/plugins/blog/admin/views/blog.js +3 -3
  123. package/plugins/blog/admin/views/categories.js +2 -2
  124. package/plugins/blog/admin/views/comments.js +4 -4
  125. package/plugins/blog/admin/views/post-editor.js +1 -1
  126. package/plugins/blog/plugin.js +2 -2
  127. package/plugins/blog/plugin.json +4 -4
  128. package/plugins/contacts/admin/views/contacts.js +8 -8
  129. package/plugins/contacts/plugin.js +4 -4
  130. package/plugins/contacts/plugin.json +1 -1
  131. package/plugins/invoice/admin/templates/editor.html +6 -6
  132. package/plugins/invoice/admin/templates/index.html +4 -4
  133. package/plugins/invoice/admin/views/editor.js +3 -3
  134. package/plugins/invoice/admin/views/index.js +4 -4
  135. package/plugins/invoice/admin/views/issuers.js +1 -1
  136. package/plugins/invoice/admin/views/party-view.js +3 -3
  137. package/plugins/invoice/admin/views/receivers.js +1 -1
  138. package/plugins/invoice/config.js +1 -1
  139. package/plugins/invoice/plugin.js +1 -1
  140. package/plugins/invoice/plugin.json +4 -4
  141. package/plugins/notes/admin/views/notes.js +4 -4
  142. package/plugins/notes/plugin.js +1 -1
  143. package/plugins/notes/plugin.json +1 -1
  144. package/plugins/site-search/admin/templates/site-search.html +3 -3
  145. package/plugins/site-search/admin/views/site-search.js +1 -1
  146. package/plugins/site-search/config.js +1 -1
  147. package/plugins/site-search/plugin.js +4 -4
  148. package/plugins/site-search/plugin.json +1 -1
  149. package/plugins/surveys/admin/templates/results.html +1 -1
  150. package/plugins/surveys/admin/templates/survey-editor.html +4 -4
  151. package/plugins/surveys/admin/views/audience.js +6 -6
  152. package/plugins/surveys/admin/views/results.js +3 -3
  153. package/plugins/surveys/admin/views/survey-editor.js +1 -1
  154. package/plugins/surveys/admin/views/surveys.js +2 -2
  155. package/plugins/surveys/plugin.js +1 -1
  156. package/plugins/surveys/plugin.json +4 -4
  157. package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
  158. package/plugins/theme-switcher/plugin.json +3 -3
  159. package/plugins/theme-switcher/public/inject-body.html +28 -28
  160. package/plugins/theme-switcher/public/inject-head.html +1 -1
  161. package/plugins/todo/plugin.js +1 -1
  162. package/public/css/forms.css +1 -1
  163. package/public/js/collection-browser.js +1 -1
  164. package/public/js/collection-context.js +1 -1
  165. package/public/js/form-logic-engine.js +1 -1
  166. package/public/js/forms.js +2 -1
  167. package/public/js/site.js +1 -1
  168. package/scripts/build.js +14 -14
  169. package/scripts/create-plugin.js +2 -2
  170. package/scripts/fresh.js +2 -2
  171. package/scripts/gen-instance-secret.js +1 -1
  172. package/scripts/pro.js +12 -12
  173. package/scripts/reset.js +2 -2
  174. package/scripts/setup.js +15 -15
  175. package/scripts/users.js +5 -5
  176. package/scripts/verify-assets.mjs +6 -6
  177. package/server/config.js +1 -1
  178. package/server/middleware/auth.js +253 -253
  179. package/server/middleware/managerAuth.js +3 -3
  180. package/server/routes/api/actions.js +4 -4
  181. package/server/routes/api/api-endpoints.js +7 -7
  182. package/server/routes/api/api-tokens.js +5 -5
  183. package/server/routes/api/auth.js +309 -309
  184. package/server/routes/api/blocks.js +3 -3
  185. package/server/routes/api/collections.js +18 -18
  186. package/server/routes/api/dashboard.js +5 -5
  187. package/server/routes/api/effects.js +2 -2
  188. package/server/routes/api/endpoints-public.js +5 -5
  189. package/server/routes/api/forms.js +47 -47
  190. package/server/routes/api/menu-locations.js +3 -3
  191. package/server/routes/api/menus.js +7 -7
  192. package/server/routes/api/navigation.js +42 -42
  193. package/server/routes/api/notifications.js +2 -2
  194. package/server/routes/api/pages.js +14 -2
  195. package/server/routes/api/plugin-marketplace.js +3 -3
  196. package/server/routes/api/plugins.js +6 -6
  197. package/server/routes/api/projects.js +9 -9
  198. package/server/routes/api/scaffold.js +7 -7
  199. package/server/routes/api/settings.js +6 -6
  200. package/server/routes/api/sidebar.js +1 -1
  201. package/server/routes/api/users.js +5 -5
  202. package/server/routes/api/versions.js +1 -1
  203. package/server/routes/api/views.js +1 -1
  204. package/server/routes/docs-public.js +3 -3
  205. package/server/routes/public.js +14 -14
  206. package/server/server.js +34 -16
  207. package/server/services/actions.js +19 -19
  208. package/server/services/adapters/FileAdapter.js +2 -2
  209. package/server/services/adapters/MongoAdapter.js +5 -5
  210. package/server/services/apiEndpoints.js +15 -15
  211. package/server/services/apiTokens.js +7 -7
  212. package/server/services/blocks.js +13 -13
  213. package/server/services/cache/drivers/MemoryDriver.js +3 -3
  214. package/server/services/cache/drivers/NoneDriver.js +1 -1
  215. package/server/services/cache/index.js +5 -5
  216. package/server/services/collections.js +14 -14
  217. package/server/services/components.js +9 -9
  218. package/server/services/connectionManager.js +4 -4
  219. package/server/services/content.js +25 -2
  220. package/server/services/docs.js +14 -14
  221. package/server/services/email.js +167 -167
  222. package/server/services/filterEngine.js +6 -6
  223. package/server/services/forms.js +18 -10
  224. package/server/services/health.js +1 -1
  225. package/server/services/hooks.js +7 -7
  226. package/server/services/images.js +4 -4
  227. package/server/services/managerClient.js +1 -1
  228. package/server/services/markdown.js +79 -79
  229. package/server/services/menuRender.js +5 -5
  230. package/server/services/menus-migration.js +3 -3
  231. package/server/services/menus.js +31 -31
  232. package/server/services/permissionRegistry.js +1 -1
  233. package/server/services/pluginFiles.js +5 -5
  234. package/server/services/pluginInstaller.js +17 -17
  235. package/server/services/pluginScaffold.js +1 -1
  236. package/server/services/plugins.js +17 -17
  237. package/server/services/presetCollections.js +2 -2
  238. package/server/services/projects.js +35 -26
  239. package/server/services/recipes/contact-list.json +1 -1
  240. package/server/services/recipes/onboarding.json +8 -8
  241. package/server/services/references.js +8 -8
  242. package/server/services/renderer.js +9 -9
  243. package/server/services/roles.js +9 -9
  244. package/server/services/rowAccess.js +10 -10
  245. package/server/services/scaffolder.js +30 -30
  246. package/server/services/sidebar-migration.js +10 -10
  247. package/server/services/sitemap.js +1 -1
  248. package/server/services/userProfiles.js +199 -199
  249. package/server/services/userRoles.js +7 -7
  250. package/server/services/users.js +302 -302
  251. package/server/services/versions.js +3 -3
  252. package/server/services/viewPipeline.js +6 -6
  253. package/server/services/views.js +11 -11
  254. package/server/templates/page.html +5 -5
@@ -61,7 +61,7 @@ export async function initCache(config = {}) {
61
61
  *
62
62
  * When turning on, re-initialise with the last config (or sensible defaults)
63
63
  * so a previously disabled cache gets a real driver. When turning off, keep
64
- * the driver in place but short-circuit reads/writes — preserves cached data
64
+ * the driver in place but short-circuit reads/writes - preserves cached data
65
65
  * if the user toggles back on. The persistence of this choice across restarts
66
66
  * is the caller's responsibility (the route handler writes to config/cache.json).
67
67
  */
@@ -69,12 +69,12 @@ export async function setEnabled(enabled) {
69
69
  if (Boolean(enabled) === _enabled) return;
70
70
  if (enabled) {
71
71
  if (_driver instanceof NoneDriver) {
72
- // No usable driver yet — bootstrap one from last-known config.
72
+ // No usable driver yet - bootstrap one from last-known config.
73
73
  const cfg = {...(_lastConfig || {}), enabled: true};
74
74
  if (!cfg.driver || cfg.driver === 'none') cfg.driver = 'memory';
75
75
  await initCache(cfg);
76
76
  } else {
77
- // Memory/Redis driver still live — flip the flag and preserve data.
77
+ // Memory/Redis driver still live - flip the flag and preserve data.
78
78
  _enabled = true;
79
79
  }
80
80
  } else {
@@ -110,7 +110,7 @@ export async function clear() {
110
110
 
111
111
  /**
112
112
  * List the cache's keys with their tags and expiry. Values are deliberately
113
- * excluded — cached entries can be megabytes of HTML.
113
+ * excluded - cached entries can be megabytes of HTML.
114
114
  * @returns {{key: string, tags: string[], expiresAt: number|null}[]}
115
115
  */
116
116
  export function listEntries() {
@@ -148,7 +148,7 @@ export function getStats() {
148
148
  * Get-or-set with in-flight coalescing.
149
149
  *
150
150
  * Producer may return either a raw value, or an object with a `_cacheTags`
151
- * array — those tags are merged into opts.tags before storing, and the
151
+ * array - those tags are merged into opts.tags before storing, and the
152
152
  * `_cacheTags` field is stripped from both the cached and returned value.
153
153
  * This lets producers report tags discovered during rendering (e.g. the
154
154
  * markdown pipeline noting embedded collection slugs) without the caller
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Collections Service
3
- * Schema-first data store — one directory per collection under content/collections/{slug}/.
3
+ * Schema-first data store - one directory per collection under content/collections/{slug}/.
4
4
  * Each collection has a schema.json (field definitions + API access config).
5
5
  *
6
6
  * Entry storage is delegated to the active adapter (FileAdapter or MongoAdapter).
@@ -65,11 +65,11 @@ function defaultApiAccess() {
65
65
  }
66
66
 
67
67
  // ---------------------------------------------------------------------------
68
- // Schema (collection) operations — always file-based
68
+ // Schema (collection) operations - always file-based
69
69
  // ---------------------------------------------------------------------------
70
70
 
71
71
  /**
72
- * List all collections — reads all schema.json files and appends entryCount.
72
+ * List all collections - reads all schema.json files and appends entryCount.
73
73
  *
74
74
  * @returns {Promise<object[]>}
75
75
  */
@@ -93,7 +93,7 @@ export async function listCollections() {
93
93
  return results
94
94
  .filter(r => r.status === 'fulfilled')
95
95
  .map(r => r.value)
96
- // Hand-rolled schemas can lack `title` — fall back to slug rather
96
+ // Hand-rolled schemas can lack `title` - fall back to slug rather
97
97
  // than crashing the whole listing (seen in the wild: a schema with
98
98
  // `name` instead of `title` 500'd /api/collections permanently).
99
99
  .sort((a, b) => String(a.title || a.slug || '').localeCompare(String(b.title || b.slug || '')));
@@ -215,7 +215,7 @@ export async function deleteCollection(slug) {
215
215
  if (!schema) throw new Error(`Collection "${slug}" not found`);
216
216
 
217
217
  // Remove adapter-side data first (handles MongoDB collections on
218
- // non-file adapters). Prefer drop() — clear() only deletes documents
218
+ // non-file adapters). Prefer drop() - clear() only deletes documents
219
219
  // and leaves the backing Mongo collection + its unique index behind
220
220
  // forever.
221
221
  try {
@@ -228,7 +228,7 @@ export async function deleteCollection(slug) {
228
228
  }
229
229
  }
230
230
  } catch {
231
- // Ignore errors — directory removal handles file-backed data.
231
+ // Ignore errors - directory removal handles file-backed data.
232
232
  }
233
233
 
234
234
  invalidate(slug);
@@ -244,7 +244,7 @@ export async function deleteCollection(slug) {
244
244
  * Validate entry data against a collection schema.
245
245
  *
246
246
  * Reference-field validation (target id existence) requires async I/O and is
247
- * therefore done in `validateReferences()` — called from create/update paths
247
+ * therefore done in `validateReferences()` - called from create/update paths
248
248
  * after this synchronous shape check passes.
249
249
  *
250
250
  * @param {object} schema
@@ -268,7 +268,7 @@ export function validateEntryData(schema, data) {
268
268
  * entry that actually exists in the target collection. Single-valued and
269
269
  * array-valued references are both supported.
270
270
  *
271
- * Empty/null references are skipped here — the required check in
271
+ * Empty/null references are skipped here - the required check in
272
272
  * `validateEntryData()` handles those.
273
273
  *
274
274
  * @param {object} schema
@@ -280,7 +280,7 @@ export async function validateReferences(schema, data) {
280
280
  for (const field of (schema.fields || [])) {
281
281
  if (field.type !== 'reference') continue;
282
282
  const targetSlug = field.reference?.collection;
283
- if (!targetSlug) continue; // misconfigured schema — skip silently
283
+ if (!targetSlug) continue; // misconfigured schema - skip silently
284
284
 
285
285
  const raw = data[field.name];
286
286
  if (raw === undefined || raw === null || raw === '') continue;
@@ -301,7 +301,7 @@ export async function validateReferences(schema, data) {
301
301
  }
302
302
 
303
303
  // ---------------------------------------------------------------------------
304
- // Entry operations — delegated to storage adapter
304
+ // Entry operations - delegated to storage adapter
305
305
  // ---------------------------------------------------------------------------
306
306
 
307
307
  /**
@@ -309,22 +309,22 @@ export async function validateReferences(schema, data) {
309
309
  * structured filtering, and reference resolution.
310
310
  *
311
311
  * Delegates to the configured adapter (File or Mongo). Both adapters honour
312
- * identical option semantics — see `filterEngine.js` for the filter DSL.
312
+ * identical option semantics - see `filterEngine.js` for the filter DSL.
313
313
  *
314
314
  * @example
315
315
  * // Paginated, sorted, simple search
316
316
  * await listEntries('jobs', { page: 1, limit: 20, sort: 'postedAt', search: 'engineer' });
317
317
  *
318
318
  * @example
319
- * // Structured filter — full-time London roles paying £50k+
319
+ * // Structured filter - full-time London roles paying £50k+
320
320
  * await listEntries('jobs', { filter: { location: 'London', type: 'full-time', salary_gte: 50000 } });
321
321
  *
322
322
  * @example
323
- * // Row-level scoping — applications belonging to one user
323
+ * // Row-level scoping - applications belonging to one user
324
324
  * await listEntries('applications', { filter: { createdBy: userId } });
325
325
  *
326
326
  * @example
327
- * // Resolve reference fields — each entry gets `_refs.<field>` with display labels
327
+ * // Resolve reference fields - each entry gets `_refs.<field>` with display labels
328
328
  * await listEntries('applications', { resolveRefs: true });
329
329
  * // → entries[0]._refs.jobId === { id, display: 'Senior Engineer', missing: false, link: null }
330
330
  *
@@ -159,7 +159,7 @@ export function compileComponent(source, name) {
159
159
  catch (e) { return {js: null, errors: [{type: e.code, message: e.message}]}; }
160
160
 
161
161
  const js = [
162
- `// Compiled from ${name}.dmc — do not edit by hand.`,
162
+ `// Compiled from ${name}.dmc - do not edit by hand.`,
163
163
  `const template = ${toBacktickLiteral(parts.template)};`,
164
164
  `const style = ${toBacktickLiteral(parts.style)};`,
165
165
  `const props = ${JSON.stringify(propsObj)};`,
@@ -219,7 +219,7 @@ export async function getCompiledJs(name) {
219
219
  // ─── CRUD operations ──────────────────────────────────────────────────────
220
220
 
221
221
  /**
222
- * List all components. Lightweight metadata — does NOT compile.
222
+ * List all components. Lightweight metadata - does NOT compile.
223
223
  * The `props` field is parsed from each file's <props> block so the admin
224
224
  * list view can show props without a follow-up fetch.
225
225
  *
@@ -245,7 +245,7 @@ export async function listComponents() {
245
245
  const src = await fs.readFile(path.join(COMPONENTS_DIR, file), 'utf8');
246
246
  const {props: propsBlock} = parseDmcSource(src);
247
247
  props = validatePropsJson(propsBlock);
248
- } catch { /* malformed — list still usable */ }
248
+ } catch { /* malformed - list still usable */ }
249
249
  entries.push({
250
250
  name,
251
251
  size: stat?.size ?? 0,
@@ -263,7 +263,7 @@ export async function listComponents() {
263
263
  try {
264
264
  const parts = parseDmcSource(pc.source);
265
265
  props = validatePropsJson(parts.props);
266
- } catch { /* malformed plugin source — list still usable */ }
266
+ } catch { /* malformed plugin source - list still usable */ }
267
267
  entries.push({
268
268
  name: pc.name,
269
269
  size: pc.source.length,
@@ -313,7 +313,7 @@ export async function getComponent(name) {
313
313
  }
314
314
 
315
315
  /**
316
- * Create or update a component. Compiles BEFORE writing — a malformed source
316
+ * Create or update a component. Compiles BEFORE writing - a malformed source
317
317
  * never reaches disk. Returns the compile errors (if any) rather than throwing.
318
318
  *
319
319
  * @param {string} name
@@ -352,12 +352,12 @@ export async function saveComponent(name, source, {bundled = false, meta} = {})
352
352
  return {success: true, name, errors: []};
353
353
  }
354
354
 
355
- /** Lazy, circular-safe import — refreshes the markdown sanitiser's dm-* tag allowlist. */
355
+ /** Lazy, circular-safe import - refreshes the markdown sanitiser's dm-* tag allowlist. */
356
356
  async function _refreshSanitiserAllowlist() {
357
357
  try {
358
358
  const {refreshComponentTagAllowlist} = await import('./markdown.js');
359
359
  await refreshComponentTagAllowlist();
360
- } catch { /* no-op — markdown module not ready at first save */ }
360
+ } catch { /* no-op - markdown module not ready at first save */ }
361
361
  }
362
362
 
363
363
  /**
@@ -465,7 +465,7 @@ const SEED_COMPONENTS = {
465
465
  }
466
466
  </props>
467
467
  <script>
468
- // NB: Domma's factory calls data() with no \`this\` binding — props must be
468
+ // NB: Domma's factory calls data() with no \`this\` binding - props must be
469
469
  // read from within onMount() / methods, which ARE bound to the component context.
470
470
  export default {
471
471
  data() { return { count: 0 }; },
@@ -633,7 +633,7 @@ export default {
633
633
 
634
634
  /**
635
635
  * Seed starter components into content/components/.
636
- * Never overwrites existing files — user customisations are preserved.
636
+ * Never overwrites existing files - user customisations are preserved.
637
637
  * All seeded components are marked `bundled: true` via their .meta.json.
638
638
  */
639
639
  export async function seedDefaultComponents() {
@@ -2,7 +2,7 @@
2
2
  * Connection Manager (Pro feature)
3
3
  *
4
4
  * Manages MongoDB client lifecycle for named database connections.
5
- * Loaded dynamically via import() — never required in the free version.
5
+ * Loaded dynamically via import() - never required in the free version.
6
6
  *
7
7
  * Usage:
8
8
  * await initialise(connectionsConfig) // on server startup
@@ -15,13 +15,13 @@ const clients = new Map();
15
15
 
16
16
  /**
17
17
  * Initialise all named MongoDB connections from config.
18
- * Connects lazily — each client is created but connect() is deferred to first use.
18
+ * Connects lazily - each client is created but connect() is deferred to first use.
19
19
  *
20
20
  * @param {Record<string, { type: string, uri: string, database: string, options?: object }>} connectionsConfig
21
21
  * @returns {Promise<void>}
22
22
  */
23
23
  export async function initialise(connectionsConfig) {
24
- // Dynamic import — mongodb package is optional and not loaded in the free version.
24
+ // Dynamic import - mongodb package is optional and not loaded in the free version.
25
25
  const { MongoClient } = await import('mongodb');
26
26
 
27
27
  for (const [name, cfg] of Object.entries(connectionsConfig)) {
@@ -43,7 +43,7 @@ export async function initialise(connectionsConfig) {
43
43
  console.log(`[connectionManager] Connected: ${name} → ${cfg.database}`);
44
44
  } catch (err) {
45
45
  console.error(`[connectionManager] Failed to connect "${name}": ${err.message}`);
46
- // Don't throw — allow server to start with degraded MongoDB support.
46
+ // Don't throw - allow server to start with degraded MongoDB support.
47
47
  }
48
48
  }
49
49
  }
@@ -79,6 +79,17 @@ export async function getPage(urlPath, opts = {}) {
79
79
  /**
80
80
  * Create a new page. Auto-creates parent directories.
81
81
  *
82
+ * Every frontmatter key the caller sends is written through - the same
83
+ * contract as updatePage. An earlier version built `meta` from a fixed
84
+ * whitelist, which silently discarded `project`, `visibility`, `tags`,
85
+ * `category` and `theme` on the FIRST save of a page: a page created inside
86
+ * a project landed in core, vanished from that project's list, and yet the
87
+ * URL was taken, so re-creating it answered "already exists".
88
+ *
89
+ * `project: ''` means "inherit by rootUrl" - the key is omitted rather than
90
+ * written as null (null is the deprecated core opt-out, and it pins the page
91
+ * to core forever).
92
+ *
82
93
  * @param {string} urlPath
83
94
  * @param {object} frontmatter
84
95
  * @param {string} body
@@ -91,6 +102,7 @@ export async function createPage(urlPath, frontmatter, body) {
91
102
  const defaults = config.content.pageDefaults;
92
103
  const now = new Date().toISOString();
93
104
  const meta = {
105
+ ...frontmatter,
94
106
  title: frontmatter.title || 'Untitled',
95
107
  slug: slugFromPath(urlPath),
96
108
  description: frontmatter.description || '',
@@ -104,6 +116,7 @@ export async function createPage(urlPath, frontmatter, body) {
104
116
  createdAt: now,
105
117
  updatedAt: now
106
118
  };
119
+ if (meta.project === '' || meta.project === undefined) delete meta.project;
107
120
 
108
121
  await fs.writeFile(filePath, serialiseMarkdown(meta, body || ''), 'utf8');
109
122
  const page = await readPageFile(filePath);
@@ -134,6 +147,11 @@ export async function updatePage(urlPath, frontmatter, body, {author} = {}) {
134
147
 
135
148
  if (existingMeta.createdAt) meta.createdAt = existingMeta.createdAt;
136
149
 
150
+ // '' is the editor's "inherit by rootUrl" choice - drop the key so the
151
+ // resolver re-derives it. (null stays meaningful: the deprecated explicit
152
+ // core opt-out.)
153
+ if (meta.project === '') delete meta.project;
154
+
137
155
  try {
138
156
  await createVersion(urlPath, {author, type: 'auto', label: null});
139
157
  } catch {
@@ -321,7 +339,7 @@ async function collectMdFiles(dir) {
321
339
  * @param {object} [opts]
322
340
  * @param {object|null} [opts.user] - Forwarded to parseMarkdown so the
323
341
  * `[menu]` shortcode can filter items by per-user visibility. Defaults to
324
- * anonymous (null) — most callers (listings, admin previews) leave this unset.
342
+ * anonymous (null) - most callers (listings, admin previews) leave this unset.
325
343
  * @returns {Promise<object>}
326
344
  */
327
345
  async function readPageFile(filePath, opts = {}) {
@@ -331,7 +349,12 @@ async function readPageFile(filePath, opts = {}) {
331
349
  // against menus bound to this page or its project, exactly as the navbar does.
332
350
  const { data, content, html, usedComponents, tags } =
333
351
  await parseMarkdown(raw, {user: opts.user || null, urlPath});
334
- return { ...data, urlPath, content, html, usedComponents, tags };
352
+ // parseMarkdown's `tags` are CACHE tags (`block:x`, `collection:y`), not
353
+ // the page's editorial tags. Spreading them last used to shadow the
354
+ // frontmatter's `tags`, so the editor loaded `block:feedback-card` into
355
+ // the tag pillbox and wrote it back on save, and the pages list showed
356
+ // cache internals in its Tags column. Keep them apart.
357
+ return { ...data, urlPath, content, html, usedComponents, cacheTags: tags };
335
358
  }
336
359
 
337
360
  /**
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Domma Docs — request-time renderer
2
+ * Domma Docs - request-time renderer
3
3
  *
4
4
  * Serves the Domma CMS handbook at /domma-docs/* directly from the admin doc
5
5
  * HTML templates (admin/js/templates/docs/*.html) at request time. There is no
6
6
  * baked content: a domma-cms version bump ships new templates and the docs
7
- * update everywhere automatically — no porter, no per-site provisioning, no
7
+ * update everywhere automatically - no porter, no per-site provisioning, no
8
8
  * staleness. (Replaces the one-shot scripts/migrate-docs.js bake.)
9
9
  *
10
10
  * The publish switch is the `domma-docs` project's enabled flag, enforced by
@@ -33,7 +33,7 @@ const TPL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../..', 'admin
33
33
  // Functional nav chrome only (breadcrumb + prev/next + section nav). Everything
34
34
  // else (hero, cards, tabs, accordion, buttons) is a native Domma component, so
35
35
  // it follows the theme. Injected as an inline <style> per docs page at render
36
- // time — no dependency on content/custom.css.
36
+ // time - no dependency on content/custom.css.
37
37
  const DOCS_BODY_CSS = `.doc-breadcrumb { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; max-width: 80ch; margin: 0 auto 1.25rem; font-size: .85rem; color: var(--dm-text-muted); }
38
38
  .doc-breadcrumb a { color: var(--dm-primary); text-decoration: none; }
39
39
  .doc-breadcrumb a:hover { text-decoration: underline; }
@@ -76,7 +76,7 @@ const MAP = [
76
76
  ];
77
77
 
78
78
  // Admin-SPA hash routes and stale /resources/… paths don't resolve on the
79
- // public site — rewrite doc-to-doc links to /domma-docs/…, admin-feature links
79
+ // public site - rewrite doc-to-doc links to /domma-docs/…, admin-feature links
80
80
  // to the admin SPA, and unwrap the rest.
81
81
  const LINK_MAP = {
82
82
  '#/docs/components-walkthrough': '/domma-docs/components/walkthrough',
@@ -97,7 +97,7 @@ const LINK_MAP = {
97
97
  '/resources/components': '/domma-docs/components'
98
98
  };
99
99
 
100
- // The four top-level doc sections — rendered as a persistent nav on every page.
100
+ // The four top-level doc sections - rendered as a persistent nav on every page.
101
101
  const SECTIONS = [
102
102
  {label: 'Usage', href: '/domma-docs/usage'},
103
103
  {label: 'Tutorials', href: '/domma-docs/tutorials'},
@@ -134,13 +134,13 @@ function rewriteLinks(html) {
134
134
  for (const [from, to] of Object.entries(LINK_MAP)) {
135
135
  out = out.split(`href="${from}"`).join(`href="${to}"`);
136
136
  }
137
- // Any remaining /resources/… link has no doc equivalent — unwrap to text.
137
+ // Any remaining /resources/… link has no doc equivalent - unwrap to text.
138
138
  out = out.replace(/<a\b[^>]*\shref="\/resources\/[^"]*"[^>]*>([\s\S]*?)<\/a>/gi, '$1');
139
139
  return out;
140
140
  }
141
141
 
142
142
  // Pull every inline <style> block out of the body, returning the cleaned HTML
143
- // plus the extracted (deduped) CSS — collected PER CALL (no shared module state,
143
+ // plus the extracted (deduped) CSS - collected PER CALL (no shared module state,
144
144
  // which would leak across requests in a long-lived server).
145
145
  function stripStyleBlocks(html) {
146
146
  const styles = new Set();
@@ -182,7 +182,7 @@ function unwrapDocsBody(html) {
182
182
  // Plain-text heading title, safe as a shortcode attribute value.
183
183
  function headingText(inner) {
184
184
  return inner.replace(/<[^>]+>/g, '')
185
- .replace(/&amp;/g, '&').replace(/&mdash;/g, '—').replace(/&ndash;/g, '–')
185
+ .replace(/&amp;/g, '&').replace(/-/g, '-').replace(/-/g, '-')
186
186
  .replace(/&quot;/g, "'").replace(/&#39;/g, "'").replace(/&lt;/g, '<').replace(/&gt;/g, '>')
187
187
  .replace(/\s+/g, ' ').trim().replace(/"/g, "'");
188
188
  }
@@ -200,7 +200,7 @@ function splitSections(inner, level) {
200
200
  return {preamble, sections};
201
201
  }
202
202
 
203
- // ≥7 sections → accordion; 2–6 → tabs; <2 → a single card.
203
+ // ≥7 sections → accordion; 2-6 → tabs; <2 → a single card.
204
204
  function structureBody(body) {
205
205
  const inner = unwrapDocsBody(body);
206
206
  const level = /<h2\b/i.test(inner) ? 2 : 3;
@@ -265,9 +265,9 @@ function buildSectionLanding(label) {
265
265
  function buildHomeLanding() {
266
266
  const hero = `[hero title="Domma Docs" tagline="The Domma CMS handbook" variant="gradient" align="center" fullwidth="true"][/hero]`;
267
267
  const cards = [
268
- {label: 'Usage', href: '/domma-docs/usage', icon: 'book-open', blurb: 'Day-to-day guides for running the CMS — pages, media, navigation, shortcodes and more.'},
268
+ {label: 'Usage', href: '/domma-docs/usage', icon: 'book-open', blurb: 'Day-to-day guides for running the CMS - pages, media, navigation, shortcodes and more.'},
269
269
  {label: 'Tutorials', href: '/domma-docs/tutorials', icon: 'academic-cap', blurb: 'Step-by-step walkthroughs that build something real from start to finish.'},
270
- {label: 'Components', href: '/domma-docs/components', icon: 'puzzle', blurb: 'The full component reference — shortcodes, how-tos, walkthroughs and rules.'},
270
+ {label: 'Components', href: '/domma-docs/components', icon: 'puzzle', blurb: 'The full component reference - shortcodes, how-tos, walkthroughs and rules.'},
271
271
  {label: 'API Reference', href: '/domma-docs/api', icon: 'code', blurb: 'Every REST endpoint for headless and external integrations.'}
272
272
  ].map(s =>
273
273
  `[card icon="${s.icon}" title="${heroAttr(s.label)}"]\n${escText(s.blurb)}\n\n[button label="Browse ${heroAttr(s.label)} →" href="${s.href}" variant="primary" /]\n[/card]`
@@ -278,7 +278,7 @@ function buildHomeLanding() {
278
278
  // ── render ───────────────────────────────────────────────────────────────────
279
279
 
280
280
  // Build the shortcode body for a doc path, or null if the path is unknown.
281
- // Returns {title, body, styles[]} — `styles` is the inline CSS extracted from a
281
+ // Returns {title, body, styles[]} - `styles` is the inline CSS extracted from a
282
282
  // leaf template (empty for synthesized landings).
283
283
  async function buildDocBody(docPath) {
284
284
  if (docPath in LANDINGS) {
@@ -292,7 +292,7 @@ async function buildDocBody(docPath) {
292
292
  try {
293
293
  src = await readFile(join(TPL_DIR, `${tpl}.html`), 'utf8');
294
294
  } catch {
295
- return null; // template missing — treat as a 404
295
+ return null; // template missing - treat as a 404
296
296
  }
297
297
  const {html: stripped, styles} = stripStyleBlocks(stripHeader(src));
298
298
  const body = escapeCodeBrackets(dedentOutsidePre(rewriteLinks(stripped))).trim();
@@ -308,7 +308,7 @@ async function buildDocBody(docPath) {
308
308
  /**
309
309
  * Render a Domma Docs page to full HTML, or return null for an unknown path
310
310
  * (the route turns that into a 404). Cached per path+origin under the static
311
- * 'docs' tag — invalidated on boot so a version bump never serves stale HTML.
311
+ * 'docs' tag - invalidated on boot so a version bump never serves stale HTML.
312
312
  *
313
313
  * @param {string} docPath url tail after /domma-docs (e.g. 'usage/pages', '' for home)
314
314
  * @param {{user?: object|null, baseUrl?: string}} [opts]