domma-cms 0.66.0 → 0.69.4
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 +64 -1
- package/README.md +14 -10
- package/admin/css/admin.css +1 -1
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -1
- package/admin/js/lib/plugin-chrome.js +1 -1
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/admin/js/templates/theme.html +38 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +24 -22
- package/admin/js/views/theme.js +1 -1
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +4 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +14 -0
- package/config/menus/admin-sidebar.json +1 -1
- package/config/server.json +3 -1
- package/package.json +3 -7
- package/plugins/_lib/admin/mail/reader-view.js +20 -0
- package/plugins/_lib/mail/store.js +21 -1
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -1
- package/plugins/blog/admin/templates/blog.html +35 -53
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +36 -13
- package/plugins/blog/admin/templates/settings.html +2 -11
- package/plugins/blog/admin/views/blog.js +288 -162
- package/plugins/blog/admin/views/categories.js +212 -201
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +269 -21
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -86
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/posts/schema.json +3 -1
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +225 -15
- package/plugins/blog/plugin.json +21 -81
- package/plugins/blog/plugin.public.js +115 -210
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/plugin.json +2 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +41 -2
- package/scripts/run-tests.mjs +94 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/{plugins/contacts/plugin.js → server/routes/api/contacts.js} +71 -43
- package/server/routes/api/dashboard.js +8 -2
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +148 -3
- package/server/routes/api/todo.js +178 -0
- package/server/server.js +61 -1
- package/server/services/collections.js +13 -3
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +54 -1
- package/server/services/permissionRegistry.js +50 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +117 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +83 -3
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +310 -0
- package/server/services/themeSettings.js +39 -0
- package/config/theme.json +0 -18
- package/plugins/analytics/admin/css/index.css +0 -1
- package/plugins/analytics/admin/templates/analytics.html +0 -70
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -60
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -254
- package/plugins/contacts/admin/views/contacts.js +0 -943
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -179
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/plugin.json +0 -36
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/css/index.css +0 -1
- package/plugins/invoice/admin/templates/editor.html +0 -220
- package/plugins/invoice/admin/templates/index.html +0 -173
- package/plugins/invoice/admin/templates/issuers.html +0 -2
- package/plugins/invoice/admin/templates/receivers.html +0 -2
- package/plugins/invoice/admin/views/contacts-source.js +0 -266
- package/plugins/invoice/admin/views/editor.js +0 -434
- package/plugins/invoice/admin/views/export.js +0 -199
- package/plugins/invoice/admin/views/help-content.js +0 -61
- package/plugins/invoice/admin/views/index.js +0 -643
- package/plugins/invoice/admin/views/issuers.js +0 -30
- package/plugins/invoice/admin/views/media.js +0 -172
- package/plugins/invoice/admin/views/party-view.js +0 -386
- package/plugins/invoice/admin/views/payments.js +0 -127
- package/plugins/invoice/admin/views/print.js +0 -130
- package/plugins/invoice/admin/views/receivers.js +0 -55
- package/plugins/invoice/admin/views/send.js +0 -212
- package/plugins/invoice/admin/views/settings.js +0 -594
- package/plugins/invoice/admin/views/view-lifecycle.js +0 -33
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -82
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -16
- package/plugins/invoice/collections/invoices/schema.json +0 -33
- package/plugins/invoice/config.js +0 -37
- package/plugins/invoice/pdf.js +0 -164
- package/plugins/invoice/plugin.js +0 -1456
- package/plugins/invoice/plugin.json +0 -86
- package/plugins/invoice/templates/_base.css +0 -1
- package/plugins/invoice/templates/classic-nologo.html +0 -100
- package/plugins/invoice/templates/classic.html +0 -91
- package/plugins/invoice/templates/invoice-print.html +0 -237
- package/plugins/invoice/templates/minimal.html +0 -99
- package/plugins/invoice/templates/modern-nologo.html +0 -114
- package/plugins/invoice/templates/modern.html +0 -113
- package/plugins/invoice/templates/templates.json +0 -11
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/plugin.json +0 -33
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/lib/audience.js +0 -37
- package/plugins/surveys/lib/campaigns.js +0 -43
- package/plugins/surveys/lib/ledger.js +0 -110
- package/plugins/surveys/lib/sending.js +0 -106
- package/plugins/surveys/lib/stats.js +0 -62
- package/plugins/surveys/lib/submit.js +0 -95
- package/plugins/surveys/lib/tokens.js +0 -28
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/plugin.public.js +0 -149
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
- /package/{plugins/contacts/admin/css/index.css → admin/css/contacts.css} +0 -0
package/plugins/blog/plugin.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import {fileURLToPath} from 'url';
|
|
1
2
|
import defaultConfig from './config.js';
|
|
2
3
|
import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../server/services/collections.js';
|
|
3
4
|
import {getPluginStates, savePluginState} from '../../server/services/plugins.js';
|
|
5
|
+
import {renderBlogPage} from '../../server/services/renderer.js';
|
|
6
|
+
import {DISPLAY_DEFAULTS, pickDisplaySettings, safeImageUrl} from './lib/layouts.js';
|
|
7
|
+
import {catalogue, seedBlocks} from './lib/render.js';
|
|
8
|
+
import {liveSettings, loadEntries as loadAll, renderPostPage} from './lib/page.js';
|
|
9
|
+
import {SAMPLE_CATEGORIES, samplePosts} from './lib/samples.js';
|
|
4
10
|
|
|
5
11
|
const POSTS_SLUG = 'blog-posts';
|
|
6
12
|
const CATEGORIES_SLUG = 'blog-categories';
|
|
@@ -16,6 +22,53 @@ function isEditorOrAdmin(user) {
|
|
|
16
22
|
return canPublish(user);
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
// Sidebar badge text helpers - kept here rather than imported from core, so the
|
|
26
|
+
// plugin still loads on an engine that predates server/services/sidebarBadges.js.
|
|
27
|
+
const plural = (n, one) => `${n} ${n === 1 ? one : `${one}s`}`;
|
|
28
|
+
function clip(text, max) {
|
|
29
|
+
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
30
|
+
return s.length <= max ? s : `${s.slice(0, max - 1).trimEnd()}…`;
|
|
31
|
+
}
|
|
32
|
+
function ago(iso) {
|
|
33
|
+
const secs = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000));
|
|
34
|
+
if (!Number.isFinite(secs)) return '';
|
|
35
|
+
if (secs < 60) return 'just now';
|
|
36
|
+
if (secs < 3600) return `${Math.round(secs / 60)} min ago`;
|
|
37
|
+
if (secs < 86400) return `${plural(Math.round(secs / 3600), 'hour')} ago`;
|
|
38
|
+
return `${plural(Math.round(secs / 86400), 'day')} ago`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** '' (the site default) or a post layout's block name. */
|
|
42
|
+
function cleanTemplate(value) {
|
|
43
|
+
const v = String(value ?? '').trim();
|
|
44
|
+
return /^blog-post-[a-z0-9-]+$/.test(v) ? v : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The fields a caller may set on a post. Status, publish date and authorship
|
|
49
|
+
* go through the publish / schedule routes, which check who is asking: taken
|
|
50
|
+
* straight from the body, a blog-author could publish their own post with
|
|
51
|
+
* `{"status": "published"}` and skip the editor entirely.
|
|
52
|
+
*/
|
|
53
|
+
export function postFields(body, user) {
|
|
54
|
+
const out = {};
|
|
55
|
+
if (typeof body.title === 'string') out.title = body.title.trim();
|
|
56
|
+
if (typeof body.slug === 'string') out.slug = body.slug.trim();
|
|
57
|
+
if (typeof body.content === 'string') out.content = body.content;
|
|
58
|
+
if (typeof body.excerpt === 'string') out.excerpt = body.excerpt.trim();
|
|
59
|
+
if (body.featuredImage !== undefined) out.featuredImage = body.featuredImage ? String(body.featuredImage) : null;
|
|
60
|
+
if (Array.isArray(body.categories)) out.categories = body.categories.map(String);
|
|
61
|
+
if (Array.isArray(body.tags)) out.tags = body.tags.map(t => String(t).trim()).filter(Boolean);
|
|
62
|
+
if (body.seo && typeof body.seo === 'object') {
|
|
63
|
+
out.seo = {title: String(body.seo.title ?? ''), description: String(body.seo.description ?? '')};
|
|
64
|
+
}
|
|
65
|
+
if (body.template !== undefined) out.template = cleanTemplate(body.template);
|
|
66
|
+
// Only a publisher may move a post out of draft here (Save Draft sends 'draft').
|
|
67
|
+
if (body.status === 'draft') out.status = 'draft';
|
|
68
|
+
else if (body.status !== undefined && canPublish(user) && ['scheduled', 'published'].includes(body.status)) out.status = body.status;
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
19
72
|
/** Derive a URL slug from a title string. */
|
|
20
73
|
function slugify(str) {
|
|
21
74
|
return String(str)
|
|
@@ -188,6 +241,120 @@ export default async function blogPlugin(fastify, options) {
|
|
|
188
241
|
registerEmbedShortcodes(options.hooks.registerShortcode, basePath);
|
|
189
242
|
}
|
|
190
243
|
|
|
244
|
+
// The layouts are blocks; put the shipped ones where the Blocks tool can
|
|
245
|
+
// edit them. Once each, never over a site's own copy (lib/render.js).
|
|
246
|
+
try {
|
|
247
|
+
const copied = await seedBlocks();
|
|
248
|
+
if (copied.length) fastify.log.info(`[blog] added layout blocks: ${copied.join(', ')}`);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
fastify.log.warn(`[blog] could not add the layout blocks: ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// -------------------------------------------------------------------------
|
|
254
|
+
// Layouts
|
|
255
|
+
// -------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
/** GET /api/plugins/blog/layouts - every post layout and listing display on offer */
|
|
258
|
+
fastify.get('/layouts', { preHandler: [authenticate] }, async () => {
|
|
259
|
+
const s = liveSettings(settings);
|
|
260
|
+
return { ...(await catalogue()), postLayout: s.postLayout, listDisplay: s.listDisplay };
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* POST /api/plugins/blog/preview - a post as the site would show it, from
|
|
265
|
+
* the editor's UNSAVED fields and in any layout, for the preview panel.
|
|
266
|
+
* The whole page, chrome and all, so what the author sees is what readers get.
|
|
267
|
+
*/
|
|
268
|
+
fastify.post('/preview', { preHandler: [authenticate] }, async (request, reply) => {
|
|
269
|
+
const body = request.body ?? {};
|
|
270
|
+
const user = request.user;
|
|
271
|
+
let stored = null;
|
|
272
|
+
if (body.id) {
|
|
273
|
+
const entry = await getEntry(POSTS_SLUG, String(body.id));
|
|
274
|
+
if (entry) {
|
|
275
|
+
if (!isEditorOrAdmin(user) && entry.data.authorId !== (user?.id ?? user?.sub)) {
|
|
276
|
+
return reply.code(403).send({ error: 'Forbidden' });
|
|
277
|
+
}
|
|
278
|
+
stored = { id: entry.id, ...entry.data };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const post = {
|
|
282
|
+
id: stored?.id ?? '__preview__',
|
|
283
|
+
status: 'published',
|
|
284
|
+
publishedAt: stored?.publishedAt ?? new Date().toISOString(),
|
|
285
|
+
authorId: stored?.authorId ?? (user?.id ?? user?.sub ?? ''),
|
|
286
|
+
...(stored ?? {}),
|
|
287
|
+
...postFields(body, user)
|
|
288
|
+
};
|
|
289
|
+
post.status = 'published';
|
|
290
|
+
post.slug = post.slug || 'preview';
|
|
291
|
+
const allPosts = (await loadAll(POSTS_SLUG)).filter(p => p.id !== post.id);
|
|
292
|
+
const s = liveSettings(settings);
|
|
293
|
+
const page = await renderPostPage(post, [...allPosts, post], s, {
|
|
294
|
+
base: basePath, layout: cleanTemplate(body.layout) || null, preview: true
|
|
295
|
+
});
|
|
296
|
+
const html = await renderBlogPage(
|
|
297
|
+
fileURLToPath(new URL('./templates/post.html', import.meta.url)),
|
|
298
|
+
{ post: page.html, assetV: '1.4.4' },
|
|
299
|
+
{ title: post.title || 'Preview', description: post.excerpt || '', ogImage: safeImageUrl(post.featuredImage),
|
|
300
|
+
usedComponents: page.usedComponents, urlPath: `${basePath}/${post.slug}`, noindex: true }
|
|
301
|
+
);
|
|
302
|
+
return reply.send({ html, layout: page.layout });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// -------------------------------------------------------------------------
|
|
306
|
+
// Sample posts
|
|
307
|
+
// -------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* POST /api/plugins/blog/samples - add the sample posts (and any sample
|
|
311
|
+
* category the site lacks). A sample whose slug is taken is skipped, so
|
|
312
|
+
* pressing it twice adds nothing the second time.
|
|
313
|
+
*/
|
|
314
|
+
fastify.post('/samples', { preHandler: [authenticate, requireAdmin] }, async (request) => {
|
|
315
|
+
const uid = request.user?.id ?? request.user?.sub ?? null;
|
|
316
|
+
const cats = await loadAll(CATEGORIES_SLUG);
|
|
317
|
+
const catIds = {};
|
|
318
|
+
for (const c of SAMPLE_CATEGORIES) {
|
|
319
|
+
const have = cats.find(x => x.slug === c.slug);
|
|
320
|
+
catIds[c.slug] = have ? have.id : (await createEntry(CATEGORIES_SLUG, { ...c, sample: true }, { createdBy: uid, source: 'admin' })).id;
|
|
321
|
+
}
|
|
322
|
+
const existing = new Set((await loadAll(POSTS_SLUG)).map(p => p.slug));
|
|
323
|
+
let added = 0;
|
|
324
|
+
for (const p of samplePosts()) {
|
|
325
|
+
if (existing.has(p.slug)) continue;
|
|
326
|
+
await createEntry(POSTS_SLUG, {
|
|
327
|
+
...p,
|
|
328
|
+
categories: p.categories.map(slug => catIds[slug]).filter(Boolean),
|
|
329
|
+
template: p.template ?? '',
|
|
330
|
+
status: 'published',
|
|
331
|
+
authorId: uid,
|
|
332
|
+
sample: true,
|
|
333
|
+
seo: { title: '', description: '' }
|
|
334
|
+
}, { createdBy: uid, source: 'admin' });
|
|
335
|
+
added++;
|
|
336
|
+
}
|
|
337
|
+
return { ok: true, added };
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* DELETE /api/plugins/blog/samples - remove exactly what "Add" put there:
|
|
342
|
+
* posts marked as samples, and sample categories no remaining post uses.
|
|
343
|
+
*/
|
|
344
|
+
fastify.delete('/samples', { preHandler: [authenticate, requireAdmin] }, async () => {
|
|
345
|
+
let posts = 0, categories = 0;
|
|
346
|
+
for (const p of (await loadAll(POSTS_SLUG)).filter(p => p.sample === true)) {
|
|
347
|
+
await deleteEntry(POSTS_SLUG, p.id);
|
|
348
|
+
posts++;
|
|
349
|
+
}
|
|
350
|
+
const inUse = new Set((await loadAll(POSTS_SLUG)).flatMap(p => p.categories ?? []));
|
|
351
|
+
for (const c of (await loadAll(CATEGORIES_SLUG)).filter(c => c.sample === true && !inUse.has(c.id))) {
|
|
352
|
+
await deleteEntry(CATEGORIES_SLUG, c.id);
|
|
353
|
+
categories++;
|
|
354
|
+
}
|
|
355
|
+
return { ok: true, posts, categories };
|
|
356
|
+
});
|
|
357
|
+
|
|
191
358
|
// -------------------------------------------------------------------------
|
|
192
359
|
// Posts
|
|
193
360
|
// -------------------------------------------------------------------------
|
|
@@ -212,10 +379,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
212
379
|
const uid = user?.id ?? user?.sub ?? null;
|
|
213
380
|
const body = request.body ?? {};
|
|
214
381
|
|
|
215
|
-
const {
|
|
216
|
-
title, content, excerpt, featuredImage,
|
|
217
|
-
categories, tags, status, slug
|
|
218
|
-
} = body;
|
|
382
|
+
const { title, slug } = body;
|
|
219
383
|
|
|
220
384
|
// The schema marks title, slug, authorId and status required, so an
|
|
221
385
|
// empty title reaches createEntry and throws - a 500 where the
|
|
@@ -228,15 +392,18 @@ export default async function blogPlugin(fastify, options) {
|
|
|
228
392
|
? String(slug).trim()
|
|
229
393
|
: slugify(title);
|
|
230
394
|
|
|
395
|
+
const fields = postFields(body, user);
|
|
231
396
|
const entry = await createEntry(POSTS_SLUG, {
|
|
397
|
+
content: '',
|
|
398
|
+
excerpt: '',
|
|
399
|
+
featuredImage: null,
|
|
400
|
+
categories: [],
|
|
401
|
+
tags: [],
|
|
402
|
+
template: '',
|
|
403
|
+
...fields,
|
|
232
404
|
title: title.trim(),
|
|
233
405
|
slug: derivedSlug,
|
|
234
|
-
|
|
235
|
-
excerpt: typeof excerpt === 'string' ? excerpt.trim() : '',
|
|
236
|
-
featuredImage: featuredImage ?? null,
|
|
237
|
-
categories: Array.isArray(categories) ? categories : [],
|
|
238
|
-
tags: Array.isArray(tags) ? tags : [],
|
|
239
|
-
status: status ?? 'draft',
|
|
406
|
+
status: fields.status ?? 'draft',
|
|
240
407
|
authorId: uid
|
|
241
408
|
}, { createdBy: uid, source: 'admin' });
|
|
242
409
|
|
|
@@ -263,8 +430,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
263
430
|
return reply.code(403).send({ error: 'Forbidden' });
|
|
264
431
|
}
|
|
265
432
|
|
|
266
|
-
const
|
|
267
|
-
const merged = { ...entry.data, ...updates };
|
|
433
|
+
const merged = { ...entry.data, ...postFields(request.body ?? {}, user) };
|
|
268
434
|
|
|
269
435
|
const updated = await updateEntry(POSTS_SLUG, id, merged);
|
|
270
436
|
return reply.send(toPost(updated));
|
|
@@ -408,10 +574,49 @@ export default async function blogPlugin(fastify, options) {
|
|
|
408
574
|
return reply.send(comments);
|
|
409
575
|
});
|
|
410
576
|
|
|
577
|
+
/** GET /api/plugins/blog/comments/pending-count - the sidebar badge on Blog: comments waiting for approval */
|
|
578
|
+
fastify.get('/comments/pending-count', { preHandler: [authenticate] }, async () => {
|
|
579
|
+
const [comments, posts] = await Promise.all([loadAll(COMMENTS_SLUG), loadAll(POSTS_SLUG)]);
|
|
580
|
+
const pending = comments.filter(c => c.status === 'pending')
|
|
581
|
+
.sort((a, b) => String(b.createdAt ?? '').localeCompare(String(a.createdAt ?? '')));
|
|
582
|
+
const titleOf = new Map(posts.map(p => [p.id, p.title || 'Untitled']));
|
|
583
|
+
const count = (status) => posts.filter(p => p.status === status).length;
|
|
584
|
+
const next = posts.filter(p => p.status === 'scheduled' && p.publishedAt)
|
|
585
|
+
.sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt)))[0];
|
|
586
|
+
const n = pending.length;
|
|
587
|
+
return {
|
|
588
|
+
count: n,
|
|
589
|
+
label: `${plural(n, 'comment')} waiting for approval`,
|
|
590
|
+
tone: 'attention',
|
|
591
|
+
details: {
|
|
592
|
+
title: 'Blog',
|
|
593
|
+
rows: [
|
|
594
|
+
{ label: 'Waiting for approval', value: String(n) },
|
|
595
|
+
{ label: 'Published', value: String(count('published')) },
|
|
596
|
+
{ label: 'Drafts', value: String(count('draft')) },
|
|
597
|
+
{ label: 'Scheduled', value: next ? `${count('scheduled')} · next ${String(next.publishedAt).slice(0, 10)}` : String(count('scheduled')) }
|
|
598
|
+
],
|
|
599
|
+
// Plain text: the popover sets it with textContent, so a comment is never markup.
|
|
600
|
+
items: pending.slice(0, 4).map(c => ({
|
|
601
|
+
text: `${clip(c.authorName, 24)}: "${clip(c.body, 50)}"`,
|
|
602
|
+
meta: `on ${clip(titleOf.get(c.postId) ?? 'a deleted post', 36)} · ${ago(c.createdAt)}`
|
|
603
|
+
})),
|
|
604
|
+
empty: 'No comments waiting.'
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
|
|
411
609
|
/** POST /api/plugins/blog/comments - public, no authenticate */
|
|
412
610
|
fastify.post('/comments', async (request, reply) => {
|
|
413
611
|
const body = request.body ?? {};
|
|
414
612
|
|
|
613
|
+
// Read the settings as they are NOW. `settings` above is captured when
|
|
614
|
+
// the plugin registers, so switching comments off - or moderation -
|
|
615
|
+
// did nothing until a restart, and a closed blog kept taking comments
|
|
616
|
+
// from anyone who posted straight to this route.
|
|
617
|
+
const live = { ...defaultConfig, ...(getPluginStates()?.blog?.settings ?? {}) };
|
|
618
|
+
if (!live.commentsEnabled) return reply.code(403).send({ error: 'Comments are closed' });
|
|
619
|
+
|
|
415
620
|
// Honeypot: non-empty `website` field = bot, silently discard
|
|
416
621
|
if (body.website) {
|
|
417
622
|
return reply.code(201).send({ ok: true });
|
|
@@ -424,7 +629,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
424
629
|
if (!authorEmail) return reply.code(400).send({ error: 'authorEmail is required' });
|
|
425
630
|
if (!commentBody) return reply.code(400).send({ error: 'body is required' });
|
|
426
631
|
|
|
427
|
-
const commentStatus =
|
|
632
|
+
const commentStatus = live.commentModeration !== false ? 'pending' : 'approved';
|
|
428
633
|
|
|
429
634
|
const entry = await createEntry(COMMENTS_SLUG, {
|
|
430
635
|
postId: String(postId),
|
|
@@ -482,7 +687,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
482
687
|
/** GET /api/plugins/blog/settings */
|
|
483
688
|
fastify.get('/settings', { preHandler: [authenticate] }, async (request, reply) => {
|
|
484
689
|
const stored = getPluginStates()?.blog?.settings ?? {};
|
|
485
|
-
return reply.send({ ...defaultConfig, ...stored });
|
|
690
|
+
return reply.send({ ...defaultConfig, ...DISPLAY_DEFAULTS, ...stored });
|
|
486
691
|
});
|
|
487
692
|
|
|
488
693
|
/** PUT /api/plugins/blog/settings */
|
|
@@ -493,13 +698,18 @@ export default async function blogPlugin(fastify, options) {
|
|
|
493
698
|
|
|
494
699
|
// Whitelist rather than merge: every key here is read back by the
|
|
495
700
|
// public routes, and storing whatever was posted invites surprises.
|
|
701
|
+
// Over what is stored, not over the defaults: a PUT carrying one field
|
|
702
|
+
// used to reset every other setting to its default.
|
|
703
|
+
const stored = getPluginStates()?.blog?.settings ?? {};
|
|
496
704
|
const merged = {
|
|
497
705
|
...defaultConfig,
|
|
706
|
+
...stored,
|
|
498
707
|
...(body.basePath !== undefined && { basePath: String(body.basePath) }),
|
|
499
708
|
...(body.postsPerPage !== undefined && { postsPerPage: Number(body.postsPerPage) || defaultConfig.postsPerPage }),
|
|
500
709
|
...(body.commentsEnabled !== undefined && { commentsEnabled: Boolean(body.commentsEnabled) }),
|
|
501
710
|
...(body.commentModeration !== undefined && { commentModeration: Boolean(body.commentModeration) }),
|
|
502
|
-
...(body.feedLength !== undefined && { feedLength: Number(body.feedLength) || defaultConfig.feedLength })
|
|
711
|
+
...(body.feedLength !== undefined && { feedLength: Number(body.feedLength) || defaultConfig.feedLength }),
|
|
712
|
+
...pickDisplaySettings(body)
|
|
503
713
|
};
|
|
504
714
|
|
|
505
715
|
savePluginState('blog', { settings: merged });
|
package/plugins/blog/plugin.json
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blog",
|
|
3
3
|
"displayName": "Blog",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"
|
|
4
|
+
"version": "1.4.4",
|
|
5
|
+
"tier": "free",
|
|
6
|
+
"description": "Public-facing blog with posts, categories and comments - layouts and listings built from editable blocks.",
|
|
6
7
|
"author": "Domma CMS",
|
|
7
|
-
"date": "2026-
|
|
8
|
+
"date": "2026-09-23",
|
|
8
9
|
"icon": "file-text",
|
|
9
10
|
"admin": {
|
|
10
|
-
"css": [
|
|
11
|
-
"admin/css/index.css"
|
|
12
|
-
],
|
|
11
|
+
"css": ["admin/css/index.css"],
|
|
13
12
|
"sidebar": [
|
|
14
13
|
{
|
|
15
14
|
"id": "blog",
|
|
@@ -17,88 +16,29 @@
|
|
|
17
16
|
"icon": "file-text",
|
|
18
17
|
"url": "#/plugins/blog",
|
|
19
18
|
"section": "#/plugins/blog",
|
|
19
|
+
"countUrl": "/api/plugins/blog/comments/pending-count",
|
|
20
20
|
"children": [
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"url": "#/plugins/blog/posts"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"id": "blog-categories",
|
|
28
|
-
"text": "Categories",
|
|
29
|
-
"url": "#/plugins/blog/categories"
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"id": "blog-comments",
|
|
33
|
-
"text": "Comments",
|
|
34
|
-
"url": "#/plugins/blog/comments"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"id": "blog-settings",
|
|
38
|
-
"text": "Settings",
|
|
39
|
-
"url": "#/plugins/blog/settings"
|
|
40
|
-
}
|
|
21
|
+
{ "id": "blog-posts", "text": "Posts", "url": "#/plugins/blog/posts" },
|
|
22
|
+
{ "id": "blog-categories", "text": "Categories", "url": "#/plugins/blog/categories" },
|
|
23
|
+
{ "id": "blog-comments", "text": "Comments", "url": "#/plugins/blog/comments" }
|
|
41
24
|
]
|
|
42
25
|
}
|
|
43
26
|
],
|
|
44
27
|
"routes": [
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
"view": "plugin-blog-posts",
|
|
53
|
-
"title": "Blog Posts - Domma CMS"
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"path": "/plugins/blog/posts/new",
|
|
57
|
-
"view": "plugin-blog-post-editor",
|
|
58
|
-
"title": "New Post - Domma CMS"
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"path": "/plugins/blog/posts/:id",
|
|
62
|
-
"view": "plugin-blog-post-editor",
|
|
63
|
-
"title": "Edit Post - Domma CMS"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"path": "/plugins/blog/categories",
|
|
67
|
-
"view": "plugin-blog-categories",
|
|
68
|
-
"title": "Blog Categories - Domma CMS"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"path": "/plugins/blog/comments",
|
|
72
|
-
"view": "plugin-blog-comments",
|
|
73
|
-
"title": "Blog Comments - Domma CMS"
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"path": "/plugins/blog/settings",
|
|
77
|
-
"view": "plugin-blog-settings",
|
|
78
|
-
"title": "Blog Settings - Domma CMS"
|
|
79
|
-
}
|
|
28
|
+
{ "path": "/plugins/blog", "view": "plugin-blog-posts", "title": "Blog - Domma CMS" },
|
|
29
|
+
{ "path": "/plugins/blog/posts", "view": "plugin-blog-posts", "title": "Blog Posts - Domma CMS" },
|
|
30
|
+
{ "path": "/plugins/blog/posts/new", "view": "plugin-blog-post-editor", "title": "New Post - Domma CMS" },
|
|
31
|
+
{ "path": "/plugins/blog/posts/:id", "view": "plugin-blog-post-editor", "title": "Edit Post - Domma CMS" },
|
|
32
|
+
{ "path": "/plugins/blog/categories", "view": "plugin-blog-categories", "title": "Blog Categories - Domma CMS" },
|
|
33
|
+
{ "path": "/plugins/blog/comments", "view": "plugin-blog-comments", "title": "Blog Comments - Domma CMS" },
|
|
34
|
+
{ "path": "/plugins/blog/settings", "view": "plugin-blog-settings", "title": "Blog Settings - Domma CMS" }
|
|
80
35
|
],
|
|
81
36
|
"views": {
|
|
82
|
-
"plugin-blog-posts":
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
"plugin-blog-
|
|
87
|
-
"entry": "blog/admin/views/post-editor.js?v=58c616ec",
|
|
88
|
-
"exportName": "postEditorView"
|
|
89
|
-
},
|
|
90
|
-
"plugin-blog-categories": {
|
|
91
|
-
"entry": "blog/admin/views/categories.js?v=17f4038c",
|
|
92
|
-
"exportName": "categoriesView"
|
|
93
|
-
},
|
|
94
|
-
"plugin-blog-comments": {
|
|
95
|
-
"entry": "blog/admin/views/comments.js?v=55dd8ad9",
|
|
96
|
-
"exportName": "commentsView"
|
|
97
|
-
},
|
|
98
|
-
"plugin-blog-settings": {
|
|
99
|
-
"entry": "blog/admin/views/settings.js?v=bd2310f3",
|
|
100
|
-
"exportName": "settingsView"
|
|
101
|
-
}
|
|
37
|
+
"plugin-blog-posts": { "entry": "blog/admin/views/blog.js?v=7", "exportName": "blogView" },
|
|
38
|
+
"plugin-blog-post-editor": { "entry": "blog/admin/views/post-editor.js?v=7", "exportName": "postEditorView" },
|
|
39
|
+
"plugin-blog-categories": { "entry": "blog/admin/views/categories.js?v=7", "exportName": "categoriesView" },
|
|
40
|
+
"plugin-blog-comments": { "entry": "blog/admin/views/comments.js?v=7", "exportName": "commentsView" },
|
|
41
|
+
"plugin-blog-settings": { "entry": "blog/admin/views/settings.js?v=7", "exportName": "settingsView" }
|
|
102
42
|
}
|
|
103
43
|
}
|
|
104
44
|
}
|