domma-cms 0.44.0 → 0.45.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 (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
@@ -1,5 +1,5 @@
1
1
  <div class="view-header">
2
- <h1><span data-icon="component"></span> Components — Rules</h1>
2
+ <h1><span data-icon="component"></span> Components - Rules</h1>
3
3
  <a href="#/docs/components" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> Reference</a>
4
4
  </div>
5
5
 
@@ -16,18 +16,18 @@
16
16
  </nav>
17
17
 
18
18
  <p class="lead">The constraints the compiler enforces, and the handful of behaviours that commonly
19
- trip people up. Everything here is validated at save time — a component that breaks a rule never
19
+ trip people up. Everything here is validated at save time - a component that breaks a rule never
20
20
  reaches disk.</p>
21
21
 
22
22
  <hr>
23
23
 
24
24
  <h2>Naming</h2>
25
25
  <ul>
26
- <li>Names must match <code>^[a-z][a-z0-9-]*$</code> — start with a lowercase letter, then lowercase
26
+ <li>Names must match <code>^[a-z][a-z0-9-]*$</code> - start with a lowercase letter, then lowercase
27
27
  letters, digits and hyphens.</li>
28
28
  <li>Valid: <code>counter</code>, <code>copy-button</code>, <code>price-tag-2</code>. Invalid:
29
29
  <code>Counter</code>, <code>1counter</code>, <code>my_component</code>, <code>../etc/passwd</code>.</li>
30
- <li>The name becomes the element tag <code>&lt;dm-<em>name</em>&gt;</code> and is <strong>permanent</strong> —
30
+ <li>The name becomes the element tag <code>&lt;dm-<em>name</em>&gt;</code> and is <strong>permanent</strong> -
31
31
  it can't be changed after creation. To rename, create a new component and delete the old one.</li>
32
32
  </ul>
33
33
 
@@ -46,7 +46,7 @@
46
46
  <h2>Props must be a typed JSON object</h2>
47
47
  <ul>
48
48
  <li>The <code>&lt;props&gt;</code> body must be valid JSON and an <strong>object</strong> (not an
49
- array, not a bare value) — otherwise <code>INVALID_PROPS_JSON</code>.</li>
49
+ array, not a bare value) - otherwise <code>INVALID_PROPS_JSON</code>.</li>
50
50
  <li>Every prop must declare a <code>type</code> that is one of <code>string</code>,
51
51
  <code>number</code>, <code>boolean</code>, <code>array</code>, <code>object</code>. Anything else
52
52
  (or a missing type) fails validation.</li>
@@ -59,9 +59,9 @@
59
59
  <h2>The script must <code>export default</code> an object</h2>
60
60
  <ul>
61
61
  <li>The <code>&lt;script&gt;</code> body must be exactly one <code>export default { … };</code>
62
- statement — the compiler strips that prefix and embeds the object. No <code>export default</code>
62
+ statement - the compiler strips that prefix and embeds the object. No <code>export default</code>
63
63
  means <code>SCRIPT_MISSING_EXPORT</code>.</li>
64
- <li>Use an object expression — not a class, not multiple exports. An empty
64
+ <li>Use an object expression - not a class, not multiple exports. An empty
65
65
  <code>export default {};</code> is valid for a purely presentational component.</li>
66
66
  </ul>
67
67
 
@@ -84,13 +84,13 @@
84
84
 
85
85
  <div class="alert alert-warning">
86
86
  <strong>3. Attribute casing.</strong> On the raw custom element, camelCase props are written as
87
- <code>kebab-case</code> attributes — <code>showReset</code> becomes <code>show-reset</code>. The
87
+ <code>kebab-case</code> attributes - <code>showReset</code> becomes <code>show-reset</code>. The
88
88
  <code>[component]</code> shortcode accepts the prop name as written.
89
89
  </div>
90
90
 
91
91
  <div class="alert alert-warning">
92
- <strong>4. Array / object props are JSON in the attribute.</strong> Pass them as a JSON string —
93
- e.g. <code>items='["a","b"]'</code> — and the runtime parses them to the declared type.
92
+ <strong>4. Array / object props are JSON in the attribute.</strong> Pass them as a JSON string -
93
+ e.g. <code>items='["a","b"]'</code> - and the runtime parses them to the declared type.
94
94
  </div>
95
95
 
96
96
  <div class="alert alert-warning">
@@ -101,7 +101,7 @@
101
101
 
102
102
  <div class="alert alert-warning">
103
103
  <strong>6. Clean up in <code>onUnmount()</code>.</strong> Anything you start in <code>onMount</code>
