domma-cms 0.55.1 → 0.69.3
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 +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- 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/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- 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 +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- 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/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -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/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- 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 +28 -24
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4473 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1022 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- 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 +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- 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 +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- 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/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +16 -13
- 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 +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- 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/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -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 +224 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- 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 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- 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/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- 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/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- 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/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- 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/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
package/plugins/blog/plugin.json
CHANGED
|
@@ -1,12 +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": {
|
|
11
|
+
"css": ["admin/css/index.css"],
|
|
10
12
|
"sidebar": [
|
|
11
13
|
{
|
|
12
14
|
"id": "blog",
|
|
@@ -14,83 +16,29 @@
|
|
|
14
16
|
"icon": "file-text",
|
|
15
17
|
"url": "#/plugins/blog",
|
|
16
18
|
"section": "#/plugins/blog",
|
|
19
|
+
"countUrl": "/api/plugins/blog/comments/pending-count",
|
|
17
20
|
"children": [
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"url": "#/plugins/blog/posts"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"id": "blog-categories",
|
|
25
|
-
"text": "Categories",
|
|
26
|
-
"url": "#/plugins/blog/categories"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"id": "blog-comments",
|
|
30
|
-
"text": "Comments",
|
|
31
|
-
"url": "#/plugins/blog/comments"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"id": "blog-settings",
|
|
35
|
-
"text": "Settings",
|
|
36
|
-
"url": "#/plugins/blog/settings"
|
|
37
|
-
}
|
|
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" }
|
|
38
24
|
]
|
|
39
25
|
}
|
|
40
26
|
],
|
|
41
27
|
"routes": [
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
"view": "plugin-blog-post-editor",
|
|
50
|
-
"title": "New Post - Domma CMS"
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
"path": "/plugins/blog/posts/:id",
|
|
54
|
-
"view": "plugin-blog-post-editor",
|
|
55
|
-
"title": "Edit Post - Domma CMS"
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"path": "/plugins/blog/categories",
|
|
59
|
-
"view": "plugin-blog-categories",
|
|
60
|
-
"title": "Blog Categories - Domma CMS"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"path": "/plugins/blog/comments",
|
|
64
|
-
"view": "plugin-blog-comments",
|
|
65
|
-
"title": "Blog Comments - Domma CMS"
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"path": "/plugins/blog/settings",
|
|
69
|
-
"view": "plugin-blog-settings",
|
|
70
|
-
"title": "Blog Settings - Domma CMS"
|
|
71
|
-
}
|
|
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" }
|
|
72
35
|
],
|
|
73
36
|
"views": {
|
|
74
|
-
"plugin-blog-posts":
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
},
|
|
78
|
-
"plugin-blog-
|
|
79
|
-
"entry": "blog/admin/views/post-editor.js?v=d4d7a514",
|
|
80
|
-
"exportName": "postEditorView"
|
|
81
|
-
},
|
|
82
|
-
"plugin-blog-categories": {
|
|
83
|
-
"entry": "blog/admin/views/categories.js?v=7e3946ca",
|
|
84
|
-
"exportName": "categoriesView"
|
|
85
|
-
},
|
|
86
|
-
"plugin-blog-comments": {
|
|
87
|
-
"entry": "blog/admin/views/comments.js?v=ee2146e9",
|
|
88
|
-
"exportName": "commentsView"
|
|
89
|
-
},
|
|
90
|
-
"plugin-blog-settings": {
|
|
91
|
-
"entry": "blog/admin/views/settings.js?v=3fab4235",
|
|
92
|
-
"exportName": "settingsView"
|
|
93
|
-
}
|
|
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" }
|
|
94
42
|
}
|
|
95
43
|
}
|
|
96
44
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog Plugin - Public Routes
|
|
3
|
+
* SSR public-facing routes for the blog. Mounted at the Fastify root.
|
|
4
|
+
*
|
|
5
|
+
* Posts and listing cards render through BLOCKS (see lib/layouts.js), and the
|
|
6
|
+
* page works fully without JavaScript. public/blog.js then takes the listing
|
|
7
|
+
* over - search, category pills, sort, display switching and a right-click
|
|
8
|
+
* menu - and adds reading progress, contents tracking, sharing and in-page
|
|
9
|
+
* comments to a post.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import {fileURLToPath} from 'url';
|
|
13
|
+
import {renderBlogPage} from '../../server/services/renderer.js';
|
|
14
|
+
import {byNewest, escapeHtml, pickDisplay, safeImageUrl} from './lib/layouts.js';
|
|
15
|
+
import {catalogue, renderCards} from './lib/render.js';
|
|
16
|
+
import {authorNamesFor, isVisible, liveSettings, loadEntries, renderPostPage, resolveAuthorName} from './lib/page.js';
|
|
17
|
+
|
|
18
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
19
|
+
const TEMPLATES_DIR = path.join(__dirname, 'templates');
|
|
20
|
+
|
|
21
|
+
/** Bumped with the plugin, so a release is never served a stale script. */
|
|
22
|
+
const ASSET_V = '1.4.4';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Above this many posts a listing pages on the server instead: the browser
|
|
26
|
+
* filters what it was sent, and sending thousands of cards is not a feature.
|
|
27
|
+
*/
|
|
28
|
+
const INTERACTIVE_CAP = 300;
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function buildPagination(page, totalPages, basePath, query = '') {
|
|
35
|
+
if (totalPages <= 1) return '';
|
|
36
|
+
const parts = [];
|
|
37
|
+
if (page > 1) {
|
|
38
|
+
const prevHref = page === 2 ? basePath : `${basePath}/page/${page - 1}`;
|
|
39
|
+
parts.push(`<a href="${prevHref}${query}" class="btn btn-sm btn-secondary">« Newer</a>`);
|
|
40
|
+
}
|
|
41
|
+
parts.push(`<span class="blog-page-of">Page ${page} of ${totalPages}</span>`);
|
|
42
|
+
if (page < totalPages) {
|
|
43
|
+
parts.push(`<a href="${basePath}/page/${page + 1}${query}" class="btn btn-sm btn-secondary">Older »</a>`);
|
|
44
|
+
}
|
|
45
|
+
return `<nav class="blog-pagination" aria-label="Pages">${parts.join('')}</nav>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** JSON inside a <script> element, safe against `</script>` in any value. */
|
|
49
|
+
function scriptJson(value) {
|
|
50
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Plugin
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export default async function blogPublicPlugin(fastify, options) {
|
|
58
|
+
const {settings, config} = options;
|
|
59
|
+
const base = (settings?.basePath ?? '/blog').replace(/\/$/, '');
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A listing: the index, a page of it, or a category / tag / author archive.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} request
|
|
65
|
+
* @param {object} reply
|
|
66
|
+
* @param {object[]} posts - candidates; drafts are dropped here
|
|
67
|
+
* @param {object} opts - {title, intro, page, pageBase, filter}
|
|
68
|
+
*/
|
|
69
|
+
async function renderArchive(request, reply, posts, {title, intro = '', page = 1, pageBase = base, filter = null}) {
|
|
70
|
+
const s = liveSettings(settings);
|
|
71
|
+
const perPage = Math.max(1, Number(s.postsPerPage) || 10);
|
|
72
|
+
const {listDisplays} = await catalogue();
|
|
73
|
+
const display = pickDisplay(String(request.query?.display ?? ''), s, listDisplays);
|
|
74
|
+
|
|
75
|
+
const visible = posts.filter(isVisible).sort(byNewest);
|
|
76
|
+
const interactive = Boolean(s.interactive) && visible.length <= INTERACTIVE_CAP;
|
|
77
|
+
const totalPages = Math.max(1, Math.ceil(visible.length / perPage));
|
|
78
|
+
const safePage = Math.min(Math.max(1, page), totalPages);
|
|
79
|
+
// Interactive listings send every post and page in the browser; the
|
|
80
|
+
// script shows the first page at once, so nothing jumps.
|
|
81
|
+
const shown = interactive ? visible : visible.slice((safePage - 1) * perPage, safePage * perPage);
|
|
82
|
+
|
|
83
|
+
const categories = await loadEntries('blog-categories');
|
|
84
|
+
const catMap = Object.fromEntries(categories.map(c => [c.id, {name: c.name, slug: c.slug}]));
|
|
85
|
+
const authorNames = await authorNamesFor(shown);
|
|
86
|
+
const {items, style} = await renderCards(shown, display, {base, catMap, authorNames});
|
|
87
|
+
|
|
88
|
+
const requested = request.query?.display ? `?display=${encodeURIComponent(display.id)}` : '';
|
|
89
|
+
const switcher = s.displaySwitcher && listDisplays.length > 1
|
|
90
|
+
? `<div class="blog-displays" role="group" aria-label="Display">${listDisplays.map(d =>
|
|
91
|
+
`<a href="?display=${encodeURIComponent(d.id)}" data-blog-display="${escapeHtml(d.id)}" title="${escapeHtml(d.description)}"${d.id === display.id ? ' aria-current="true"' : ''}>${escapeHtml(d.label)}</a>`
|
|
92
|
+
).join('')}</div>`
|
|
93
|
+
: '';
|
|
94
|
+
|
|
95
|
+
const cfg = {
|
|
96
|
+
base, perPage, interactive, filter,
|
|
97
|
+
display: display.id,
|
|
98
|
+
displays: listDisplays.map(d => ({id: d.id, arrange: d.arrange, lead: Boolean(d.lead)})),
|
|
99
|
+
categories: categories.map(c => ({slug: c.slug, name: c.name}))
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const archive = `${style}
|
|
103
|
+
<section class="blog-archive" data-blog-archive>
|
|
104
|
+
<script type="application/json" data-blog-config>${scriptJson(cfg)}</script>
|
|
105
|
+
<div class="blog-archive-bar">${switcher}</div>
|
|
106
|
+
<div class="blog-items blog-items--${escapeHtml(display.arrange)}${display.lead ? ' blog-items--lead' : ''}" data-display="${escapeHtml(display.id)}">
|
|
107
|
+
${items || ''}
|
|
108
|
+
</div>
|
|
109
|
+
<p class="blog-empty"${shown.length ? ' hidden' : ''}>No posts here yet.</p>
|
|
110
|
+
</section>`;
|
|
111
|
+
|
|
112
|
+
const html = await renderBlogPage(
|
|
113
|
+
path.join(TEMPLATES_DIR, 'index.html'),
|
|
114
|
+
{
|
|
115
|
+
archiveTitle: escapeHtml(title),
|
|
116
|
+
archiveIntro: intro ? `<p class="blog-archive-intro">${escapeHtml(intro)}</p>` : '',
|
|
117
|
+
archive,
|
|
118
|
+
pagination: interactive ? '' : buildPagination(safePage, totalPages, pageBase, requested),
|
|
119
|
+
assetV: ASSET_V
|
|
120
|
+
},
|
|
121
|
+
{title, urlPath: request.url.split('?')[0]}
|
|
122
|
+
);
|
|
123
|
+
return reply.type('text/html').send(html);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 1. RSS feed
|
|
127
|
+
fastify.get(`${base}/feed.xml`, async (request, reply) => {
|
|
128
|
+
const s = liveSettings(settings);
|
|
129
|
+
const allPosts = await loadEntries('blog-posts');
|
|
130
|
+
const posts = allPosts.filter(isVisible).sort(byNewest).slice(0, s.feedLength ?? 20);
|
|
131
|
+
|
|
132
|
+
const siteTitle = config?.site?.title ?? 'Blog';
|
|
133
|
+
const siteUrl = (config?.site?.url ?? '').replace(/\/$/, '');
|
|
134
|
+
|
|
135
|
+
const items = posts.map((post) => ` <item>
|
|
136
|
+
<title>${escapeHtml(post.title)}</title>
|
|
137
|
+
<link>${siteUrl}${base}/${escapeHtml(post.slug)}</link>
|
|
138
|
+
<description>${escapeHtml(post.excerpt || '')}</description>
|
|
139
|
+
<pubDate>${new Date(post.publishedAt).toUTCString()}</pubDate>
|
|
140
|
+
<guid>${siteUrl}${base}/${escapeHtml(post.slug)}</guid>
|
|
141
|
+
</item>`).join('\n');
|
|
142
|
+
|
|
143
|
+
return reply.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
|
|
144
|
+
<rss version="2.0">
|
|
145
|
+
<channel>
|
|
146
|
+
<title>${escapeHtml(siteTitle)}</title>
|
|
147
|
+
<link>${siteUrl}${base}</link>
|
|
148
|
+
<description>Blog feed</description>
|
|
149
|
+
${items}
|
|
150
|
+
</channel>
|
|
151
|
+
</rss>`);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// 2. Paginated index
|
|
155
|
+
fastify.get(`${base}/page/:n`, async (request, reply) => {
|
|
156
|
+
const page = parseInt(request.params.n, 10) || 1;
|
|
157
|
+
return renderArchive(request, reply, await loadEntries('blog-posts'), {title: 'Blog', page});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// 3. Category archive
|
|
161
|
+
fastify.get(`${base}/category/:slug`, async (request, reply) => {
|
|
162
|
+
const {slug} = request.params;
|
|
163
|
+
const [allPosts, allCategories] = await Promise.all([loadEntries('blog-posts'), loadEntries('blog-categories')]);
|
|
164
|
+
const category = allCategories.find((c) => c.slug === slug);
|
|
165
|
+
if (!category) return reply.code(404).send('Category not found');
|
|
166
|
+
const posts = allPosts.filter((p) => Array.isArray(p.categories) && p.categories.includes(category.id));
|
|
167
|
+
return renderArchive(request, reply, posts, {
|
|
168
|
+
title: category.name, intro: category.description || '', filter: {type: 'category', value: category.slug}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// 4. Tag archive
|
|
173
|
+
fastify.get(`${base}/tag/:tag`, async (request, reply) => {
|
|
174
|
+
const {tag} = request.params;
|
|
175
|
+
const posts = (await loadEntries('blog-posts')).filter((p) => Array.isArray(p.tags) && p.tags.includes(tag));
|
|
176
|
+
return renderArchive(request, reply, posts, {title: `#${tag}`, filter: {type: 'tag', value: tag}});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// 5. Author archive
|
|
180
|
+
fastify.get(`${base}/author/:id`, async (request, reply) => {
|
|
181
|
+
const {id} = request.params;
|
|
182
|
+
const posts = (await loadEntries('blog-posts')).filter((p) => p.authorId === id);
|
|
183
|
+
const authorName = await resolveAuthorName(id);
|
|
184
|
+
return renderArchive(request, reply, posts, {title: `Posts by ${authorName}`, filter: {type: 'author', value: id}});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 6. Blog index
|
|
188
|
+
fastify.get(base, async (request, reply) => {
|
|
189
|
+
return renderArchive(request, reply, await loadEntries('blog-posts'), {title: 'Blog'});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// 7. Individual post - registered LAST so the slug match doesn't shadow archives
|
|
193
|
+
fastify.get(`${base}/:slug`, async (request, reply) => {
|
|
194
|
+
const {slug} = request.params;
|
|
195
|
+
const allPosts = await loadEntries('blog-posts');
|
|
196
|
+
const post = allPosts.find((p) => p.slug === slug);
|
|
197
|
+
if (!post || !isVisible(post)) return reply.code(404).send('Not found');
|
|
198
|
+
|
|
199
|
+
const siteUrl = (config?.site?.url || `${request.protocol}://${request.hostname}`).replace(/\/$/, '');
|
|
200
|
+
const page = await renderPostPage(post, allPosts, liveSettings(settings), {base, siteUrl});
|
|
201
|
+
|
|
202
|
+
const html = await renderBlogPage(
|
|
203
|
+
path.join(TEMPLATES_DIR, 'post.html'),
|
|
204
|
+
{post: page.html, assetV: ASSET_V},
|
|
205
|
+
{
|
|
206
|
+
title: post.seo?.title || post.title,
|
|
207
|
+
description: post.seo?.description || post.excerpt || '',
|
|
208
|
+
ogImage: safeImageUrl(post.featuredImage),
|
|
209
|
+
ogType: 'article',
|
|
210
|
+
usedComponents: page.usedComponents,
|
|
211
|
+
urlPath: `${base}/${slug}`
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
return reply.type('text/html').send(html);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Blog - what every layout shares: the listing's frame and toolbar, chips,
|
|
3
|
+
* post typography, contents list, sharing, comments and the right-click menu.
|
|
4
|
+
* Each layout's own look lives in its block (content/blocks/blog-*.css), so a
|
|
5
|
+
* site restyles a layout in the Blocks tool without touching this file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.blog-page { padding-top: 2rem; padding-bottom: 3rem; }
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* The site theme colours and underlines every link in .page-body, with
|
|
12
|
+
* !important and a selector as specific as (0,8,4) (public/css/site.css). A
|
|
13
|
+
* blog's links are its interface - titles, chips, the display switcher - so they
|
|
14
|
+
* opt out here, by id to outrank it. Links written in a post keep the theme's
|
|
15
|
+
* look. A block's CSS sets link colour on the link's parent (it inherits).
|
|
16
|
+
* The reset sits in :where() so it weighs only its id (1,0,0) - still above the
|
|
17
|
+
* theme, and below every exception here. Without it, `:not(.blog-content a)`
|
|
18
|
+
* lifted the reset over the exceptions and the active display button went
|
|
19
|
+
* dark-on-dark.
|
|
20
|
+
*/
|
|
21
|
+
#dm-blog :where(a[href]:not(.blog-content a)) { color: inherit !important; text-decoration: none !important; }
|
|
22
|
+
#dm-blog :where(a[href]:not(.blog-content a)):hover { color: var(--dm-primary, #3b82f6) !important; }
|
|
23
|
+
#dm-blog a.blog-chip { color: var(--dm-primary, #3b82f6) !important; }
|
|
24
|
+
#dm-blog .blog-displays a[aria-current="true"] { color: #fff !important; }
|
|
25
|
+
#dm-blog a.blog-tag, #dm-blog .blog-toc a { color: var(--dm-text-muted) !important; }
|
|
26
|
+
#dm-blog a.blog-tag:hover, #dm-blog .blog-toc a:hover { color: var(--dm-text) !important; }
|
|
27
|
+
#dm-blog .blog-toc a.is-active { color: var(--dm-primary, #3b82f6) !important; }
|
|
28
|
+
|
|
29
|
+
/* ── Listing ──────────────────────────────────────────────────────────── */
|
|
30
|
+
.blog-archive-head { margin-bottom: 1.25rem; }
|
|
31
|
+
.blog-archive-head h1 { margin: 0 0 .35rem; font-size: clamp(2rem, 4vw, 2.6rem); letter-spacing: -.015em; }
|
|
32
|
+
.blog-archive-intro { margin: 0; color: var(--dm-text-muted); font-size: 1.1rem; max-width: 42rem; }
|
|
33
|
+
|
|
34
|
+
.blog-archive-bar { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
35
|
+
.blog-archive-bar:empty { display: none; }
|
|
36
|
+
|
|
37
|
+
.blog-search { position: relative; flex: 1 1 16rem; max-width: 24rem; }
|
|
38
|
+
.blog-search .blog-search-input { width: 100%; padding: .5rem .75rem .5rem 2.1rem; border-radius: 999px;
|
|
39
|
+
border: 1px solid var(--dm-border, rgba(127,127,127,.3)); background: var(--dm-surface, transparent); color: inherit; font: inherit; }
|
|
40
|
+
.blog-search .blog-search-input:focus { outline: 2px solid color-mix(in srgb, var(--dm-primary, #3b82f6) 45%, transparent); outline-offset: 1px; }
|
|
41
|
+
.blog-search svg { position: absolute; left: .75rem; top: 50%; transform: translateY(-50%); opacity: .55; pointer-events: none; }
|
|
42
|
+
|
|
43
|
+
.blog-sort { padding: .45rem .6rem; border-radius: 999px; border: 1px solid var(--dm-border, rgba(127,127,127,.3));
|
|
44
|
+
background: var(--dm-surface, transparent); color: inherit; font: inherit; font-size: .9rem; }
|
|
45
|
+
|
|
46
|
+
.blog-displays { display: inline-flex; margin-left: auto; padding: 3px; border-radius: 999px;
|
|
47
|
+
border: 1px solid var(--dm-border, rgba(127,127,127,.3)); }
|
|
48
|
+
.blog-displays a { padding: .3rem .8rem; border-radius: 999px; font-size: .85rem; text-decoration: none; color: var(--dm-text-muted); }
|
|
49
|
+
.blog-displays a:hover { color: var(--dm-text); }
|
|
50
|
+
.blog-displays a[aria-current="true"] { background: var(--dm-primary, #3b82f6); color: #fff; }
|
|
51
|
+
|
|
52
|
+
.blog-pills { display: flex; gap: .4rem; flex-wrap: wrap; margin: 0 0 1rem; }
|
|
53
|
+
.blog-pill { display: inline-flex; align-items: center; gap: .4rem; padding: .3rem .8rem; border-radius: 999px; cursor: pointer;
|
|
54
|
+
border: 1px solid var(--dm-border, rgba(127,127,127,.3)); background: none; color: inherit; font: inherit; font-size: .88rem; }
|
|
55
|
+
.blog-pill:hover { border-color: var(--dm-primary, #3b82f6); }
|
|
56
|
+
.blog-pill.is-active { background: var(--dm-primary, #3b82f6); border-color: var(--dm-primary, #3b82f6); color: #fff; }
|
|
57
|
+
.blog-pill .n { font-size: .75rem; opacity: .7; font-variant-numeric: tabular-nums; }
|
|
58
|
+
.blog-pill[disabled] { opacity: .4; cursor: default; }
|
|
59
|
+
|
|
60
|
+
.blog-active { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: -.25rem 0 1rem; font-size: .88rem; color: var(--dm-text-muted); }
|
|
61
|
+
.blog-active:empty { display: none; }
|
|
62
|
+
.blog-active .blog-filter-chip { display: inline-flex; align-items: center; gap: .35rem; padding: .2rem .35rem .2rem .7rem; border-radius: 999px;
|
|
63
|
+
background: color-mix(in srgb, var(--dm-primary, #3b82f6) 14%, transparent); color: var(--dm-text); }
|
|
64
|
+
.blog-active .blog-filter-chip button { border: 0; background: none; color: inherit; cursor: pointer; font-size: 1rem; line-height: 1; padding: 0 .25rem; border-radius: 50%; }
|
|
65
|
+
.blog-active .blog-filter-chip button:hover { background: rgba(127,127,127,.2); }
|
|
66
|
+
.blog-active .blog-clear { border: 0; background: none; color: var(--dm-primary); cursor: pointer; font: inherit; text-decoration: underline; padding: 0; }
|
|
67
|
+
|
|
68
|
+
.blog-items--grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); gap: 1.5rem; }
|
|
69
|
+
.blog-items--stack { display: flex; flex-direction: column; }
|
|
70
|
+
.blog-items .blog-item[hidden] { display: none !important; }
|
|
71
|
+
.blog-items.is-swapping { opacity: .35; transition: opacity .15s; }
|
|
72
|
+
|
|
73
|
+
.blog-empty { padding: 3rem 1rem; text-align: center; color: var(--dm-text-muted); }
|
|
74
|
+
.blog-more { display: flex; flex-direction: column; align-items: center; gap: .5rem; margin-top: 2rem; }
|
|
75
|
+
.blog-more-count { font-size: .85rem; color: var(--dm-text-muted); }
|
|
76
|
+
.blog-pagination { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-top: 2rem; }
|
|
77
|
+
.blog-page-of { font-size: .9rem; color: var(--dm-text-muted); }
|
|
78
|
+
|
|
79
|
+
/* Right-click affordance on a listing that answers it. */
|
|
80
|
+
.blog-archive.is-live .blog-item { cursor: context-menu; }
|
|
81
|
+
.blog-archive.is-live .blog-item a { cursor: pointer; }
|
|
82
|
+
.blog-item.is-flash { animation: blog-flash .9s ease; }
|
|
83
|
+
@keyframes blog-flash { 0% { box-shadow: 0 0 0 3px var(--dm-primary, #3b82f6); } 100% { box-shadow: 0 0 0 3px transparent; } }
|
|
84
|
+
|
|
85
|
+
/* ── Chips and tags ───────────────────────────────────────────────────── */
|
|
86
|
+
.blog-chip { display: inline-block; padding: .12rem .6rem; border-radius: 999px; font-size: .72rem; font-weight: 600;
|
|
87
|
+
letter-spacing: .03em; text-transform: uppercase; text-decoration: none;
|
|
88
|
+
color: var(--dm-primary, #3b82f6); background: color-mix(in srgb, var(--dm-primary, #3b82f6) 12%, transparent);
|
|
89
|
+
border: 1px solid transparent; }
|
|
90
|
+
.blog-chip:hover { border-color: currentColor; }
|
|
91
|
+
.blog-tag { font-size: .85rem; color: var(--dm-text-muted); text-decoration: none; }
|
|
92
|
+
.blog-tag:hover { color: var(--dm-primary); }
|
|
93
|
+
|
|
94
|
+
/* ── A post ───────────────────────────────────────────────────────────── */
|
|
95
|
+
.blog-progress { position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 1100; pointer-events: none; }
|
|
96
|
+
.blog-progress span { display: block; height: 100%; width: 100%; transform-origin: 0 50%; transform: scaleX(0);
|
|
97
|
+
background: var(--dm-primary, #3b82f6); transition: transform .08s linear; }
|
|
98
|
+
|
|
99
|
+
.blog-content { font-size: 1.075rem; line-height: 1.75; }
|
|
100
|
+
.blog-content > * + * { margin-top: 1.1em; }
|
|
101
|
+
.blog-content h2 { font-size: 1.6rem; margin-top: 2em; line-height: 1.25; scroll-margin-top: calc(var(--dm-navbar-height, 64px) + 1rem); }
|
|
102
|
+
.blog-content h3 { font-size: 1.25rem; margin-top: 1.6em; scroll-margin-top: calc(var(--dm-navbar-height, 64px) + 1rem); }
|
|
103
|
+
.blog-content img { max-width: 100%; height: auto; border-radius: var(--dm-radius-md, 8px); }
|
|
104
|
+
.blog-content blockquote { margin: 1.75em 0; padding: .25em 0 .25em 1.25em; border-left: 3px solid var(--dm-primary, #3b82f6);
|
|
105
|
+
font-size: 1.2em; font-style: italic; color: var(--dm-text-muted); }
|
|
106
|
+
.blog-content pre { padding: 1rem; border-radius: var(--dm-radius-md, 8px); overflow-x: auto; background: rgba(127,127,127,.1); font-size: .9em; }
|
|
107
|
+
.blog-content :not(pre) > code { padding: .1em .35em; border-radius: 4px; background: rgba(127,127,127,.14); font-size: .9em; }
|
|
108
|
+
|
|
109
|
+
.blog-toc { font-size: .9rem; }
|
|
110
|
+
.blog-toc-title { margin: 0 0 .5rem; font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--dm-text-muted); }
|
|
111
|
+
.blog-toc ol { list-style: none; margin: 0; padding: 0; border-left: 2px solid var(--dm-border, rgba(127,127,127,.25)); }
|
|
112
|
+
.blog-toc li a { display: block; padding: .3rem 0 .3rem .9rem; margin-left: -2px; border-left: 2px solid transparent;
|
|
113
|
+
color: var(--dm-text-muted); text-decoration: none; line-height: 1.35; }
|
|
114
|
+
.blog-toc li.blog-toc-l3 a { padding-left: 1.7rem; font-size: .85rem; }
|
|
115
|
+
.blog-toc li a:hover { color: var(--dm-text); }
|
|
116
|
+
.blog-toc li a.is-active { color: var(--dm-primary, #3b82f6); border-left-color: var(--dm-primary, #3b82f6); font-weight: 600; }
|
|
117
|
+
|
|
118
|
+
.blog-share { display: flex; gap: .4rem; flex-wrap: wrap; }
|
|
119
|
+
.blog-share-btn { display: inline-flex; align-items: center; gap: .35rem; padding: .35rem .8rem; border-radius: 999px; cursor: pointer;
|
|
120
|
+
border: 1px solid var(--dm-border, rgba(127,127,127,.3)); background: none; color: inherit; font: inherit; font-size: .85rem; text-decoration: none; }
|
|
121
|
+
.blog-share-btn:hover { border-color: var(--dm-primary, #3b82f6); color: var(--dm-primary, #3b82f6); }
|
|
122
|
+
|
|
123
|
+
.blog-prevnext { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 2.5rem 0 0; }
|
|
124
|
+
.blog-prevnext a { display: flex; flex-direction: column; gap: .25rem; padding: 1rem 1.1rem; border-radius: var(--dm-radius-lg, 12px);
|
|
125
|
+
border: 1px solid var(--dm-border, rgba(127,127,127,.25)); text-decoration: none; color: inherit; }
|
|
126
|
+
.blog-prevnext a:hover { border-color: var(--dm-primary, #3b82f6); }
|
|
127
|
+
.blog-prevnext a span { font-size: .78rem; color: var(--dm-text-muted); text-transform: uppercase; letter-spacing: .05em; }
|
|
128
|
+
.blog-prevnext .is-newer { text-align: right; grid-column: 2; }
|
|
129
|
+
@media (max-width: 600px) { .blog-prevnext { grid-template-columns: 1fr; } .blog-prevnext .is-newer { grid-column: 1; } }
|
|
130
|
+
|
|
131
|
+
.blog-section-title { font-size: 1.35rem; margin: 0 0 1.1rem; display: flex; align-items: center; gap: .5rem; }
|
|
132
|
+
.blog-related { margin-top: 3.5rem; padding-top: 2rem; border-top: 1px solid var(--dm-border, rgba(127,127,127,.25)); }
|
|
133
|
+
.blog-count { font-size: .8rem; padding: .05rem .5rem; border-radius: 999px; background: rgba(127,127,127,.16); font-weight: 600; }
|
|
134
|
+
|
|
135
|
+
/* ── Comments ─────────────────────────────────────────────────────────── */
|
|
136
|
+
.blog-comments { margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--dm-border, rgba(127,127,127,.25)); }
|
|
137
|
+
.blog-comment-list { list-style: none; margin: 0 0 2rem; padding: 0; display: flex; flex-direction: column; gap: 1.1rem; }
|
|
138
|
+
.blog-comment-list:empty { display: none; }
|
|
139
|
+
.blog-comment { padding: .9rem 1rem; border-radius: var(--dm-radius-md, 8px); background: rgba(127,127,127,.07); }
|
|
140
|
+
.blog-comment.is-new { outline: 2px solid color-mix(in srgb, var(--dm-success, #16a34a) 55%, transparent); }
|
|
141
|
+
.blog-comment-head { margin: 0 0 .35rem; font-size: .9rem; }
|
|
142
|
+
.blog-comment-head time { color: var(--dm-text-muted); font-size: .82rem; margin-left: .35rem; }
|
|
143
|
+
.blog-comment-body { margin: 0; white-space: pre-line; }
|
|
144
|
+
.blog-comment-form { display: flex; flex-direction: column; gap: .8rem; }
|
|
145
|
+
.blog-comment-form h3 { margin: 0; font-size: 1.1rem; }
|
|
146
|
+
.blog-comment-form label { display: flex; flex-direction: column; gap: .3rem; font-size: .9rem; font-weight: 600; }
|
|
147
|
+
.blog-comment-form label small { font-weight: 400; color: var(--dm-text-muted); }
|
|
148
|
+
.blog-comment-row { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
|
|
149
|
+
@media (max-width: 600px) { .blog-comment-row { grid-template-columns: 1fr; } }
|
|
150
|
+
.blog-comment-actions { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
|
|
151
|
+
.blog-comment-status { font-size: .9rem; }
|
|
152
|
+
.blog-comment-status.is-ok { color: var(--dm-success, #16a34a); }
|
|
153
|
+
.blog-comment-status.is-error { color: var(--dm-danger, #dc2626); }
|
|
154
|
+
.blog-hp { position: absolute !important; left: -9999px !important; width: 1px; height: 1px; overflow: hidden; }
|
|
155
|
+
|
|
156
|
+
/* ── The right-click menu ─────────────────────────────────────────────── */
|
|
157
|
+
.blog-menu { position: fixed; z-index: 10000; min-width: 220px; max-width: 300px; padding: .3rem;
|
|
158
|
+
background: var(--dm-surface, #fff); color: var(--dm-text, #111); border: 1px solid var(--dm-border, rgba(127,127,127,.3));
|
|
159
|
+
border-radius: 10px; box-shadow: 0 12px 32px rgba(0,0,0,.22); font-size: .88rem; }
|
|
160
|
+
.blog-menu[hidden] { display: none; }
|
|
161
|
+
.blog-menu-head { padding: .45rem .65rem .35rem; font-size: .72rem; letter-spacing: .05em; text-transform: uppercase;
|
|
162
|
+
color: var(--dm-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
163
|
+
.blog-menu button { display: flex; align-items: center; gap: .55rem; width: 100%; padding: .45rem .65rem; border: 0; border-radius: 6px;
|
|
164
|
+
background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
|
|
165
|
+
.blog-menu button:hover, .blog-menu button:focus-visible { background: color-mix(in srgb, var(--dm-primary, #3b82f6) 12%, transparent); outline: none; }
|
|
166
|
+
.blog-menu button .k { margin-left: auto; font-size: .78rem; color: var(--dm-text-muted); }
|
|
167
|
+
.blog-menu hr { border: 0; border-top: 1px solid var(--dm-border, rgba(127,127,127,.25)); margin: .3rem .2rem; }
|
|
168
|
+
.blog-toast { position: fixed; left: 50%; bottom: 1.5rem; transform: translateX(-50%); z-index: 10001; padding: .55rem 1rem;
|
|
169
|
+
border-radius: 999px; background: var(--dm-text, #111); color: var(--dm-background, #fff); font-size: .88rem; box-shadow: 0 6px 20px rgba(0,0,0,.25); }
|
|
170
|
+
|
|
171
|
+
@media (prefers-reduced-motion: reduce) {
|
|
172
|
+
.blog-item, .blog-item * { transition: none !important; animation: none !important; }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* A Feature post's hero starts where the theme's content does - no page padding
|
|
176
|
+
above it. (It cannot reach the navbar: the theme's wrapper clips overflow.) */
|
|
177
|
+
.blog-page:has(.pf-hero) { padding-top: 0; }
|