domma-cms 0.25.15 → 0.30.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.
- package/admin/js/app.js +2 -2
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/templates/api-reference.html +68 -1233
- package/admin/js/templates/docs/api-actions.html +146 -0
- package/admin/js/templates/docs/api-authentication.html +202 -0
- package/admin/js/templates/docs/api-collections.html +347 -0
- package/admin/js/templates/docs/api-layouts.html +123 -0
- package/admin/js/templates/docs/api-media.html +158 -0
- package/admin/js/templates/docs/api-navigation.html +99 -0
- package/admin/js/templates/docs/api-pages.html +214 -0
- package/admin/js/templates/docs/api-plugins.html +136 -0
- package/admin/js/templates/docs/api-settings.html +148 -0
- package/admin/js/templates/docs/api-users.html +189 -0
- package/admin/js/templates/docs/api-views.html +142 -0
- package/admin/js/templates/docs/components-howto.html +175 -0
- package/admin/js/templates/docs/components-reference.html +259 -0
- package/admin/js/templates/docs/components-rules.html +167 -0
- package/admin/js/templates/docs/components-walkthrough.html +167 -0
- package/admin/js/templates/docs/tutorial-crud.html +344 -0
- package/admin/js/templates/docs/tutorial-forms.html +93 -0
- package/admin/js/templates/docs/tutorial-plugin.html +234 -0
- package/admin/js/templates/docs/usage-actions.html +122 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +124 -0
- package/admin/js/templates/docs/usage-dconfig.html +148 -0
- package/admin/js/templates/docs/usage-media.html +25 -0
- package/admin/js/templates/docs/usage-navigation.html +31 -0
- package/admin/js/templates/docs/usage-pages.html +68 -0
- package/admin/js/templates/docs/usage-plugins.html +36 -0
- package/admin/js/templates/docs/usage-shortcodes.html +810 -0
- package/admin/js/templates/docs/usage-site-settings.html +52 -0
- package/admin/js/templates/docs/usage-users-roles.html +84 -0
- package/admin/js/templates/docs/usage-views.html +105 -0
- package/admin/js/templates/documentation.html +60 -1522
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/project-settings.html +7 -0
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/templates/tutorials.html +46 -659
- package/admin/js/views/api-reference.js +1 -1
- package/admin/js/views/doc-pages.js +1 -0
- package/admin/js/views/documentation.js +1 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +3 -7
- package/admin/js/views/tutorials.js +1 -1
- package/bin/lib/config-merge.js +44 -44
- package/config/connections.json.bak +9 -0
- package/config/menus/admin-sidebar.json +76 -6
- package/package.json +1 -1
- package/public/js/site.js +1 -1
- package/scripts/migrate-docs.js +280 -0
- package/server/middleware/auth.js +253 -253
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/collections.js +10 -1
- package/server/routes/api/navigation.js +42 -42
- package/server/routes/api/projects.js +9 -1
- package/server/routes/api/settings.js +141 -141
- package/server/routes/public.js +9 -0
- package/server/server.js +6 -2
- package/server/services/email.js +167 -167
- package/server/services/presetCollections.js +1 -0
- package/server/services/projects.js +72 -3
- package/server/services/sidebar-migration.js +145 -3
- package/server/services/userProfiles.js +199 -199
- package/server/services/users.js +302 -302
|
@@ -4,669 +4,56 @@
|
|
|
4
4
|
|
|
5
5
|
<div class="row">
|
|
6
6
|
<div class="col-12">
|
|
7
|
+
<div class="docs-body">
|
|
8
|
+
|
|
9
|
+
<p class="lead">
|
|
10
|
+
Step-by-step, end-to-end guides for building real things with Domma CMS. Pick a track below —
|
|
11
|
+
each opens as its own page, and you can jump straight to any tutorial from the
|
|
12
|
+
<strong>Documentation → Tutorials</strong> menu in the sidebar.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:1rem;margin-top:1.5rem;">
|
|
16
|
+
|
|
17
|
+
<a href="#/tutorials/crud" class="card" style="text-decoration:none;display:block;">
|
|
18
|
+
<div class="card-body">
|
|
19
|
+
<h3 style="margin-top:0;"><span data-icon="database"></span> Building a CRUD App</h3>
|
|
20
|
+
<p class="text-muted" style="margin-bottom:0;">
|
|
21
|
+
The headline tutorial. Compose Collections, Forms, Actions and Pages into a complete
|
|
22
|
+
data-driven app — with workflows, row-level access and a live scaffolder to try at the end.
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
</a>
|
|
26
|
+
|
|
27
|
+
<a href="#/tutorials/plugin" class="card" style="text-decoration:none;display:block;">
|
|
28
|
+
<div class="card-body">
|
|
29
|
+
<h3 style="margin-top:0;"><span data-icon="package"></span> Writing a Plugin</h3>
|
|
30
|
+
<p class="text-muted" style="margin-bottom:0;">
|
|
31
|
+
Extend the CMS with your own server routes, admin pages and page-injection snippets.
|
|
32
|
+
The three required files, the Fastify entry point, and how to register and test.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
</a>
|
|
36
|
+
|
|
37
|
+
<a href="#/tutorials/forms" class="card" style="text-decoration:none;display:block;">
|
|
38
|
+
<div class="card-body">
|
|
39
|
+
<h3 style="margin-top:0;"><span data-icon="layout"></span> Form Follow-Up</h3>
|
|
40
|
+
<p class="text-muted" style="margin-bottom:0;">
|
|
41
|
+
What happens after a form is submitted: email notifications, webhooks, CMS Actions, and
|
|
42
|
+
success message vs. redirect — plus the fixed execution order of the pipeline.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</a>
|
|
7
46
|
|
|
8
|
-
<div class="tabs" id="tutorials-tabs">
|
|
9
|
-
<div class="tab-list" style="flex-wrap: wrap;">
|
|
10
|
-
<button class="tab-item active">Building a CRUD App</button>
|
|
11
|
-
<button class="tab-item">Writing a Plugin</button>
|
|
12
|
-
<button class="tab-item">Form Follow-Up</button>
|
|
13
47
|
</div>
|
|
14
|
-
<div class="tab-content">
|
|
15
|
-
|
|
16
|
-
<!-- Building a CRUD App — the headline tutorial -->
|
|
17
|
-
<div class="tab-panel active docs-body" id="tutorial-crud">
|
|
18
|
-
|
|
19
|
-
<p class="lead">
|
|
20
|
-
A "CRUD app" — Create, Read, Update, Delete — is the shape of almost every business
|
|
21
|
-
tool you'll ever build. Onboarding, bookings, contacts, tickets, orders, RSVPs,
|
|
22
|
-
classified ads. Domma CMS gives you everything to build one without writing
|
|
23
|
-
JavaScript, but the trick is understanding <em>why</em> the pieces compose the way
|
|
24
|
-
they do. That's what this tutorial is for.
|
|
25
|
-
</p>
|
|
26
|
-
|
|
27
|
-
<hr>
|
|
28
|
-
|
|
29
|
-
<h2>The mental model</h2>
|
|
30
|
-
|
|
31
|
-
<p>Before we build anything, the picture you want in your head:</p>
|
|
32
|
-
|
|
33
|
-
<p>A CRUD app is just <strong>somewhere to put data</strong>, <strong>a way for people to give it to you</strong>,
|
|
34
|
-
<strong>buttons that change it once it's there</strong>, and <strong>a page that shows it back</strong>.
|
|
35
|
-
Domma CMS gives each of those a name:</p>
|
|
36
|
-
|
|
37
|
-
<ul>
|
|
38
|
-
<li><strong>Collection</strong> — the data store. Think "spreadsheet": rows are records, columns are fields with types.</li>
|
|
39
|
-
<li><strong>Form</strong> — how new records get added. It writes to a Collection on submit.</li>
|
|
40
|
-
<li><strong>Action</strong> — a server-side button that changes existing records. "Approve", "Withdraw", "Send invoice".</li>
|
|
41
|
-
<li><strong>Page</strong> — a Markdown page with <em>shortcodes</em> that render forms and lists from your collections.</li>
|
|
42
|
-
</ul>
|
|
43
|
-
|
|
44
|
-
<p>These are deliberately separate. A single Collection might be written to by three different Forms
|
|
45
|
-
(one for staff, one for the public, one for an import job) and read by four different Pages (a
|
|
46
|
-
dashboard, a public listing, a per-user "my entries" view, a printable report). Keeping them
|
|
47
|
-
separate is what lets the same data drive multiple experiences.</p>
|
|
48
|
-
|
|
49
|
-
<div class="alert alert-info">
|
|
50
|
-
<strong>Why not a database?</strong> Collections store as flat JSON files on disk by default —
|
|
51
|
-
no database to install, no SQL to learn, no migrations. When you outgrow that, switch
|
|
52
|
-
individual collections to MongoDB without changing any of your pages or forms. The
|
|
53
|
-
compatibility layer is the point.
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<hr>
|
|
57
|
-
|
|
58
|
-
<h2>Step 1 — Create the Collection (your data store)</h2>
|
|
59
|
-
|
|
60
|
-
<p>Open <a href="#/collections">Collections</a> → <strong>New collection</strong>. You'll give it a
|
|
61
|
-
slug (the URL-safe name we use in shortcodes), a title, and a list of fields.</p>
|
|
62
|
-
|
|
63
|
-
<p><strong>The slug matters more than you'd think.</strong> It's what every Form, Action, and shortcode
|
|
64
|
-
uses to refer to this collection — so pick a noun and stick with it. <code>jobs</code>,
|
|
65
|
-
<code>applications</code>, <code>contacts</code>, <code>events</code>. Don't pluralise inconsistently
|
|
66
|
-
and don't include the word "data" or "collection" in the slug — that's redundant.</p>
|
|
67
|
-
|
|
68
|
-
<p><strong>Fields are where the design lives.</strong> Each field has a <em>type</em>, and the type isn't
|
|
69
|
-
just for show — it drives every downstream behaviour:</p>
|
|
70
|
-
|
|
71
|
-
<ul>
|
|
72
|
-
<li><code>text</code> renders a text input on forms and is searchable in the Browser</li>
|
|
73
|
-
<li><code>number</code> renders a number input, gets a min/max range filter automatically</li>
|
|
74
|
-
<li><code>select</code> with options becomes a dropdown both on forms AND in the filter rail</li>
|
|
75
|
-
<li><code>multiselect</code> becomes a checkbox group AND a filterable tag chip set</li>
|
|
76
|
-
<li><code>date</code> renders a date picker AND gets a from/to range filter</li>
|
|
77
|
-
<li><code>file</code> renders a file upload with mime/size validation</li>
|
|
78
|
-
<li><code>reference</code> stores a link to another collection's entry — auto-renders as a populated dropdown</li>
|
|
79
|
-
</ul>
|
|
80
|
-
|
|
81
|
-
<p>Pick the right type at design time and you get the right form input, the right filter UI, and the
|
|
82
|
-
right validation, all for free. Pick <code>text</code> for everything and you have to recreate all
|
|
83
|
-
that yourself.</p>
|
|
84
|
-
|
|
85
|
-
<p><strong>Tip:</strong> always include a <code>status</code> field as a <code>select</code> with values
|
|
86
|
-
like <em>pending</em>, <em>reviewing</em>, <em>approved</em>, <em>rejected</em>. This is what makes
|
|
87
|
-
state-machine transitions possible later. You don't need it until you do, but adding it later means
|
|
88
|
-
back-filling every existing record. Add it on day one.</p>
|
|
89
|
-
|
|
90
|
-
<hr>
|
|
91
|
-
|
|
92
|
-
<h2>Step 2 — Read the data (display it on a page)</h2>
|
|
93
|
-
|
|
94
|
-
<p>You don't need code to list a Collection on a public page. Drop this in any Markdown page:</p>
|
|
95
|
-
|
|
96
|
-
<pre class="code-block"><code>[collection slug="jobs" display="cards" columns="3"
|
|
97
|
-
title-field="title" sort="postedAt" order="desc"
|
|
98
|
-
fields="company,location,salary,type" /]</code></pre>
|
|
99
|
-
|
|
100
|
-
<p>That single shortcode gives you a server-rendered, cached, SEO-friendly grid of cards. The server
|
|
101
|
-
reads the collection at request time, filters it, sorts it, and produces static HTML — every
|
|
102
|
-
visitor gets the same instant render. The page is cached per role, so a million visitors viewing
|
|
103
|
-
a public page hit the cache and never touch the data store.</p>
|
|
104
|
-
|
|
105
|
-
<p><strong>Five display modes are built in:</strong></p>
|
|
106
|
-
|
|
107
|
-
<ul>
|
|
108
|
-
<li><code>display="table"</code> — sortable, paginated, with a search box</li>
|
|
109
|
-
<li><code>display="cards"</code> — visual grid (default 3 columns; configurable)</li>
|
|
110
|
-
<li><code>display="list"</code> — vertical stack with title + meta</li>
|
|
111
|
-
<li><code>display="accordion"</code> — expandable rows, title visible, body hidden until clicked</li>
|
|
112
|
-
<li><code>display="timeline"</code> — chronological with dates and statuses</li>
|
|
113
|
-
</ul>
|
|
114
|
-
|
|
115
|
-
<p>All five display the same data; you pick the right shape for the page. A directory might use
|
|
116
|
-
<code>cards</code>; an admin overview might use <code>table</code>; a history page might use
|
|
117
|
-
<code>timeline</code>.</p>
|
|
118
|
-
|
|
119
|
-
<h3>Make it interactive — flip on the Browser</h3>
|
|
120
|
-
|
|
121
|
-
<p>Adding any of <code>searchable</code>, <code>filterable</code>, or <code>sortable</code> upgrades the
|
|
122
|
-
static list to a full <strong>Collection Browser</strong>:</p>
|
|
123
|
-
|
|
124
|
-
<pre class="code-block"><code>[collection slug="jobs" display="cards" columns="3"
|
|
125
|
-
searchable
|
|
126
|
-
filterable="location,type,salary,tags"
|
|
127
|
-
sortable
|
|
128
|
-
page-size="9"
|
|
129
|
-
empty="No matching jobs." /]</code></pre>
|
|
130
|
-
|
|
131
|
-
<p>Now the page has a search box, a filter rail down the left side, a sort dropdown, and pagination —
|
|
132
|
-
all generated automatically from the schema you wrote in Step 1. The Browser is the moment your
|
|
133
|
-
app starts to feel like an app rather than a brochure.</p>
|
|
134
|
-
|
|
135
|
-
<p><strong>Why the filter rail is automatic:</strong> remember each field has a type? The Browser
|
|
136
|
-
reads those types and builds the appropriate control. <code>location</code> (text) becomes a
|
|
137
|
-
dropdown of distinct values harvested from the data. <code>salary</code> (number) becomes a
|
|
138
|
-
min/max range with a slider. <code>tags</code> (multiselect) becomes toggleable chips with counts.
|
|
139
|
-
You authored zero filter logic.</p>
|
|
140
|
-
|
|
141
|
-
<hr>
|
|
142
|
-
|
|
143
|
-
<h2>Step 3 — Create new records (the Form)</h2>
|
|
144
|
-
|
|
145
|
-
<p>Open <a href="#/forms">Forms</a> → <strong>New form</strong>. The slug here is the form's identity —
|
|
146
|
-
what you embed on a page with <code>[form name="..." /]</code>.</p>
|
|
147
|
-
|
|
148
|
-
<p>Form fields can mirror the collection fields exactly, or they can be a subset. <strong>You almost
|
|
149
|
-
always want a subset.</strong> Fields like <code>status</code> shouldn't appear on a public-facing
|
|
150
|
-
submission form — the form should set status to <em>"pending"</em> automatically. Fields like
|
|
151
|
-
<code>internalNotes</code> shouldn't be visible to the submitter at all. The form is your
|
|
152
|
-
audience-facing slice of the collection; design it for who's filling it in.</p>
|
|
153
|
-
|
|
154
|
-
<p>Wire the form to the collection in <strong>Actions → Collection</strong>: enable it and pick the
|
|
155
|
-
target collection slug. Every submission becomes one new entry in that collection. Done.</p>
|
|
156
|
-
|
|
157
|
-
<h3>Identity capture (the small thing that makes everything work)</h3>
|
|
158
|
-
|
|
159
|
-
<p>When a signed-in user submits a form, the CMS automatically stamps the new entry with their user
|
|
160
|
-
id (in the entry's <code>meta.createdBy</code>) AND passes their identity into any actions the
|
|
161
|
-
form fires. That's how the "My applications" view later works without any extra config: you ask
|
|
162
|
-
for "entries created by the current user" and the platform already has that data.</p>
|
|
163
|
-
|
|
164
|
-
<p>Anonymous submissions still work — <code>createdBy</code> is null and the action receives an empty
|
|
165
|
-
user. So one form can serve both anonymous contact-us submissions AND authenticated apply-for-this-job
|
|
166
|
-
submissions; the difference shows up in what the action templates can resolve.</p>
|
|
167
|
-
|
|
168
|
-
<hr>
|
|
169
|
-
|
|
170
|
-
<h2>Step 4 — Change records over time (the Action)</h2>
|
|
171
|
-
|
|
172
|
-
<p>This is where most no-code platforms top out. An Action is a server-side button: visitors click it
|
|
173
|
-
on a page, the server runs a sequence of steps against the entry they clicked on, and the page
|
|
174
|
-
refreshes.</p>
|
|
175
|
-
|
|
176
|
-
<p>Steps include: <code>updateField</code> (set one field to a new value), <code>deleteEntry</code>
|
|
177
|
-
(remove the entry), <code>createInCollection</code> (write a related entry to another collection
|
|
178
|
-
— this is how "apply for this job" creates an application without losing the job), <code>email</code>
|
|
179
|
-
(send a notification using the entry's fields as template variables), and <code>webhook</code>
|
|
180
|
-
(POST to an external service).</p>
|
|
181
|
-
|
|
182
|
-
<h3>The transition is the magic word</h3>
|
|
183
|
-
|
|
184
|
-
<p>Actions can declare a <strong>transition</strong> — "this action moves the entry's status from
|
|
185
|
-
<em>pending</em> to <em>reviewing</em>". That single addition unlocks two huge things:</p>
|
|
186
|
-
|
|
187
|
-
<ol>
|
|
188
|
-
<li><strong>Server-side guard.</strong> Try to run "withdraw" on an already-rejected application
|
|
189
|
-
and the server refuses with HTTP 409. The illegal move can't happen, even if someone copies
|
|
190
|
-
and edits the URL.
|
|
191
|
-
</li>
|
|
192
|
-
<li><strong>Per-row UI.</strong> Add <code>transitions</code> to a <code>[collection]</code> block
|
|
193
|
-
and each row sprouts buttons for exactly the transitions legally available given that row's
|
|
194
|
-
current status AND the viewer's role. A candidate sees "Withdraw" on their pending application;
|
|
195
|
-
an admin sees "Move to reviewing"; a candidate viewing a rejected application sees nothing
|
|
196
|
-
to click. No client-side conditionals; the rules live entirely in the action definitions.
|
|
197
|
-
</li>
|
|
198
|
-
</ol>
|
|
199
|
-
|
|
200
|
-
<pre class="code-block"><code>{
|
|
201
|
-
"slug": "withdraw-application",
|
|
202
|
-
"title": "Withdraw",
|
|
203
|
-
"collection": "applications",
|
|
204
|
-
"transition": { "field": "status", "from": ["submitted", "reviewing"], "to": "withdrawn" },
|
|
205
|
-
"access": { "roles": ["candidate"], "rowLevel": { "mode": "owner" } },
|
|
206
|
-
"steps": [
|
|
207
|
-
{ "type": "updateField", "config": { "field": "status", "value": "withdrawn" } }
|
|
208
|
-
]
|
|
209
|
-
}</code></pre>
|
|
210
|
-
|
|
211
|
-
<p>Read that JSON like a sentence: <em>"Anyone with the candidate role can withdraw their own
|
|
212
|
-
application as long as it's currently submitted or reviewing."</em> The platform enforces every
|
|
213
|
-
clause: <code>access.roles</code> for the role check, <code>rowLevel.mode: 'owner'</code> so
|
|
214
|
-
candidates can only touch their own entries, <code>transition.from</code> for the status guard.
|
|
215
|
-
Together they're the whole authorisation policy for this one button.</p>
|
|
216
|
-
|
|
217
|
-
<hr>
|
|
218
|
-
|
|
219
|
-
<h2>Step 5 — Who sees what (Visibility + scope)</h2>
|
|
220
|
-
|
|
221
|
-
<p>Pages have a <code>visibility</code> frontmatter field that decides who can load the page at all.
|
|
222
|
-
It accepts a single role (<em>"editor and everyone above"</em>) or an array of roles
|
|
223
|
-
(<em>"candidates OR employers"</em>). Roles are hierarchical — higher-privilege roles inherit
|
|
224
|
-
access to lower-privilege gated pages without you adding them explicitly.</p>
|
|
225
|
-
|
|
226
|
-
<p>For per-user data <em>within</em> a page that mixed-role users share — like a "My applications"
|
|
227
|
-
block on a dashboard that both candidates and employers visit — use <code>scope="mine"</code>:</p>
|
|
228
|
-
|
|
229
|
-
<pre class="code-block"><code>[collection slug="applications" scope="mine"
|
|
230
|
-
display="cards" title-field="jobId"
|
|
231
|
-
fields="jobId,status,submittedAt"
|
|
232
|
-
empty="You haven't applied yet." /]</code></pre>
|
|
233
|
-
|
|
234
|
-
<p>The page itself stays cached per role; the per-user block renders client-side via a small hydration
|
|
235
|
-
request that injects <code>createdBy = current-user-id</code> server-side (the client can never
|
|
236
|
-
tamper with which user's data they see). Anonymous visitors see a sign-in prompt where the block
|
|
237
|
-
would render.</p>
|
|
238
|
-
|
|
239
|
-
<p>For cross-collection scoping — <em>"recruiter sees only applications for jobs they posted"</em> —
|
|
240
|
-
use the <code>reference</code> row-access mode in your action's <code>access.rowLevel</code>. The
|
|
241
|
-
platform resolves the reference to check ownership on the target. <a
|
|
242
|
-
href="/docs/configuration.md" target="_blank">Full row-access reference</a> covers all three
|
|
243
|
-
modes (<code>owner</code>, <code>field</code>, <code>reference</code>) with worked examples.</p>
|
|
244
|
-
|
|
245
|
-
<hr>
|
|
246
|
-
|
|
247
|
-
<h2>Step 6 — Files, references, and "feels like a real app"</h2>
|
|
248
|
-
|
|
249
|
-
<p>Three field types deserve their own mention because they're where Domma stops looking like a CMS
|
|
250
|
-
and starts looking like a development platform:</p>
|
|
251
|
-
|
|
252
|
-
<p><strong>File fields</strong> (<code>type: "file"</code>) render a native file picker that uploads
|
|
253
|
-
to <code>/content/media/</code> with mime + size validation, then stores a reference object
|
|
254
|
-
<code>{url, name, size, mime}</code> on the entry. Image mimes auto-display as thumbnails on the
|
|
255
|
-
page; PDFs and docs become "download" links. The form switches to multipart submission
|
|
256
|
-
automatically the moment any file input has a selection — no extra config.</p>
|
|
257
48
|
|
|
258
|
-
|
|
259
|
-
collection's entry. The form picker becomes a populated dropdown of the target collection's
|
|
260
|
-
entries (showing the <code>displayField</code>); the page display resolves to the readable label
|
|
261
|
-
everywhere; with a <code>linkTemplate</code> the label becomes a clickable link to the target's
|
|
262
|
-
detail page. Validation refuses to save an entry pointing at a non-existent target. Dangling
|
|
263
|
-
references (target deleted later) render as "<em>id</em> (missing)" instead of crashing the page.</p>
|
|
49
|
+
<hr style="margin-top:2rem;">
|
|
264
50
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
<hr>
|
|
272
|
-
|
|
273
|
-
<h2>Step 7 — The Browser tour (advanced reading UI)</h2>
|
|
274
|
-
|
|
275
|
-
<p>We touched on the Browser in Step 2. Here's the rest of what it does for free once you turn it
|
|
276
|
-
on with <code>searchable filterable=... sortable</code>:</p>
|
|
277
|
-
|
|
278
|
-
<ul>
|
|
279
|
-
<li><strong>Faceted filter counts</strong> — every filter chip shows the count of entries that
|
|
280
|
-
would match if that option were toggled with current other filters. Standard ecommerce-style
|
|
281
|
-
faceted browsing.</li>
|
|
282
|
-
<li><strong>Empty state with "Clear filters"</strong> — when the user narrows to zero results,
|
|
283
|
-
one click resets and gets them un-stuck.</li>
|
|
284
|
-
<li><strong>Saved searches</strong> — dropdown in the header lets users name and recall their
|
|
285
|
-
favourite filter combinations.</li>
|
|
286
|
-
<li><strong>CSV export</strong> — add <code>exportable</code> and a button generates a CSV of the
|
|
287
|
-
current filtered set.</li>
|
|
288
|
-
<li><strong>Mobile filter drawer</strong> — narrow viewports get a "Filters (n)" button that
|
|
289
|
-
slides the rail in from the left as an overlay.</li>
|
|
290
|
-
<li><strong>URL state sync</strong> — every change writes to the URL query string, so deep-links
|
|
291
|
-
and the browser back button work.</li>
|
|
292
|
-
<li><strong>Relevance sort during search</strong> — when the search box has a term, results
|
|
293
|
-
re-order by match count with a 3× boost on matches in the title field.</li>
|
|
294
|
-
<li><strong>Keyboard shortcuts</strong> — <code>/</code> focuses search, <code>←</code>/<code>→</code>
|
|
295
|
-
pages.</li>
|
|
296
|
-
<li><strong>Infinite scroll</strong> — <code>pagination="scroll"</code> replaces the pager with
|
|
297
|
-
a sentinel that loads more on scroll.</li>
|
|
298
|
-
<li><strong>Server-mode for huge datasets</strong> — <code>mode="server"</code> makes every
|
|
299
|
-
change round-trip to the API; the storage adapter (Mongo or File) handles the query;
|
|
300
|
-
authoring stays identical.</li>
|
|
301
|
-
</ul>
|
|
302
|
-
|
|
303
|
-
<p>None of this requires any JavaScript on your part. You add attribute names to a shortcode.</p>
|
|
304
|
-
|
|
305
|
-
<hr>
|
|
306
|
-
|
|
307
|
-
<h2>Pulling it together — the worked example</h2>
|
|
308
|
-
|
|
309
|
-
<p>A <strong>job board</strong> uses every primitive in this tutorial:</p>
|
|
310
|
-
|
|
311
|
-
<ol>
|
|
312
|
-
<li>Two collections: <code>jobs</code> (employer posts roles) and <code>applications</code> (candidates apply)</li>
|
|
313
|
-
<li><code>applications</code> has a <code>reference</code> field <code>jobId</code> pointing at <code>jobs</code></li>
|
|
314
|
-
<li><code>applications</code> has a <code>file</code> field <code>resume</code> for the candidate's PDF</li>
|
|
315
|
-
<li>A public <code>[collection slug="jobs"]</code> on <code>/jobs</code> with the Browser turned on</li>
|
|
316
|
-
<li>An apply form that writes to <code>applications</code> + an action that emails HR</li>
|
|
317
|
-
<li>A dashboard at <code>/dashboard</code> with <code>visibility: [candidate, employer]</code> and
|
|
318
|
-
two <code>[collection scope="mine"]</code> blocks (one per role)</li>
|
|
319
|
-
<li>Transition actions: <em>start-review</em>, <em>invite-to-interview</em>, <em>make-offer</em>,
|
|
320
|
-
<em>reject</em>, <em>withdraw</em> — each with its own role + state guards</li>
|
|
321
|
-
<li>The candidate dashboard adds <code>transitions</code> and each row gets the right buttons
|
|
322
|
-
for its current status</li>
|
|
323
|
-
<li>Recruiters get <code>access.rowLevel: { mode: 'reference', field: 'jobId', targetCollection: 'jobs' }</code>
|
|
324
|
-
on their transition actions so they only ever see applications for jobs they posted</li>
|
|
325
|
-
</ol>
|
|
326
|
-
|
|
327
|
-
<p>That's a complete recruitment platform built in pure JSON + Markdown. The full annotated build
|
|
328
|
-
lives at <a href="/docs/recruitment-recipe.md" target="_blank">docs/recruitment-recipe.md</a>.</p>
|
|
329
|
-
|
|
330
|
-
<hr>
|
|
331
|
-
|
|
332
|
-
<h2>Skip ahead — scaffold a working CRUD system in one click</h2>
|
|
333
|
-
|
|
334
|
-
<p>Reading is one thing; having a real working subsystem to poke at is another. The CMS ships with
|
|
335
|
-
starter <em>recipes</em> that scaffold a complete Collection + Form + Actions in a single
|
|
336
|
-
operation. Try one now:</p>
|
|
337
|
-
|
|
338
|
-
<div id="tutorial-scaffolder-mount" style="margin-top:1rem;"></div>
|
|
339
|
-
|
|
340
|
-
<p class="text-muted" style="margin-top:1rem;font-size:.9rem;">
|
|
341
|
-
Recipes are JSON files under <code>server/services/recipes/</code> — copy one as a starting
|
|
342
|
-
point for your own. The <a href="/docs/scaffolding.md" target="_blank">scaffolding docs</a>
|
|
343
|
-
cover the recipe format in full.
|
|
344
|
-
</p>
|
|
345
|
-
|
|
346
|
-
</div>
|
|
347
|
-
|
|
348
|
-
<!-- Writing a Plugin -->
|
|
349
|
-
<div class="tab-panel docs-body">
|
|
350
|
-
<p>Plugins live in the <code>plugins/</code> directory. Each plugin is a self-contained folder with
|
|
351
|
-
three
|
|
352
|
-
required files and optional <code>admin/</code> and <code>public/</code> subdirectories.</p>
|
|
353
|
-
|
|
354
|
-
<h3>Directory structure</h3>
|
|
355
|
-
<pre class="code-block"><code>plugins/
|
|
356
|
-
my-plugin/
|
|
357
|
-
plugin.json ← manifest (required)
|
|
358
|
-
plugin.js ← Fastify plugin (required)
|
|
359
|
-
config.js ← settings defaults (required)
|
|
360
|
-
admin/
|
|
361
|
-
views/
|
|
362
|
-
my-view.js ← admin SPA view (optional)
|
|
363
|
-
templates/
|
|
364
|
-
my-view.html ← view template (optional)
|
|
365
|
-
public/
|
|
366
|
-
inject-head.html ← injected into <head> on every page (optional)
|
|
367
|
-
inject-body.html ← injected before </body> on every page (optional)
|
|
368
|
-
data/ ← plugin data store (optional, not publicly served)</code></pre>
|
|
369
|
-
|
|
370
|
-
<hr>
|
|
371
|
-
|
|
372
|
-
<h3>1. plugin.json — the manifest</h3>
|
|
373
|
-
<p>All fields below are <strong>required</strong>. Missing any will cause the plugin to be skipped on
|
|
374
|
-
startup
|
|
375
|
-
with a warning in the server log.</p>
|
|
376
|
-
<pre class="code-block"><code>{
|
|
377
|
-
"name": "my-plugin",
|
|
378
|
-
"displayName": "My Plugin",
|
|
379
|
-
"version": "1.0.0",
|
|
380
|
-
"description": "A short description shown on the Plugins page.",
|
|
381
|
-
"author": "Your Name",
|
|
382
|
-
"date": "2026-03-01",
|
|
383
|
-
"icon": "star"
|
|
384
|
-
}</code></pre>
|
|
385
|
-
|
|
386
|
-
<p>Optional fields:</p>
|
|
387
|
-
<table class="table table-sm">
|
|
388
|
-
<thead>
|
|
389
|
-
<tr>
|
|
390
|
-
<th>Field</th>
|
|
391
|
-
<th>Type</th>
|
|
392
|
-
<th>Description</th>
|
|
393
|
-
</tr>
|
|
394
|
-
</thead>
|
|
395
|
-
<tbody>
|
|
396
|
-
<tr>
|
|
397
|
-
<td><code>inject.head</code></td>
|
|
398
|
-
<td>string</td>
|
|
399
|
-
<td>Path (relative to plugin root) to an HTML snippet injected into <code><head></code>.
|
|
400
|
-
</td>
|
|
401
|
-
</tr>
|
|
402
|
-
<tr>
|
|
403
|
-
<td><code>inject.bodyEnd</code></td>
|
|
404
|
-
<td>string</td>
|
|
405
|
-
<td>Path to an HTML snippet injected before <code></body></code>.</td>
|
|
406
|
-
</tr>
|
|
407
|
-
<tr>
|
|
408
|
-
<td><code>admin.sidebar</code></td>
|
|
409
|
-
<td>array</td>
|
|
410
|
-
<td>Sidebar items to add to the admin panel.</td>
|
|
411
|
-
</tr>
|
|
412
|
-
<tr>
|
|
413
|
-
<td><code>admin.routes</code></td>
|
|
414
|
-
<td>array</td>
|
|
415
|
-
<td>SPA routes to register in the admin router.</td>
|
|
416
|
-
</tr>
|
|
417
|
-
<tr>
|
|
418
|
-
<td><code>admin.views</code></td>
|
|
419
|
-
<td>object</td>
|
|
420
|
-
<td>View modules to dynamically import into the admin SPA.</td>
|
|
421
|
-
</tr>
|
|
422
|
-
</tbody>
|
|
423
|
-
</table>
|
|
424
|
-
|
|
425
|
-
<hr>
|
|
426
|
-
|
|
427
|
-
<h3>2. plugin.js — the Fastify plugin</h3>
|
|
428
|
-
<p>This is the server-side entry point. It must export a default <strong>async function</strong> that
|
|
429
|
-
Fastify
|
|
430
|
-
will call with <code>(fastify, options)</code>.</p>
|
|
431
|
-
<p>The CMS injects auth middleware through <code>options.auth</code> — always destructure from there
|
|
432
|
-
rather than
|
|
433
|
-
importing directly.</p>
|
|
434
|
-
|
|
435
|
-
<pre class="code-block"><code>import { getPluginSettings, savePluginState } from '../../server/services/plugins.js';
|
|
436
|
-
|
|
437
|
-
export default async function myPlugin(fastify, options) {
|
|
438
|
-
const { authenticate, requireAdmin } = options.auth;
|
|
439
|
-
|
|
440
|
-
// Public endpoint — no auth needed
|
|
441
|
-
fastify.get('/hello', async () => {
|
|
442
|
-
return { message: 'Hello from my plugin!' };
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
// Admin-only endpoint
|
|
446
|
-
fastify.get('/settings', { preHandler: [authenticate, requireAdmin] }, async () => {
|
|
447
|
-
return getPluginSettings('my-plugin');
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
fastify.put('/settings', { preHandler: [authenticate, requireAdmin] }, async (request) => {
|
|
451
|
-
savePluginState('my-plugin', { settings: request.body });
|
|
452
|
-
return { ok: true };
|
|
453
|
-
});
|
|
454
|
-
}</code></pre>
|
|
455
|
-
|
|
456
|
-
<p>Routes are registered under the prefix <code>/api/plugins/{name}</code> automatically. You do not
|
|
457
|
-
set the
|
|
458
|
-
prefix yourself — it is always locked to your plugin's directory name.</p>
|
|
459
|
-
|
|
460
|
-
<hr>
|
|
461
|
-
|
|
462
|
-
<h3>3. config.js — settings defaults</h3>
|
|
463
|
-
<p>Export a plain object of default settings. These are merged with any user overrides stored in
|
|
464
|
-
<code>config/plugins.json</code>
|
|
465
|
-
when <code>getPluginSettings()</code> is called.</p>
|
|
466
|
-
|
|
467
|
-
<pre class="code-block"><code>export default {
|
|
468
|
-
greeting: 'Hello, world!',
|
|
469
|
-
enableFeature: true,
|
|
470
|
-
maxItems: 10
|
|
471
|
-
};</code></pre>
|
|
472
|
-
|
|
473
|
-
<p><code>config.js</code> is only loaded for <strong>enabled</strong> plugins. Side-effect code here
|
|
474
|
-
will not
|
|
475
|
-
run for disabled plugins.</p>
|
|
476
|
-
|
|
477
|
-
<hr>
|
|
478
|
-
|
|
479
|
-
<h3>4. Admin views (optional)</h3>
|
|
480
|
-
<p>To add a page to the admin panel, declare the route and view in <code>plugin.json</code>:</p>
|
|
481
|
-
|
|
482
|
-
<pre class="code-block"><code>"admin": {
|
|
483
|
-
"sidebar": [
|
|
484
|
-
{
|
|
485
|
-
"id": "my-plugin",
|
|
486
|
-
"text": "My Plugin",
|
|
487
|
-
"icon": "star",
|
|
488
|
-
"url": "#/plugins/my-plugin",
|
|
489
|
-
"section": "#/plugins/my-plugin"
|
|
490
|
-
}
|
|
491
|
-
],
|
|
492
|
-
"routes": [
|
|
493
|
-
{
|
|
494
|
-
"path": "/plugins/my-plugin",
|
|
495
|
-
"view": "plugin-my-plugin",
|
|
496
|
-
"title": "My Plugin - Domma CMS"
|
|
497
|
-
}
|
|
498
|
-
],
|
|
499
|
-
"views": {
|
|
500
|
-
"plugin-my-plugin": {
|
|
501
|
-
"entry": "my-plugin/admin/views/my-view.js",
|
|
502
|
-
"exportName": "myPluginView"
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}</code></pre>
|
|
506
|
-
|
|
507
|
-
<p>The view file follows the standard Domma view pattern — a <code>templateUrl</code> and an <code>onMount($container)</code>
|
|
508
|
-
function:</p>
|
|
509
|
-
|
|
510
|
-
<pre class="code-block"><code>// admin/views/my-view.js
|
|
511
|
-
export const myPluginView = {
|
|
512
|
-
templateUrl: '/plugins/my-plugin/admin/templates/my-view.html',
|
|
513
|
-
|
|
514
|
-
async onMount($container) {
|
|
515
|
-
const res = await fetch('/api/plugins/my-plugin/settings', {
|
|
516
|
-
headers: { 'Authorization': 'Bearer ' + (S.get('auth_token') || '') }
|
|
517
|
-
});
|
|
518
|
-
const settings = await res.json();
|
|
519
|
-
|
|
520
|
-
$container.find('#greeting').text(settings.greeting);
|
|
521
|
-
Domma.icons.scan();
|
|
522
|
-
}
|
|
523
|
-
};</code></pre>
|
|
524
|
-
|
|
525
|
-
<p>The template is a plain HTML fragment (no <code><html></code> wrapper). Use the same card and
|
|
526
|
-
form
|
|
527
|
-
patterns as the rest of the admin panel:</p>
|
|
528
|
-
|
|
529
|
-
<pre class="code-block"><code><!-- admin/templates/my-view.html -->
|
|
530
|
-
<div class="view-header">
|
|
531
|
-
<h1><span data-icon="star"></span> My Plugin</h1>
|
|
532
|
-
</div>
|
|
533
|
-
|
|
534
|
-
<div class="card">
|
|
535
|
-
<div class="card-body">
|
|
536
|
-
<p id="greeting">Loading…</p>
|
|
537
|
-
</div>
|
|
538
|
-
</div></code></pre>
|
|
539
|
-
|
|
540
|
-
<hr>
|
|
541
|
-
|
|
542
|
-
<h3>5. Injection snippets (optional)</h3>
|
|
543
|
-
<p>HTML snippets declared in <code>inject.head</code> and <code>inject.bodyEnd</code> are read from
|
|
544
|
-
the plugin's
|
|
545
|
-
<code>public/</code> directory and inserted into every public page. Use this for analytics
|
|
546
|
-
scripts,
|
|
547
|
-
stylesheets, or widgets.</p>
|
|
548
|
-
|
|
549
|
-
<pre class="code-block"><code><!-- public/inject-body.html -->
|
|
550
|
-
<script>
|
|
551
|
-
(function () {
|
|
552
|
-
// This runs on every public page
|
|
553
|
-
fetch('/api/plugins/my-plugin/hello')
|
|
554
|
-
.then(r => r.json())
|
|
555
|
-
.then(d => console.log(d.message));
|
|
556
|
-
})();
|
|
557
|
-
</script></code></pre>
|
|
558
|
-
|
|
559
|
-
<p>Snippet paths are validated — they must stay within the plugin's own directory. Paths containing
|
|
560
|
-
<code>..</code> are blocked.</p>
|
|
561
|
-
|
|
562
|
-
<hr>
|
|
563
|
-
|
|
564
|
-
<h3>6. Registering and testing</h3>
|
|
565
|
-
<ol>
|
|
566
|
-
<li>Create the <code>plugins/my-plugin/</code> directory with all three required files.</li>
|
|
567
|
-
<li>Restart the server — you should see <code>[plugins] Loaded N plugins: …, my-plugin</code> in
|
|
568
|
-
the log.
|
|
569
|
-
</li>
|
|
570
|
-
<li>Go to the <a href="#/plugins">Plugins page</a> and enable your plugin.</li>
|
|
571
|
-
<li>Restart the server again to register the routes.</li>
|
|
572
|
-
<li>Verify your endpoint: <code>GET /api/plugins/my-plugin/hello</code></li>
|
|
573
|
-
</ol>
|
|
574
|
-
|
|
575
|
-
<p class="text-muted" style="font-size:.9rem">Tip: use <code>npm run dev</code> during development —
|
|
576
|
-
the server
|
|
577
|
-
restarts automatically on file changes.</p>
|
|
578
|
-
</div>
|
|
579
|
-
|
|
580
|
-
<!-- Form Follow-Up -->
|
|
581
|
-
<div class="tab-panel docs-body">
|
|
582
|
-
<p>Every form in Domma CMS can trigger up to four things after a submission is stored:</p>
|
|
583
|
-
<ol>
|
|
584
|
-
<li>Send an <strong>email notification</strong> to one or more recipients</li>
|
|
585
|
-
<li>POST to a <strong>webhook URL</strong></li>
|
|
586
|
-
<li>Execute a <strong>CMS Action</strong> (Pro)</li>
|
|
587
|
-
<li>Redirect the visitor to a <strong>success page</strong> (or show an inline message)</li>
|
|
588
|
-
</ol>
|
|
589
|
-
<p>These are configured per-form in the admin. Open any form in <a href="#/forms">Forms</a> and go to
|
|
590
|
-
the
|
|
591
|
-
<strong>Settings</strong> and <strong>Actions</strong> tabs.</p>
|
|
592
|
-
|
|
593
|
-
<hr>
|
|
594
|
-
|
|
595
|
-
<h3>1. Email notification</h3>
|
|
596
|
-
<p>Go to the <strong>Actions</strong> tab of your form and enable <em>Send email on submit</em>. Enter
|
|
597
|
-
one or
|
|
598
|
-
more comma-separated recipient addresses. This uses the SMTP settings configured in
|
|
599
|
-
<a href="#/settings">Site Settings → Email / SMTP</a>.</p>
|
|
600
|
-
|
|
601
|
-
<pre class="code-block"><code>Recipients: admin@example.com, team@example.com
|
|
602
|
-
Subject Prefix: [Contact Form]</code></pre>
|
|
603
|
-
|
|
604
|
-
<h3>2. Webhook</h3>
|
|
605
|
-
<p>Enable <em>POST to webhook on submit</em> and enter a URL. Domma will POST the following JSON body:
|
|
606
|
-
</p>
|
|
607
|
-
<pre class="code-block"><code>{
|
|
608
|
-
"form": "enquiries",
|
|
609
|
-
"data": {
|
|
610
|
-
"full_name": "Jane Smith",
|
|
611
|
-
"email": "jane@example.com",
|
|
612
|
-
"message": "Hello!"
|
|
613
|
-
}
|
|
614
|
-
}</code></pre>
|
|
615
|
-
<p>Use this to integrate with Zapier, Make, Slack, or any HTTP endpoint.</p>
|
|
616
|
-
|
|
617
|
-
<h3>3. CMS Action (Pro)</h3>
|
|
618
|
-
<p>Actions are reusable workflow steps defined in <a href="#/actions">Actions</a>. A single action can
|
|
619
|
-
chain
|
|
620
|
-
multiple steps: update a field, move an entry to another collection, send an email, call a
|
|
621
|
-
webhook, or
|
|
622
|
-
delete an entry.</p>
|
|
623
|
-
<p>To wire an Action to a form:</p>
|
|
624
|
-
<ol>
|
|
625
|
-
<li>Create an Action in <a href="#/actions">Actions</a> targeting the same collection as your
|
|
626
|
-
form.
|
|
627
|
-
</li>
|
|
628
|
-
<li>Open the form in <a href="#/forms">Forms</a> → <strong>Actions</strong> tab → <strong>CMS
|
|
629
|
-
Action</strong>
|
|
630
|
-
card.
|
|
631
|
-
</li>
|
|
632
|
-
<li>Select the Action from the dropdown and save.</li>
|
|
633
|
-
</ol>
|
|
634
|
-
<p>The Action runs server-side, after the entry is saved. If it fails (e.g. MongoDB is not
|
|
635
|
-
configured),
|
|
636
|
-
the submission is still stored — the action failure is non-fatal and logged as a warning.</p>
|
|
637
|
-
|
|
638
|
-
<h3>4. Success message vs. redirect</h3>
|
|
639
|
-
<p>After a successful submission the visitor sees one of two things:</p>
|
|
640
|
-
<ul>
|
|
641
|
-
<li><strong>Inline success message</strong> — the form is replaced by the text set in
|
|
642
|
-
<em>Settings → Success Message</em>. Good for simple acknowledgements.
|
|
643
|
-
</li>
|
|
644
|
-
<li><strong>Page redirect</strong> — the visitor is sent to the URL set in
|
|
645
|
-
<em>Settings → Success Redirect URL</em>. Good for registration flows, checkouts, or when you
|
|
646
|
-
want
|
|
647
|
-
a full thank-you page with additional content. <strong>Takes priority</strong> if both are
|
|
648
|
-
set.
|
|
649
|
-
</li>
|
|
650
|
-
</ul>
|
|
651
|
-
<p>Example: set <em>Success Redirect URL</em> to <code>/thank-you</code> and create a <code>thank-you.md</code>
|
|
652
|
-
page in the CMS with any content you like.</p>
|
|
653
|
-
|
|
654
|
-
<h3>Execution order</h3>
|
|
655
|
-
<p>On every submission, the pipeline runs in this fixed order:</p>
|
|
656
|
-
<ol>
|
|
657
|
-
<li>Validate fields + honeypot + rate limit</li>
|
|
658
|
-
<li>Store entry to collection</li>
|
|
659
|
-
<li>Send email (if enabled)</li>
|
|
660
|
-
<li>Call webhook (if enabled)</li>
|
|
661
|
-
<li>Execute CMS Action (if set)</li>
|
|
662
|
-
<li>Return success response → client redirects or shows message</li>
|
|
663
|
-
</ol>
|
|
664
|
-
<p>Steps 3–5 are non-fatal: a failure in any of them is logged as a warning but does not prevent the
|
|
665
|
-
submission from being stored or the success response from being returned.</p>
|
|
666
|
-
</div>
|
|
667
|
-
|
|
668
|
-
</div>
|
|
669
|
-
</div>
|
|
51
|
+
<p class="text-muted" style="font-size:.9rem;">
|
|
52
|
+
Looking for reusable custom elements? See <a href="#/docs/components">Documentation → Components</a>.
|
|
53
|
+
For task-by-task usage, see <a href="#/documentation">Usage</a>; for the REST surface, see
|
|
54
|
+
<a href="#/api-reference">API Reference</a>.
|
|
55
|
+
</p>
|
|
670
56
|
|
|
57
|
+
</div>
|
|
671
58
|
</div>
|
|
672
59
|
</div>
|