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 +4 -0
- package/docs/cache.md +2 -0
- package/package.json +1 -1
- package/server/routes/docs-public.js +9 -3
- package/server/routes/public.js +3 -4
- package/server/services/docs.js +7 -3
- package/server/services/userRoles.js +15 -0
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
package/server/routes/public.js
CHANGED
|
@@ -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 {
|
|
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${
|
|
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});
|
package/server/services/docs.js
CHANGED
|
@@ -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+
|
|
325
|
-
* 'docs' tag
|
|
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
|