domma-cms 0.10.0 → 0.13.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 (121) 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/safe-html.js +1 -0
  8. package/admin/js/templates/documentation.html +611 -2
  9. package/admin/js/templates/layouts.html +5 -4
  10. package/admin/js/templates/notifications.html +14 -0
  11. package/admin/js/templates/plugin-marketplace.html +16 -0
  12. package/admin/js/templates/plugins.html +17 -5
  13. package/admin/js/views/index.js +1 -1
  14. package/admin/js/views/layouts.js +1 -16
  15. package/admin/js/views/notifications.js +1 -0
  16. package/admin/js/views/plugin-marketplace.js +1 -0
  17. package/admin/js/views/plugins.js +16 -16
  18. package/config/navigation.json +5 -72
  19. package/config/plugins.json +10 -14
  20. package/config/presets.json +50 -13
  21. package/config/site.json +11 -63
  22. package/package.json +2 -1
  23. package/plugins/_template/admin/templates/index.html +17 -0
  24. package/plugins/_template/admin/views/index.js +19 -0
  25. package/plugins/_template/config.js +8 -0
  26. package/plugins/_template/plugin.js +23 -0
  27. package/plugins/_template/plugin.json +34 -0
  28. package/plugins/analytics/plugin.json +41 -31
  29. package/plugins/blog/admin/templates/blog.html +22 -0
  30. package/plugins/blog/admin/templates/categories.html +7 -0
  31. package/plugins/blog/admin/templates/comments.html +11 -0
  32. package/plugins/blog/admin/templates/post-editor.html +97 -0
  33. package/plugins/blog/admin/templates/settings.html +11 -0
  34. package/plugins/blog/admin/views/blog.js +183 -0
  35. package/plugins/blog/admin/views/categories.js +235 -0
  36. package/plugins/blog/admin/views/comments.js +187 -0
  37. package/plugins/blog/admin/views/post-editor.js +291 -0
  38. package/plugins/blog/admin/views/settings.js +100 -0
  39. package/plugins/blog/collections/categories/schema.json +12 -0
  40. package/plugins/blog/collections/comments/schema.json +16 -0
  41. package/plugins/blog/collections/posts/schema.json +19 -0
  42. package/plugins/blog/config.js +8 -0
  43. package/plugins/blog/plugin.js +352 -0
  44. package/plugins/blog/plugin.json +96 -0
  45. package/plugins/blog/roles/blog-author.json +10 -0
  46. package/plugins/blog/roles/blog-editor.json +12 -0
  47. package/plugins/blog/templates/author.html +9 -0
  48. package/plugins/blog/templates/category.html +9 -0
  49. package/plugins/blog/templates/index.html +9 -0
  50. package/plugins/blog/templates/post.html +17 -0
  51. package/plugins/blog/templates/tag.html +9 -0
  52. package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
  53. package/plugins/contacts/collections/user-contacts/schema.json +1 -1
  54. package/plugins/contacts/plugin.js +4 -10
  55. package/plugins/contacts/plugin.json +13 -3
  56. package/plugins/notes/collections/user-notes/schema.json +1 -1
  57. package/plugins/notes/plugin.js +3 -9
  58. package/plugins/notes/plugin.json +13 -3
  59. package/plugins/site-search/plugin.json +5 -2
  60. package/plugins/theme-switcher/plugin.json +1 -1
  61. package/plugins/todo/collections/todos/schema.json +1 -1
  62. package/plugins/todo/plugin.js +3 -9
  63. package/plugins/todo/plugin.json +13 -3
  64. package/public/css/site.css +1 -1
  65. package/scripts/build.js +48 -0
  66. package/scripts/create-plugin.js +113 -0
  67. package/scripts/fresh.js +6 -7
  68. package/scripts/gen-instance-secret.js +46 -0
  69. package/scripts/reset.js +3 -3
  70. package/scripts/setup.js +31 -13
  71. package/server/middleware/auth.js +48 -0
  72. package/server/middleware/managerAuth.js +36 -0
  73. package/server/routes/api/actions.js +1 -1
  74. package/server/routes/api/auth.js +4 -3
  75. package/server/routes/api/layouts.js +173 -49
  76. package/server/routes/api/notifications.js +155 -0
  77. package/server/routes/api/plugin-marketplace.js +75 -0
  78. package/server/routes/api/users.js +1 -1
  79. package/server/routes/api/views.js +1 -1
  80. package/server/routes/public.js +4 -9
  81. package/server/server.js +32 -3
  82. package/server/services/actions.js +1 -1
  83. package/server/services/managerClient.js +182 -0
  84. package/server/services/markdown.js +52 -14
  85. package/server/services/permissionRegistry.js +245 -173
  86. package/server/services/pluginInstaller.js +301 -0
  87. package/server/services/plugins.js +117 -10
  88. package/server/services/presetCollections.js +66 -251
  89. package/server/services/renderer.js +99 -0
  90. package/server/services/roles.js +191 -39
  91. package/server/services/users.js +1 -1
  92. package/server/services/views.js +1 -1
  93. package/server/templates/page.html +2 -2
  94. package/plugins/docs/admin/templates/docs.html +0 -69
  95. package/plugins/docs/admin/views/docs.js +0 -276
  96. package/plugins/docs/config.js +0 -8
  97. package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
  98. package/plugins/docs/data/folders.json +0 -9
  99. package/plugins/docs/data/templates.json +0 -1
  100. package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
  101. package/plugins/docs/plugin.js +0 -375
  102. package/plugins/docs/plugin.json +0 -23
  103. package/plugins/form-builder/data/forms/contacts.json +0 -66
  104. package/plugins/form-builder/data/forms/enquiries.json +0 -103
  105. package/plugins/form-builder/data/forms/feedback.json +0 -131
  106. package/plugins/form-builder/data/forms/notes.json +0 -79
  107. package/plugins/form-builder/data/forms/to-do.json +0 -100
  108. package/plugins/form-builder/data/submissions/contacts.json +0 -1
  109. package/plugins/form-builder/data/submissions/enquiries.json +0 -1
  110. package/plugins/form-builder/data/submissions/feedback.json +0 -1
  111. package/plugins/form-builder/data/submissions/notes.json +0 -1
  112. package/plugins/form-builder/data/submissions/to-do.json +0 -1
  113. package/plugins/garage/admin/templates/garage.html +0 -111
  114. package/plugins/garage/admin/views/garage.js +0 -622
  115. package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
  116. package/plugins/garage/config.js +0 -18
  117. package/plugins/garage/data/vehicles.json +0 -70
  118. package/plugins/garage/plugin.js +0 -398
  119. package/plugins/garage/plugin.json +0 -33
  120. package/scripts/seed.js +0 -1996
  121. package/server/services/userTypes.js +0 -227
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Blog post editor view — create and edit posts.
3
+ *
4
+ * @module blog/admin/views/post-editor
5
+ */
6
+
7
+ function escapeHtml(s) {
8
+ return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
9
+ }
10
+
11
+ function slugify(s) {
12
+ return String(s ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
13
+ }
14
+
15
+ const BASE = '/api/plugins/blog';
16
+
17
+ export const postEditorView = {
18
+ templateUrl: '/plugins/blog/admin/templates/post-editor.html',
19
+
20
+ /**
21
+ * Mount the post editor view.
22
+ *
23
+ * @param {object} $container - Domma-wrapped container element
24
+ * @returns {Promise<void>}
25
+ */
26
+ async onMount($container) {
27
+ // Resolve post id from router params
28
+ const id = R.current?.params?.id !== 'new' ? R.current?.params?.id : null;
29
+ const isEdit = Boolean(id);
30
+
31
+ let post = null;
32
+ let categories = [];
33
+ let selectedCategoryIds = [];
34
+
35
+ // ---------------------------------------------------------------
36
+ // Load data
37
+ // ---------------------------------------------------------------
38
+
39
+ try {
40
+ const [catsRes] = await Promise.all([
41
+ H.get(`${BASE}/categories`).catch(() => ({ data: [] }))
42
+ ]);
43
+ categories = catsRes.data ?? catsRes ?? [];
44
+ } catch {
45
+ categories = [];
46
+ }
47
+
48
+ if (isEdit) {
49
+ try {
50
+ const res = await H.get(`${BASE}/posts/${id}`);
51
+ if (res.error) throw new Error(res.error);
52
+ post = res.data ?? res;
53
+ } catch {
54
+ E.toast('Failed to load post.', { type: 'error' });
55
+ R.navigate('#/plugins/blog');
56
+ return;
57
+ }
58
+ }
59
+
60
+ // ---------------------------------------------------------------
61
+ // DOM refs
62
+ // ---------------------------------------------------------------
63
+
64
+ const titleEl = $container.find('#editor-title').get(0);
65
+ const postTitleEl = $container.find('#post-title').get(0);
66
+ const postSlugEl = $container.find('#post-slug').get(0);
67
+ const slugAutoEl = $container.find('#slug-auto').get(0);
68
+ const slugWarnEl = $container.find('#slug-warning').get(0);
69
+ const excerptEl = $container.find('#post-excerpt').get(0);
70
+ const contentEl = $container.find('#post-content').get(0);
71
+ const tagsEl = $container.find('#post-tags').get(0);
72
+ const seoTitleEl = $container.find('#seo-title').get(0);
73
+ const seoDescEl = $container.find('#seo-description').get(0);
74
+ const statusBadge = $container.find('#post-status-badge').get(0);
75
+ const scheduleEl = $container.find('#schedule-picker').get(0);
76
+ const schedAtEl = $container.find('#post-scheduled-at').get(0);
77
+ const catsListEl = $container.find('#categories-checklist').get(0);
78
+
79
+ // ---------------------------------------------------------------
80
+ // Populate
81
+ // ---------------------------------------------------------------
82
+
83
+ if (titleEl) titleEl.textContent = isEdit ? 'Edit Post' : 'New Post';
84
+
85
+ if (isEdit && post) {
86
+ if (postTitleEl) postTitleEl.value = post.title ?? '';
87
+ if (postSlugEl) postSlugEl.value = post.slug ?? '';
88
+ if (excerptEl) excerptEl.value = post.excerpt ?? '';
89
+ if (contentEl) contentEl.value = post.content ?? '';
90
+ if (tagsEl) tagsEl.value = Array.isArray(post.tags) ? post.tags.join(', ') : (post.tags ?? '');
91
+ if (seoTitleEl) seoTitleEl.value = post.seoTitle ?? '';
92
+ if (seoDescEl) seoDescEl.value = post.seoDescription ?? '';
93
+ if (statusBadge) statusBadge.textContent = post.status ?? 'draft';
94
+ selectedCategoryIds = Array.isArray(post.categoryIds) ? [...post.categoryIds] : [];
95
+ } else {
96
+ // New post: enable auto-slug by default
97
+ if (slugAutoEl) slugAutoEl.checked = true;
98
+ }
99
+
100
+ // Build categories checklist using DOM APIs (not innerHTML — interactive elements)
101
+ if (catsListEl) {
102
+ while (catsListEl.firstChild) catsListEl.removeChild(catsListEl.firstChild);
103
+
104
+ if (categories.length === 0) {
105
+ const p = document.createElement('p');
106
+ p.className = 'text-muted small';
107
+ p.textContent = 'No categories yet.';
108
+ catsListEl.appendChild(p);
109
+ } else {
110
+ categories.forEach((cat) => {
111
+ const label = document.createElement('label');
112
+ label.className = 'd-flex align-items-center gap-2 mb-2';
113
+ label.style.cursor = 'pointer';
114
+
115
+ const cb = document.createElement('input');
116
+ cb.type = 'checkbox';
117
+ cb.value = cat.id;
118
+ cb.checked = selectedCategoryIds.includes(cat.id);
119
+ cb.addEventListener('change', () => {
120
+ if (cb.checked) {
121
+ if (!selectedCategoryIds.includes(cat.id)) selectedCategoryIds.push(cat.id);
122
+ } else {
123
+ selectedCategoryIds = selectedCategoryIds.filter((x) => x !== cat.id);
124
+ }
125
+ });
126
+
127
+ const span = document.createElement('span');
128
+ span.textContent = cat.name ?? cat.slug ?? cat.id;
129
+
130
+ label.appendChild(cb);
131
+ label.appendChild(span);
132
+ catsListEl.appendChild(label);
133
+ });
134
+ }
135
+ }
136
+
137
+ // ---------------------------------------------------------------
138
+ // Collect form values
139
+ // ---------------------------------------------------------------
140
+
141
+ function collectValues() {
142
+ return {
143
+ title: postTitleEl?.value?.trim() ?? '',
144
+ slug: postSlugEl?.value?.trim() ?? '',
145
+ excerpt: excerptEl?.value?.trim() ?? '',
146
+ content: contentEl?.value ?? '',
147
+ tags: (tagsEl?.value ?? '').split(',').map((t) => t.trim()).filter(Boolean),
148
+ categoryIds: selectedCategoryIds,
149
+ seoTitle: seoTitleEl?.value?.trim() ?? '',
150
+ seoDescription: seoDescEl?.value?.trim() ?? ''
151
+ };
152
+ }
153
+
154
+ // ---------------------------------------------------------------
155
+ // Save helpers
156
+ // ---------------------------------------------------------------
157
+
158
+ async function doSave(extra = {}) {
159
+ const body = { ...collectValues(), ...extra };
160
+ if (!body.title) {
161
+ E.toast('Title is required.', { type: 'error' });
162
+ return null;
163
+ }
164
+ try {
165
+ let saved;
166
+ if (isEdit) {
167
+ const res = await H.put(`${BASE}/posts/${id}`, body);
168
+ if (res.error) throw new Error(res.error);
169
+ saved = res.data ?? res;
170
+ } else {
171
+ const res = await H.post(`${BASE}/posts`, body);
172
+ if (res.error) throw new Error(res.error);
173
+ saved = res.data ?? res;
174
+ }
175
+ return saved;
176
+ } catch (err) {
177
+ E.toast(err?.message ?? 'Failed to save post.', { type: 'error' });
178
+ return null;
179
+ }
180
+ }
181
+
182
+ // ---------------------------------------------------------------
183
+ // Event bindings
184
+ // ---------------------------------------------------------------
185
+
186
+ // Auto-slug from title
187
+ if (postTitleEl) {
188
+ postTitleEl.addEventListener('input', () => {
189
+ if (slugAutoEl?.checked) {
190
+ if (postSlugEl) postSlugEl.value = slugify(postTitleEl.value);
191
+ }
192
+ });
193
+ }
194
+
195
+ // Slug-auto checkbox
196
+ if (slugAutoEl) {
197
+ slugAutoEl.addEventListener('change', () => {
198
+ if (!slugAutoEl.checked && isEdit && post?.status === 'published') {
199
+ if (slugWarnEl) slugWarnEl.style.display = '';
200
+ } else {
201
+ if (slugWarnEl) slugWarnEl.style.display = 'none';
202
+ }
203
+ if (slugAutoEl.checked && postTitleEl) {
204
+ if (postSlugEl) postSlugEl.value = slugify(postTitleEl.value);
205
+ }
206
+ });
207
+ }
208
+
209
+ // SEO panel toggle
210
+ const seoToggle = $container.find('#seo-toggle').get(0);
211
+ const seoPanel = $container.find('#seo-panel').get(0);
212
+ if (seoToggle && seoPanel) {
213
+ seoToggle.addEventListener('click', () => {
214
+ seoPanel.style.display = seoPanel.style.display === 'none' ? '' : 'none';
215
+ });
216
+ }
217
+
218
+ // Save draft
219
+ const saveDraftBtn = $container.find('#btn-save-draft').get(0);
220
+ if (saveDraftBtn) {
221
+ saveDraftBtn.addEventListener('click', async () => {
222
+ const saved = await doSave({ status: 'draft' });
223
+ if (saved) {
224
+ E.toast('Draft saved.', { type: 'success' });
225
+ R.navigate('#/plugins/blog');
226
+ }
227
+ });
228
+ }
229
+
230
+ // Publish now
231
+ const publishBtn = $container.find('#btn-publish').get(0);
232
+ if (publishBtn) {
233
+ publishBtn.addEventListener('click', async () => {
234
+ const saved = await doSave();
235
+ if (!saved) return;
236
+ const savedId = saved.id ?? id;
237
+ try {
238
+ const res = await H.post(`${BASE}/posts/${savedId}/publish`);
239
+ if (res.error) throw new Error(res.error);
240
+ E.toast('Post published.', { type: 'success' });
241
+ R.navigate('#/plugins/blog');
242
+ } catch (err) {
243
+ E.toast(err?.message ?? 'Failed to publish post.', { type: 'error' });
244
+ }
245
+ });
246
+ }
247
+
248
+ // Schedule
249
+ const scheduleBtn = $container.find('#btn-schedule').get(0);
250
+ if (scheduleBtn) {
251
+ scheduleBtn.style.display = '';
252
+ scheduleBtn.addEventListener('click', async () => {
253
+ // Toggle schedule picker visibility
254
+ if (scheduleEl) scheduleEl.style.display = scheduleEl.style.display === 'none' ? '' : 'none';
255
+
256
+ // If picker is now visible, wait for user to confirm
257
+ if (scheduleEl && scheduleEl.style.display !== 'none') return;
258
+
259
+ const publishedAt = schedAtEl?.value;
260
+ if (!publishedAt) {
261
+ E.toast('Please select a scheduled date/time.', { type: 'error' });
262
+ if (scheduleEl) scheduleEl.style.display = '';
263
+ return;
264
+ }
265
+
266
+ const saved = await doSave();
267
+ if (!saved) return;
268
+ const savedId = saved.id ?? id;
269
+ try {
270
+ const res = await H.post(`${BASE}/posts/${savedId}/schedule`, { publishedAt });
271
+ if (res.error) throw new Error(res.error);
272
+ E.toast('Post scheduled.', { type: 'success' });
273
+ R.navigate('#/plugins/blog');
274
+ } catch (err) {
275
+ E.toast(err?.message ?? 'Failed to schedule post.', { type: 'error' });
276
+ }
277
+ });
278
+ }
279
+
280
+ // Pick image (TBD)
281
+ const pickImageBtn = $container.find('#btn-pick-image').get(0);
282
+ if (pickImageBtn) {
283
+ pickImageBtn.addEventListener('click', () => {
284
+ console.log('media picker TBD');
285
+ E.toast('Media picker coming soon.', { type: 'info' });
286
+ });
287
+ }
288
+
289
+ Domma.icons.scan();
290
+ }
291
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Blog settings admin view.
3
+ *
4
+ * @module blog/admin/views/settings
5
+ */
6
+
7
+ const BASE = '/api/plugins/blog';
8
+
9
+ export const settingsView = {
10
+ templateUrl: '/plugins/blog/admin/templates/settings.html',
11
+
12
+ /**
13
+ * Mount the blog settings view.
14
+ *
15
+ * @param {object} $container - Domma-wrapped container element
16
+ * @returns {Promise<void>}
17
+ */
18
+ async onMount($container) {
19
+ let settings = {};
20
+
21
+ // ---------------------------------------------------------------
22
+ // Load
23
+ // ---------------------------------------------------------------
24
+
25
+ try {
26
+ const res = await H.get(`${BASE}/settings`);
27
+ if (res.error) throw new Error(res.error);
28
+ settings = res.data ?? res ?? {};
29
+ } catch {
30
+ E.toast('Failed to load settings.', { type: 'error' });
31
+ }
32
+
33
+ // ---------------------------------------------------------------
34
+ // Render form using F.create
35
+ // ---------------------------------------------------------------
36
+
37
+ const formEl = $container.find('#settings-form').get(0);
38
+ if (!formEl) return;
39
+
40
+ F.create(formEl, {
41
+ blueprint: [
42
+ {
43
+ key: 'basePath',
44
+ label: 'Base path',
45
+ type: 'text',
46
+ placeholder: '/blog',
47
+ value: settings.basePath ?? '/blog',
48
+ hint: 'URL prefix for all blog pages (e.g. /blog).'
49
+ },
50
+ {
51
+ key: 'postsPerPage',
52
+ label: 'Posts per page',
53
+ type: 'number',
54
+ placeholder: '10',
55
+ value: settings.postsPerPage ?? 10,
56
+ hint: 'Number of posts shown per listing page.'
57
+ },
58
+ {
59
+ key: 'commentsEnabled',
60
+ label: 'Enable comments',
61
+ type: 'toggle',
62
+ value: settings.commentsEnabled ?? true,
63
+ hint: 'Allow readers to submit comments on posts.'
64
+ },
65
+ {
66
+ key: 'commentModeration',
67
+ label: 'Moderate comments',
68
+ type: 'toggle',
69
+ value: settings.commentModeration ?? true,
70
+ hint: 'Hold new comments for approval before they appear.'
71
+ },
72
+ {
73
+ key: 'feedLength',
74
+ label: 'RSS feed length',
75
+ type: 'number',
76
+ placeholder: '20',
77
+ value: settings.feedLength ?? 20,
78
+ hint: 'Maximum number of items in the RSS/Atom feed.'
79
+ }
80
+ ],
81
+ onSubmit: async (values) => {
82
+ try {
83
+ const res = await H.put(`${BASE}/settings`, {
84
+ basePath: values.basePath,
85
+ postsPerPage: Number(values.postsPerPage) || 10,
86
+ commentsEnabled: Boolean(values.commentsEnabled),
87
+ commentModeration: Boolean(values.commentModeration),
88
+ feedLength: Number(values.feedLength) || 20
89
+ });
90
+ if (res.error) throw new Error(res.error);
91
+ E.toast('Settings saved.', { type: 'success' });
92
+ } catch (err) {
93
+ E.toast(err?.message ?? 'Failed to save settings.', { type: 'error' });
94
+ }
95
+ }
96
+ });
97
+
98
+ Domma.icons.scan();
99
+ }
100
+ };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "blog-categories",
3
+ "label": "Blog Categories",
4
+ "slug": "blog-categories",
5
+ "storage": { "adapter": "file" },
6
+ "fields": [
7
+ { "key": "name", "label": "Name", "type": "text", "required": true },
8
+ { "key": "slug", "label": "Slug", "type": "text", "required": true },
9
+ { "key": "description", "label": "Description", "type": "textarea", "required": false },
10
+ { "key": "parentId", "label": "Parent", "type": "text", "required": false }
11
+ ]
12
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "blog-comments",
3
+ "label": "Blog Comments",
4
+ "slug": "blog-comments",
5
+ "storage": { "adapter": "file" },
6
+ "fields": [
7
+ { "key": "postId", "label": "Post", "type": "text", "required": true },
8
+ { "key": "parentId", "label": "Parent", "type": "text", "required": false },
9
+ { "key": "authorName", "label": "Author Name", "type": "text", "required": true },
10
+ { "key": "authorEmail", "label": "Author Email", "type": "text", "required": true },
11
+ { "key": "authorUrl", "label": "Author URL", "type": "text", "required": false },
12
+ { "key": "body", "label": "Body", "type": "textarea", "required": true },
13
+ { "key": "status", "label": "Status", "type": "select", "required": true, "options": ["pending", "approved", "rejected", "spam"] },
14
+ { "key": "createdAt", "label": "Created At", "type": "datetime", "required": false }
15
+ ]
16
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "blog-posts",
3
+ "label": "Blog Posts",
4
+ "slug": "blog-posts",
5
+ "storage": { "adapter": "file" },
6
+ "fields": [
7
+ { "key": "title", "label": "Title", "type": "text", "required": true },
8
+ { "key": "slug", "label": "Slug", "type": "text", "required": true },
9
+ { "key": "excerpt", "label": "Excerpt", "type": "textarea", "required": false },
10
+ { "key": "content", "label": "Content", "type": "markdown", "required": false },
11
+ { "key": "featuredImage", "label": "Featured Image", "type": "text", "required": false },
12
+ { "key": "categories", "label": "Categories", "type": "array", "required": false },
13
+ { "key": "tags", "label": "Tags", "type": "array", "required": false },
14
+ { "key": "authorId", "label": "Author", "type": "text", "required": true },
15
+ { "key": "status", "label": "Status", "type": "select", "required": true, "options": ["draft", "scheduled", "published"] },
16
+ { "key": "publishedAt", "label": "Published At", "type": "datetime", "required": false },
17
+ { "key": "seo", "label": "SEO", "type": "object", "required": false }
18
+ ]
19
+ }
@@ -0,0 +1,8 @@
1
+ export default {
2
+ basePath: '/blog',
3
+ postsPerPage: 10,
4
+ commentsEnabled: false,
5
+ commentModeration: true,
6
+ feedLength: 20,
7
+ storage: { adapter: 'file' }
8
+ };