domma-cms 0.89.0 → 0.90.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 (41) hide show
  1. package/admin/dist/domma/domma-tools.css +3 -3
  2. package/admin/dist/domma/domma-tools.min.js +3 -3
  3. package/admin/js/app.js +2 -2
  4. package/admin/js/lib/page-editor-arrange.js +1 -1
  5. package/admin/js/lib/page-editor-reference.js +153 -0
  6. package/admin/js/lib/seo-analyse.js +1 -0
  7. package/admin/js/lib/seo-redirects.js +1 -0
  8. package/admin/js/lib/seo-schema.js +2 -0
  9. package/admin/js/lib/seo-settings.js +2 -0
  10. package/admin/js/templates/page-editor.html +119 -80
  11. package/{plugins/seo/admin → admin/js}/templates/seo.html +1 -2
  12. package/admin/js/views/index.js +1 -1
  13. package/admin/js/views/page-editor.js +62 -90
  14. package/admin/js/views/seo.js +144 -0
  15. package/package.json +2 -2
  16. package/plugins/free-tier.lock.json +0 -19
  17. package/server/routes/api/pages.js +10 -13
  18. package/server/routes/api/seo.js +201 -0
  19. package/server/server.js +12 -1
  20. package/server/services/menus.js +43 -0
  21. package/server/services/permissionRegistry.js +11 -0
  22. package/server/services/plugins.js +12 -2
  23. package/{plugins/seo/server → server/services/seo}/audit.js +2 -2
  24. package/server/services/seo/index.js +345 -0
  25. package/{plugins/seo/server → server/services/seo}/registry.js +5 -3
  26. package/server/services/seo/store.js +110 -0
  27. package/server/services/siteGitignore.js +4 -1
  28. package/server/services/tools.js +2 -1
  29. package/admin/js/templates/navigation.html +0 -226
  30. package/admin/js/views/navigation.js +0 -23
  31. package/plugins/seo/CLAUDE.md +0 -48
  32. package/plugins/seo/admin/lib/analyse.js +0 -286
  33. package/plugins/seo/admin/lib/redirects.js +0 -163
  34. package/plugins/seo/admin/lib/schema.js +0 -113
  35. package/plugins/seo/admin/lib/settings.js +0 -141
  36. package/plugins/seo/admin/views/seo.js +0 -912
  37. package/plugins/seo/config.js +0 -7
  38. package/plugins/seo/plugin.js +0 -477
  39. package/plugins/seo/plugin.json +0 -64
  40. package/plugins/seo/server/store.js +0 -76
  41. package/plugins/seo/tests/seo.test.js +0 -192
@@ -1,44 +1,50 @@
1
1
  <!-- Page editor. No page heading: the Tool banner above is the heading ("Pages › Edit …", set by
2
2
  admin/js/views/page-editor.js), with Back, Editor reference, View on the site and Save in it. This bar
3
3
  carries the page switcher and the unsaved state; Ctrl+S saves; leaving with unsaved changes asks and
4
- keeps a draft. -->
4
+ keeps a draft.
5
+ Bound with M.applyBindings (see the view): every field is a data-model, and what shows - unsaved,
6
+ the problem note, the view mode, the CSS pane, Live Preview, History - is read from the state. The
7
+ router renders this file through _.render, which eats any {{…}}: bindings only, never mustache. -->
5
8
  <div class="pex-bar">
6
- <select id="quick-switch" class="form-select form-select-sm" style="display:none; width:auto;" aria-label="Switch to another page">
9
+ <select id="quick-switch" class="form-select form-select-sm" style="width:auto;" aria-label="Switch to another page"
10
+ hidden data-bind-hidden="!hasPages" data-bind-disabled="dirty.value" data-on-change="switchPage">
7
11
  <option value="">Switch page...</option>
8
12
  </select>
9
13
  <span data-help-title="Switching pages" data-help="Opens another page in the editor. It is locked while this page has unsaved changes - save or discard them first."></span>
10
- <span id="pe-unsaved" class="pex-chip" hidden title="Changes not saved yet">Unsaved</span>
11
- <small id="pe-problem" class="pex-problem" hidden></small>
14
+ <span class="pex-chip" hidden data-bind-hidden="!dirty.value" title="Changes not saved yet">Unsaved</span>
15
+ <small class="pex-problem" hidden data-bind-hidden="!(dirty.value && problem.value)" data-bind-text="problem.value"></small>
12
16
  <span class="pex-spacer"></span>
