domma-cms 1.8.9 → 1.8.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.10] - 2026-10-16
4
+
5
+ - Domma Docs pages now show signed-in visitors their own menus. They were always drawn as for an anonymous visitor and cached as one copy for everybody; they now recognise who is looking, like every other page, and keep one cached copy per set of roles, so a menu item meant for admins is never shown to the public.
6
+
3
7
  ## [1.8.9] - 2026-10-16
4
8
 
5
9
  - **Import from WordPress brings the widgets across.** A WordPress export has no widgets in it, so the import screen now takes a second, optional file: the `.wie` file the free "Widget Importer & Exporter" WordPress plugin exports (or put it in the same zip as the export). Each widget area becomes a block named wp-… (Content › Blocks), its widgets in order under their titles. Text and custom HTML come across cleaned - no scripts, WordPress shortcodes left as text to rebuild - with links and pictures pointing at their new addresses; an image widget brings its picture with the media; a navigation menu shows the menu imported from it; recent posts and categories show the Blog's own lists, and archives a link to the Blog; a search widget becomes a link that opens the site search. Anything else (a calendar, an RSS feed, a plugin's widget) is listed to rebuild - nothing is guessed. A footer widget area is also added to the footer of every page, after whatever is there, unless you switch that off in the preview's new Widgets card; other areas are blocks you can put on a page with `[block]`. Run it again and a block you edited here is left alone unless you choose Overwrite, nothing is added to the footer twice, and an area you took out of the footer is not put back.
package/docs/cache.md CHANGED
@@ -37,6 +37,8 @@ page:<urlPath>:m<fileMtime>:role<roles>:o<origin>
37
37
 
38
38
  For example: `page:/about:m1727430000000:roleanon:ohttps://example.com`.
39
39
 
40
+ Domma Docs pages (`/domma-docs/...`) are cached the same way under `docs:<path>:role<roles>:o<origin>`, because their navbar, footers and overlays filter by role too. Both keys build the role part with `viewerRoleKey()` in `server/services/userRoles.js` - use it for any new cached render whose output depends on the viewer, and never key on the user id.
41
+
40
42
  Admin API responses, login pages and error pages are not cached. Drafts (including draft previews) are never cached, and a page containing a row-level `[view]` is rendered fresh for each signed-in viewer.
41
43
 
42
44
  Cache keys are versioned with a `v1:` prefix. Bumping the version (in `server/services/cache/index.js`) auto-invalidates the entire cache without needing `clear()` - useful if the cached value shape ever changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.8.9",
3
+ "version": "1.8.10",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -17,7 +17,8 @@
17
17
  */
18
18
  import {isProjectEnabled} from '../services/projects.js';
19
19
  import {renderDocPage} from '../services/docs.js';
20
- import {getBaseUrl, render404} from './public.js';
20
+ import {getBaseUrl, render404, sendHtml} from './public.js';
21
+ import {resolveViewer} from '../services/viewerSession.js';
21
22
 
22
23
  const DOCS_PROJECT_SLUG = 'domma-docs';
23
24
 
@@ -30,12 +31,17 @@ export async function docsPublicRoutes(fastify) {
30
31
  }
31
32
 
32
33
  const docPath = String(request.params['*'] || '');
33
- const html = await renderDocPage(docPath, {user: null, baseUrl: getBaseUrl(request)});
34
+ // Resolved like any public page: the navbar, footers and overlays in
35
+ // the docs shell filter by role, and renderDocPage keys its cache on
36
+ // the viewer's role set, so anonymous traffic still shares one entry.
37
+ const user = await resolveViewer(request);
38
+ const html = await renderDocPage(docPath, {user, baseUrl: getBaseUrl(request)});
34
39
  if (html == null) {
35
40
  reply.status(404);
36
41
  return reply.type('text/html; charset=utf-8').send(await render404(`/domma-docs${docPath ? '/' + docPath : ''}`));
37
42
  }
38
- return reply.type('text/html; charset=utf-8').send(html);
43
+ // private + ETag: the response is role-dependent, so no shared proxy may reuse it.
44
+ return sendHtml(request, reply, html);
39
45
  };
40
46
 
41
47
  fastify.get('/domma-docs', handle); // exact - params['*'] === '' (home landing)
@@ -14,7 +14,7 @@ import {buildRobotsTxt, generate as generateSitemap} from '../services/sitemap.j
14
14
  import {resolveViewer} from '../services/viewerSession.js';
15
15
  import {validSessionCookie, newSessionCookie} from '../services/shortcodeExpiry.js';
16
16
  import {touchPreviewLink, verifyPreviewToken} from '../services/previewLinks.js';
17
- import {getEffectiveRoles} from '../services/userRoles.js';
17
+ import {viewerRoleKey} from '../services/userRoles.js';
18
18
  import {hooks} from '../services/hooks.js';
19
19
  import {getConfig} from '../config.js';
20
20
  import * as cache from '../services/cache/index.js';
