domma-cms 0.9.10 → 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.
Files changed (125) hide show
  1. package/CLAUDE.md +248 -159
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/api.js +1 -1
  4. package/admin/js/app.js +7 -3
  5. package/admin/js/config/sidebar-config.js +1 -1
  6. package/admin/js/http-interceptor.js +1 -0
  7. package/admin/js/lib/card-builder.js +2 -2
  8. package/admin/js/lib/markdown-toolbar.js +5 -5
  9. package/admin/js/lib/safe-html.js +1 -0
  10. package/admin/js/lib/shortcode-modal.js +1 -0
  11. package/admin/js/templates/layouts.html +5 -4
  12. package/admin/js/templates/notifications.html +14 -0
  13. package/admin/js/templates/plugin-marketplace.html +16 -0
  14. package/admin/js/templates/plugins.html +17 -5
  15. package/admin/js/views/index.js +1 -1
  16. package/admin/js/views/layouts.js +1 -16
  17. package/admin/js/views/notifications.js +1 -0
  18. package/admin/js/views/page-editor.js +37 -33
  19. package/admin/js/views/plugin-marketplace.js +1 -0
  20. package/admin/js/views/plugins.js +16 -16
  21. package/config/navigation.json +5 -72
  22. package/config/plugins.json +10 -14
  23. package/config/presets.json +50 -13
  24. package/config/site.json +11 -63
  25. package/package.json +2 -1
  26. package/plugins/_template/admin/templates/index.html +17 -0
  27. package/plugins/_template/admin/views/index.js +19 -0
  28. package/plugins/_template/config.js +8 -0
  29. package/plugins/_template/plugin.js +23 -0
  30. package/plugins/_template/plugin.json +34 -0
  31. package/plugins/analytics/plugin.json +41 -31
  32. package/plugins/blog/admin/templates/blog.html +22 -0
  33. package/plugins/blog/admin/templates/categories.html +7 -0
  34. package/plugins/blog/admin/templates/comments.html +11 -0
  35. package/plugins/blog/admin/templates/post-editor.html +97 -0
  36. package/plugins/blog/admin/templates/settings.html +11 -0
  37. package/plugins/blog/admin/views/blog.js +183 -0
  38. package/plugins/blog/admin/views/categories.js +235 -0
  39. package/plugins/blog/admin/views/comments.js +187 -0
  40. package/plugins/blog/admin/views/post-editor.js +291 -0
  41. package/plugins/blog/admin/views/settings.js +100 -0
  42. package/plugins/blog/collections/categories/schema.json +12 -0
  43. package/plugins/blog/collections/comments/schema.json +16 -0
  44. package/plugins/blog/collections/posts/schema.json +19 -0
  45. package/plugins/blog/config.js +8 -0
  46. package/plugins/blog/plugin.js +352 -0
  47. package/plugins/blog/plugin.json +96 -0
  48. package/plugins/blog/roles/blog-author.json +10 -0
  49. package/plugins/blog/roles/blog-editor.json +12 -0
  50. package/plugins/blog/templates/author.html +9 -0
  51. package/plugins/blog/templates/category.html +9 -0
  52. package/plugins/blog/templates/index.html +9 -0
  53. package/plugins/blog/templates/post.html +17 -0
  54. package/plugins/blog/templates/tag.html +9 -0
  55. package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
  56. package/plugins/contacts/collections/user-contacts/schema.json +1 -1
  57. package/plugins/contacts/plugin.js +4 -10
  58. package/plugins/contacts/plugin.json +13 -3
  59. package/plugins/notes/collections/user-notes/schema.json +1 -1
  60. package/plugins/notes/plugin.js +3 -9
  61. package/plugins/notes/plugin.json +13 -3
  62. package/plugins/site-search/plugin.json +5 -2
  63. package/plugins/theme-switcher/plugin.json +1 -1
  64. package/plugins/todo/collections/todos/schema.json +1 -1
  65. package/plugins/todo/plugin.js +3 -9
  66. package/plugins/todo/plugin.json +13 -3
  67. package/public/css/site.css +1 -1
  68. package/public/js/site.js +1 -1
  69. package/scripts/build.js +48 -0
  70. package/scripts/create-plugin.js +113 -0
  71. package/scripts/fresh.js +6 -7
  72. package/scripts/gen-instance-secret.js +46 -0
  73. package/scripts/reset.js +3 -3
  74. package/scripts/setup.js +31 -13
  75. package/server/middleware/auth.js +48 -0
  76. package/server/middleware/managerAuth.js +36 -0
  77. package/server/routes/api/actions.js +1 -1
  78. package/server/routes/api/auth.js +4 -3
  79. package/server/routes/api/layouts.js +173 -49
  80. package/server/routes/api/notifications.js +155 -0
  81. package/server/routes/api/plugin-marketplace.js +75 -0
  82. package/server/routes/api/users.js +1 -1
  83. package/server/routes/api/views.js +1 -1
  84. package/server/routes/public.js +4 -9
  85. package/server/server.js +32 -3
  86. package/server/services/actions.js +1 -1
  87. package/server/services/managerClient.js +182 -0
  88. package/server/services/markdown.js +76 -9
  89. package/server/services/permissionRegistry.js +245 -173
  90. package/server/services/pluginInstaller.js +301 -0
  91. package/server/services/plugins.js +117 -10
  92. package/server/services/presetCollections.js +66 -251
  93. package/server/services/renderer.js +99 -0
  94. package/server/services/roles.js +191 -39
  95. package/server/services/users.js +1 -1
  96. package/server/services/views.js +1 -1
  97. package/server/templates/page.html +2 -2
  98. package/plugins/docs/admin/templates/docs.html +0 -69
  99. package/plugins/docs/admin/views/docs.js +0 -276
  100. package/plugins/docs/config.js +0 -8
  101. package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
  102. package/plugins/docs/data/folders.json +0 -9
  103. package/plugins/docs/data/templates.json +0 -1
  104. package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
  105. package/plugins/docs/plugin.js +0 -375
  106. package/plugins/docs/plugin.json +0 -23
  107. package/plugins/form-builder/data/forms/contacts.json +0 -66
  108. package/plugins/form-builder/data/forms/enquiries.json +0 -103
  109. package/plugins/form-builder/data/forms/feedback.json +0 -131
  110. package/plugins/form-builder/data/forms/notes.json +0 -79
  111. package/plugins/form-builder/data/forms/to-do.json +0 -100
  112. package/plugins/form-builder/data/submissions/contacts.json +0 -1
  113. package/plugins/form-builder/data/submissions/enquiries.json +0 -1
  114. package/plugins/form-builder/data/submissions/feedback.json +0 -1
  115. package/plugins/form-builder/data/submissions/notes.json +0 -1
  116. package/plugins/form-builder/data/submissions/to-do.json +0 -1
  117. package/plugins/garage/admin/templates/garage.html +0 -111
  118. package/plugins/garage/admin/views/garage.js +0 -622
  119. package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
  120. package/plugins/garage/config.js +0 -18
  121. package/plugins/garage/data/vehicles.json +0 -70
  122. package/plugins/garage/plugin.js +0 -398
  123. package/plugins/garage/plugin.json +0 -33
  124. package/scripts/seed.js +0 -1996
  125. 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