13
- <a id="view-page-btn" class="btn btn-ghost btn-sm" target="_blank" rel="noopener" style="display:none;">
17
+ <a class="btn btn-ghost btn-sm" target="_blank" rel="noopener" hidden data-bind-hidden="!isEdit" data-bind-href="savedPath">
14
18
  <span data-icon="external-link"></span> View page
15
19
  </a>
16
- <button type="button" id="discard-btn" class="btn btn-ghost btn-sm" hidden>Discard</button>
17
- <button type="button" id="save-btn" class="btn btn-primary btn-sm"><span data-icon="save"></span> Save</button>
20
+ <button type="button" id="discard-btn" class="btn btn-ghost btn-sm" hidden data-bind-hidden="!dirty.value" data-on-click="discard">Discard</button>
21
+ <button type="button" id="save-btn" class="btn btn-primary btn-sm" data-on-click="save" data-bind-disabled="saving.value"><span data-icon="save"></span>
22
+ <span data-bind-text="saving.value ? 'Saving…' : 'Save'">Save</span></button>
18
23
  </div>
19
24
 
20
25
  <div class="tabs" id="editor-meta-tabs">
21
26
  <div class="tab-list">
22
- <button class="tab-item active">Edit</button>
23
- <button class="tab-item">Page Details</button>
24
- <button class="tab-item">SEO</button>
25
- <button class="tab-item">DConfig</button>
26
- <button class="tab-item" id="live-preview-tab" style="display:none;">Live Preview</button>
27
- <button class="tab-item" id="history-tab" style="display:none;">History</button>
27
+ <button class="tab-item active" data-on-click="showTab('edit')">Edit</button>
28
+ <button class="tab-item" data-on-click="showTab('details')">Page Details</button>
29
+ <button class="tab-item" data-on-click="showTab('seo')">SEO</button>
30
+ <button class="tab-item" data-on-click="showTab('dconfig')">DConfig</button>
31
+ <button class="tab-item" data-on-click="showTab('live')">Live Preview</button>
32
+ <button class="tab-item" hidden data-bind-hidden="!isEdit" data-on-click="showTab('history')">History</button>
28
33
  </div>
29
34
  <div class="tab-content">
30
35
 
31
36
  <!-- Edit -->
32
37
  <div class="tab-panel active">
33
- <div class="card editor-card">
38
+ <div class="card editor-card" data-bind-class="fullscreen.value && 'editor-fullscreen'">
34
39
  <div class="card-body p-0">
35
40
  <div class="editor-toolbar" id="editor-toolbar"></div>
36
- <div class="editor-body" id="editor-body">
41
+ <div class="editor-body" id="editor-body" data-bind-class="'editor-mode-' + viewMode.value">
37
42
  <div class="editor-pane editor-pane--write">
38
- <textarea id="markdown-editor" class="editor-textarea"
43
+ <!-- The body is not a data-model: the folding manager owns what the textarea shows. -->
44
+ <textarea id="markdown-editor" class="editor-textarea" data-bind-hidden="cssMode.value"
39
45
  placeholder="Write your page content in Markdown..."></textarea>
40
- <textarea id="css-editor" class="editor-textarea" style="display:none;font-family:monospace;"
41
- placeholder="/* Custom CSS… */"></textarea>
46
+ <textarea id="css-editor" class="editor-textarea" style="font-family:monospace;" hidden
47
+ data-bind-hidden="!cssMode.value" data-model="css.value" placeholder="/* Custom CSS… */"></textarea>
42
48
  </div>
43
49
  <div class="editor-divider" id="editor-divider" role="separator" aria-orientation="vertical"
44
50
  tabindex="0" aria-label="Resize editor panes" aria-valuemin="15" aria-valuemax="85"
@@ -61,7 +67,7 @@
61
67
  <div class="row mb-3">
62
68
  <div class="col">
63
69
  <label class="form-label">URL Path <span data-help-title="URL path" data-help="Where the page lives on the site, starting with /. Changing it moves the page; menu links to the old address are updated. A project whose root URL is a prefix of this address files the page under it."></span></label>
64
- <input id="page-url-path" type="text" class="form-input" placeholder="/about or /services/web-dev">
70
+ <input id="page-url-path" type="text" data-model="urlPath.value" class="form-input" placeholder="/about or /services/web-dev">
65
71
  <small class="form-hint">The URL where this page will live. Changing this will update any navigation links
