domma-cms 1.6.59 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +4344 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/api.js +1 -1
  6. package/admin/js/app.js +2 -2
  7. package/admin/js/lib/draft-owner.js +1 -0
  8. package/admin/js/lib/effect-defs.js +1 -1
  9. package/admin/js/lib/effects-builder.js +2 -2
  10. package/admin/js/lib/page-editor-reference.js +5 -5
  11. package/admin/js/lib/shortcode-tags.js +1 -1
  12. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  13. package/admin/js/views/user-editor.js +1 -1
  14. package/docs/DommaFeatures.md +3005 -0
  15. package/docs/MultiTenant.md +176 -0
  16. package/docs/api-reference.md +1667 -0
  17. package/docs/cache.md +96 -0
  18. package/docs/configuration.md +1811 -0
  19. package/docs/editions-and-licences.md +190 -0
  20. package/docs/getting-started.md +201 -0
  21. package/docs/guides/form-triggers.md +260 -0
  22. package/docs/guides/mongodb-storage-adapter.md +282 -0
  23. package/docs/guides/multi-page-forms.md +121 -0
  24. package/docs/images/scribe-composer.png +0 -0
  25. package/docs/markdown-shortcodes.md +1738 -0
  26. package/docs/pentest-report-2026-04-03.md +181 -0
  27. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  28. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  29. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  30. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  31. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  32. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  33. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  34. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  35. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  36. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  37. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  38. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  39. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  40. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  41. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  42. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  43. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  44. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  45. package/docs/plugin-development.md +827 -0
  46. package/docs/recruitment-recipe.md +501 -0
  47. package/docs/scaffolding.md +312 -0
  48. package/docs/screenshots/calculator-floating.png +0 -0
  49. package/docs/screenshots/calculator-standard.png +0 -0
  50. package/docs/screenshots/email-settings.png +0 -0
  51. package/docs/screenshots/messages-count.png +0 -0
  52. package/docs/structure/user-types.md +107 -0
  53. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  54. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  55. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  56. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  57. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  58. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  59. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  60. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  61. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  62. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  63. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  64. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  65. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  66. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  67. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  68. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  69. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  70. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  71. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  72. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  73. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  74. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  75. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  76. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  77. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  78. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  79. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  80. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  81. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  82. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  83. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  84. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  85. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  86. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  87. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  88. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  89. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  90. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  91. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  92. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  93. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  94. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  95. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  96. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  97. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  98. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  99. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  100. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  101. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  102. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  103. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  104. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  105. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  106. package/docs/theming.md +320 -0
  107. package/package.json +2 -2
  108. package/plugins/_lib/admin/mail/keyboard.js +49 -0
  109. package/plugins/_lib/admin/mail/reader-view.js +4 -2
  110. package/public/js/celebrations/core/canvas.js +1 -1
  111. package/public/js/celebrations/index.js +1 -1
  112. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  113. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  114. package/public/js/celebrations/message-attrs.mjs +1 -0
  115. package/public/js/celebrations/themes/anniversary.js +1 -0
  116. package/public/js/celebrations/themes/autumn.js +1 -0
  117. package/public/js/celebrations/themes/birthday.js +1 -0
  118. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  119. package/public/js/celebrations/themes/calendar.js +1 -0
  120. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  121. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  122. package/public/js/celebrations/themes/christmas.js +1 -1
  123. package/public/js/celebrations/themes/easter.js +1 -0
  124. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  125. package/public/js/celebrations/themes/garden.js +1 -0
  126. package/public/js/celebrations/themes/graduation.js +1 -0
  127. package/public/js/celebrations/themes/greeting.js +1 -0
  128. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  129. package/public/js/celebrations/themes/money-rain.js +1 -0
  130. package/public/js/celebrations/themes/new-year.js +1 -0
  131. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  132. package/public/js/celebrations/themes/occasions.js +1 -0
  133. package/public/js/celebrations/themes/rain-control.js +1 -0
  134. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  135. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  136. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  137. package/public/js/celebrations/themes/summer-garden.js +1 -0
  138. package/public/js/celebrations/themes/winter.js +1 -0
  139. package/public/js/celebrations/trait-data.mjs +1 -1
  140. package/public/js/effects.js +1 -1
  141. package/scripts/build.js +4 -0
  142. package/server/services/markdown.js +5 -2
  143. package/server/templates/page.html +1 -1