104
- — intervals, timeouts, external listeners — must be torn down in <code>onUnmount</code> so it
104
+ - intervals, timeouts, external listeners - must be torn down in <code>onUnmount</code> so it
105
105
  doesn't leak when the element is removed.
106
106
  </div>
107
107
 
@@ -112,7 +112,7 @@
112
112
  <li>Components contributed by a plugin show a <span class="badge badge-secondary">plugin</span>
113
113
  badge, are served from memory, and <strong>cannot be edited or deleted</strong> from the admin
114
114
  (attempting it returns <code>PLUGIN_OWNED</code>).</li>
115
- <li>You also can't save a disk component whose name a plugin already owns — pick a different name.</li>
115
+ <li>You also can't save a disk component whose name a plugin already owns - pick a different name.</li>
116
116
  </ul>
117
117
 
118
118
  <hr>
@@ -120,7 +120,7 @@
120
120
  <h2>Import / export constraints</h2>
121
121
  <ul>
122
122
  <li>A bundle must have a numeric <code>format</code> no newer than the CMS supports, plus
123
- <code>name</code> and <code>source</code> strings — otherwise <code>INVALID_BUNDLE</code>.</li>
123
+ <code>name</code> and <code>source</code> strings - otherwise <code>INVALID_BUNDLE</code>.</li>
124
124
  <li>Importing over an existing name returns <code>CONFLICT</code> unless you confirm the overwrite
125
125
  (the Import button prompts you).</li>
126
126
  <li>Imported source is compiled like any save, so a broken bundle is rejected with the same errors.</li>
@@ -132,7 +132,7 @@
132
132
  <ul>
133
133
  <li><code>{{ }}</code> interpolation is HTML-escaped, so prop values can't inject markup.</li>
134
134
  <li>The markdown sanitiser keeps a live allowlist of <code>dm-*</code> tags, refreshed whenever you
135
- save or delete a component — a component you just created is immediately usable in page content.
135
+ save or delete a component - a component you just created is immediately usable in page content.
136
136
  <code>on*</code> handler attributes and <code>javascript:</code> URLs are still stripped globally.</li>
137
137
  <li>Saving recompiles and invalidates the cached module, so edits take effect on the next page load
138
138
  without a server restart.</li>
@@ -1,5 +1,5 @@
1
1
  <div class="view-header">
2
- <h1><span data-icon="component"></span> Components — Walkthrough</h1>
2
+ <h1><span data-icon="component"></span> Components - Walkthrough</h1>
3
3
  <a href="#/components/new" class="btn btn-primary btn-sm"><span data-icon="plus"></span> New Component</a>
4
4
  </div>
5
5
 
@@ -24,7 +24,7 @@
24
24
 
25
25
  <hr>
26
26
 
27
- <h2>Step 1 — Create the component</h2>
27
+ <h2>Step 1 - Create the component</h2>
28
28
  <p>Open <a href="#/components">Data → Components</a> → <strong>New Component</strong> and name it
29
29
  <code>star-rating</code>. Remember: lowercase + hyphens, and the name is permanent because it
30
30
  becomes the element tag <code>&lt;dm-star-rating&gt;</code>. The four source tabs
@@ -33,7 +33,7 @@
33
33
 
34
34
  <hr>
35
35
 
36
- <h2>Step 2 — Declare the props</h2>
36
+ <h2>Step 2 - Declare the props</h2>
37
37
  <p>Start with the inputs, because they shape everything else. A rating needs a maximum number of
38
38
  stars and a current value. Put this in the <code>&lt;props&gt;</code> tab:</p>