66
72
  pointing to the old URL.</small>
67
73
  </div>
@@ -69,15 +75,15 @@
69
75
  <div class="row mb-3">
70
76
  <div class="col-6">
71
77
  <label class="form-label">Title *</label>
72
- <input id="field-title" type="text" class="form-input" placeholder="Page title">
78
+ <input id="field-title" type="text" data-model="title.value" class="form-input" placeholder="Page title">
73
79
  </div>
74
80
  <div class="col-3">
75
81
  <label class="form-label">Layout <span data-help="The page shell: navbar, width, sidebar, footer. Layouts are managed under System › Layouts."></span></label>
76
- <select id="field-layout" class="form-select"></select>
82
+ <select id="field-layout" class="form-select" data-model="layout.value"></select>
77
83
  </div>
78
84
  <div class="col-3">
79
85
  <label class="form-label">Status <span data-help-title="Draft or published" data-help="A draft is hidden from visitors (404) - but anyone who can read pages sees it with a banner, and Live Preview and share links show it before it is published."></span></label>
80
- <select id="field-status" class="form-select">
86
+ <select id="field-status" class="form-select" data-model="status.value">
81
87
  <option value="draft">Draft</option>
82
88
  <option value="published">Published</option>
83
89
  </select>
@@ -86,28 +92,28 @@
86
92
  <div class="row mb-3">
87
93
  <div class="col-8">
88
94
  <label class="form-label">Description</label>
89
- <input id="field-description" type="text" class="form-input" placeholder="Short description">
95
+ <input id="field-description" type="text" data-model="description.value" class="form-input" placeholder="Short description">
90
96
  </div>
91
97
  <div class="col-4">
92
98
  <label class="form-label">Sort Order <span data-help="Lower numbers come first in navigation and in page lists sorted by navigation order."></span></label>
93
- <input id="field-sort-order" type="number" class="form-input" value="99" min="0">
99
+ <input id="field-sort-order" type="number" class="form-input" min="0" data-model="sortOrder.value">
94
100
  </div>
95
101
  </div>
96
102
  <div class="row mb-3">
97
103
  <div class="col-4">
98
104
  <label class="form-label">Category <span data-help="A free label for grouping pages - collection displays and search can filter by it."></span></label>
99
- <input id="field-category" type="text" class="form-input" placeholder="e.g. news, docs, products">
105
+ <input id="field-category" type="text" data-model="category.value" class="form-input" placeholder="e.g. news, docs, products">
100
106
  </div>
101
107
  <div class="col-4">
102
108
  <label class="form-label">Visibility <span data-help-title="Who can see the page" data-help="Public: everyone. A role &quot;and above&quot;: that role and every more senior one. A role &quot;only&quot;: holders of that role (and the super-admin). Private: the super-admin alone. Signed-out visitors are sent to sign in."></span></label>
103
- <select id="field-visibility" class="form-select">
109
+ <select id="field-visibility" class="form-select" data-model="visibility.value">
104
110
  <option value="public">Public - everyone</option>
105
111
  <option value="private">Private - super-admin only</option>
106
112
  </select>
107
113
  </div>
108
114
  <div class="col-4">
109
115
  <label class="form-label">Project <span data-help="Which project the page belongs to. Inherit from URL files it under the project whose root URL matches its address, else Core."></span></label>
110
- <select id="field-project" class="form-select">
116
+ <select id="field-project" class="form-select" data-model="project.value">
111
117
  <option value="">Inherit from URL</option>
112
118
  </select>
113
119
  <small class="form-hint">Pre-filled from the project you came from, or from URL inheritance; choose explicitly to override.</small>
@@ -116,7 +122,7 @@
116
122
  <div class="row mb-3">
117
123
  <div class="col-6">
118
124
  <label class="form-label">Theme Override <span data-help="Shows this page in a different theme from the rest of the site - built-in or custom."></span></label>
119
- <select id="field-theme" class="form-select"></select>
125
+ <select id="field-theme" class="form-select" data-model="theme.value"></select>
120
126
  <small class="form-hint">Overrides the site-wide theme for this page only.</small>
121
127
  </div>
122
128
  </div>
@@ -130,22 +136,22 @@
130
136
  <div class="row">
131
137
  <div class="col-auto">
132
138
  <label class="form-check-label">