@@ -73,7 +73,7 @@ function escapeHtml(str) {
73
73
  * keys on role for exactly that reason - and a shared proxy must never hand one
74
74
  * visitor a page rendered for another's permissions.
75
75
  */
76
- function sendHtml(request, reply, html) {
76
+ export function sendHtml(request, reply, html) {
77
77
  const etag = `"${createHash('sha1').update(html).digest('base64')}"`;
78
78
  reply.header('cache-control', 'private, no-cache');
79
79
  reply.header('etag', etag);
@@ -196,7 +196,6 @@ export async function publicRoutes(fastify) {
196
196
  // what renders. Real sites hold a handful of role combinations, so
197
197
  // the entry count stays bounded by what users actually have.
198
198
  const userObj = await resolveViewer(request);
199
- const userRole = userObj ? [...getEffectiveRoles(userObj)].sort().join('+') : null;
200
199
 
201
200
  // The access decision - the directory-index fallback, the project
202
201
  // kill-switch, drafts and visibility - is resolvePublishedPage(), shared
@@ -274,7 +273,7 @@ export async function publicRoutes(fastify) {
274
273
  // script, git pull - yields a fresh entry even if no invalidation
275
274
  // hook fired. Stale-mtime entries age out via TTL/LRU.
276
275
  const pageMtime = await getPageMtime(page.urlPath || urlPath);
277
- const cacheKey = `page:${urlPath}:m${pageMtime}:role${userRole ?? 'anon'}:o${baseUrl}`;
276
+ const cacheKey = `page:${urlPath}:m${pageMtime}:role${viewerRoleKey(userObj)}:o${baseUrl}`;
278
277
  // The footer's blocks (site.json footer.blocks) are on every page: editing one must reach the cached pages.
279
278
  const cacheTags = [`page:${urlPath}`, ...(page.cacheTags || []), 'nav', 'site', ...footerBlockCacheTags(getConfig('site'))];
280
279
  const html = await cache.wrap(cacheKey, render, {tags: cacheTags});
@@ -25,6 +25,7 @@ import {parseMarkdown} from './markdown.js';
25
25
  import {footerBlockCacheTags, renderPage} from './renderer.js';
26
26
  import {getConfig} from '../config.js';
27
27
  import * as cache from './cache/index.js';
28
+ import {viewerRoleKey} from './userRoles.js';
28
29
 
29
30
  // import.meta.url-relative (NOT cwd-relative): the templates live beside the
30
31
  // engine's admin dir in both the npm-package layout and each child site's
@@ -321,8 +322,11 @@ async function buildDocBody(docPath) {
321
322
 
322
323
  /**
323
324
  * Render a Domma Docs page to full HTML, or return null for an unknown path
324
- * (the route turns that into a 404). Cached per path+origin under the static
325
- * 'docs' tag - invalidated on boot so a version bump never serves stale HTML.
325
+ * (the route turns that into a 404). Cached per path + viewer role set + origin
326
+ * under the 'docs' tag. Nothing flushes that tag; a version bump never serves
327
+ * stale HTML only because the in-memory cache starts empty on every restart.
328
+ * The role set is in the key because the shell's menus and footer blocks
329
+ * filter by role, exactly as a normal page's do.
326
330
  *
327
331
  * @param {string} docPath url tail after /domma-docs (e.g. 'usage/pages', '' for home)
328
332
  * @param {{user?: object|null, baseUrl?: string}} [opts]
@@ -335,7 +339,7 @@ export async function renderDocPage(docPath, opts = {}) {
335
339
 
336
340
  const {user = null, baseUrl} = opts;
337
341
  return cache.wrap(
338
- `docs:${path}:o${baseUrl || ''}`,
342
+ `docs:${path}:role${viewerRoleKey(user)}:o${baseUrl || ''}`,
339
343
  async () => {
340
344
  const parsed = await parseMarkdown(built.body, {user});
341
345
  const css = [DOCS_BODY_CSS, ...built.styles].filter(Boolean).join('\n\n');
@@ -39,6 +39,21 @@ export function getEffectiveRoles(user) {
39
39
  return out;
40
40
  }
41
41
 
42
+ /**
43
+ * The viewer's role set as it appears in a response-cache key: every role the
44
+ * user holds (primary plus additional), sorted and joined with `+`, or `anon`
45
+ * for no viewer. Every cached render whose output depends on the viewer (menu
46
+ * visibility, role-gated shortcodes) keys on this, so two viewers share an
47
+ * entry exactly when they hold the same roles - never per user.
48
+ *
49
+ * @param {object|null} user
50
+ * @returns {string} e.g. 'anon', 'admin', 'blog-editor+user'
51
+ */
52
+ export function viewerRoleKey(user) {
53
+ const roles = getEffectiveRoles(user);
54
+ return roles.length ? [...roles].sort().join('+') : 'anon';
55
+ }
56
+
42
57
  /**
43
58
  * Return the effective role level for a user - the LOWEST level number
44
59
  * across all roles. Lower number = higher privilege, so the user's most