domma-cms 0.43.1 → 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.
- package/CLAUDE.md +53 -53
- package/README.md +27 -27
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +2 -2
- package/admin/js/app.js +1 -1
- package/admin/js/lib/card-builder.js +2 -2
- package/admin/js/lib/crud-tutorial.js +1 -1
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/image-editor.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +2 -2
- package/admin/js/lib/project-context.js +1 -1
- package/admin/js/lib/project-quick-create.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/server-restart.js +1 -1
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/simple-editor.js +1 -1
- package/admin/js/lib/themes.js +1 -1
- package/admin/js/lib/timeline-builder.js +1 -1
- package/admin/js/templates/action-editor.html +8 -8
- package/admin/js/templates/api-endpoint-editor.html +7 -7
- package/admin/js/templates/api-endpoints.html +1 -1
- package/admin/js/templates/api-reference.html +1 -1
- package/admin/js/templates/block-editor.html +6 -6
- package/admin/js/templates/collection-editor.html +1 -1
- package/admin/js/templates/component-editor.html +2 -2
- package/admin/js/templates/dashboard/cache.html +3 -3
- package/admin/js/templates/dashboard/journeys.html +3 -3
- package/admin/js/templates/dashboard/kpi-strip.html +3 -3
- package/admin/js/templates/dashboard/top-pages.html +1 -1
- package/admin/js/templates/dashboard/traffic-chart.html +1 -1
- package/admin/js/templates/docs/api-actions.html +1 -1
- package/admin/js/templates/docs/api-collections.html +4 -4
- package/admin/js/templates/docs/api-pages.html +2 -2
- package/admin/js/templates/docs/api-plugins.html +1 -1
- package/admin/js/templates/docs/api-settings.html +2 -2
- package/admin/js/templates/docs/api-users.html +1 -1
- package/admin/js/templates/docs/components-howto.html +4 -4
- package/admin/js/templates/docs/components-reference.html +21 -21
- package/admin/js/templates/docs/components-rules.html +14 -14
- package/admin/js/templates/docs/components-walkthrough.html +13 -13
- package/admin/js/templates/docs/tutorial-crud.html +49 -49
- package/admin/js/templates/docs/tutorial-forms.html +4 -4
- package/admin/js/templates/docs/tutorial-plugin.html +10 -10
- package/admin/js/templates/docs/usage-actions.html +6 -6
- package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
- package/admin/js/templates/docs/usage-dconfig.html +4 -4
- package/admin/js/templates/docs/usage-navigation.html +1 -1
- package/admin/js/templates/docs/usage-pages.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +24 -24
- package/admin/js/templates/docs/usage-users-roles.html +3 -3
- package/admin/js/templates/docs/usage-views.html +4 -4
- package/admin/js/templates/documentation.html +1 -1
- package/admin/js/templates/effects.html +25 -25
- package/admin/js/templates/form-editor.html +5 -5
- package/admin/js/templates/login.html +5 -5
- package/admin/js/templates/menu-editor.html +10 -10
- package/admin/js/templates/navigation.html +11 -11
- package/admin/js/templates/notifications.html +1 -1
- package/admin/js/templates/page-editor.html +4 -4
- package/admin/js/templates/pro-docs.html +14 -14
- package/admin/js/templates/role-editor.html +1 -1
- package/admin/js/templates/settings.html +10 -10
- package/admin/js/templates/tutorials.html +3 -3
- package/admin/js/templates/view-editor.html +15 -15
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/api-endpoints.js +2 -2
- package/admin/js/views/api-tokens.js +3 -3
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +1 -1
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/component-editor.js +1 -1
- package/admin/js/views/components.js +4 -4
- package/admin/js/views/dashboard/widgets/cache.js +1 -1
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +1 -1
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/layouts.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +3 -3
- package/admin/js/views/menu-locations.js +1 -1
- package/admin/js/views/menus.js +3 -3
- package/admin/js/views/navigation.js +8 -8
- package/admin/js/views/page-editor.js +40 -40
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/plugin-code.js +2 -2
- package/admin/js/views/plugins.js +1 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +1 -1
- package/admin/js/views/roles.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/bin/cli.js +8 -8
- package/bin/lib/config-merge.js +5 -5
- package/bin/update.js +16 -16
- package/config/site.json +86 -86
- package/package.json +2 -2
- package/plugins/_template/admin/views/index.js +1 -1
- package/plugins/_template/config.js +1 -1
- package/plugins/_template/plugin.js +1 -1
- package/plugins/analytics/admin/templates/analytics.html +3 -3
- package/plugins/analytics/admin/views/analytics.js +1 -1
- package/plugins/analytics/config.js +1 -1
- package/plugins/analytics/plugin.js +4 -4
- package/plugins/analytics/plugin.json +1 -1
- package/plugins/analytics/public/inject-body.html +2 -2
- package/plugins/analytics/public/inject-head.html +1 -1
- package/plugins/blog/admin/templates/blog.html +4 -4
- package/plugins/blog/admin/views/blog.js +3 -3
- package/plugins/blog/admin/views/categories.js +2 -2
- package/plugins/blog/admin/views/comments.js +4 -4
- package/plugins/blog/admin/views/post-editor.js +1 -1
- package/plugins/blog/plugin.js +2 -2
- package/plugins/blog/plugin.json +4 -4
- package/plugins/contacts/admin/views/contacts.js +8 -8
- package/plugins/contacts/plugin.js +4 -4
- package/plugins/contacts/plugin.json +1 -1
- package/plugins/invoice/admin/templates/editor.html +6 -6
- package/plugins/invoice/admin/templates/index.html +4 -4
- package/plugins/invoice/admin/views/editor.js +3 -3
- package/plugins/invoice/admin/views/index.js +4 -4
- package/plugins/invoice/admin/views/issuers.js +1 -1
- package/plugins/invoice/admin/views/party-view.js +3 -3
- package/plugins/invoice/admin/views/receivers.js +1 -1
- package/plugins/invoice/config.js +1 -1
- package/plugins/invoice/plugin.js +1 -1
- package/plugins/invoice/plugin.json +4 -4
- package/plugins/notes/admin/views/notes.js +4 -4
- package/plugins/notes/plugin.js +1 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +3 -3
- package/plugins/site-search/admin/views/site-search.js +1 -1
- package/plugins/site-search/config.js +1 -1
- package/plugins/site-search/plugin.js +4 -4
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/surveys/admin/templates/results.html +1 -1
- package/plugins/surveys/admin/templates/survey-editor.html +4 -4
- package/plugins/surveys/admin/views/audience.js +6 -6
- package/plugins/surveys/admin/views/results.js +3 -3
- package/plugins/surveys/admin/views/survey-editor.js +1 -1
- package/plugins/surveys/admin/views/surveys.js +2 -2
- package/plugins/surveys/plugin.js +1 -1
- package/plugins/surveys/plugin.json +4 -4
- package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
- package/plugins/theme-switcher/plugin.json +3 -3
- package/plugins/theme-switcher/public/inject-body.html +28 -28
- package/plugins/theme-switcher/public/inject-head.html +1 -1
- package/plugins/todo/plugin.js +1 -1
- package/public/css/forms.css +1 -1
- package/public/js/collection-browser.js +1 -1
- package/public/js/collection-context.js +1 -1
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -1
- package/public/js/site.js +1 -1
- package/scripts/build.js +100 -6
- package/scripts/create-plugin.js +2 -2
- package/scripts/fresh.js +2 -2
- package/scripts/gen-instance-secret.js +1 -1
- package/scripts/pro.js +12 -12
- package/scripts/reset.js +2 -2
- package/scripts/setup.js +15 -15
- package/scripts/users.js +5 -5
- package/scripts/verify-assets.mjs +6 -6
- package/server/config.js +1 -1
- package/server/middleware/auth.js +11 -11
- package/server/middleware/managerAuth.js +3 -3
- package/server/routes/api/actions.js +4 -4
- package/server/routes/api/api-endpoints.js +7 -7
- package/server/routes/api/api-tokens.js +5 -5
- package/server/routes/api/auth.js +10 -10
- package/server/routes/api/blocks.js +3 -3
- package/server/routes/api/collections.js +18 -18
- package/server/routes/api/dashboard.js +5 -5
- package/server/routes/api/effects.js +2 -2
- package/server/routes/api/endpoints-public.js +5 -5
- package/server/routes/api/forms.js +47 -47
- package/server/routes/api/menu-locations.js +3 -3
- package/server/routes/api/menus.js +7 -7
- package/server/routes/api/notifications.js +2 -2
- package/server/routes/api/pages.js +14 -2
- package/server/routes/api/plugin-marketplace.js +3 -3
- package/server/routes/api/plugins.js +6 -6
- package/server/routes/api/projects.js +9 -9
- package/server/routes/api/scaffold.js +7 -7
- package/server/routes/api/settings.js +6 -6
- package/server/routes/api/sidebar.js +1 -1
- package/server/routes/api/users.js +5 -5
- package/server/routes/api/versions.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/docs-public.js +3 -3
- package/server/routes/public.js +14 -14
- package/server/server.js +34 -16
- package/server/services/actions.js +19 -19
- package/server/services/adapters/FileAdapter.js +2 -2
- package/server/services/adapters/MongoAdapter.js +5 -5
- package/server/services/apiEndpoints.js +15 -15
- package/server/services/apiTokens.js +7 -7
- package/server/services/blocks.js +13 -13
- package/server/services/cache/drivers/MemoryDriver.js +3 -3
- package/server/services/cache/drivers/NoneDriver.js +1 -1
- package/server/services/cache/index.js +5 -5
- package/server/services/collections.js +14 -14
- package/server/services/components.js +9 -9
- package/server/services/connectionManager.js +4 -4
- package/server/services/content.js +25 -2
- package/server/services/docs.js +14 -14
- package/server/services/email.js +1 -1
- package/server/services/filterEngine.js +6 -6
- package/server/services/forms.js +18 -10
- package/server/services/health.js +1 -1
- package/server/services/hooks.js +7 -7
- package/server/services/images.js +4 -4
- package/server/services/managerClient.js +1 -1
- package/server/services/markdown.js +79 -79
- package/server/services/menuRender.js +5 -5
- package/server/services/menus-migration.js +3 -3
- package/server/services/menus.js +31 -31
- package/server/services/permissionRegistry.js +1 -1
- package/server/services/pluginFiles.js +5 -5
- package/server/services/pluginInstaller.js +17 -17
- package/server/services/pluginScaffold.js +1 -1
- package/server/services/plugins.js +17 -17
- package/server/services/presetCollections.js +2 -2
- package/server/services/projects.js +35 -26
- package/server/services/recipes/contact-list.json +1 -1
- package/server/services/recipes/onboarding.json +8 -8
- package/server/services/references.js +8 -8
- package/server/services/renderer.js +9 -9
- package/server/services/roles.js +9 -9
- package/server/services/rowAccess.js +10 -10
- package/server/services/scaffolder.js +30 -30
- package/server/services/sidebar-migration.js +10 -10
- package/server/services/sitemap.js +1 -1
- package/server/services/userProfiles.js +4 -4
- package/server/services/userRoles.js +7 -7
- package/server/services/users.js +5 -5
- package/server/services/versions.js +3 -3
- package/server/services/viewPipeline.js +6 -6
- package/server/services/views.js +11 -11
- package/server/templates/page.html +5 -5
- package/config/navigation.json.bak +0 -31
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Project-scoped machine credentials for the external collections API.
|
|
4
4
|
*
|
|
5
5
|
* Tokens are entries in the file-based `api-tokens` preset collection.
|
|
6
|
-
* Only a SHA-256 hash is stored
|
|
6
|
+
* Only a SHA-256 hash is stored - the plaintext (`dcms_<64 hex>`) is returned
|
|
7
7
|
* exactly once, from createToken(). A token is accepted only on collection
|
|
8
8
|
* verbs configured with `api.<verb>.access === 'token'`, and only for
|
|
9
9
|
* collections whose resolved project matches the token's `project` binding.
|
|
@@ -26,7 +26,7 @@ let tokenCache = null;
|
|
|
26
26
|
* Serialises this service's fire-and-forget writes (the throttled lastUsedAt
|
|
27
27
|
* update) against its mutations. FileAdapter does unlocked read-modify-write,
|
|
28
28
|
* so an in-flight lastUsedAt update that read the data file BEFORE a revoke
|
|
29
|
-
* wrote it would write the revoked token straight back
|
|
29
|
+
* wrote it would write the revoked token straight back - resurrection.
|
|
30
30
|
* Mutations await the chain; lastUsedAt updates append to it.
|
|
31
31
|
*/
|
|
32
32
|
let writeChain = Promise.resolve();
|
|
@@ -35,7 +35,7 @@ function invalidateCache() {
|
|
|
35
35
|
tokenCache = null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Generic admin collection endpoints emit these for ALL collections
|
|
38
|
+
// Generic admin collection endpoints emit these for ALL collections - the
|
|
39
39
|
// service's own mutations invalidate directly, this catches edits made
|
|
40
40
|
// through the admin entries grid.
|
|
41
41
|
for (const ev of ['collection:entryCreated', 'collection:entryUpdated', 'collection:entryDeleted']) {
|
|
@@ -162,7 +162,7 @@ export async function findTokenByName(name, project) {
|
|
|
162
162
|
/**
|
|
163
163
|
* Validate a presented plaintext token.
|
|
164
164
|
* Returns the sanitised entry when the token exists, is enabled, and has not
|
|
165
|
-
* expired
|
|
165
|
+
* expired - otherwise null. Updates lastUsedAt at most once per minute
|
|
166
166
|
* (fire-and-forget; the cached copy is patched in place to avoid thrash).
|
|
167
167
|
*
|
|
168
168
|
* @param {string} plaintext
|
|
@@ -184,7 +184,7 @@ export async function validateToken(plaintext) {
|
|
|
184
184
|
|
|
185
185
|
const lastUsed = entry.data.lastUsedAt ? Date.parse(entry.data.lastUsedAt) : 0;
|
|
186
186
|
if (Date.now() - lastUsed > LAST_USED_THROTTLE_MS) {
|
|
187
|
-
// updateEntry replaces data wholesale
|
|
187
|
+
// updateEntry replaces data wholesale - pass the full object.
|
|
188
188
|
const data = {...entry.data, lastUsedAt: new Date().toISOString()};
|
|
189
189
|
entry.data = data; // keep the cached copy current without invalidating
|
|
190
190
|
// Fire-and-forget, but chained so revoke/update can await it.
|
|
@@ -222,7 +222,7 @@ export async function getTokenSanitised(id) {
|
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
224
|
* Update mutable token fields. The project binding, hash, and hint are fixed
|
|
225
|
-
* for the token's lifetime
|
|
225
|
+
* for the token's lifetime - revoke and re-issue to rebind.
|
|
226
226
|
*
|
|
227
227
|
* @param {string} id
|
|
228
228
|
* @param {{name?: string, enabled?: boolean, scopes?: object[], expiresAt?: string|null}} patch
|
|
@@ -264,7 +264,7 @@ export async function updateToken(id, patch = {}) {
|
|
|
264
264
|
* @returns {Promise<boolean>}
|
|
265
265
|
*/
|
|
266
266
|
export async function revokeToken(id) {
|
|
267
|
-
await writeChain; // drain any in-flight lastUsedAt write
|
|
267
|
+
await writeChain; // drain any in-flight lastUsedAt write - revoke must be final
|
|
268
268
|
const entry = await getEntry(API_TOKENS_COLLECTION_SLUG, id);
|
|
269
269
|
if (!entry) return false;
|
|
270
270
|
await deleteEntry(API_TOKENS_COLLECTION_SLUG, id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Blocks Service
|
|
3
3
|
* CRUD operations and seeding for reusable HTML block templates in content/blocks/.
|
|
4
|
-
* Never overwrites existing files during seeding
|
|
4
|
+
* Never overwrites existing files during seeding - user customisations are preserved.
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
7
|
import path from 'path';
|
|
@@ -21,7 +21,7 @@ async function seedBlock(name, content) {
|
|
|
21
21
|
const filePath = path.join(BLOCKS_DIR, `${name}.html`);
|
|
22
22
|
try {
|
|
23
23
|
await fs.access(filePath);
|
|
24
|
-
// File exists
|
|
24
|
+
// File exists - leave it alone
|
|
25
25
|
} catch {
|
|
26
26
|
await fs.writeFile(filePath, content.trim() + '\n', 'utf8');
|
|
27
27
|
}
|
|
@@ -34,7 +34,7 @@ async function seedBlock(name, content) {
|
|
|
34
34
|
const BLOCKS = {
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* contact-info
|
|
37
|
+
* contact-info - standalone contact details panel for landing pages.
|
|
38
38
|
* Fields: heading, intro, phone, phoneHref, email, address, hours,
|
|
39
39
|
* facebook, facebookLabel, instagram, instagramLabel,
|
|
40
40
|
* website, websiteLabel, footnote
|
|
@@ -99,7 +99,7 @@ const BLOCKS = {
|
|
|
99
99
|
`,
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* contact-card
|
|
102
|
+
* contact-card - for the Contacts form
|
|
103
103
|
* Fields: full_name, phone_number, email_address
|
|
104
104
|
*/
|
|
105
105
|
'contact-card': `
|
|
@@ -120,7 +120,7 @@ const BLOCKS = {
|
|
|
120
120
|
`,
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* enquiry-card
|
|
123
|
+
* enquiry-card - for the Enquiries form
|
|
124
124
|
* Fields: full_name, email, phone, subject, message
|
|
125
125
|
*/
|
|
126
126
|
'enquiry-card': `
|
|
@@ -143,7 +143,7 @@ const BLOCKS = {
|
|
|
143
143
|
`,
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
|
-
* feedback-card
|
|
146
|
+
* feedback-card - for the Feedback form
|
|
147
147
|
* Fields: name, email, rating, category, subject, message, recommend
|
|
148
148
|
*/
|
|
149
149
|
'feedback-card': `
|
|
@@ -159,7 +159,7 @@ const BLOCKS = {
|
|
|
159
159
|
<div class="card-body">
|
|
160
160
|
<p style="margin:0 0 1rem;line-height:1.65;">{{message}}</p>
|
|
161
161
|
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.8rem;color:var(--dm-text-muted,#888);">
|
|
162
|
-
<span>{{name}}
|
|
162
|
+
<span>{{name}} - {{email}}</span>
|
|
163
163
|
<span>Would recommend: <strong>{{recommend}}</strong></span>
|
|
164
164
|
</div>
|
|
165
165
|
</div>
|
|
@@ -167,7 +167,7 @@ const BLOCKS = {
|
|
|
167
167
|
`,
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
-
* note-card
|
|
170
|
+
* note-card - for the Notes form
|
|
171
171
|
* Fields: title, content, category, tags
|
|
172
172
|
*/
|
|
173
173
|
'note-card': `
|
|
@@ -187,7 +187,7 @@ const BLOCKS = {
|
|
|
187
187
|
`,
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
|
-
* todo-item
|
|
190
|
+
* todo-item - for the To-Do form
|
|
191
191
|
* Fields: title, description, status, priority, due_date, assigned_to
|
|
192
192
|
*/
|
|
193
193
|
'todo-item': `
|
|
@@ -238,7 +238,7 @@ function blockMetaPath(name) {
|
|
|
238
238
|
return path.join(BLOCKS_DIR, `${name}.meta.json`);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
/** Per-block CSS size cap
|
|
241
|
+
/** Per-block CSS size cap - matches the 100 KB site-wide cap, scaled down. */
|
|
242
242
|
const MAX_CSS_SIZE = 50 * 1024;
|
|
243
243
|
|
|
244
244
|
// ---------------------------------------------------------------------------
|
|
@@ -300,7 +300,7 @@ export async function getBlock(name) {
|
|
|
300
300
|
let css = '';
|
|
301
301
|
try {
|
|
302
302
|
css = await fs.readFile(blockCssPath(name), 'utf8');
|
|
303
|
-
} catch { /* no CSS file
|
|
303
|
+
} catch { /* no CSS file - treat as empty */
|
|
304
304
|
}
|
|
305
305
|
return {name, content, css, bundled, ...(meta && {meta})};
|
|
306
306
|
} catch (err) {
|
|
@@ -334,7 +334,7 @@ export async function saveBlock(name, content, {bundled, css, meta} = {}) {
|
|
|
334
334
|
await fs.mkdir(BLOCKS_DIR, {recursive: true});
|
|
335
335
|
await fs.writeFile(blockFilePath(name), content, 'utf8');
|
|
336
336
|
const metaPath = blockMetaPath(name);
|
|
337
|
-
// Merge sidecar meta
|
|
337
|
+
// Merge sidecar meta - bundled flag plus any caller-supplied fields (eg meta.project).
|
|
338
338
|
const sidecar = {
|
|
339
339
|
...(meta && typeof meta === 'object' ? meta : {}),
|
|
340
340
|
...(bundled ? {bundled: true} : {})
|
|
@@ -345,7 +345,7 @@ export async function saveBlock(name, content, {bundled, css, meta} = {}) {
|
|
|
345
345
|
await fs.unlink(metaPath).catch(() => {
|
|
346
346
|
});
|
|
347
347
|
}
|
|
348
|
-
// CSS is optional
|
|
348
|
+
// CSS is optional - only touch the file when the caller passed the field explicitly.
|
|
349
349
|
if (typeof css === 'string') {
|
|
350
350
|
if (css.trim().length > 0) {
|
|
351
351
|
await fs.writeFile(blockCssPath(name), css, 'utf8');
|
|
@@ -3,7 +3,7 @@ import {LRU} from '../lru.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* In-memory cache driver with TTL and tag invalidation.
|
|
5
5
|
*
|
|
6
|
-
* Each entry is stored as {value, expiresAt, tags}. TTL is lazy
|
|
6
|
+
* Each entry is stored as {value, expiresAt, tags}. TTL is lazy - entries
|
|
7
7
|
* past their expiresAt are returned as undefined on the next get and dropped.
|
|
8
8
|
*
|
|
9
9
|
* Tag index: a Map<tag, Set<key>>. set() with {tags} adds the key to each
|
|
@@ -79,14 +79,14 @@ export class MemoryDriver {
|
|
|
79
79
|
this._tagIndex.clear();
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** Current entry count (sync
|
|
82
|
+
/** Current entry count (sync - for the dashboard widget). */
|
|
83
83
|
size() {
|
|
84
84
|
return this._lru.size;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* Snapshot the currently cached entries as `{key, tags, expiresAt}`.
|
|
89
|
-
* No values
|
|
89
|
+
* No values - they can be megabytes of HTML. Uses `entries()` (non-mutating)
|
|
90
90
|
* so the LRU order is preserved through the iteration.
|
|
91
91
|
*/
|
|
92
92
|
listEntries() {
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
68
|
+
// Schema (collection) operations - always file-based
|
|
69
69
|
// ---------------------------------------------------------------------------
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* List all collections
|
|
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`
|
|
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()
|
|
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
|
|
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()`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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()
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
/**
|
package/server/services/docs.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Domma Docs
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(/&/g, '&').replace(
|
|
185
|
+
.replace(/&/g, '&').replace(/-/g, '-').replace(/-/g, '-')
|
|
186
186
|
.replace(/"/g, "'").replace(/'/g, "'").replace(/</g, '<').replace(/>/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
|
|
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
|
|
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
|
|
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[]}
|
|
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
|
|
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
|
|
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]
|