133
- <input id="field-show-in-nav" type="checkbox" class="form-check"> Show in Navigation
139
+ <input id="field-show-in-nav" type="checkbox" class="form-check" data-model="showInNav.value"> Show in Navigation
134
140
  </label>
135
141
  </div>
136
142
  <div class="col-auto">
137
143
  <label class="form-check-label">
138
- <input id="field-sidebar" type="checkbox" class="form-check"> Show Sidebar
144
+ <input id="field-sidebar" type="checkbox" class="form-check" data-model="sidebar.value"> Show Sidebar
139
145
  </label>
140
146
  </div>
141
147
  <div class="col-auto">
142
148
  <label class="form-check-label">
143
- <input id="field-show-breadcrumbs" type="checkbox" class="form-check" checked> Show Breadcrumbs
149
+ <input id="field-show-breadcrumbs" type="checkbox" class="form-check" data-model="breadcrumbs.value"> Show Breadcrumbs
144
150
  </label>
145
151
  </div>
146
152
  <div class="col-auto">
147
153
  <label class="form-check-label" title="Included in fresh installs via the seed script">
148
- <input id="field-bundled" type="checkbox" class="form-check"> Bundled
154
+ <input id="field-bundled" type="checkbox" class="form-check" data-model="bundled.value"> Bundled
149
155
  </label>
150
156
  <span data-help="Included when a fresh site is seeded. Leave off for pages that belong to this site only."></span>
151
157
  </div>
@@ -157,13 +163,13 @@
157
163
  <div class="row mb-3">
158
164
  <div class="col">
159
165
  <label class="form-label">SEO Title <span data-help="The title search engines and browser tabs show. Blank uses the page title."></span></label>
160
- <input id="field-seo-title" type="text" class="form-input" placeholder="Overrides default title">
166
+ <input id="field-seo-title" type="text" data-model="seoTitle.value" class="form-input" placeholder="Overrides default title">
161
167
  </div>
162
168
  </div>
163
169
  <div class="row">
164
170
  <div class="col">
165
171
  <label class="form-label">SEO Description <span data-help="The summary search engines show under the title. Blank uses the page description."></span></label>
166
- <input id="field-seo-desc" type="text" class="form-input" placeholder="Overrides default description">
172
+ <input id="field-seo-desc" type="text" data-model="seoDesc.value" class="form-input" placeholder="Overrides default description">
167
173
  </div>
168
174
  </div>
169
175
  </div>
@@ -171,7 +177,7 @@
171
177
  <!-- DConfig -->
172
178
  <div class="tab-panel">
173
179
  <label class="form-label">Config JSON <span data-help-title="DConfig" data-help="Declarative behaviour for elements on this page - click handlers, class toggles - as a JSON object keyed by CSS selector. Must be valid JSON before the page can be saved."></span></label>
174
- <textarea id="field-dconfig" class="form-input dconfig-textarea"
180
+ <textarea id="field-dconfig" class="form-input dconfig-textarea" data-model="dconfig.value"
175
181
  placeholder='{ "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }'></textarea>
176
182
  <small class="form-hint">Declarative click handlers and class toggles applied on page load. Merged with any <code>[dconfig]</code>
177
183
  shortcodes in the content body - inline shortcodes win on selector conflict. Leave blank if unused.</small>
@@ -181,74 +187,91 @@
181
187
  <div class="tab-panel" id="live-preview-panel">
182
188
  <div class="live-preview-bar">
183
189
  <div class="live-preview-widths" role="group" aria-label="Preview width">
184
- <button type="button" class="live-preview-width active" data-width="full" data-tooltip="Full width">
190
+ <button type="button" class="live-preview-width" data-tooltip="Full width" data-on-click="setWidth('full')"
191
+ data-bind-class="liveWidth.value === 'full' && 'active'">
185
192
  <span data-icon="monitor"></span>
186
193
  </button>
187
- <button type="button" class="live-preview-width" data-width="1024" data-tooltip="Tablet (1024px)">
194
+ <button type="button" class="live-preview-width" data-tooltip="Tablet (1024px)" data-on-click="setWidth('1024')"
195
+ data-bind-class="liveWidth.value === '1024' && 'active'">
188
196
  <span data-icon="tablet"></span>
189
197
  </button>
190
- <button type="button" class="live-preview-width" data-width="390" data-tooltip="Mobile (390px)">
198
+ <button type="button" class="live-preview-width" data-tooltip="Mobile (390px)" data-on-click="setWidth('390')"
199
+ data-bind-class="liveWidth.value === '390' && 'active'">
191
200
  <span data-icon="smartphone"></span>