39
39
  <pre class="code-block"><code>{
@@ -45,7 +45,7 @@
45
45
 
46
46
  <hr>
47
47
 
48
- <h2>Step 3 — Write the template</h2>
48
+ <h2>Step 3 - Write the template</h2>
49
49
  <p>We render <code>max</code> stars and mark each as filled if its index is below the current rating.
50
50
  Because <code>{{#each}}</code> needs a list, we'll build a <code>stars</code> array in state
51
51
  (next step) where each item knows whether it's <code>on</code>. For now, the markup:</p>
@@ -61,7 +61,7 @@
61
61
 
62
62
  <hr>
63
63
 
64
- <h2>Step 4 — Add the script (state + behaviour)</h2>
64
+ <h2>Step 4 - Add the script (state + behaviour)</h2>
65
65
  <p>Three jobs: build the <code>stars</code> array from <code>value</code>, rebuild it whenever the
66
66
  rating changes, and handle clicks. Remember <code>data()</code> can't see props, so we seed state
67
67
  in <code>onMount()</code>.</p>
@@ -102,7 +102,7 @@ export default {
102
102
 
103
103
  <hr>
104
104
 
105
- <h2>Step 5 — Style it (scoped)</h2>
105
+ <h2>Step 5 - Style it (scoped)</h2>
106
106
  <pre class="code-block"><code>&lt;style&gt;
107
107
  .dm-stars { display: inline-flex; gap: .15rem; }
108
108
  .dm-stars .star {
@@ -116,15 +116,15 @@ export default {
116
116
 
117
117
  <hr>
118
118
 
119
- <h2>Step 6 — Watch the preview, then save</h2>
119
+ <h2>Step 6 - Watch the preview, then save</h2>
120
120
  <p>As you typed, the editor recompiled and re-mounted the component in the preview iframe. Toggle
121
121
  <code>readonly</code> and change <code>value</code> in the <em>Preview props</em> panel to sanity-check
122
- both modes. When it looks right, hit <strong>Save Component</strong> — the source compiles before it
122
+ both modes. When it looks right, hit <strong>Save Component</strong> - the source compiles before it
123
123
  is written, so a typo surfaces as a clear error rather than a broken page.</p>
124
124
 
125
125
  <hr>
126
126
 
127
- <h2>Step 7 — Use it on a page</h2>
127
+ <h2>Step 7 - Use it on a page</h2>
128
128
  <p>Display-only, in any Markdown page:</p>
129
129
  <pre class="code-block"><code>[component name="star-rating" max="5" value="4" readonly="true" /]</code></pre>
130
130
  <p>Interactive, reacting to the custom event with a little page script:</p>
@@ -137,9 +137,9 @@ document.getElementById('r1').addEventListener('rating-change', (e) =&gt; {
137
137
 
138
138
  <hr>
139
139
 
140
- <h2>Step 8 — Share it</h2>
140
+ <h2>Step 8 - Share it</h2>
141
141
  <p>Back on <a href="#/components">Components</a>, the <span data-icon="download"></span> Export button
142
- gives you <code>star-rating.dmcomponent.json</code> — import that on another Domma site to reuse the
142
+ gives you <code>star-rating.dmcomponent.json</code> - import that on another Domma site to reuse the
143
143
  widget verbatim.</p>
144
144
 
145
145
  <hr>
@@ -154,8 +154,8 @@ document.getElementById('r1').addEventListener('rating-change', (e) =&gt; {
154
154
  <li><strong>Scoped CSS</strong>, the live preview, and export/import</li>
155
155
  </ul>
156
156
 
157
- <p>For the rules that keep components well-behaved — naming, the <code>data()</code>-can't-see-props
158
- gotcha, boolean coercion, plugin-owned names — read <a href="#/docs/components-rules">Rules</a>.</p>
157
+ <p>For the rules that keep components well-behaved - naming, the <code>data()</code>-can't-see-props
158
+ gotcha, boolean coercion, plugin-owned names - read <a href="#/docs/components-rules">Rules</a>.</p>
159
159
 
160
160
  <hr>
161
161
  <p class="text-muted" style="font-size:.9rem;">
@@ -8,7 +8,7 @@
8
8
  <div class="docs-body" id="tutorial-crud">
9
9
 
10
10
  <p class="lead">
11
- A "CRUD app" — Create, Read, Update, Delete — is the shape of almost every business
11
+ A "CRUD app" - Create, Read, Update, Delete - is the shape of almost every business
12
12
  tool you'll ever build. Onboarding, bookings, contacts, tickets, orders, RSVPs,
13
13
  classified ads. Domma CMS gives you everything to build one without writing
14
14
  JavaScript, but the trick is understanding <em>why</em> the pieces compose the way
@@ -26,10 +26,10 @@
26
26
  Domma CMS gives each of those a name:</p>
27
27
 
28
28
  <ul>
29
- <li><strong>Collection</strong> — the data store. Think "spreadsheet": rows are records, columns are fields with types.</li>
30
- <li><strong>Form</strong> — how new records get added. It writes to a Collection on submit.</li>
31
- <li><strong>Action</strong> — a server-side button that changes existing records. "Approve", "Withdraw", "Send invoice".</li>
32
- <li><strong>Page</strong> — a Markdown page with <em>shortcodes</em> that render forms and lists from your collections.</li>
29
+ <li><strong>Collection</strong> - the data store. Think "spreadsheet": rows are records, columns are fields with types.</li>
30
+ <li><strong>Form</strong> - how new records get added. It writes to a Collection on submit.</li>
31
+ <li><strong>Action</strong> - a server-side button that changes existing records. "Approve", "Withdraw", "Send invoice".</li>
32
+ <li><strong>Page</strong> - a Markdown page with <em>shortcodes</em> that render forms and lists from your collections.</li>
33
33
  </ul>
34
34
 
35
35
  <p>These are deliberately separate. A single Collection might be written to by three different Forms
@@ -38,7 +38,7 @@
38
38
  separate is what lets the same data drive multiple experiences.</p>
39
39
 
40
40
  <div class="alert alert-info">
41
- <strong>Why not a database?</strong> Collections store as flat JSON files on disk by default —
41
+ <strong>Why not a database?</strong> Collections store as flat JSON files on disk by default -
42
42
  no database to install, no SQL to learn, no migrations. When you outgrow that, switch
43
43
  individual collections to MongoDB without changing any of your pages or forms. The
44
44
  compatibility layer is the point.
@@ -46,18 +46,18 @@
46
46
 
47
47
  <hr>
48
48
 
49
- <h2>Step 1 — Create the Collection (your data store)</h2>
49
+ <h2>Step 1 - Create the Collection (your data store)</h2>
50
50
 
51
51
  <p>Open <a href="#/collections">Collections</a> → <strong>New collection</strong>. You'll give it a
52
52
  slug (the URL-safe name we use in shortcodes), a title, and a list of fields.</p>
53
53
 
54
54
  <p><strong>The slug matters more than you'd think.</strong> It's what every Form, Action, and shortcode
55
- uses to refer to this collection — so pick a noun and stick with it. <code>jobs</code>,
55
+ uses to refer to this collection - so pick a noun and stick with it. <code>jobs</code>,
56
56
  <code>applications</code>, <code>contacts</code>, <code>events</code>. Don't pluralise inconsistently
57
- and don't include the word "data" or "collection" in the slug — that's redundant.</p>
57
+ and don't include the word "data" or "collection" in the slug - that's redundant.</p>
58
58
 
59
59
  <p><strong>Fields are where the design lives.</strong> Each field has a <em>type</em>, and the type isn't
60
- just for show — it drives every downstream behaviour:</p>
60
+ just for show - it drives every downstream behaviour:</p>
61
61
 
62
62
  <ul>
63
63
  <li><code>text</code> renders a text input on forms and is searchable in the Browser</li>
@@ -66,7 +66,7 @@
66
66
  <li><code>multiselect</code> becomes a checkbox group AND a filterable tag chip set</li>
67
67
  <li><code>date</code> renders a date picker AND gets a from/to range filter</li>
68
68
  <li><code>file</code> renders a file upload with mime/size validation</li>
69
- <li><code>reference</code> stores a link to another collection's entry — auto-renders as a populated dropdown</li>
69
+ <li><code>reference</code> stores a link to another collection's entry - auto-renders as a populated dropdown</li>
70
70
  </ul>
71
71
 
72
72
  <p>Pick the right type at design time and you get the right form input, the right filter UI, and the
@@ -80,7 +80,7 @@
80
80
 
81
81
  <hr>
82
82
 
83
- <h2>Step 2 — Read the data (display it on a page)</h2>
83
+ <h2>Step 2 - Read the data (display it on a page)</h2>
84
84
 
85
85
  <p>You don't need code to list a Collection on a public page. Drop this in any Markdown page:</p>
86
86
 
@@ -89,25 +89,25 @@
89
89
  fields="company,location,salary,type" /]</code></pre>
90
90
 
91
91
  <p>That single shortcode gives you a server-rendered, cached, SEO-friendly grid of cards. The server
92
- reads the collection at request time, filters it, sorts it, and produces static HTML — every
92
+ reads the collection at request time, filters it, sorts it, and produces static HTML - every
93
93
  visitor gets the same instant render. The page is cached per role, so a million visitors viewing
94
94
  a public page hit the cache and never touch the data store.</p>
95
95
 
96
96
  <p><strong>Five display modes are built in:</strong></p>
97
97
 
98
98
  <ul>
99
- <li><code>display="table"</code> — sortable, paginated, with a search box</li>
100
- <li><code>display="cards"</code> — visual grid (default 3 columns; configurable)</li>
101
- <li><code>display="list"</code> — vertical stack with title + meta</li>
102
- <li><code>display="accordion"</code> — expandable rows, title visible, body hidden until clicked</li>
103
- <li><code>display="timeline"</code> — chronological with dates and statuses</li>
99
+ <li><code>display="table"</code> - sortable, paginated, with a search box</li>
100
+ <li><code>display="cards"</code> - visual grid (default 3 columns; configurable)</li>
101
+ <li><code>display="list"</code> - vertical stack with title + meta</li>
102
+ <li><code>display="accordion"</code> - expandable rows, title visible, body hidden until clicked</li>
103
+ <li><code>display="timeline"</code> - chronological with dates and statuses</li>
104
104
  </ul>
105
105
 
106
106
  <p>All five display the same data; you pick the right shape for the page. A directory might use
107
107
  <code>cards</code>; an admin overview might use <code>table</code>; a history page might use
108
108
  <code>timeline</code>.</p>
109
109
 
110
- <h3>Make it interactive — flip on the Browser</h3>
110
+ <h3>Make it interactive - flip on the Browser</h3>
111
111
 
112
112
  <p>Adding any of <code>searchable</code>, <code>filterable</code>, or <code>sortable</code> upgrades the
113
113
  static list to a full <strong>Collection Browser</strong>:</p>
@@ -119,7 +119,7 @@
119
119
  page-size="9"
120
120
  empty="No matching jobs." /]</code></pre>
121
121
 
122
- <p>Now the page has a search box, a filter rail down the left side, a sort dropdown, and pagination —
122
+ <p>Now the page has a search box, a filter rail down the left side, a sort dropdown, and pagination -
123
123
  all generated automatically from the schema you wrote in Step 1. The Browser is the moment your
124
124
  app starts to feel like an app rather than a brochure.</p>
125
125
 
@@ -131,14 +131,14 @@
131
131
 
132
132
  <hr>
133
133
 
134
- <h2>Step 3 — Create new records (the Form)</h2>
134
+ <h2>Step 3 - Create new records (the Form)</h2>
135
135
 
136
- <p>Open <a href="#/forms">Forms</a> → <strong>New form</strong>. The slug here is the form's identity —
136
+ <p>Open <a href="#/forms">Forms</a> → <strong>New form</strong>. The slug here is the form's identity -
137
137
  what you embed on a page with <code>[form name="..." /]</code>.</p>
138
138
 
139
139
  <p>Form fields can mirror the collection fields exactly, or they can be a subset. <strong>You almost
140
140
  always want a subset.</strong> Fields like <code>status</code> shouldn't appear on a public-facing
141
- submission form — the form should set status to <em>"pending"</em> automatically. Fields like
141
+ submission form - the form should set status to <em>"pending"</em> automatically. Fields like
142
142
  <code>internalNotes</code> shouldn't be visible to the submitter at all. The form is your
143
143
  audience-facing slice of the collection; design it for who's filling it in.</p>
144
144
 
@@ -152,13 +152,13 @@
152
152
  form fires. That's how the "My applications" view later works without any extra config: you ask
153
153
  for "entries created by the current user" and the platform already has that data.</p>
154
154
 
155
- <p>Anonymous submissions still work — <code>createdBy</code> is null and the action receives an empty
155
+ <p>Anonymous submissions still work - <code>createdBy</code> is null and the action receives an empty
156
156
  user. So one form can serve both anonymous contact-us submissions AND authenticated apply-for-this-job
157
157
  submissions; the difference shows up in what the action templates can resolve.</p>
158
158
 
159
159
  <hr>
160
160
 
161
- <h2>Step 4 — Change records over time (the Action)</h2>
161
+ <h2>Step 4 - Change records over time (the Action)</h2>
162
162
 
163
163
  <p>This is where most no-code platforms top out. An Action is a server-side button: visitors click it
164
164
  on a page, the server runs a sequence of steps against the entry they clicked on, and the page
@@ -166,13 +166,13 @@
166
166
 
167
167
  <p>Steps include: <code>updateField</code> (set one field to a new value), <code>deleteEntry</code>
168
168
  (remove the entry), <code>createInCollection</code> (write a related entry to another collection
169
- — this is how "apply for this job" creates an application without losing the job), <code>email</code>
169
+ - this is how "apply for this job" creates an application without losing the job), <code>email</code>
170
170
  (send a notification using the entry's fields as template variables), and <code>webhook</code>
171
171
  (POST to an external service).</p>
172
172
 
173
173
  <h3>The transition is the magic word</h3>
174
174
 
175
- <p>Actions can declare a <strong>transition</strong> — "this action moves the entry's status from
175
+ <p>Actions can declare a <strong>transition</strong> - "this action moves the entry's status from
176
176
  <em>pending</em> to <em>reviewing</em>". That single addition unlocks two huge things:</p>
177
177
 
178
178
  <ol>
@@ -207,15 +207,15 @@
207
207
 
208
208
  <hr>
209
209
 
210
- <h2>Step 5 — Who sees what (Visibility + scope)</h2>
210
+ <h2>Step 5 - Who sees what (Visibility + scope)</h2>
211
211
 
212
212
  <p>Pages have a <code>visibility</code> frontmatter field that decides who can load the page at all.
213
213
  It accepts a single role (<em>"editor and everyone above"</em>) or an array of roles
214
- (<em>"candidates OR employers"</em>). Roles are hierarchical — higher-privilege roles inherit
214
+ (<em>"candidates OR employers"</em>). Roles are hierarchical - higher-privilege roles inherit
215
215
  access to lower-privilege gated pages without you adding them explicitly.</p>
216
216
 
217
- <p>For per-user data <em>within</em> a page that mixed-role users share — like a "My applications"
218
- block on a dashboard that both candidates and employers visit — use <code>scope="mine"</code>:</p>
217
+ <p>For per-user data <em>within</em> a page that mixed-role users share - like a "My applications"
218
+ block on a dashboard that both candidates and employers visit - use <code>scope="mine"</code>:</p>
219
219
 
220
220
  <pre class="code-block"><code>[collection slug="applications" scope="mine"
221
221
  display="cards" title-field="jobId"
@@ -227,7 +227,7 @@
227
227
  tamper with which user's data they see). Anonymous visitors see a sign-in prompt where the block
228
228
  would render.</p>
229
229
 
230
- <p>For cross-collection scoping — <em>"recruiter sees only applications for jobs they posted"</em> —
230
+ <p>For cross-collection scoping - <em>"recruiter sees only applications for jobs they posted"</em> -
231
231
  use the <code>reference</code> row-access mode in your action's <code>access.rowLevel</code>. The
232
232
  platform resolves the reference to check ownership on the target. <a
233
233
  href="/docs/configuration.md" target="_blank">Full row-access reference</a> covers all three
@@ -235,7 +235,7 @@
235
235
 
236
236
  <hr>
237
237
 
238
- <h2>Step 6 — Files, references, and "feels like a real app"</h2>
238
+ <h2>Step 6 - Files, references, and "feels like a real app"</h2>
239
239
 
240
240
  <p>Three field types deserve their own mention because they're where Domma stops looking like a CMS
241
241
  and starts looking like a development platform:</p>
@@ -244,7 +244,7 @@
244
244
  to <code>/content/media/</code> with mime + size validation, then stores a reference object
245
245
  <code>{url, name, size, mime}</code> on the entry. Image mimes auto-display as thumbnails on the
246
246
  page; PDFs and docs become "download" links. The form switches to multipart submission
247
- automatically the moment any file input has a selection — no extra config.</p>
247
+ automatically the moment any file input has a selection - no extra config.</p>
248
248
 
249
249
  <p><strong>Reference fields</strong> (<code>type: "reference"</code>) store the id of another
250
250
  collection's entry. The form picker becomes a populated dropdown of the target collection's
@@ -261,32 +261,32 @@
261
261
 
262
262
  <hr>
263
263
 
264
- <h2>Step 7 — The Browser tour (advanced reading UI)</h2>
264
+ <h2>Step 7 - The Browser tour (advanced reading UI)</h2>
265
265
 
266
266
  <p>We touched on the Browser in Step 2. Here's the rest of what it does for free once you turn it
267
267
  on with <code>searchable filterable=... sortable</code>:</p>
268
268
 
269
269
  <ul>
270
- <li><strong>Faceted filter counts</strong> — every filter chip shows the count of entries that
270
+ <li><strong>Faceted filter counts</strong> - every filter chip shows the count of entries that
271
271
  would match if that option were toggled with current other filters. Standard ecommerce-style
272
272
  faceted browsing.</li>
273
- <li><strong>Empty state with "Clear filters"</strong> — when the user narrows to zero results,
273
+ <li><strong>Empty state with "Clear filters"</strong> - when the user narrows to zero results,
274
274
  one click resets and gets them un-stuck.</li>
275
- <li><strong>Saved searches</strong> — dropdown in the header lets users name and recall their
275
+ <li><strong>Saved searches</strong> - dropdown in the header lets users name and recall their
276
276
  favourite filter combinations.</li>
277
- <li><strong>CSV export</strong> — add <code>exportable</code> and a button generates a CSV of the
277
+ <li><strong>CSV export</strong> - add <code>exportable</code> and a button generates a CSV of the
278
278
  current filtered set.</li>
279
- <li><strong>Mobile filter drawer</strong> — narrow viewports get a "Filters (n)" button that
279
+ <li><strong>Mobile filter drawer</strong> - narrow viewports get a "Filters (n)" button that
280
280
  slides the rail in from the left as an overlay.</li>
281
- <li><strong>URL state sync</strong> — every change writes to the URL query string, so deep-links
281
+ <li><strong>URL state sync</strong> - every change writes to the URL query string, so deep-links
282
282
  and the browser back button work.</li>
283
- <li><strong>Relevance sort during search</strong> — when the search box has a term, results
283
+ <li><strong>Relevance sort during search</strong> - when the search box has a term, results
284
284
  re-order by match count with a 3× boost on matches in the title field.</li>
285
- <li><strong>Keyboard shortcuts</strong> — <code>/</code> focuses search, <code>←</code>/<code>→</code>
285
+ <li><strong>Keyboard shortcuts</strong> - <code>/</code> focuses search, <code>←</code>/<code>→</code>
286
286
  pages.</li>
287
- <li><strong>Infinite scroll</strong> — <code>pagination="scroll"</code> replaces the pager with
287
+ <li><strong>Infinite scroll</strong> - <code>pagination="scroll"</code> replaces the pager with
288
288
  a sentinel that loads more on scroll.</li>
289
- <li><strong>Server-mode for huge datasets</strong> — <code>mode="server"</code> makes every
289
+ <li><strong>Server-mode for huge datasets</strong> - <code>mode="server"</code> makes every
290
290
  change round-trip to the API; the storage adapter (Mongo or File) handles the query;
291
291
  authoring stays identical.</li>
292
292
  </ul>
@@ -295,7 +295,7 @@
295
295
 
296
296
  <hr>
297
297
 
298
- <h2>Pulling it together — the worked example</h2>
298
+ <h2>Pulling it together - the worked example</h2>
299
299
 
300
300
  <p>A <strong>job board</strong> uses every primitive in this tutorial:</p>
301
301
 
@@ -308,7 +308,7 @@
308
308
  <li>A dashboard at <code>/dashboard</code> with <code>visibility: [candidate, employer]</code> and
309
309
  two <code>[collection scope="mine"]</code> blocks (one per role)</li>
310
310
  <li>Transition actions: <em>start-review</em>, <em>invite-to-interview</em>, <em>make-offer</em>,
311
- <em>reject</em>, <em>withdraw</em> — each with its own role + state guards</li>
311
+ <em>reject</em>, <em>withdraw</em> - each with its own role + state guards</li>
312
312
  <li>The candidate dashboard adds <code>transitions</code> and each row gets the right buttons
313
313
  for its current status</li>
314
314
  <li>Recruiters get <code>access.rowLevel: { mode: 'reference', field: 'jobId', targetCollection: 'jobs' }</code>
@@ -320,7 +320,7 @@
320
320
 
321
321
  <hr>
322
322
 
323
- <h2>Skip ahead — scaffold a working CRUD system in one click</h2>
323
+ <h2>Skip ahead - scaffold a working CRUD system in one click</h2>
324
324
 
325
325
  <p>Reading is one thing; having a real working subsystem to poke at is another. The CMS ships with
326
326
  starter <em>recipes</em> that scaffold a complete Collection + Form + Actions in a single
@@ -329,7 +329,7 @@
329
329
  <div id="tutorial-scaffolder-mount" style="margin-top:1rem;"></div>
330
330
 
331
331
  <p class="text-muted" style="margin-top:1rem;font-size:.9rem;">
332
- Recipes are JSON files under <code>server/services/recipes/</code> — copy one as a starting
332
+ Recipes are JSON files under <code>server/services/recipes/</code> - copy one as a starting
333
333
  point for your own. The <a href="/docs/scaffolding.md" target="_blank">scaffolding docs</a>
334
334
  cover the recipe format in full.
335
335
  </p>
@@ -52,16 +52,16 @@ Subject Prefix: [Contact Form]</code></pre>
52
52
  <li>Select the Action from the dropdown and save.</li>
53
53
  </ol>
54
54
  <p>The Action runs server-side, after the entry is saved. If it fails (e.g. MongoDB is not
55
- configured), the submission is still stored — the action failure is non-fatal and logged as a
55
+ configured), the submission is still stored - the action failure is non-fatal and logged as a
56
56
  warning.</p>
57
57
 
58
58
  <h3>4. Success message vs. redirect</h3>
59
59
  <p>After a successful submission the visitor sees one of two things:</p>
60
60
  <ul>
61
- <li><strong>Inline success message</strong> — the form is replaced by the text set in
61
+ <li><strong>Inline success message</strong> - the form is replaced by the text set in
62
62
  <em>Settings → Success Message</em>. Good for simple acknowledgements.
63
63
  </li>
64
- <li><strong>Page redirect</strong> — the visitor is sent to the URL set in
64
+ <li><strong>Page redirect</strong> - the visitor is sent to the URL set in
65
65
  <em>Settings → Success Redirect URL</em>. Good for registration flows, checkouts, or when you
66
66
  want a full thank-you page with additional content. <strong>Takes priority</strong> if both are
67
67
  set.
@@ -80,7 +80,7 @@ Subject Prefix: [Contact Form]</code></pre>
80
80
  <li>Execute CMS Action (if set)</li>
81
81
  <li>Return success response → client redirects or shows message</li>
82
82
  </ol>
83
- <p>Steps 3–5 are non-fatal: a failure in any of them is logged as a warning but does not prevent the
83
+ <p>Steps 3-5 are non-fatal: a failure in any of them is logged as a warning but does not prevent the
84
84
  submission from being stored or the success response from being returned.</p>
85
85
 
86
86
  <hr>
@@ -28,7 +28,7 @@
28
28
 
29
29
  <hr>
30
30
 
31
- <h3>1. plugin.json — the manifest</h3>
31
+ <h3>1. plugin.json - the manifest</h3>
32
32
  <p>All fields below are <strong>required</strong>. Missing any will cause the plugin to be skipped on
33
33
  startup with a warning in the server log.</p>
34
34
  <pre class="code-block"><code>{
@@ -81,10 +81,10 @@
81
81
 
82
82
  <hr>
83
83
 
84
- <h3>2. plugin.js — the Fastify plugin</h3>
84
+ <h3>2. plugin.js - the Fastify plugin</h3>
85
85
  <p>This is the server-side entry point. It must export a default <strong>async function</strong> that
86
86
  Fastify will call with <code>(fastify, options)</code>.</p>
87
- <p>The CMS injects auth middleware through <code>options.auth</code> — always destructure from there
87
+ <p>The CMS injects auth middleware through <code>options.auth</code> - always destructure from there
88
88
  rather than importing directly.</p>
89
89
 
90
90
  <pre class="code-block"><code>import { getPluginSettings, savePluginState } from '../../server/services/plugins.js';
@@ -92,7 +92,7 @@
92
92
  export default async function myPlugin(fastify, options) {
93
93
  const { authenticate, requireAdmin } = options.auth;
94
94
 
95
- // Public endpoint — no auth needed
95
+ // Public endpoint - no auth needed
96
96
  fastify.get('/hello', async () => {
97
97
  return { message: 'Hello from my plugin!' };
98
98
  });
@@ -109,11 +109,11 @@ export default async function myPlugin(fastify, options) {
109
109
  }</code></pre>
110
110
 
111
111
  <p>Routes are registered under the prefix <code>/api/plugins/{name}</code> automatically. You do not
112
- set the prefix yourself — it is always locked to your plugin's directory name.</p>
112
+ set the prefix yourself - it is always locked to your plugin's directory name.</p>
113
113
 
114
114
  <hr>
115
115
 
116
- <h3>3. config.js — settings defaults</h3>
116
+ <h3>3. config.js - settings defaults</h3>
117
117
  <p>Export a plain object of default settings. These are merged with any user overrides stored in
118
118
  <code>config/plugins.json</code> when <code>getPluginSettings()</code> is called.</p>
119
119
 
@@ -156,7 +156,7 @@ export default async function myPlugin(fastify, options) {
156
156
  }
157
157
  }</code></pre>
158
158
 
159
- <p>The view file follows the standard Domma view pattern — a <code>templateUrl</code> and an
159
+ <p>The view file follows the standard Domma view pattern - a <code>templateUrl</code> and an
160
160
  <code>onMount($container)</code> function:</p>
161
161
 
162
162
  <pre class="code-block"><code>// admin/views/my-view.js
@@ -205,7 +205,7 @@ export const myPluginView = {
205
205
  })();
206
206
  &lt;/script&gt;</code></pre>
207
207
 
208
- <p>Snippet paths are validated — they must stay within the plugin's own directory. Paths containing
208
+ <p>Snippet paths are validated - they must stay within the plugin's own directory. Paths containing
209
209
  <code>..</code> are blocked.</p>
210
210
 
211
211
  <hr>
@@ -213,7 +213,7 @@ export const myPluginView = {
213
213
  <h3>6. Registering and testing</h3>
214
214
  <ol>
215
215
  <li>Create the <code>plugins/my-plugin/</code> directory with all three required files.</li>
216
- <li>Restart the server — you should see <code>[plugins] Loaded N plugins: …, my-plugin</code> in
216
+ <li>Restart the server - you should see <code>[plugins] Loaded N plugins: …, my-plugin</code> in
217
217
  the log.
218
218
  </li>
219
219
  <li>Go to the <a href="#/plugins">Plugins page</a> and enable your plugin.</li>
@@ -221,7 +221,7 @@ export const myPluginView = {
221
221
  <li>Verify your endpoint: <code>GET /api/plugins/my-plugin/hello</code></li>
222
222
  </ol>
223
223
 
224
- <p class="text-muted" style="font-size:.9rem">Tip: use <code>npm run dev</code> during development —
224
+ <p class="text-muted" style="font-size:.9rem">Tip: use <code>npm run dev</code> during development -
225
225
  the server restarts automatically on file changes.</p>
226
226
 
227
227
  <hr>