@@ -0,0 +1,300 @@
1
+ # Blog Plugin - Design Spec
2
+
3
+ **Date:** 2026-04-07
4
+ **Status:** Approved
5
+
6
+ ---
7
+
8
+ ## Context
9
+
10
+ Domma CMS ships with a clean plugin architecture where bundled plugins (Todo, Notes, Contacts, Messages) register their collections and roles at enable-time. The Blog plugin extends this pattern in two meaningful ways:
11
+
12
+ 1. It is the first plugin to own **public-facing URLs** - blog posts need proper routes at `/blog/*`, not just admin API endpoints.
13
+ 2. It is the first plugin to exercise the **plugin-registered roles** mechanism (`plugins/<name>/roles/*.json`) in a real-world scenario, with `blog-author` and `blog-editor` as distinct contributor tiers below `admin`.
14
+
15
+ The `posts`, `categories`, and `comments` collections that previously existed as static seeds or demo data are removed from the core. They exist only when the Blog plugin is enabled.
16
+
17
+ ---
18
+
19
+ ## Decisions
20
+
21
+ | Concern | Decision |
22
+ |---|---|
23
+ | Public routing | `plugin.public.js` convention - new optional file, registered at Fastify root before the catch-all |
24
+ | Comments | Built-in but `commentsEnabled: false` by default |
25
+ | Roles | Plugin-registered: `blog-author`, `blog-editor` |
26
+ | Base path | Configurable (`basePath: '/blog'`); restart required to apply change |
27
+ | Publishing workflow | Draft → Scheduled → Published; lazy scheduled-post check on every request |
28
+
29
+ ---
30
+
31
+ ## Platform Extension: `plugin.public.js`
32
+
33
+ ### Problem
34
+
35
+ `plugin.js` is always registered under `/api/plugins/<name>`. There is no mechanism for a plugin to own a public URL path such as `/blog/*`.
36
+
37
+ ### Solution
38
+
39
+ Introduce `plugin.public.js` as an optional convention alongside `plugin.js`:
40
+
41
+ - `server/services/plugins.js` `setupPlugin` detects `plugin.public.js` and stores its resolved path in the plugin registry entry (`publicEntry`).
42
+ - `server/server.js` iterates enabled plugins after loading them and calls `fastify.register(publicPlugin, { prefix: settings.basePath ?? '/blog', ...opts })` for each that has a `publicEntry`. `settings` is resolved at registration time via `getPluginSettings(name)` (the same function used in `plugin.js` registration) so the value is always current at startup. This registration happens **before** `routes/public.js` is registered, guaranteeing blog routes take precedence over the catch-all.
43
+ - `plugin.public.js` exports a default Fastify plugin function. It receives `{ settings, auth, config }` in `options`, identical to `plugin.js`.
44
+ - `teardownPlugin` marks the public entry as removed; a server restart is required for route changes to take effect (Fastify does not support live route deregistration).
45
+
46
+ ### Benefit
47
+
48
+ Any future plugin that needs public URL ownership - Jobs board, Events, Portfolio, Shop - drops a `plugin.public.js` and gets it for free. No further platform changes needed.
49
+
50
+ ---
51
+
52
+ ## Data Model
53
+
54
+ ### Collection: `posts`
55
+
56
+ Registered via `plugins/blog/collections/posts/schema.json`. Managed through `server/services/collections.js`.
57
+
58
+ | Field | Type | Required | Notes |
59
+ |---|---|---|---|
60
+ | `title` | text | yes | |
61
+ | `slug` | text | yes | URL-safe, unique; auto-derived from title on create; editable |
62
+ | `excerpt` | textarea | no | Falls back to first 160 chars of content for meta description |
63
+ | `content` | markdown | no | Full body; CMS shortcode pipeline applied on render |
64
+ | `featuredImage` | text | no | Media ID from `/api/media` |
65
+ | `categories` | array | no | Array of category IDs |
66
+ | `tags` | array | no | Free-form strings |
67
+ | `authorId` | text | yes | User ID of post author |
68
+ | `status` | select | yes | `draft` \| `scheduled` \| `published` |
69
+ | `publishedAt` | datetime | no | Required when `status: scheduled`; set to now on manual publish |
70
+ | `seo.title` | text | no | Overrides site title pattern in `<title>` |
71
+ | `seo.description` | text | no | Overrides excerpt for `<meta name="description">` |
72
+
73
+ ### Collection: `categories`
74
+
75
+ | Field | Type | Notes |
76
+ |---|---|---|
77
+ | `name` | text | Required |
78
+ | `slug` | text | Required, URL-safe |
79
+ | `description` | textarea | |
80
+ | `parentId` | text | Optional; enables sub-categories |
81
+
82
+ ### Collection: `comments`
83
+
84
+ | Field | Type | Notes |
85
+ |---|---|---|
86
+ | `postId` | text | Required; FK to posts entry |
87
+ | `parentId` | text | Optional; FK to parent comment (threaded) |
88
+ | `authorName` | text | Required |
89
+ | `authorEmail` | text | Required |
90
+ | `authorUrl` | text | Optional |
91
+ | `body` | textarea | Required |
92
+ | `status` | select | `pending` \| `approved` \| `rejected` \| `spam` |
93
+ | `createdAt` | datetime | Set server-side on create |
94
+
95
+ ---
96
+
97
+ ## Plugin Roles
98
+
99
+ Registered via `plugins/blog/roles/`:
100
+
101
+ ### `blog-author`
102
+ ```json
103
+ {
104
+ "name": "blog-author",
105
+ "label": "Blog Author",
106
+ "level": 2,
107
+ "permissions": ["blog-posts"],
108
+ "badgeClass": "badge-info",
109
+ "resources": [
110
+ { "key": "blog-posts", "label": "Blog Posts", "actions": ["read", "create", "update"] }
111
+ ]
112
+ }
113
+ ```
114
+ Authors can create and edit their own draft posts. **They cannot publish.** Status changes to `published` or `scheduled` are blocked server-side for anyone whose role is not `blog-editor`, `admin`, or `super-admin`. The gate uses a **named-role check** (`['blog-editor', 'admin', 'super-admin'].includes(request.user.role)`), not a level comparison - both `blog-author` and `blog-editor` are level 2, so a level-based check would incorrectly grant authors publish access. An author attempting to call `POST /posts/:id/publish` or `POST /posts/:id/schedule` receives 403 regardless of whether the post is their own.
115
+
116
+ ### `blog-editor`
117
+ ```json
118
+ {
119
+ "name": "blog-editor",
120
+ "label": "Blog Editor",
121
+ "level": 2,
122
+ "permissions": ["blog-posts", "blog-categories", "blog-comments"],
123
+ "badgeClass": "badge-success",
124
+ "resources": [
125
+ { "key": "blog-posts", "label": "Blog Posts", "actions": ["read", "create", "update", "delete", "publish"] },
126
+ { "key": "blog-categories", "label": "Blog Categories", "actions": ["read", "create", "update", "delete"] },
127
+ { "key": "blog-comments", "label": "Blog Comments", "actions": ["read", "update", "delete"] }
128
+ ]
129
+ }
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Public Routes (`plugin.public.js`)
135
+
136
+ All routes registered at the Fastify root under `basePath` (default `/blog`). Static routes (`/feed.xml`, `/page/:n`, `/category/:slug`, `/tag/:tag`, `/author/:id`) **must be registered before** `/:slug` - Fastify prefers static over parametric routes only when they are registered first for the same prefix depth.
137
+
138
+ | Method | Path | Purpose |
139
+ |---|---|---|
140
+ | GET | `/` | Blog index - paginated published posts |
141
+ | GET | `/feed.xml` | RSS 2.0 feed (registered before `/:slug`) |
142
+ | GET | `/page/:n` | Pagination |
143
+ | GET | `/category/:slug` | Category archive |
144
+ | GET | `/tag/:tag` | Tag archive |
145
+ | GET | `/author/:id` | Author archive |
146
+ | GET | `/:slug` | Individual post - lazy scheduled check (`publishedAt <= now`) - registered last |
147
+
148
+ **Scheduled post visibility**: Every `GET /:slug` request checks `status === 'published' || (status === 'scheduled' && new Date(publishedAt) <= new Date())`. No background scheduler - lazy evaluation on each request. A post whose scheduled time arrives between requests becomes visible automatically on the next hit.
149
+
150
+ **Rendering**: Public blog pages use `server/templates/page.html` (the same shell as CMS pages). The plugin extends `renderer.js` with `renderBlogPage(template, data, postMeta)` which injects post-specific SEO: `<title>`, `<meta name="description">`, Open Graph `og:title`, `og:description`, `og:image` (featured image URL). Plugin-owned HTML templates live in `plugins/blog/templates/` - `index.html`, `post.html`, `category.html`, `tag.html`, `author.html`. These render the body content that slots into the page shell.
151
+
152
+ **Comment form**: When `commentsEnabled: true`, `post.html` includes a comment submission form that POSTs to `/api/plugins/blog/comments`. When `commentModeration: true` (default), new comments land in `status: pending` and are not shown until approved.
153
+
154
+ ---
155
+
156
+ ## API Routes (`plugin.js`)
157
+
158
+ All under `/api/plugins/blog`. All routes require `authenticate` **except** `POST /comments`, which is registered separately without the global authenticate hook to allow anonymous comment submission from public visitors.
159
+
160
+ | Method | Path | Auth | Purpose |
161
+ |---|---|---|---|
162
+ | GET | `/posts` | any | List posts (authors see own; editors/admins see all) |
163
+ | POST | `/posts` | author+ | Create post |
164
+ | GET | `/posts/:id` | any | Get post |
165
+ | PUT | `/posts/:id` | author (own) / editor+ | Update post |
166
+ | DELETE | `/posts/:id` | editor+ | Delete post |
167
+ | POST | `/posts/:id/publish` | editor+ | Set status:published, publishedAt:now |
168
+ | POST | `/posts/:id/schedule` | editor+ | Set status:scheduled + publishedAt |
169
+ | GET | `/categories` | any | List categories |
170
+ | POST | `/categories` | editor+ | Create category |
171
+ | PUT | `/categories/:id` | editor+ | Update category |
172
+ | DELETE | `/categories/:id` | editor+ | Delete category |
173
+ | GET | `/comments` | editor+ | List comments (filterable by status, postId) |
174
+ | POST | `/comments` | public (no auth) | Submit comment (rate-limited, honeypot field) |
175
+ | PUT | `/comments/:id` | editor+ | Moderate (approve/reject/spam) |
176
+ | DELETE | `/comments/:id` | editor+ | Delete comment |
177
+ | GET | `/settings` | admin+ | Get blog plugin settings |
178
+ | PUT | `/settings` | admin+ | Save blog plugin settings to config/plugins.json |
179
+
180
+ ---
181
+
182
+ ## Admin UI
183
+
184
+ Single "Blog" sidebar entry, with sub-navigation to four views:
185
+
186
+ ### Posts list (`#/plugins/blog/posts`)
187
+ - `T.create()` table: title, status badge, author, categories, publishedAt, edit/delete actions
188
+ - Filter bar: status select, category select, author select, search input
189
+ - "New Post" → navigates to editor with blank state
190
+
191
+ ### Post editor (`#/plugins/blog/posts/new` and `#/plugins/blog/posts/:id`)
192
+ - Two-column layout: main area (title input, slug input with auto-derive toggle, content textarea), sidebar (status/publishedAt picker, categories checklist, tags input, featured image picker via media modal, SEO accordion)
193
+ - Status transitions: Save Draft → always available; Publish Now → sets status:published; Schedule → reveals datetime picker, sets status:scheduled
194
+ - Slug derives from title on first edit; once manually touched it becomes independent with a "⚠ Changing slug breaks existing links" warning on published posts
195
+
196
+ ### Categories (`#/plugins/blog/categories`)
197
+ - Inline-editable table: name, slug, description, parent, post count
198
+ - Create/edit/delete - no separate modal needed for simple fields
199
+
200
+ ### Comments (`#/plugins/blog/comments`)
201
+ - Visible only when `commentsEnabled: true`
202
+ - Three tabs: Pending | Approved | Spam
203
+ - Per-row actions: Approve, Reject, Spam, Delete
204
+ - Post title shown as a link to the admin post editor
205
+
206
+ ### Blog settings (`#/plugins/blog/settings`)
207
+ - `F.create()` form: basePath, postsPerPage, commentsEnabled toggle, commentModeration toggle, feedLength
208
+ - Save POSTs to `PUT /api/plugins/blog/settings`
209
+ - Banner: "Base path and posts-per-page changes require a server restart"
210
+
211
+ ---
212
+
213
+ ## `config.js` Defaults
214
+
215
+ ```js
216
+ export default {
217
+ basePath: '/blog',
218
+ postsPerPage: 10,
219
+ commentsEnabled: false,
220
+ commentModeration: true,
221
+ feedLength: 20,
222
+ storage: { adapter: 'file' }
223
+ };
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Removing Innate Collections
229
+
230
+ `posts`, `categories`, and `comments` were previously demo-seeded or referenced as preset collections. As of the role-collapse cleanup they no longer exist in `content/collections/` or `server/services/presetCollections.js`. This spec confirms they belong exclusively to the Blog plugin. No further removal is needed - the audit step in the build sequence should grep for any lingering hardcoded references to these slugs in non-blog code.
231
+
232
+ ---
233
+
234
+ ## Plugin Layout
235
+
236
+ ```
237
+ plugins/blog/
238
+ plugin.json
239
+ plugin.js API routes
240
+ plugin.public.js Public SSR routes
241
+ config.js
242
+ CLAUDE.md
243
+ roles/
244
+ blog-author.json
245
+ blog-editor.json
246
+ collections/
247
+ posts/schema.json
248
+ categories/schema.json
249
+ comments/schema.json
250
+ templates/ Public SSR HTML (slots into page.html shell)
251
+ index.html
252
+ post.html
253
+ category.html
254
+ tag.html
255
+ author.html
256
+ admin/
257
+ views/
258
+ blog.js Entry view (posts list)
259
+ post-editor.js Post editor
260
+ categories.js
261
+ comments.js
262
+ settings.js
263
+ templates/
264
+ blog.html
265
+ post-editor.html
266
+ categories.html
267
+ comments.html
268
+ settings.html
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Build Sequence
274
+
275
+ 1. **Platform extension** - extend `plugins.js` to detect and store `publicEntry`; extend `server.js` to register public plugins before `routes/public.js`
276
+ 2. **renderer.js** - add `renderBlogPage()` helper
277
+ 3. **Collection schemas** - write the three `schema.json` files
278
+ 4. **Role files** - write `blog-author.json` and `blog-editor.json`
279
+ 5. **`plugin.js`** - API routes (posts CRUD + publish/schedule, categories, comments, settings)
280
+ 6. **`plugin.public.js`** - public SSR routes + RSS feed
281
+ 7. **Public templates** - `index.html`, `post.html`, `category.html`, `tag.html`, `author.html`
282
+ 8. **Admin views** - posts list, post editor, categories, comments, settings
283
+ 9. **`plugin.json` + `config.js` + `CLAUDE.md`**
284
+ 10. **`config/plugins.json`** - add blog entry (disabled by default; enable to test)
285
+ 11. **Audit** - grep codebase for hardcoded `posts`/`categories`/`comments` collection slug references outside `plugins/blog/`
286
+
287
+ ---
288
+
289
+ ## Verification
290
+
291
+ - **Fresh install**: `npm run fresh` → enable Blog plugin → confirm three collections appear in `content/collections/`, two roles in `content/collections/roles/data.json`, and `/blog` returns a valid (empty) index page
292
+ - **Public routing**: create a published post with slug `hello-world` → `GET /blog/hello-world` returns 200 with correct title in `<title>` tag; a draft post with the same slug returns 404
293
+ - **Scheduled post**: create a post with `status:scheduled`, `publishedAt` 10 seconds in the future → `GET /blog/:slug` returns 404; wait 10 seconds; same URL returns 200
294
+ - **plugin.public.js convention**: disable Blog plugin → restart → `/blog` returns 404 (catch-all serves a CMS 404 page); re-enable → restart → `/blog` returns 200
295
+ - **Role gate**: log in as `blog-author` → can create/edit own draft posts → attempting to publish returns 403; log in as `blog-editor` → publish succeeds
296
+ - **Comments**: enable `commentsEnabled`, submit a comment on a post → lands in Pending tab in admin; approve it → appears below the post on the public page
297
+ - **RSS**: `GET /blog/feed.xml` returns valid XML with `<rss version="2.0">` and correct `<item>` entries for published posts only
298
+ - **Category archive**: assign a post to category `news` → `GET /blog/category/news` lists only posts in that category
299
+ - **SEO injection**: post with custom `seo.title` → `<title>` matches the custom value; post without → title follows the site pattern `{post.title} | {site.title}`
300
+ - **Base path change**: change `basePath` to `/news` in settings → restart → `/news` returns 200, `/blog` falls through to CMS catch-all