192
201
  </button>
193
202
  </div>
194
- <span class="live-preview-status" id="live-preview-status">Showing unsaved changes</span>
203
+ <span class="live-preview-status" data-bind-text="liveStatus.value">Showing unsaved changes</span>
195
204
  <span data-help-title="Live Preview" data-help="The whole page - navbar, footer, theme - rendered by the server from what is in the editor now, before saving. Share link makes a private, revocable address for someone without an account."></span>
196
205
  <div class="live-preview-actions">
197
- <button type="button" class="btn btn-ghost btn-sm" id="live-preview-refresh">
206
+ <button type="button" class="btn btn-ghost btn-sm" data-on-click="refreshLive">
198
207
  <span data-icon="refresh-cw"></span> Refresh
199
208
  </button>
200
- <button type="button" class="btn btn-ghost btn-sm" id="share-preview-btn">
209
+ <button type="button" class="btn btn-ghost btn-sm" data-on-click="share">
201
210
  <span data-icon="link"></span> Share link
202
211
  </button>
203
212
  </div>
204
213
  </div>
205
214
  <div class="live-preview-stage" id="live-preview-stage">
206
215
  <iframe id="live-preview-frame" class="page-live-preview-frame" src="about:blank"
216
+ data-bind-data-width="liveWidth.value === 'full' ? null : liveWidth.value"
207
217
  title="Full page preview" sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
208
218
  </div>
209
219
  </div>
210
220
 
211
- <!-- History -->
212
- <div class="tab-panel" id="history-panel">
213
- <div class="mb-3" style="display:flex;align-items:center;justify-content:space-between;">
214
- <h3 style="margin:0;font-size:1rem;">Version History <span data-help="A version is kept on every save. Click one to compare it with the editor; right-click it (or ⋮, or Shift+F10) to restore or delete it. Restoring keeps a snapshot of the current page first."></span></h3>
215
- <div style="display:flex;gap:.5rem;align-items:center;">
216
- <button class="btn btn-ghost btn-sm" id="history-delete-selected-btn"
217
- style="display:none;color:var(--dm-danger);">
218
- <span data-icon="trash"></span> Delete Selected
219
- </button>
220
- <button class="btn btn-ghost btn-sm" id="history-save-named-btn">
221
- <span data-icon="bookmark"></span> Save Named Version
222
- </button>
223
- </div>
221
+ <!-- History - loaded the first time the tab is opened. -->
222
+ <div class="tab-panel" id="history-panel">
223
+ <div class="pe-hist-head">
224
+ <h3>Version History <span data-help="A version is kept on every save. Click one to compare it with the editor; right-click it (or ⋮, or Shift+F10) to restore or delete it. Restoring keeps a snapshot of the current page first."></span></h3>
225
+ <div class="pe-hist-actions">
226
+ <button class="btn btn-ghost btn-sm pe-danger" hidden data-bind-hidden="!anySelected.value" data-on-click="deleteSelected">
227
+ <span data-icon="trash"></span> Delete Selected
228
+ </button>
229
+ <button class="btn btn-ghost btn-sm" data-on-click="saveNamed"><span data-icon="bookmark"></span> Save Named Version</button>
230
+ </div>
231
+ </div>
232
+
233
+ <div hidden data-bind-hidden="compare.value">
234
+ <p class="pe-hist-note" hidden data-bind-hidden="!histLoading.value">Loading…</p>
235
+ <p class="pe-hist-note is-bad" hidden data-bind-hidden="!histError.value" data-bind-text="histError.value"></p>
236
+ <p class="pe-hist-note" hidden data-bind-hidden="histLoading.value || histError.value || versions.value.length">No versions saved yet.
237
+ Versions are created automatically on each save.</p>
238
+ <table class="pe-hist-table" hidden data-bind-hidden="!versions.value.length">
239
+ <thead><tr>
240
+ <th class="pe-hist-check"><input type="checkbox" aria-label="Select all versions" data-on-change="toggleAll"
241
+ data-bind-checked="allSelected.value" data-bind-indeterminate="someSelected.value"></th>
242
+ <th>Date</th><th>Author</th><th>Type</th><th>Label</th><th>Actions</th>
243
+ </tr></thead>
244
+ <tbody class="pe-version-list" data-each="versions.value key=filename">
245
+ <tr class="pe-version-row" tabindex="0" data-bind-data-file="filename">
246
+ <td><input type="checkbox" data-model="checked.value" aria-label="Select this version"></td>
247
+ <td data-bind-text="when"></td>
248
+ <td data-bind-text="author || '-'"></td>
249
+ <td><span class="badge pe-hist-type" data-bind-class="type === 'manual' ? 'badge-primary' : 'badge-secondary'" data-bind-text="type"></span></td>
250
+ <td class="pe-muted" data-bind-text="label || '-'"></td>
251
+ <td><button type="button" class="pe-kebab" title="Actions" aria-label="Actions for this version">⋮</button></td>
252
+ </tr>
253
+ </tbody>
254
+ </table>
255
+ </div>
256
+
257
+ <div hidden data-bind-hidden="!compare.value">
258
+ <div class="pe-hist-compare-bar">
259
+ <button class="btn btn-ghost btn-sm" data-on-click="closeCompare"><span data-icon="arrow-left"></span> Back to List</button>
260
+ <button class="btn btn-primary btn-sm" data-on-click="restoreCompared"><span data-icon="rotate-ccw"></span> Restore This Version</button>
261
+ <span class="pe-muted" data-bind-text="compare.value ? compare.value.label : ''"></span>
262
+ </div>
263
+ <div class="row" style="gap:1rem;">
264
+ <div class="col">
265
+ <p class="pe-hist-caption">SELECTED VERSION</p>
266
+ <pre class="pe-hist-pre" data-bind-text="compare.value ? compare.value.old : ''"></pre>
224
267
  </div>