- import {ensureFormForCollection} from './forms.js';
10
-
11
- const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
12
-
13
- const PRESETS = [
14
- {
15
- slug: 'contacts',
16
- title: 'Contacts',
17
- description: 'People and their contact information.',
18
- preset: true,
19
- fields: [
20
- {name: 'full_name', label: 'Full Name', type: 'string', required: true},
21
- {name: 'email_address', label: 'Email', type: 'string', required: true},
22
- {name: 'phone_number', label: 'Phone', type: 'string', required: false}
23
- ],
24
- api: {
25
- create: {enabled: true, access: 'public'},
26
- read: {enabled: true, access: 'admin'},
27
- update: {enabled: false, access: 'admin'},
28
- delete: {enabled: false, access: 'admin'}
29
- }
30
- },
31
- {
32
- slug: 'enquiries',
33
- title: 'Enquiries',
34
- description: 'Messages and enquiries from website visitors.',
35
- preset: true,
36
- fields: [
37
- {name: 'full_name', label: 'Full Name', type: 'text', required: true},
38
- {name: 'email', label: 'Email', type: 'text', required: true},
39
- {name: 'phone', label: 'Phone', type: 'text'},
40
- {
41
- name: 'subject', label: 'Subject', type: 'select', required: true,
42
- options: [
43
- {value: 'general-enquiry', label: 'General Enquiry'},
44
- {value: 'support', label: 'Support'},
45
- {value: 'sales', label: 'Sales'},
46
- {value: 'partnership', label: 'Partnership'},
47
- {value: 'other', label: 'Other'}
48
- ]
49
- },
50
- {name: 'message', label: 'Message', type: 'text', required: true}
51
- ],
52
- api: {
53
- create: {enabled: true, access: 'public'},
54
- read: {enabled: true, access: 'admin'},
55
- update: {enabled: false, access: 'admin'},
56
- delete: {enabled: false, access: 'admin'}
57
- }
58
- },
59
- {
60
- slug: 'feedback',
61
- title: 'Feedback',
62
- description: 'User feedback and ratings.',
63
- preset: true,
64
- fields: [
65
- {name: 'name', label: 'Name', type: 'string', required: true},
66
- {name: 'email', label: 'Email', type: 'string', required: true},
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}}.