domma-cms 0.10.0 → 0.12.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 +248 -159
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +7 -3
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/http-interceptor.js +1 -0
- package/admin/js/lib/safe-html.js +1 -0
- package/admin/js/templates/layouts.html +5 -4
- package/admin/js/templates/notifications.html +14 -0
- package/admin/js/templates/plugin-marketplace.html +16 -0
- package/admin/js/templates/plugins.html +17 -5
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +1 -16
- package/admin/js/views/notifications.js +1 -0
- package/admin/js/views/plugin-marketplace.js +1 -0
- package/admin/js/views/plugins.js +16 -16
- package/config/navigation.json +5 -72
- package/config/plugins.json +10 -14
- package/config/presets.json +50 -13
- package/config/site.json +11 -63
- package/package.json +2 -1
- package/plugins/_template/admin/templates/index.html +17 -0
- package/plugins/_template/admin/views/index.js +19 -0
- package/plugins/_template/config.js +8 -0
- package/plugins/_template/plugin.js +23 -0
- package/plugins/_template/plugin.json +34 -0
- package/plugins/analytics/plugin.json +41 -31
- package/plugins/blog/admin/templates/blog.html +22 -0
- package/plugins/blog/admin/templates/categories.html +7 -0
- package/plugins/blog/admin/templates/comments.html +11 -0
- package/plugins/blog/admin/templates/post-editor.html +97 -0
- package/plugins/blog/admin/templates/settings.html +11 -0
- package/plugins/blog/admin/views/blog.js +183 -0
- package/plugins/blog/admin/views/categories.js +235 -0
- package/plugins/blog/admin/views/comments.js +187 -0
- package/plugins/blog/admin/views/post-editor.js +291 -0
- package/plugins/blog/admin/views/settings.js +100 -0
- package/plugins/blog/collections/categories/schema.json +12 -0
- package/plugins/blog/collections/comments/schema.json +16 -0
- package/plugins/blog/collections/posts/schema.json +19 -0
- package/plugins/blog/config.js +8 -0
- package/plugins/blog/plugin.js +352 -0
- package/plugins/blog/plugin.json +96 -0
- package/plugins/blog/roles/blog-author.json +10 -0
- package/plugins/blog/roles/blog-editor.json +12 -0
- package/plugins/blog/templates/author.html +9 -0
- package/plugins/blog/templates/category.html +9 -0
- package/plugins/blog/templates/index.html +9 -0
- package/plugins/blog/templates/post.html +17 -0
- package/plugins/blog/templates/tag.html +9 -0
- package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
- package/plugins/contacts/collections/user-contacts/schema.json +1 -1
- package/plugins/contacts/plugin.js +4 -10
- package/plugins/contacts/plugin.json +13 -3
- package/plugins/notes/collections/user-notes/schema.json +1 -1
- package/plugins/notes/plugin.js +3 -9
- package/plugins/notes/plugin.json +13 -3
- package/plugins/site-search/plugin.json +5 -2
- package/plugins/theme-switcher/plugin.json +1 -1
- package/plugins/todo/collections/todos/schema.json +1 -1
- package/plugins/todo/plugin.js +3 -9
- package/plugins/todo/plugin.json +13 -3
- package/public/css/site.css +1 -1
- package/scripts/build.js +48 -0
- package/scripts/create-plugin.js +113 -0
- package/scripts/fresh.js +6 -7
- package/scripts/gen-instance-secret.js +46 -0
- package/scripts/reset.js +3 -3
- package/scripts/setup.js +31 -13
- package/server/middleware/auth.js +48 -0
- package/server/middleware/managerAuth.js +36 -0
- package/server/routes/api/actions.js +1 -1
- package/server/routes/api/auth.js +4 -3
- package/server/routes/api/layouts.js +173 -49
- package/server/routes/api/notifications.js +155 -0
- package/server/routes/api/plugin-marketplace.js +75 -0
- package/server/routes/api/users.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/public.js +4 -9
- package/server/server.js +32 -3
- package/server/services/actions.js +1 -1
- package/server/services/managerClient.js +182 -0
- package/server/services/permissionRegistry.js +245 -173
- package/server/services/pluginInstaller.js +301 -0
- package/server/services/plugins.js +117 -10
- package/server/services/presetCollections.js +66 -251
- package/server/services/renderer.js +99 -0
- package/server/services/roles.js +191 -39
- package/server/services/users.js +1 -1
- package/server/services/views.js +1 -1
- package/server/templates/page.html +2 -2
- package/plugins/docs/admin/templates/docs.html +0 -69
- package/plugins/docs/admin/views/docs.js +0 -276
- package/plugins/docs/config.js +0 -8
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
- package/plugins/docs/data/folders.json +0 -9
- package/plugins/docs/data/templates.json +0 -1
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
- package/plugins/docs/plugin.js +0 -375
- package/plugins/docs/plugin.json +0 -23
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/forms/enquiries.json +0 -103
- package/plugins/form-builder/data/forms/feedback.json +0 -131
- package/plugins/form-builder/data/forms/notes.json +0 -79
- package/plugins/form-builder/data/forms/to-do.json +0 -100
- package/plugins/form-builder/data/submissions/contacts.json +0 -1
- package/plugins/form-builder/data/submissions/enquiries.json +0 -1
- package/plugins/form-builder/data/submissions/feedback.json +0 -1
- package/plugins/form-builder/data/submissions/notes.json +0 -1
- package/plugins/form-builder/data/submissions/to-do.json +0 -1
- package/plugins/garage/admin/templates/garage.html +0 -111
- package/plugins/garage/admin/views/garage.js +0 -622
- package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
- package/plugins/garage/config.js +0 -18
- package/plugins/garage/data/vehicles.json +0 -70
- package/plugins/garage/plugin.js +0 -398
- package/plugins/garage/plugin.json +0 -33
- package/scripts/seed.js +0 -1996
- package/server/services/userTypes.js +0 -227
|
@@ -1,251 +1,66 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Preset Collections
|
|
3
|
-
* Built-in collections whose schemas are defined in code and overwritten on boot.
|
|
4
|
-
* Data files are never touched — only schema.json is reset each startup.
|
|
5
|
-
*/
|
|
6
|
-
import fs from 'fs/promises';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import {config} from '../config.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
name: 'rating', label: 'Rating', type: 'select', required: true,
|
|
69
|
-
options: [
|
|
70
|
-
{value: 'excellent', label: 'Excellent'},
|
|
71
|
-
{value: 'good', label: 'Good'},
|
|
72
|
-
{value: 'average', label: 'Average'},
|
|
73
|
-
{value: 'poor', label: 'Poor'}
|
|
74
|
-
]
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'category', label: 'Category', type: 'select',
|
|
78
|
-
options: [
|
|
79
|
-
{value: 'feature-request', label: 'Feature Request'},
|
|
80
|
-
{value: 'praise', label: 'Praise'},
|
|
81
|
-
{value: 'bug-report', label: 'Bug Report'},
|
|
82
|
-
{value: 'support', label: 'Support'}
|
|
83
|
-
]
|
|
84
|
-
},
|
|
85
|
-
{name: 'subject', label: 'Subject', type: 'string'},
|
|
86
|
-
{name: 'message', label: 'Message', type: 'string', required: true}
|
|
87
|
-
],
|
|
88
|
-
api: {
|
|
89
|
-
create: {enabled: true, access: 'public'},
|
|
90
|
-
read: {enabled: true, access: 'admin'},
|
|
91
|
-
update: {enabled: false, access: 'admin'},
|
|
92
|
-
delete: {enabled: false, access: 'admin'}
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
slug: 'notes',
|
|
97
|
-
title: 'Notes',
|
|
98
|
-
description: 'Free-form notes with categories and tags.',
|
|
99
|
-
preset: true,
|
|
100
|
-
fields: [
|
|
101
|
-
{name: 'title', label: 'Title', type: 'text', required: true},
|
|
102
|
-
{name: 'content', label: 'Content', type: 'text', required: true},
|
|
103
|
-
{
|
|
104
|
-
name: 'category', label: 'Category', type: 'select',
|
|
105
|
-
options: [
|
|
106
|
-
{value: 'general', label: 'General'},
|
|
107
|
-
{value: 'idea', label: 'Idea'},
|
|
108
|
-
{value: 'reminder', label: 'Reminder'},
|
|
109
|
-
{value: 'reference', label: 'Reference'}
|
|
110
|
-
]
|
|
111
|
-
},
|
|
112
|
-
{name: 'tags', label: 'Tags', type: 'text'}
|
|
113
|
-
],
|
|
114
|
-
api: {
|
|
115
|
-
create: {enabled: false, access: 'admin'},
|
|
116
|
-
read: {enabled: false, access: 'admin'},
|
|
117
|
-
update: {enabled: false, access: 'admin'},
|
|
118
|
-
delete: {enabled: false, access: 'admin'}
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
slug: 'categories',
|
|
123
|
-
title: 'Categories',
|
|
124
|
-
description: 'Content categories for organising posts.',
|
|
125
|
-
preset: true,
|
|
126
|
-
fields: [
|
|
127
|
-
{name: 'name', label: 'Name', type: 'text', required: true},
|
|
128
|
-
{name: 'slug', label: 'Slug', type: 'text', required: true},
|
|
129
|
-
{name: 'description', label: 'Description', type: 'textarea'},
|
|
130
|
-
{name: 'parent_category', label: 'Parent Category', type: 'text'},
|
|
131
|
-
{name: 'sort_order', label: 'Sort Order', type: 'number'}
|
|
132
|
-
],
|
|
133
|
-
api: {
|
|
134
|
-
create: {enabled: false, access: 'admin'},
|
|
135
|
-
read: {enabled: true, access: 'public'},
|
|
136
|
-
update: {enabled: false, access: 'admin'},
|
|
137
|
-
delete: {enabled: false, access: 'admin'}
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
slug: 'posts',
|
|
142
|
-
title: 'Posts',
|
|
143
|
-
description: 'Blog posts and articles.',
|
|
144
|
-
preset: true,
|
|
145
|
-
fields: [
|
|
146
|
-
{name: 'title', label: 'Title', type: 'text', required: true},
|
|
147
|
-
{name: 'slug', label: 'Slug', type: 'text', required: true},
|
|
148
|
-
{name: 'content', label: 'Content', type: 'textarea', required: true},
|
|
149
|
-
{name: 'excerpt', label: 'Excerpt', type: 'textarea'},
|
|
150
|
-
{name: 'featured_image', label: 'Featured Image', type: 'url'},
|
|
151
|
-
{name: 'category', label: 'Category', type: 'text'},
|
|
152
|
-
{name: 'tags', label: 'Tags', type: 'text'},
|
|
153
|
-
{
|
|
154
|
-
name: 'status', label: 'Status', type: 'select', required: true,
|
|
155
|
-
options: [
|
|
156
|
-
{value: 'draft', label: 'Draft'},
|
|
157
|
-
{value: 'published', label: 'Published'},
|
|
158
|
-
{value: 'archived', label: 'Archived'}
|
|
159
|
-
]
|
|
160
|
-
},
|
|
161
|
-
{name: 'publish_date', label: 'Publish Date', type: 'date'},
|
|
162
|
-
{name: 'author', label: 'Author', type: 'text'}
|
|
163
|
-
],
|
|
164
|
-
api: {
|
|
165
|
-
create: {enabled: false, access: 'admin'},
|
|
166
|
-
read: {enabled: true, access: 'public'},
|
|
167
|
-
update: {enabled: false, access: 'admin'},
|
|
168
|
-
delete: {enabled: false, access: 'admin'}
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
slug: 'comments',
|
|
173
|
-
title: 'Comments',
|
|
174
|
-
description: 'User comments on posts and content.',
|
|
175
|
-
preset: true,
|
|
176
|
-
fields: [
|
|
177
|
-
{name: 'post_slug', label: 'Post', type: 'text', required: true},
|
|
178
|
-
{name: 'author_name', label: 'Name', type: 'text', required: true},
|
|
179
|
-
{name: 'author_email', label: 'Email', type: 'email', required: true},
|
|
180
|
-
{name: 'body', label: 'Comment', type: 'textarea', required: true},
|
|
181
|
-
{
|
|
182
|
-
name: 'status', label: 'Status', type: 'select', required: true,
|
|
183
|
-
options: [
|
|
184
|
-
{value: 'pending', label: 'Pending'},
|
|
185
|
-
{value: 'approved', label: 'Approved'},
|
|
186
|
-
{value: 'rejected', label: 'Rejected'}
|
|
187
|
-
]
|
|
188
|
-
}
|
|
189
|
-
],
|
|
190
|
-
api: {
|
|
191
|
-
create: {enabled: true, access: 'public'},
|
|
192
|
-
read: {enabled: true, access: 'public'},
|
|
193
|
-
update: {enabled: false, access: 'admin'},
|
|
194
|
-
delete: {enabled: false, access: 'admin'}
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
slug: 'to-do',
|
|
199
|
-
title: 'To-Do',
|
|
200
|
-
description: 'Task tracking with priorities and due dates.',
|
|
201
|
-
preset: true,
|
|
202
|
-
fields: [
|
|
203
|
-
{name: 'title', label: 'Title', type: 'text', required: true},
|
|
204
|
-
{name: 'description', label: 'Description', type: 'text'},
|
|
205
|
-
{
|
|
206
|
-
name: 'status', label: 'Status', type: 'select', required: true,
|
|
207
|
-
options: ['Pending', 'In Progress', 'Done']
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
name: 'priority', label: 'Priority', type: 'select',
|
|
211
|
-
options: ['Low', 'Medium', 'High']
|
|
212
|
-
},
|
|
213
|
-
{name: 'due_date', label: 'Due Date', type: 'text'},
|
|
214
|
-
{name: 'assigned_to', label: 'Assigned To', type: 'text'}
|
|
215
|
-
],
|
|
216
|
-
api: {
|
|
217
|
-
create: {enabled: false, access: 'admin'},
|
|
218
|
-
read: {enabled: false, access: 'admin'},
|
|
219
|
-
update: {enabled: false, access: 'admin'},
|
|
220
|
-
delete: {enabled: false, access: 'admin'}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
];
|
|
224
|
-
|
|
225
|
-
/** Slugs exported for use in adapterRegistry and the delete guard. */
|
|
226
|
-
export const PRESET_COLLECTION_SLUGS = PRESETS.map(p => p.slug);
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Seed all preset collections at boot.
|
|
230
|
-
* Always overwrites schema.json; only creates data.json if absent.
|
|
231
|
-
*
|
|
232
|
-
* @returns {Promise<void>}
|
|
233
|
-
*/
|
|
234
|
-
export async function seedAll() {
|
|
235
|
-
for (const schema of PRESETS) {
|
|
236
|
-
const dir = path.join(COLLECTIONS_DIR, schema.slug);
|
|
237
|
-
const schemaPath = path.join(dir, 'schema.json');
|
|
238
|
-
const dataPath = path.join(dir, 'data.json');
|
|
239
|
-
|
|
240
|
-
await fs.mkdir(dir, {recursive: true});
|
|
241
|
-
await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n', 'utf8');
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
await fs.access(dataPath);
|
|
245
|
-
} catch {
|
|
246
|
-
await fs.writeFile(dataPath, '[]\n', 'utf8');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
await ensureFormForCollection(schema);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Preset Collections
|
|
3
|
+
* Built-in collections whose schemas are defined in code and overwritten on boot.
|
|
4
|
+
* Data files are never touched — only schema.json is reset each startup.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import {config} from '../config.js';
|
|
9
|
+
|
|
10
|
+
const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
|
|
11
|
+
|
|
12
|
+
// contacts, enquiries, feedback, notes, categories, posts, comments, to-do
|
|
13
|
+
// have been removed — they belong to plugins and are seeded on plugin enable.
|
|
14
|
+
|
|
15
|
+
const PRESETS = [
|
|
16
|
+
{
|
|
17
|
+
slug: 'notifications',
|
|
18
|
+
title: 'Notifications',
|
|
19
|
+
description: 'System notifications from external manager service.',
|
|
20
|
+
preset: true,
|
|
21
|
+
systemManaged: true,
|
|
22
|
+
fields: [
|
|
23
|
+
{name: 'title', label: 'Title', type: 'text', required: true},
|
|
24
|
+
{name: 'body', label: 'Body', type: 'text', required: true},
|
|
25
|
+
{name: 'severity', label: 'Severity', type: 'select', options: ['info', 'success', 'warning', 'critical'], default: 'info'},
|
|
26
|
+
{name: 'source', label: 'Source', type: 'text', default: 'manager'},
|
|
27
|
+
{name: 'link', label: 'Link', type: 'text'},
|
|
28
|
+
{name: 'createdAt', label: 'Created At', type: 'datetime', required: true},
|
|
29
|
+
{name: 'expiresAt', label: 'Expires At', type: 'datetime'},
|
|
30
|
+
{name: 'readBy', label: 'Read By', type: 'array', items: 'string', default: []},
|
|
31
|
+
{name: 'dismissedBy', label: 'Dismissed By', type: 'array', items: 'string', default: []}
|
|
32
|
+
],
|
|
33
|
+
api: {
|
|
34
|
+
create: {enabled: false, access: 'admin'},
|
|
35
|
+
read: {enabled: false, access: 'admin'},
|
|
36
|
+
update: {enabled: false, access: 'admin'},
|
|
37
|
+
delete: {enabled: false, access: 'admin'}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** Slugs exported for use in adapterRegistry and the delete guard. */
|
|
43
|
+
export const PRESET_COLLECTION_SLUGS = PRESETS.map(p => p.slug);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Seed all preset collections at boot.
|
|
47
|
+
* Always overwrites schema.json; only creates data.json if absent.
|
|
48
|
+
*
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
export async function seedAll() {
|
|
52
|
+
for (const schema of PRESETS) {
|
|
53
|
+
const dir = path.join(COLLECTIONS_DIR, schema.slug);
|
|
54
|
+
const schemaPath = path.join(dir, 'schema.json');
|
|
55
|
+
const dataPath = path.join(dir, 'data.json');
|
|
56
|
+
|
|
57
|
+
await fs.mkdir(dir, {recursive: true});
|
|
58
|
+
await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n', 'utf8');
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await fs.access(dataPath);
|
|
62
|
+
} catch {
|
|
63
|
+
await fs.writeFile(dataPath, '[]\n', 'utf8');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -9,6 +9,7 @@ import {getConfig} from '../config.js';
|
|
|
9
9
|
import {getInjectionSnippets} from './plugins.js';
|
|
10
10
|
import {applyTransforms} from './hooks.js';
|
|
11
11
|
|
|
12
|
+
const VALID_LAYOUT_WIDTHS = new Set(['narrow', 'normal', 'wide', 'full']);
|
|
12
13
|
const CUSTOM_CSS_PATH = new URL('../../content/custom.css', import.meta.url).pathname;
|
|
13
14
|
|
|
14
15
|
async function getCustomCss() {
|
|
@@ -68,6 +69,17 @@ export async function renderPage(page) {
|
|
|
68
69
|
|
|
69
70
|
const preset = presets[page.layout] || presets['default'] || {};
|
|
70
71
|
|
|
72
|
+
const layoutWidth = VALID_LAYOUT_WIDTHS.has(preset.width) ? preset.width : 'normal';
|
|
73
|
+
const layoutBodyClass = ['dm-layout-' + layoutWidth, preset.class || ''].filter(Boolean).join(' ');
|
|
74
|
+
const pageBodyStyleParts = [];
|
|
75
|
+
const BG_COLOR_SAFE = /^[a-zA-Z0-9#(),.\s%/-]+$/;
|
|
76
|
+
if (preset.bgColor && BG_COLOR_SAFE.test(preset.bgColor)) pageBodyStyleParts.push(`background-color:${preset.bgColor}`);
|
|
77
|
+
if (preset.bgImage) {
|
|
78
|
+
const safeBgImage = preset.bgImage.replace(/'/g, '%27').replace(/\\/g, '%5C').replace(/"/g, '%22');
|
|
79
|
+
pageBodyStyleParts.push(`background-image:url('${safeBgImage}');background-size:cover;background-position:center`);
|
|
80
|
+
}
|
|
81
|
+
const pageBodyStyle = pageBodyStyleParts.join(';');
|
|
82
|
+
|
|
71
83
|
const seoTitle = escapeHtml(page.seo?.title || `${page.title}${site.seo?.titleSeparator || ' | '}${site.seo?.defaultTitle || site.title}`);
|
|
72
84
|
const seoDescription = escapeHtml(page.seo?.description || site.seo?.defaultDescription || '');
|
|
73
85
|
const ogImage = escapeHtml(page.seo?.image || site.seo?.defaultImage || '');
|
|
@@ -117,6 +129,8 @@ export async function renderPage(page) {
|
|
|
117
129
|
fontLink,
|
|
118
130
|
fontStyleTag,
|
|
119
131
|
layout: page.layout || 'default',
|
|
132
|
+
layoutBodyClass,
|
|
133
|
+
pageBodyStyle,
|
|
120
134
|
showNavbar: preset.navbar !== false,
|
|
121
135
|
showFooter: preset.footer !== false,
|
|
122
136
|
showSidebar: preset.sidebar === true || page.sidebar === true,
|
|
@@ -264,6 +278,91 @@ function buildFontVars(fontFamily, fontSize) {
|
|
|
264
278
|
return {fontLink, fontOverride: rules.length ? rules.join(' ') : null};
|
|
265
279
|
}
|
|
266
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Render a plugin-owned HTML fragment into the full page shell.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} templatePath - Absolute path to the plugin HTML fragment
|
|
285
|
+
* @param {object} data - Token replacement map; keys become {{key}} placeholders
|
|
286
|
+
* @param {object} seoMeta - Optional SEO overrides: { title, description, ogImage }
|
|
287
|
+
* @returns {Promise<string>}
|
|
288
|
+
*/
|
|
289
|
+
export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
290
|
+
const [fragment, template, site, navigation, injection, customCss] = await Promise.all([
|
|
291
|
+
fs.readFile(templatePath, 'utf8'),
|
|
292
|
+
getTemplate(),
|
|
293
|
+
Promise.resolve(getConfig('site')),
|
|
294
|
+
Promise.resolve(getConfig('navigation')),
|
|
295
|
+
getInjectionSnippets(),
|
|
296
|
+
getCustomCss()
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
// Replace {{key}} tokens in the plugin fragment with data values
|
|
300
|
+
const renderedFragment = fragment.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
301
|
+
const val = data[key];
|
|
302
|
+
if (val === undefined || val === null) return '';
|
|
303
|
+
return String(val);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const seoTitle = escapeHtml(seoMeta.title ?? site.seo?.defaultTitle ?? site.title ?? 'Blog');
|
|
307
|
+
const seoDescription = escapeHtml(seoMeta.description ?? site.seo?.defaultDescription ?? '');
|
|
308
|
+
const ogImage = escapeHtml(seoMeta.ogImage ?? '');
|
|
309
|
+
|
|
310
|
+
const ogTags = [
|
|
311
|
+
`<meta property="og:title" content="${seoTitle}">`,
|
|
312
|
+
`<meta property="og:description" content="${seoDescription}">`,
|
|
313
|
+
`<meta property="og:image" content="${ogImage}">`
|
|
314
|
+
].join('\n');
|
|
315
|
+
|
|
316
|
+
const {fontLink, fontOverride} = buildFontVars(site.fontFamily, site.fontSize);
|
|
317
|
+
const fontStyleTag = fontOverride ? `<style>${fontOverride}</style>` : '';
|
|
318
|
+
const {navbarFontLink, navbarStyleTag} = buildNavbarStyleTag(navigation);
|
|
319
|
+
|
|
320
|
+
const customCssTag = customCss.trim()
|
|
321
|
+
? `<style>${customCss.replace(/<\/style>/gi, '<\\/style>')}</style>`
|
|
322
|
+
: '';
|
|
323
|
+
|
|
324
|
+
const activeTheme = site.theme || 'charcoal-dark';
|
|
325
|
+
const dommaTheme = site.baseTheme || activeTheme;
|
|
326
|
+
const customThemeClass = site.baseTheme ? `dm-theme-${activeTheme}` : '';
|
|
327
|
+
|
|
328
|
+
const vars = {
|
|
329
|
+
seoTitle,
|
|
330
|
+
seoDescription,
|
|
331
|
+
ogImage,
|
|
332
|
+
title: seoTitle,
|
|
333
|
+
html: renderedFragment,
|
|
334
|
+
breadcrumbsHtml: '',
|
|
335
|
+
theme: activeTheme,
|
|
336
|
+
dommaTheme,
|
|
337
|
+
customThemeClass,
|
|
338
|
+
fontLink,
|
|
339
|
+
fontStyleTag,
|
|
340
|
+
layout: 'default',
|
|
341
|
+
layoutBodyClass: 'dm-layout-normal',
|
|
342
|
+
pageBodyStyle: '',
|
|
343
|
+
showNavbar: true,
|
|
344
|
+
showFooter: true,
|
|
345
|
+
showSidebar: false,
|
|
346
|
+
navJson: JSON.stringify(filterHiddenNavItems(navigation)).replace(/<\/script>/gi, '<\\/script>'),
|
|
347
|
+
siteJson: JSON.stringify({
|
|
348
|
+
title: site.title,
|
|
349
|
+
footer: site.footer
|
|
350
|
+
? {
|
|
351
|
+
...site.footer,
|
|
352
|
+
links: (site.footer.links || []).filter(l => !l.hidden)
|
|
353
|
+
}
|
|
354
|
+
: site.footer,
|
|
355
|
+
social: site.social || null
|
|
356
|
+
}).replace(/<\/script>/gi, '<\\/script>'),
|
|
357
|
+
headInject: [ogTags, injection.head, navbarFontLink].filter(Boolean).join('\n'),
|
|
358
|
+
headInjectLate: [injection.headLate, customCssTag, navbarStyleTag].filter(Boolean).join('\n'),
|
|
359
|
+
bodyEndInject: injection.bodyEnd || '',
|
|
360
|
+
dconfigScript: ''
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return interpolate(template, vars);
|
|
364
|
+
}
|
|
365
|
+
|
|
267
366
|
/**
|
|
268
367
|
* Simple template interpolation.
|
|
269
368
|
* Handles {{variable}}, {{#if flag}}...{{/if}}.
|