225
- <div id="version-list"></div>
226
- <div id="version-compare" style="display:none;">
227
- <div class="mb-3" style="display:flex;align-items:center;gap:.75rem;">
228
- <button class="btn btn-ghost btn-sm" id="version-compare-back-btn">
229
- <span data-icon="arrow-left"></span> Back to List
230
- </button>
231
- <button class="btn btn-primary btn-sm" id="version-restore-btn">
232
- <span data-icon="rotate-ccw"></span> Restore This Version
233
- </button>
234
- <span id="version-compare-label" style="font-size:.875rem;color:var(--dm-text-muted);"></span>
235
- </div>
236
- <div class="row" style="gap:1rem;">
237
- <div class="col">
238
- <p style="font-size:.8rem;font-weight:600;margin-bottom:.5rem;color:var(--dm-text-muted);">
239
- SELECTED VERSION</p>
240
- <pre id="version-compare-old"
241
- style="background:var(--dm-background-alt);padding:1rem;border-radius:6px;overflow:auto;white-space:pre-wrap;font-size:.8rem;max-height:60vh;"></pre>
242
- </div>
243
- <div class="col">
244
- <p style="font-size:.8rem;font-weight:600;margin-bottom:.5rem;color:var(--dm-text-muted);">CURRENT
245
- (UNSAVED)</p>
246
- <pre id="version-compare-new"
247
- style="background:var(--dm-background-alt);padding:1rem;border-radius:6px;overflow:auto;white-space:pre-wrap;font-size:.8rem;max-height:60vh;"></pre>
248
- </div>
249
- </div>
268
+ <div class="col">
269
+ <p class="pe-hist-caption">CURRENT (UNSAVED)</p>
270
+ <pre class="pe-hist-pre" data-bind-text="compare.value ? compare.value.current : ''"></pre>
250
271
  </div>
272
+ </div>
251
273
  </div>
274
+ </div>
252
275
 
253
276
  </div>
254
277
  </div>
@@ -262,7 +285,23 @@
262
285
  .pex-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px;
263
286
  background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
264
287
  .pex-problem { color: var(--dm-danger); font-size: var(--dm-text-sm); }
