domma-cms 0.66.0 → 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 +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 +92 -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 +307 -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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# blog - AI Assistant Guide
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Public-facing blog for the CMS. Supports posts with categories, tags, and optional moderated comments. Authors can draft posts; only blog-editors (and admins) can publish or schedule them. Public SSR routes serve an archive index, category/tag/author archives, individual post pages, and an RSS feed. All collection I/O goes through `server/services/collections.js` (FileAdapter by default).
|
|
6
|
+
|
|
7
|
+
## Files
|
|
8
|
+
|
|
9
|
+
- `plugin.json`, `plugin.js`, `config.js`, `plugin.public.js`
|
|
10
|
+
- `collections/posts/schema.json` - post entries
|
|
11
|
+
- `collections/categories/schema.json` - category entries
|
|
12
|
+
- `collections/comments/schema.json` - comment entries
|
|
13
|
+
- `templates/index.html` - every listing (index, page n, category, tag, author)
|
|
14
|
+
- `templates/post.html` - a post page (also the admin preview)
|
|
15
|
+
- `lib/layouts.js` - PURE: layouts on offer, interpolation, contents list, card values, settings whitelist
|
|
16
|
+
- `lib/render.js` - blocks I/O: seed, load (site copy, else shipped), render cards
|
|
17
|
+
- `lib/page.js` - a post page through its layout; shared by the public route and `POST /preview`
|
|
18
|
+
- `lib/samples.js` - the sample posts and categories
|
|
19
|
+
- `blocks/*.html|css` - the shipped layouts, copied into content/blocks/ once
|
|
20
|
+
- `public/blog.js`, `public/blog.css` - the browser half (listing, right-click menu, post extras)
|
|
21
|
+
- `public/samples/*.svg` - sample covers
|
|
22
|
+
- `tests/layouts.test.js`, `tests/public.test.js`
|
|
23
|
+
- `admin/views/blog.js` - post list (KPIs + status/category/search filters)
|
|
24
|
+
- `admin/views/post-editor.js` - create / edit a post
|
|
25
|
+
- `admin/views/categories.js`, `admin/views/comments.js`, `admin/views/settings.js`
|
|
26
|
+
- `admin/templates/*.html`
|
|
27
|
+
- `admin/css/index.css` - admin styles (declared in `plugin.json` `admin.css`)
|
|
28
|
+
|
|
29
|
+
## API Routes
|
|
30
|
+
|
|
31
|
+
All routes are under `/api/plugins/blog`. All require authentication unless noted.
|
|
32
|
+
|
|
33
|
+
| Method | Path | Purpose | Auth |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| GET | `/posts` | List posts; blog-authors see only their own | authenticate |
|
|
36
|
+
| POST | `/posts` | Create a new post (draft by default) | authenticate |
|
|
37
|
+
| GET | `/posts/:id` | Get a single post by ID | authenticate |
|
|
38
|
+
| PUT | `/posts/:id` | Update post; blog-authors may only edit their own | authenticate |
|
|
39
|
+
| DELETE | `/posts/:id` | Delete post; blog-editor / admin only | authenticate |
|
|
40
|
+
| POST | `/posts/:id/publish` | Set status to `published`; blog-editor / admin only | authenticate |
|
|
41
|
+
| POST | `/posts/:id/schedule` | Set status to `scheduled` + `publishedAt`; blog-editor / admin only | authenticate |
|
|
42
|
+
| GET | `/categories` | List all categories | authenticate |
|
|
43
|
+
| POST | `/categories` | Create category; blog-editor / admin only | authenticate |
|
|
44
|
+
| PUT | `/categories/:id` | Update category; blog-editor / admin only | authenticate |
|
|
45
|
+
| DELETE | `/categories/:id` | Delete category; blog-editor / admin only | authenticate |
|
|
46
|
+
| GET | `/comments` | List comments; supports `?status=` and `?postId=` filters | authenticate |
|
|
47
|
+
| POST | `/comments` | Submit a comment; **no authentication required** (public route) | - |
|
|
48
|
+
| PUT | `/comments/:id` | Moderate comment (update status field) | authenticate |
|
|
49
|
+
| DELETE | `/comments/:id` | Delete comment | authenticate |
|
|
50
|
+
| GET | `/layouts` | Post layouts and listing displays on offer, plus the defaults | authenticate |
|
|
51
|
+
| POST | `/preview` | A post page from UNSAVED fields, in any layout (`{html, layout}`) | authenticate |
|
|
52
|
+
| POST | `/samples` | Add the sample posts (idempotent by slug) | authenticate + requireAdmin |
|
|
53
|
+
| DELETE | `/samples` | Remove `sample: true` posts, and sample categories nothing uses | authenticate + requireAdmin |
|
|
54
|
+
| GET | `/settings` | Read blog plugin settings | authenticate |
|
|
55
|
+
| PUT | `/settings` | Save blog plugin settings | authenticate + requireAdmin |
|
|
56
|
+
|
|
57
|
+
## Public Routes
|
|
58
|
+
|
|
59
|
+
Registered at the Fastify root (no `/api/plugins/blog` prefix). Base path defaults to `/blog` (overridable via `basePath` config key).
|
|
60
|
+
|
|
61
|
+
| Method | Path | Purpose |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| GET | `{base}/feed.xml` | RSS 2.0 feed (published + scheduled-due posts) |
|
|
64
|
+
| GET | `{base}/page/:n` | Paginated post archive |
|
|
65
|
+
| GET | `{base}/category/:slug` | Posts filtered by category slug |
|
|
66
|
+
| GET | `{base}/tag/:tag` | Posts filtered by tag |
|
|
67
|
+
| GET | `{base}/author/:id` | Posts filtered by author user ID |
|
|
68
|
+
| GET | `{base}` | Blog index (page 1) |
|
|
69
|
+
| GET | `{base}/:slug` | Single post page - registered LAST |
|
|
70
|
+
|
|
71
|
+
## Data Model
|
|
72
|
+
|
|
73
|
+
### posts
|
|
74
|
+
| Field | Type | Notes |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `title` | string | Required |
|
|
77
|
+
| `slug` | string | URL slug; auto-derived from title if omitted |
|
|
78
|
+
| `content` | string | Raw Markdown or HTML body |
|
|
79
|
+
| `excerpt` | string | Short summary |
|
|
80
|
+
| `featuredImage` | string\|null | Media path or URL |
|
|
81
|
+
| `categories` | string[] | Array of category entry IDs |
|
|
82
|
+
| `tags` | string[] | Array of tag strings |
|
|
83
|
+
| `status` | string | `draft` \| `published` \| `scheduled` |
|
|
84
|
+
| `authorId` | string | User UUID of the author |
|
|
85
|
+
| `publishedAt` | ISO string | Set on publish or schedule |
|
|
86
|
+
| `seo.title` | string | SEO title override |
|
|
87
|
+
| `seo.description` | string | SEO description override |
|
|
88
|
+
| `template` | string | Post layout block (`blog-post-*`); `''` = the site default |
|
|
89
|
+
| `sample` | boolean | Added by "Add sample posts"; removed by "Remove" |
|
|
90
|
+
|
|
91
|
+
### categories
|
|
92
|
+
| Field | Type | Notes |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `name` | string | Display name |
|
|
95
|
+
| `slug` | string | URL slug |
|
|
96
|
+
| `description` | string | Optional |
|
|
97
|
+
|
|
98
|
+
### comments
|
|
99
|
+
| Field | Type | Notes |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `postId` | string | ID of the parent post entry |
|
|
102
|
+
| `authorName` | string | Commenter display name |
|
|
103
|
+
| `authorEmail` | string | Commenter email (not publicly displayed) |
|
|
104
|
+
| `authorUrl` | string | Optional website URL |
|
|
105
|
+
| `body` | string | Comment text |
|
|
106
|
+
| `status` | string | `pending` \| `approved` \| `rejected` \| `spam` |
|
|
107
|
+
| `createdAt` | ISO string | Set at submission time |
|
|
108
|
+
|
|
109
|
+
## Config Keys (`config.js`)
|
|
110
|
+
|
|
111
|
+
| Key | Default | Purpose |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `basePath` | `'/blog'` | URL prefix for all public blog routes |
|
|
114
|
+
| `postsPerPage` | `10` | Archive pagination page size |
|
|
115
|
+
| `commentsEnabled` | `false` | Show comment list and form on post pages |
|
|
116
|
+
| `commentModeration` | `true` | New comments start as `pending`; `false` = auto-approve |
|
|
117
|
+
| `feedLength` | `20` | Maximum items in RSS feed |
|
|
118
|
+
| `postLayout` | `'blog-post-classic'` | Default post layout (read live) |
|
|
119
|
+
| `listDisplay` | `'cards'` | `cards` \| `list` \| `magazine` \| `compact` \| a `blog-card-*` block (read live) |
|
|
120
|
+
| `displaySwitcher` | `true` | Readers can switch display above listings |
|
|
121
|
+
| `interactive` | `true` | Listings search/filter/sort/right-click in the browser (≤300 posts) |
|
|
122
|
+
| `relatedCount` | `3` | Related posts under a post; 0 = none |
|
|
123
|
+
| `storage.adapter` | `'file'` | Storage adapter; `'file'` works on free installs |
|
|
124
|
+
|
|
125
|
+
## Roles
|
|
126
|
+
|
|
127
|
+
Two custom roles are seeded for this plugin:
|
|
128
|
+
|
|
129
|
+
- **`blog-author`** - can create posts and edit their own; cannot publish, schedule, or delete
|
|
130
|
+
- **`blog-editor`** - can do everything including publish, schedule, delete, and manage categories/comments
|
|
131
|
+
|
|
132
|
+
Both roles are at **level 2** in the role hierarchy (same as `editor`). The publish gate is therefore a **named-role check**, not a level comparison.
|
|
133
|
+
|
|
134
|
+
## Embed Shortcodes
|
|
135
|
+
|
|
136
|
+
Registered via `options.hooks.registerShortcode` so users can drop blog content into any page:
|
|
137
|
+
|
|
138
|
+
| Shortcode | Attributes | Purpose |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `[blog-list count="5" category="news" /]` | `count`, `limit`, `category` (slug) | Vertical list of recent posts (title + date) |
|
|
141
|
+
| `[blog-grid count="6" cols="3" category="..." /]` | `count`, `limit`, `cols` (1-4), `category` | Card grid with featured images |
|
|
142
|
+
| `[blog-featured slug="..." /]` | `slug` (omit for newest) | Single hero card for one post |
|
|
143
|
+
| `[blog-categories show-counts="true" /]` | `show-counts` | Linked category list with optional post counts |
|
|
144
|
+
|
|
145
|
+
Shortcode handlers are async (markdown.js was upgraded to await them). They reuse `loadPosts()` / `loadCategories()` helpers that always return arrays regardless of adapter shape.
|
|
146
|
+
|
|
147
|
+
## Layouts are blocks
|
|
148
|
+
|
|
149
|
+
A post page and a listing card are BLOCKS. `blog-post-<name>` is a post layout, `blog-card-<name>` a
|
|
150
|
+
card; any block with those prefixes is offered automatically (`lib/layouts.js`). The shipped ones are
|
|
151
|
+
copied into content/blocks/ on start, once each (remembered as `seededBlocks` in config/plugins.json),
|
|
152
|
+
so a site edits them in the Blocks tool; a deleted one falls back to `blocks/` here. Placeholders are
|
|
153
|
+
documented in each block's header comment: `{{x}}` escaped, `{{x:D MMM YYYY}}` a date, `{{{x}}}` raw
|
|
154
|
+
for fragments this plugin builds and escapes (content, image, categories, tags, toc, share, prevNext,
|
|
155
|
+
related, comments). Block CSS is scoped under `[data-block="name"]` like every other block.
|
|
156
|
+
|
|
157
|
+
The listing works without JS; `public/blog.js` then reads each card's `data-post` (base64 JSON of
|
|
158
|
+
plain values - never markup) into domma-reactive observables. Display switching fetches the same URL
|
|
159
|
+
with `?display=` and swaps the cards, since each display is different block markup.
|
|
160
|
+
|
|
161
|
+
## Gotchas
|
|
162
|
+
|
|
163
|
+
- **Public handlers must RETURN the reply.** `reply.type(...).send(html)` without `return` in an
|
|
164
|
+
async handler loses the body under @fastify/compress - every browser got a 200 with nothing in it,
|
|
165
|
+
while curl (no Accept-Encoding) saw the page. `tests/public.test.js` drives gzip and brotli.
|
|
166
|
+
|
|
167
|
+
- **The theme styles every `.page-body a` with `!important`** (public/css/site.css, specificity
|
|
168
|
+
~(0,8,4)). The blog's interface links opt out by id (`#dm-blog` on the templates' `<main>`); links
|
|
169
|
+
inside `.blog-content` keep the theme's look. Theme inputs style `input:not([class])`, so the
|
|
170
|
+
listing's search box carries a class.
|
|
171
|
+
|
|
172
|
+
- **The theme wrapper has `overflow: hidden`**, which disables `position: sticky` below it. The
|
|
173
|
+
Sidebar layout's contents list is kept in view by `keepInView()` in public/blog.js instead.
|
|
174
|
+
|
|
175
|
+
- **Status is not a field a PUT can set.** `postFields()` whitelists what a body may change; only
|
|
176
|
+
`draft` is accepted from anyone. Publishing goes through `/publish` and `/schedule`, which check the
|
|
177
|
+
role. It used to merge the body wholesale, so a blog-author could publish with `{"status": "published"}`.
|
|
178
|
+
|
|
179
|
+
- **Admin styling is Domma's, not Bootstrap's**: `form-input` / `form-textarea`
|
|
180
|
+
(not `form-control`), `flex` / `items-center` / `justify-between` (not
|
|
181
|
+
`d-flex` / `align-items-center` / `justify-content-between`), `text-sm`,
|
|
182
|
+
`font-mono`, `font-bold`, `form-input-lg`, `btn-outline`. The Bootstrap
|
|
183
|
+
spellings resolve to nothing in this admin, which left every input in the
|
|
184
|
+
plugin unstyled. KPI tiles use the admin's own `.stat-card` / `.stat-icon` /
|
|
185
|
+
`.stat-value` / `.stat-label`, and everything else lives in
|
|
186
|
+
`admin/css/index.css` - the templates carry no inline styles.
|
|
187
|
+
|
|
188
|
+
- **`GET /posts` sends a BARE ARRAY**, so `postsRes.data` is undefined. The list
|
|
189
|
+
view read exactly that and showed an empty table with four zero KPIs while the
|
|
190
|
+
API was returning posts perfectly well. Read it as
|
|
191
|
+
`res.data ?? (Array.isArray(res) ? res : [])`, the way `categories.js` and
|
|
192
|
+
`comments.js` already do.
|
|
193
|
+
|
|
194
|
+
- **Collection schemas follow the CMS contract**: a collection is named by
|
|
195
|
+
`title` (not `label`) and a field by `name` (not `key`). They were `label` and
|
|
196
|
+
`key`, so `createCollection` threw on all three, `setupPlugin`'s bare
|
|
197
|
+
`catch {}` swallowed it, and a fresh install got three empty directories and
|
|
198
|
+
`Collection "blog-posts" not found` from every route. Existing sites were
|
|
199
|
+
unaffected only because the collections were already on disk.
|
|
200
|
+
|
|
201
|
+
- **The post editor reads its id off the HASH**: Domma's router hands `onMount`
|
|
202
|
+
one argument and never the params, and `R.current` is a method, so
|
|
203
|
+
`R.current?.params?.id` was always undefined - the editor opened blank on
|
|
204
|
+
every edit and Save created a duplicate.
|
|
205
|
+
|
|
206
|
+
- **`/plugins/blog` is a real route**, mapped to the post list. The sidebar's
|
|
207
|
+
parent item points at it and `post-editor.js` navigates to it on a failed
|
|
208
|
+
load; without the route both went nowhere.
|
|
209
|
+
|
|
210
|
+
- **Settings**: `getConfig`/`saveConfig` in `server/config.js` take a FILE NAME.
|
|
211
|
+
These routes called `getConfig()` with no argument (reading
|
|
212
|
+
`config/undefined.json`, so GET answered 500 every time) and
|
|
213
|
+
`saveConfig(cfg)`. Plugin settings belong in `config/plugins.json` via
|
|
214
|
+
`getPluginStates()` / `savePluginState('blog', {settings})`. The PUT also
|
|
215
|
+
guarded with `requireAdmin` alone, which reads `request.user` and therefore
|
|
216
|
+
401s everyone - it needs `[authenticate, requireAdmin]`.
|
|
217
|
+
|
|
218
|
+
- **`POST /posts` answers 400 on an empty title.** The schema marks title, slug,
|
|
219
|
+
authorId and status required, so without the guard an empty title reached
|
|
220
|
+
`createEntry` and came back 500 where the categories and comments routes both
|
|
221
|
+
answer 400.
|
|
222
|
+
|
|
223
|
+
- **Named-role publish gate**: `canPublish()` uses `['blog-editor', 'admin', 'super-admin'].includes(user.role)` - NOT a level comparison. Do not refactor this to `getRoleLevel() <= N`; `blog-author` and `blog-editor` are both level 2, so a level check would give blog-authors publish access.
|
|
224
|
+
|
|
225
|
+
- **Route registration order in plugin.public.js**: Static path segments (`/feed.xml`, `/page/:n`, `/category/:slug`, `/tag/:tag`, `/author/:id`, base index) are all registered **before** `/:slug`. Fastify matches routes in registration order for same-prefix paths, so `/:slug` must be last to avoid swallowing archive routes.
|
|
226
|
+
|
|
227
|
+
- **Lazy scheduled post check**: `plugin.public.js` calls `isVisible(post)` on every `GET /:slug` request. A post transitions from `scheduled` to visible the moment `new Date(post.publishedAt) <= new Date()` - no background job needed, no cron required.
|
|
228
|
+
|
|
229
|
+
- **POST /comments has no authenticate preHandler**: It is intentionally a public route so anonymous visitors can submit comments. The honeypot field (`website`) silently discards bot submissions. Do not add `authenticate` to this route.
|
|
230
|
+
|
|
231
|
+
- **Comments disabled by default**: `commentsEnabled: false` in `config.js`. The comment list and form are only rendered in `plugin.public.js` when `settings.commentsEnabled` is truthy. Enable in plugin settings via the admin panel.
|
|
232
|
+
|
|
233
|
+
- **plugin.public.js registered at Fastify root**: This file is registered without the `/api/plugins/blog` prefix. Routes use the full absolute path starting from `base` (default `/blog`). The server registers it separately from `plugin.js`.
|
|
234
|
+
|
|
235
|
+
- **`storage.adapter: 'file'` default**: Unlike the messages plugin (which defaults to `mongodb`), blog defaults to `file` so it works on free installs without MongoDB. Do not change this default.
|
|
236
|
+
|
|
237
|
+
- **`listEntries` called with bare slug strings**: In `plugin.public.js`, `listEntries('posts')`, `listEntries('categories')`, and `listEntries('comments')` are called with plain string slugs (not constants). This is consistent with the rest of the public file but differs from `plugin.js`, which uses `POSTS_SLUG` / `CATEGORIES_SLUG` / `COMMENTS_SLUG` constants.
|
|
@@ -1 +1,178 @@
|
|
|
1
|
-
|
|
1
|
+
/* Blog plugin - admin styling.
|
|
2
|
+
The templates carry no inline styles and no Bootstrap class names; Domma's
|
|
3
|
+
spellings are form-input / form-textarea, flex, items-center, justify-between,
|
|
4
|
+
text-sm and font-mono. */
|
|
5
|
+
|
|
6
|
+
.blog-hidden { display: none !important; }
|
|
7
|
+
.blog-clickable { cursor: pointer; }
|
|
8
|
+
.blog-nowrap { white-space: nowrap; }
|
|
9
|
+
|
|
10
|
+
/* Post editor: body on the left, status / categories / tags rail on the right. */
|
|
11
|
+
.blog-editor { align-items: flex-start; }
|
|
12
|
+
.blog-editor-main { flex: 1; min-width: 0; }
|
|
13
|
+
.blog-editor-side { width: 280px; flex-shrink: 0; }
|
|
14
|
+
|
|
15
|
+
@media (max-width: 900px) {
|
|
16
|
+
.blog-editor { flex-wrap: wrap; }
|
|
17
|
+
.blog-editor-side { width: 100%; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Media picker (the Featured Image slideover) */
|
|
21
|
+
.blog-media-grid {
|
|
22
|
+
display: grid;
|
|
23
|
+
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
24
|
+
gap: 0.75rem;
|
|
25
|
+
padding: 0.5rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.blog-media-tile {
|
|
29
|
+
border: 1px solid var(--dm-border, #333);
|
|
30
|
+
background: transparent;
|
|
31
|
+
border-radius: 0.25rem;
|
|
32
|
+
padding: 0.25rem;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 0.25rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.blog-media-tile img {
|
|
41
|
+
width: 100%;
|
|
42
|
+
height: 90px;
|
|
43
|
+
object-fit: cover;
|
|
44
|
+
border-radius: 0.15rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.blog-media-tile span {
|
|
48
|
+
font-size: var(--dm-text-xs);
|
|
49
|
+
text-overflow: ellipsis;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.blog-image-preview {
|
|
56
|
+
max-width: 100%;
|
|
57
|
+
border-radius: 0.25rem;
|
|
58
|
+
display: block;
|
|
59
|
+
margin-bottom: 0.5rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ---- The list screens (Posts, Categories, Comments) ----
|
|
63
|
+
One compact bar of filters, then rows. Every row action lives on its
|
|
64
|
+
context menu, reached by right-click, Shift+F10 or the ⋮. */
|
|
65
|
+
.blog-screen { display: flex; flex-direction: column; gap: .75rem; }
|
|
66
|
+
.blog-bar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
|
67
|
+
.blog-bar-end { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
|
|
68
|
+
.blog-search { width: 240px; max-width: 100%; height: 2rem; }
|
|
69
|
+
.blog-select { width: auto; height: 2rem; padding-top: 0; padding-bottom: 0; }
|
|
70
|
+
.blog-count { font-size: var(--dm-text-sm); color: var(--dm-text-muted); }
|
|
71
|
+
.blog-state { margin: 1rem 0; text-align: center; color: var(--dm-text-muted); }
|
|
72
|
+
.blog-note { margin: 0; display: flex; align-items: center; gap: .5rem; font-size: var(--dm-text-sm);
|
|
73
|
+
padding: .5rem .75rem; border-radius: var(--dm-radius-md);
|
|
74
|
+
background: color-mix(in srgb, var(--dm-warning, #d97706) 12%, transparent); }
|
|
75
|
+
.blog-note--quiet { background: none; padding: 0; color: var(--dm-text-muted); }
|
|
76
|
+
|
|
77
|
+
/* Status chips: a count and a filter in one. */
|
|
78
|
+
.blog-chips { display: inline-flex; flex-wrap: wrap; gap: .25rem; }
|
|
79
|
+
.blog-chip {
|
|
80
|
+
border: 1px solid var(--dm-border); background: none; color: var(--dm-text); cursor: pointer;
|
|
81
|
+
font: inherit; font-size: var(--dm-text-xs); padding: .15rem .6rem; border-radius: 999px;
|
|
82
|
+
}
|
|
83
|
+
.blog-chip b { font-weight: 600; margin-left: .15rem; color: var(--dm-text-muted); }
|
|
84
|
+
.blog-chip:hover { border-color: var(--dm-primary); }
|
|
85
|
+
.blog-chip.is-active { background: var(--dm-primary); border-color: var(--dm-primary); color: var(--dm-text-inverse, #fff); }
|
|
86
|
+
.blog-chip.is-active b { color: inherit; }
|
|
87
|
+
|
|
88
|
+
.blog-rows:not(:empty) { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
|
|
89
|
+
.blog-row {
|
|
90
|
+
display: flex; align-items: center; gap: .75rem; padding: .5rem .5rem .5rem .75rem; cursor: pointer;
|
|
91
|
+
border-bottom: 1px solid var(--dm-border); font-size: var(--dm-text-sm);
|
|
92
|
+
}
|
|
93
|
+
.blog-row:last-child { border-bottom: 0; }
|
|
94
|
+
.blog-row:hover, .blog-row:focus-visible { background: var(--dm-surface-hover, rgba(127,127,127,.08)); outline: none; }
|
|
95
|
+
.blog-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
96
|
+
.blog-row-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
97
|
+
.blog-row-sub { color: var(--dm-text-muted); font-size: var(--dm-text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
98
|
+
.blog-row-sub:empty { display: none; }
|
|
99
|
+
.blog-row-sub-inline { font-weight: 400; color: var(--dm-text-muted); margin-left: .35rem; }
|
|
100
|
+
.blog-row-cats { flex: 0 1 12rem; min-width: 0; font-size: var(--dm-text-xs); color: var(--dm-text-muted);
|
|
101
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
102
|
+
.blog-row-slug { flex: 0 1 12rem; font-size: var(--dm-text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
103
|
+
.blog-row-count { flex: 0 0 5rem; text-align: right; font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
104
|
+
.blog-row-date { flex: 0 0 6.5rem; text-align: right; font-size: var(--dm-text-xs); color: var(--dm-text-muted); white-space: nowrap; }
|
|
105
|
+
.blog-kebab {
|
|
106
|
+
flex: 0 0 auto; border: 0; background: none; cursor: pointer; padding: 0 .3rem;
|
|
107
|
+
font-size: var(--dm-text-lg); line-height: 1.2; color: var(--dm-text-muted); border-radius: var(--dm-radius-sm);
|
|
108
|
+
}
|
|
109
|
+
.blog-kebab:hover { color: var(--dm-primary); background: var(--dm-surface-hover, rgba(127,127,127,.15)); }
|
|
110
|
+
|
|
111
|
+
/* A comment reads as one line until clicked open. */
|
|
112
|
+
.blog-comment-text { font-size: var(--dm-text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
113
|
+
.blog-comment.is-open .blog-comment-text { white-space: pre-wrap; overflow-wrap: anywhere; padding: .25rem 0; }
|
|
114
|
+
|
|
115
|
+
@media (max-width: 720px) {
|
|
116
|
+
.blog-row-cats, .blog-row-slug, .blog-row-count { display: none; }
|
|
117
|
+
.blog-bar-end { margin-left: 0; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ---- Slideover panels (settings, category editor) ---- */
|
|
121
|
+
.blog-panel { display: flex; flex-direction: column; gap: 1rem; padding: .25rem 0 .5rem; }
|
|
122
|
+
.blog-field { display: flex; flex-direction: column; gap: .3rem; }
|
|
123
|
+
.blog-field .form-label { display: flex; align-items: center; margin: 0; font-weight: 500; }
|
|
124
|
+
.blog-field-row { flex-direction: row; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
125
|
+
.blog-panel-heading { display: flex; align-items: center; margin: .5rem 0 -.25rem; padding-top: .9rem; border-top: 1px solid var(--dm-border);
|
|
126
|
+
font-size: var(--dm-text-xs); font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--dm-text-muted); }
|
|
127
|
+
.blog-panel-link { align-self: flex-start; }
|
|
128
|
+
.blog-panel-note { margin: 0; font-size: var(--dm-text-xs); color: var(--dm-warning, #d97706); }
|
|
129
|
+
.blog-panel-actions { display: flex; align-items: center; gap: .5rem; padding-top: .75rem; border-top: 1px solid var(--dm-border); }
|
|
130
|
+
.blog-panel-status { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
131
|
+
.blog-spacer { flex: 1; }
|
|
132
|
+
|
|
133
|
+
/* The old settings page renders the same panel in a card. */
|
|
134
|
+
.blog-settings-card { max-width: 600px; }
|
|
135
|
+
|
|
136
|
+
/* ── Post editor: layout picker ─────────────────────────────────────── */
|
|
137
|
+
.blog-card-head { display: flex; align-items: center; gap: .25rem; }
|
|
138
|
+
.blog-preview-btn { margin-left: auto; }
|
|
139
|
+
.blog-layouts { display: grid; grid-template-columns: repeat(auto-fill, minmax(5.6rem, 1fr)); gap: .5rem; }
|
|
140
|
+
.blog-layout { display: flex; flex-direction: column; align-items: center; gap: .35rem; padding: .5rem .35rem .45rem; cursor: pointer;
|
|
141
|
+
border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); background: none; color: var(--dm-text-muted); font: inherit; font-size: var(--dm-text-xs); }
|
|
142
|
+
.blog-layout:hover { border-color: var(--dm-primary); color: var(--dm-text); }
|
|
143
|
+
.blog-layout.is-on { border-color: var(--dm-primary); color: var(--dm-primary);
|
|
144
|
+
background: color-mix(in srgb, var(--dm-primary) 10%, transparent); box-shadow: 0 0 0 1px var(--dm-primary) inset; }
|
|
145
|
+
.blog-layout-name { line-height: 1.2; text-align: center; overflow-wrap: anywhere; }
|
|
146
|
+
/* Schematic thumbnails, drawn as masks so they take the theme's colour. */
|
|
147
|
+
.blog-thumb { width: 3.6rem; height: 2.4rem; background: currentColor; -webkit-mask: var(--thumb) center / contain no-repeat; mask: var(--thumb) center / contain no-repeat; }
|
|
148
|
+
.blog-thumb[data-thumb="classic"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='18'%20y='4'%20width='24'%20height='3'%20rx='1'%20opacity='1'/%3E%3Crect%20x='22'%20y='9'%20width='16'%20height='2'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='12'%20y='13'%20width='36'%20height='11'%20rx='1'%20opacity='0.45'/%3E%3Crect%20x='14'%20y='27'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='30'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='33'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='36'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3C/svg%3E"); }
|
|
149
|
+
.blog-thumb[data-thumb="feature"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='0'%20y='0'%20width='60'%20height='20'%20rx='1'%20opacity='0.45'/%3E%3Crect%20x='6'%20y='11'%20width='30'%20height='3.2'%20rx='1'%20opacity='1'/%3E%3Crect%20x='6'%20y='16'%20width='20'%20height='1.8'%20rx='1'%20opacity='0.8'/%3E%3Crect%20x='14'%20y='24.0'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='27.2'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='30.4'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='33.6'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='36.8'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3C/svg%3E"); }
|
|
150
|
+
.blog-thumb[data-thumb="minimal"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='20'%20y='5'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='9'%20width='22'%20height='3'%20rx='1'%20opacity='1'/%3E%3Crect%20x='20'%20y='16.0'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='19.2'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='22.4'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='25.6'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='28.8'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='32.0'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='35.2'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3C/svg%3E"); }
|
|
151
|
+
.blog-thumb[data-thumb="sidebar"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='4'%20y='4'%20width='30'%20height='3'%20rx='1'%20opacity='1'/%3E%3Crect%20x='4'%20y='10'%20width='34'%20height='9'%20rx='1'%20opacity='0.45'/%3E%3Crect%20x='4'%20y='22.0'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='4'%20y='25.2'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='4'%20y='28.4'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='4'%20y='31.6'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='4'%20y='34.8'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='42'%20y='10'%20width='14'%20height='10'%20rx='1'%20opacity='0.35'/%3E%3Crect%20x='43'%20y='23'%20width='10'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='43'%20y='26'%20width='10'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='43'%20y='29'%20width='10'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='43'%20y='32'%20width='10'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3C/svg%3E"); }
|
|
152
|
+
.blog-thumb[data-thumb="custom"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='6'%20y='5'%20width='48'%20height='30'%20rx='3'%20fill='none'%20stroke='%23000'%20stroke-width='1.6'%20stroke-dasharray='3%202.4'/%3E%3Crect%20x='28.8'%20y='14'%20width='2.4'%20height='12'%20rx='1'%20opacity='1'/%3E%3Crect%20x='24'%20y='18.8'%20width='12'%20height='2.4'%20rx='1'%20opacity='1'/%3E%3C/svg%3E"); }
|
|
153
|
+
.blog-layout-desc { margin: .6rem 0 0; font-size: var(--dm-text-xs); color: var(--dm-text-muted); min-height: 2.2em; }
|
|
154
|
+
|
|
155
|
+
/* ── Post editor: preview slideover ──────────────────────────────────── */
|
|
156
|
+
.blog-preview { display: flex; flex-direction: column; gap: .6rem; height: 100%; min-height: 70vh; }
|
|
157
|
+
.blog-preview-bar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
|
158
|
+
.blog-preview-bar .form-select { width: auto; }
|
|
159
|
+
.blog-preview-state { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
160
|
+
.blog-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
|
|
161
|
+
.blog-seg button { border: 0; background: none; padding: .3rem .55rem; cursor: pointer; color: var(--dm-text-muted); display: inline-flex; }
|
|
162
|
+
.blog-seg button + button { border-left: 1px solid var(--dm-border); }
|
|
163
|
+
.blog-seg button.is-on { background: color-mix(in srgb, var(--dm-primary) 14%, transparent); color: var(--dm-primary); }
|
|
164
|
+
.blog-preview-stage { flex: 1; display: flex; justify-content: center; overflow: auto; padding: .5rem;
|
|
165
|
+
border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); background: repeating-linear-gradient(45deg, transparent 0 8px, rgba(127,127,127,.06) 8px 16px); }
|
|
166
|
+
.blog-preview-stage iframe { width: 100%; height: 100%; min-height: 65vh; border: 0; border-radius: var(--dm-radius-sm); background: #fff;
|
|
167
|
+
box-shadow: 0 4px 18px rgba(0,0,0,.18); transition: width .2s ease; }
|
|
168
|
+
.blog-preview-stage.is-tablet iframe { width: 820px; max-width: 100%; }
|
|
169
|
+
.blog-preview-stage.is-phone iframe { width: 390px; max-width: 100%; }
|
|
170
|
+
|
|
171
|
+
/* ── Post editor header: Back, title, save state, actions ──────────────── */
|
|
172
|
+
.blog-editor-bar { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
|
|
173
|
+
.blog-editor-bar .view-header-actions { margin-left: auto; display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
|
|
174
|
+
.blog-editor-title { font-size: var(--dm-text-sm); font-weight: 600; color: var(--dm-text-muted); }
|
|
175
|
+
.blog-save-state { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
176
|
+
.blog-save-state.is-dirty { color: var(--dm-warning, #d97706); }
|
|
177
|
+
.blog-save-state.is-dirty::before { content: '●'; margin-right: .3rem; font-size: .7em; vertical-align: .1em; }
|
|
178
|
+
.blog-editor-bar .btn { display: inline-flex; align-items: center; gap: .35rem; }
|
|
@@ -1,57 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<!-- No heading: the banner above is the title. Bound with M.applyBindings - see
|
|
2
|
+
admin/views/blog.js, which also inserts the row and chip templates (the router
|
|
3
|
+
renders this file through _.render first, which would eat any {{…}}). -->
|
|
4
|
+
<div class="blog-screen">
|
|
5
|
+
<div class="blog-bar">
|
|
6
|
+
<div class="blog-chips" data-each="chips.value key=key"></div>
|
|
7
|
+
<span data-help="Published posts are live. Drafts are only visible here. Scheduled posts go live on their date. Click a count to show only those."></span>
|
|
8
|
+
<span class="blog-bar-end">
|
|
9
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="create">
|
|
10
|
+
<span data-icon="plus" data-icon-size="14"></span> New post
|
|
11
|
+
</button>
|
|
12
|
+
<button type="button" class="btn btn-sm btn-ghost blog-more" aria-label="More" title="View the blog, sample posts, layouts">⋮</button>
|
|
13
|
+
<span data-help-title="Sample posts and layouts"
|
|
14
|
+
data-help="⋮ adds a set of sample posts - spread over every layout, with categories and pictures - so there is something to look at before you have written anything. Remove them from the same menu; only the samples go. Each post's look is a block you can edit under Blocks (blog-post-… and blog-card-…)."></span>
|
|
15
|
+
</span>
|
|
16
|
+
</div>
|
|
7
17
|
|
|
8
|
-
<div class="
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
<div>
|
|
26
|
-
<div class="stat-value" id="kpi-drafts">-</div>
|
|
27
|
-
<div class="stat-label">Drafts</div>
|
|
28
|
-
</div>
|
|
29
|
-
</div></div>
|
|
30
|
-
<div class="card stat-card"><div class="card-body">
|
|
31
|
-
<div class="stat-icon stat-icon--warning"><span data-icon="clock"></span></div>
|
|
32
|
-
<div>
|
|
33
|
-
<div class="stat-value" id="kpi-scheduled">-</div>
|
|
34
|
-
<div class="stat-label">Scheduled</div>
|
|
35
|
-
</div>
|
|
36
|
-
</div></div>
|
|
37
|
-
</div>
|
|
18
|
+
<div class="blog-bar">
|
|
19
|
+
<input type="search" class="form-input blog-search" placeholder="Search posts…" aria-label="Search posts"
|
|
20
|
+
data-model="query.value">
|
|
21
|
+
<select class="form-select blog-select" aria-label="Category"
|
|
22
|
+
data-options="categoryOptions.value" data-options-text="name" data-options-value="id"
|
|
23
|
+
data-options-caption="'All categories'" data-model="category.value"></select>
|
|
24
|
+
<select class="form-select blog-select" aria-label="Sort" data-model="sort.value">
|
|
25
|
+
<option value="newest">Newest first</option>
|
|
26
|
+
<option value="oldest">Oldest first</option>
|
|
27
|
+
<option value="title">Title, A to Z</option>
|
|
28
|
+
</select>
|
|
29
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="reset" data-if="canReset.value">Clear filters</button>
|
|
30
|
+
<span data-help="Search matches titles, excerpts and slugs. Right-click a post, or use its ⋮, to publish, unpublish, duplicate or delete it."></span>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<p class="blog-state" data-if="loading.value">Loading posts…</p>
|
|
34
|
+
<p class="blog-state" data-if="isEmpty.value" data-bind-text="emptyText.value"></p>
|
|
38
35
|
|
|
39
|
-
<div class="
|
|
40
|
-
|
|
41
|
-
<div class="flex gap-2 flex-wrap items-center">
|
|
42
|
-
<select id="filter-status" class="form-select blog-filter-select">
|
|
43
|
-
<option value="">All statuses</option>
|
|
44
|
-
<option value="draft">Draft</option>
|
|
45
|
-
<option value="scheduled">Scheduled</option>
|
|
46
|
-
<option value="published">Published</option>
|
|
47
|
-
</select>
|
|
48
|
-
<select id="filter-category" class="form-select blog-filter-select">
|
|
49
|
-
<option value="">All categories</option>
|
|
50
|
-
</select>
|
|
51
|
-
<input id="filter-search" type="search" class="form-input blog-filter-search" placeholder="Search title or excerpt…">
|
|
52
|
-
<button id="filter-reset" class="btn btn-ghost btn-sm">Reset</button>
|
|
53
|
-
</div>
|
|
36
|
+
<div class="blog-list">
|
|
37
|
+
<div class="blog-rows" data-each="rows.value key=id"></div>
|
|
54
38
|
</div>
|
|
55
39
|
</div>
|
|
56
|
-
|
|
57
|
-
<div id="posts-table"></div>
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<!-- No heading: the banner above is the title. Bound with M.applyBindings - see
|
|
2
|
+
admin/views/categories.js, which also inserts the row template. -->
|
|
3
|
+
<div class="blog-screen">
|
|
4
|
+
<div class="blog-bar">
|
|
5
|
+
<span class="blog-count" data-bind-text="countText.value"></span>
|
|
6
|
+
<span data-help="Categories group posts on the site, each with its own page at /blog/category/<slug>. Right-click a category, or use its ⋮, to edit it, see its posts or delete it."></span>
|
|
7
|
+
<span class="blog-bar-end">
|
|
8
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="create">
|
|
9
|
+
<span data-icon="plus" data-icon-size="14"></span> New category
|
|
10
|
+
</button>
|
|
11
|
+
</span>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<p class="blog-state" data-if="loading.value">Loading categories…</p>
|
|
15
|
+
<p class="blog-state" data-if="isEmpty.value">No categories yet - New category adds one.</p>
|
|
16
|
+
|
|
17
|
+
<div class="blog-list">
|
|
18
|
+
<div class="blog-rows" data-each="rows.value key=id"></div>
|
|
19
|
+
</div>
|
|
6
20
|
</div>
|
|
7
|
-
<div id="categories-table"></div>
|
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<!-- No heading: the banner above is the title. Bound with M.applyBindings - see
|
|
2
|
+
admin/views/comments.js, which also inserts the row and chip templates. -->
|
|
3
|
+
<div class="blog-screen">
|
|
4
|
+
<div class="blog-bar">
|
|
5
|
+
<div class="blog-chips" data-each="chips.value key=key"></div>
|
|
6
|
+
<span data-help="Pending comments wait for you before they appear on the site; Approved ones are live; Rejected and Spam never show. Right-click a comment, or use its ⋮, to moderate it. Click it to read all of it."></span>
|
|
7
|
+
<span class="blog-bar-end">
|
|
8
|
+
<input type="search" class="form-input blog-search" placeholder="Search comments…" aria-label="Search comments"
|
|
9
|
+
data-model="query.value">
|
|
10
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="approveAll" data-bind-disabled="!pendingCount.value" title="Approve every pending comment">
|
|
11
|
+
<span data-icon="check-circle" data-icon-size="14"></span> Approve all pending
|
|
12
|
+
</button>
|
|
13
|
+
</span>
|
|
14
|
+
</div>
|
|
4
15
|
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
<p class="blog-note" data-if="commentsOff.value">
|
|
17
|
+
Comments are switched off, so readers cannot leave new ones.
|
|
18
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="settings">Blog settings</button>
|
|
19
|
+
</p>
|
|
20
|
+
<p class="blog-note blog-note--quiet" data-if="!commentsOff.value" data-bind-text="moderationText.value"></p>
|
|
10
21
|
|
|
11
|
-
<
|
|
22
|
+
<p class="blog-state" data-if="loading.value">Loading comments…</p>
|
|
23
|
+
<p class="blog-state" data-if="isEmpty.value" data-bind-text="emptyText.value"></p>
|
|
24
|
+
|
|
25
|
+
<div class="blog-list">
|
|
26
|
+
<div class="blog-rows" data-each="rows.value key=id"></div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|