265
- .pex-bar [hidden] { display: none !important; }
288
+ .pex-bar [hidden], .editor-card [hidden], #editor-meta-tabs [hidden] { display: none !important; }
289
+ .pe-hist-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
290
+ .pe-hist-head h3 { margin: 0; font-size: 1rem; }
291
+ .pe-hist-actions { display: flex; gap: .5rem; align-items: center; }
292
+ .pe-danger { color: var(--dm-danger); }
293
+ .pe-hist-note { color: var(--dm-text-muted); }
294
+ .pe-hist-note.is-bad { color: var(--dm-danger); }
295
+ .pe-hist-table { width: 100%; border-collapse: collapse; font-size: .875rem; }
296
+ .pe-hist-table th { text-align: left; padding: .5rem .75rem; border-bottom: 1px solid var(--dm-border); font-size: .8rem; color: var(--dm-text-muted); }
297
+ .pe-hist-table th.pe-hist-check { width: 2rem; }
298
+ .pe-hist-table td { padding: .5rem .75rem; }
299
+ .pe-hist-table tbody tr { border-bottom: 1px solid var(--dm-border); }
300
+ .pe-hist-type { font-size: .75rem; padding: .2rem .5rem; border-radius: 4px; }
301
+ .pe-muted { color: var(--dm-text-muted); font-size: .875rem; }
302
+ .pe-hist-compare-bar { display: flex; align-items: center; gap: .75rem; margin-bottom: 1rem; }
303
+ .pe-hist-caption { font-size: .8rem; font-weight: 600; margin-bottom: .5rem; color: var(--dm-text-muted); }
304
+ .pe-hist-pre { background: var(--dm-background-alt); padding: 1rem; border-radius: 6px; overflow: auto; white-space: pre-wrap; font-size: .8rem; max-height: 60vh; }
266
305
  #history-panel .pe-version-row { cursor: pointer; }
267
306
  #history-panel .pe-version-row:hover, #history-panel .pe-version-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
268
307
  .pe-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
@@ -1,8 +1,7 @@
1
- <!-- SEO. Bound with M.applyBindings - admin/views/seo.js inserts the item templates (the router renders a
1
+ <!-- SEO. Bound with M.applyBindings - admin/js/views/seo.js inserts the item templates (the router renders a
2
2
  templateUrl through _.render, eating any {{…}}). Refresh and Settings are banner actions. -->
3
3
  <div class="sox">
4
4
  <p class="sox-note is-bad" data-bind-hidden="!error.value" data-bind-text="error.value"></p>
5
- <p class="sox-note is-warn" data-bind-hidden="hooksOk.value">This version of Domma CMS cannot take SEO's changes to the sitemap, robots.txt and page tags - they are core's own until it is updated (0.89 or later). The audit, the editor and redirects work.</p>
6
5
 
7
6
  <nav class="sox-tabs" role="tablist" aria-label="SEO">
8
7
  <button type="button" role="tab" data-on-click="tab_overview" data-bind-class="tab.value === 'overview' && 'is-on'"><span data-icon="search" data-icon-size="14"></span> Pages <small class="sox-count" data-bind-hidden="!attention.value" data-bind-text="attention.value"></small></button>
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{navigationView as m}from"./navigation.js";import{notificationsView as p}from"./notifications.js";import{layoutsView as n}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as f}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as c}from"./user-editor.js";import{pluginsView as V}from"./plugins.js";import{pluginCodeView as a}from"./plugin-code.js";import{documentationView as d}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{docViews as u}from"./doc-pages.js";import{apiReferenceView as E}from"./api-reference.js";import{collectionsView as g}from"./collections.js";import{collectionEditorView as v}from"./collection-editor.js";import{collectionEntriesView as b}from"./collection-entries.js";import{formsView as h}from"./forms.js";import{formEditorView as y}from"./form-editor.js";import{formSubmissionsView as j}from"./form-submissions.js";import{viewsListView as k}from"./views-list.js";import{viewEditorView as L}from"./view-editor.js";import{viewPreviewView as x}from"./view-preview.js";import{actionsListView as M}from"./actions-list.js";import{actionEditorView as D}from"./action-editor.js";import{proDocsView as P}from"./pro-docs.js";import{blocksView as R}from"./blocks.js";import{blockEditorView as S}from"./block-editor.js";import{componentsView as C}from"./components.js";import{componentEditorView as T}from"./component-editor.js";import{myProfileView as U}from"./my-profile.js";import{rolesView as q}from"./roles.js";import{roleEditorView as z}from"./role-editor.js";import{effectsView as A}from"./effects.js";import{searchView as B}from"./search.js";import{notesView as F}from"./notes.js";import{todoView as G}from"./todo.js";import{analyticsView as H}from"./analytics.js";import{contactsView as I}from"./contacts.js";import{menusView as J}from"./menus.js";import{contextMenusView as K}from"./context-menus.js";import{contextMenuEditorView as N}from"./context-menu-editor.js";import{menuEditorView as O}from"./menu-editor.js";import{menuLocationsView as Q}from"./menu-locations.js";import{projectsView as W}from"./projects.js";import{projectDetailView as X}from"./project-detail.js";import{projectSettingsView as Y}from"./project-settings.js";import{apiTokensView as Z}from"./api-tokens.js";import{apiEndpointsView as _}from"./api-endpoints.js";import{apiEndpointEditorView as $}from"./api-endpoint-editor.js";const oo={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:W,projectDetail:X,projectSettings:Y,apiTokens:Z,apiEndpoints:_,apiEndpointEditor:$,dashboard:o,pages:i,pageEditor:r,settings:t,theme:e,navigation:m,layouts:n,media:s,login:f,users:w,userEditor:c,plugins:V,pluginCode:a,documentation:d,tutorials:l,apiReference:E,collections:g,collectionEditor:v,collectionEntries:b,forms:h,formEditor:y,formSubmissions:j,viewsList:k,viewEditor:L,viewPreview:x,actionsList:M,actionEditor:D,proDocs:P,blocks:R,blockEditor:S,components:C,componentEditor:T,myProfile:U,roles:q,roleEditor:z,effects:A,search:B,notes:F,todo:G,analytics:H,contacts:I,notifications:p,menus:J,contextMenus:K,contextMenuEditor:N,menuEditor:O,menuLocations:Q,menusRedirect:oo,...u};
1
+ import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as n}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{pluginCodeView as V}from"./plugin-code.js";import{documentationView as a}from"./documentation.js";import{tutorialsView as d}from"./tutorials.js";import{docViews as l}from"./doc-pages.js";import{apiReferenceView as u}from"./api-reference.js";import{collectionsView as E}from"./collections.js";import{collectionEditorView as g}from"./collection-editor.js";import{collectionEntriesView as v}from"./collection-entries.js";import{formsView as b}from"./forms.js";import{formEditorView as h}from"./form-editor.js";import{formSubmissionsView as y}from"./form-submissions.js";import{viewsListView as j}from"./views-list.js";import{viewEditorView as k}from"./view-editor.js";import{viewPreviewView as L}from"./view-preview.js";import{actionsListView as x}from"./actions-list.js";import{actionEditorView as M}from"./action-editor.js";import{proDocsView as D}from"./pro-docs.js";import{blocksView as P}from"./blocks.js";import{blockEditorView as R}from"./block-editor.js";import{componentsView as S}from"./components.js";import{componentEditorView as C}from"./component-editor.js";import{myProfileView as T}from"./my-profile.js";import{rolesView as U}from"./roles.js";import{roleEditorView as q}from"./role-editor.js";import{effectsView as z}from"./effects.js";import{searchView as A}from"./search.js";import{notesView as B}from"./notes.js";import{todoView as F}from"./todo.js";import{analyticsView as G}from"./analytics.js";import{seoView as H}from"./seo.js";import{contactsView as I}from"./contacts.js";import{menusView as J}from"./menus.js";import{contextMenusView as K}from"./context-menus.js";import{contextMenuEditorView as N}from"./context-menu-editor.js";import{menuEditorView as O}from"./menu-editor.js";import{menuLocationsView as Q}from"./menu-locations.js";import{projectsView as W}from"./projects.js";import{projectDetailView as X}from"./project-detail.js";import{projectSettingsView as Y}from"./project-settings.js";import{apiTokensView as Z}from"./api-tokens.js";import{apiEndpointsView as _}from"./api-endpoints.js";import{apiEndpointEditorView as $}from"./api-endpoint-editor.js";const oo={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:W,projectDetail:X,projectSettings:Y,apiTokens:Z,apiEndpoints:_,apiEndpointEditor:$,dashboard:o,pages:i,pageEditor:r,settings:t,theme:e,layouts:p,media:s,login:n,users:f,userEditor:w,plugins:c,pluginCode:V,documentation:a,tutorials:d,apiReference:u,collections:E,collectionEditor:g,collectionEntries:v,forms:b,formEditor:h,formSubmissions:y,viewsList:j,viewEditor:k,viewPreview:L,actionsList:x,actionEditor:M,proDocs:D,blocks:P,blockEditor:R,components:S,componentEditor:C,myProfile:T,roles:U,roleEditor:q,effects:z,search:A,notes:B,todo:F,analytics:G,seo:H,contacts:I,notifications:m,menus:J,contextMenus:K,contextMenuEditor:N,menuEditor:O,menuLocations:Q,menusRedirect:oo,...l};