domma-cms 0.9.10 → 0.12.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 (125) hide show
  1. package/CLAUDE.md +248 -159
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/api.js +1 -1
  4. package/admin/js/app.js +7 -3
  5. package/admin/js/config/sidebar-config.js +1 -1
  6. package/admin/js/http-interceptor.js +1 -0
  7. package/admin/js/lib/card-builder.js +2 -2
  8. package/admin/js/lib/markdown-toolbar.js +5 -5
  9. package/admin/js/lib/safe-html.js +1 -0
  10. package/admin/js/lib/shortcode-modal.js +1 -0
  11. package/admin/js/templates/layouts.html +5 -4
  12. package/admin/js/templates/notifications.html +14 -0
  13. package/admin/js/templates/plugin-marketplace.html +16 -0
  14. package/admin/js/templates/plugins.html +17 -5
  15. package/admin/js/views/index.js +1 -1
  16. package/admin/js/views/layouts.js +1 -16
  17. package/admin/js/views/notifications.js +1 -0
  18. package/admin/js/views/page-editor.js +37 -33
  19. package/admin/js/views/plugin-marketplace.js +1 -0
  20. package/admin/js/views/plugins.js +16 -16
  21. package/config/navigation.json +5 -72
  22. package/config/plugins.json +10 -14
  23. package/config/presets.json +50 -13
  24. package/config/site.json +11 -63
  25. package/package.json +2 -1
  26. package/plugins/_template/admin/templates/index.html +17 -0
  27. package/plugins/_template/admin/views/index.js +19 -0
  28. package/plugins/_template/config.js +8 -0
  29. package/plugins/_template/plugin.js +23 -0
  30. package/plugins/_template/plugin.json +34 -0
  31. package/plugins/analytics/plugin.json +41 -31
  32. package/plugins/blog/admin/templates/blog.html +22 -0
  33. package/plugins/blog/admin/templates/categories.html +7 -0
  34. package/plugins/blog/admin/templates/comments.html +11 -0
  35. package/plugins/blog/admin/templates/post-editor.html +97 -0
  36. package/plugins/blog/admin/templates/settings.html +11 -0
  37. package/plugins/blog/admin/views/blog.js +183 -0
  38. package/plugins/blog/admin/views/categories.js +235 -0
  39. package/plugins/blog/admin/views/comments.js +187 -0
  40. package/plugins/blog/admin/views/post-editor.js +291 -0
  41. package/plugins/blog/admin/views/settings.js +100 -0
  42. package/plugins/blog/collections/categories/schema.json +12 -0
  43. package/plugins/blog/collections/comments/schema.json +16 -0
  44. package/plugins/blog/collections/posts/schema.json +19 -0
  45. package/plugins/blog/config.js +8 -0
  46. package/plugins/blog/plugin.js +352 -0
  47. package/plugins/blog/plugin.json +96 -0
  48. package/plugins/blog/roles/blog-author.json +10 -0
  49. package/plugins/blog/roles/blog-editor.json +12 -0
  50. package/plugins/blog/templates/author.html +9 -0
  51. package/plugins/blog/templates/category.html +9 -0
  52. package/plugins/blog/templates/index.html +9 -0
  53. package/plugins/blog/templates/post.html +17 -0
  54. package/plugins/blog/templates/tag.html +9 -0
  55. package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
  56. package/plugins/contacts/collections/user-contacts/schema.json +1 -1
  57. package/plugins/contacts/plugin.js +4 -10
  58. package/plugins/contacts/plugin.json +13 -3
  59. package/plugins/notes/collections/user-notes/schema.json +1 -1
  60. package/plugins/notes/plugin.js +3 -9
  61. package/plugins/notes/plugin.json +13 -3
  62. package/plugins/site-search/plugin.json +5 -2
  63. package/plugins/theme-switcher/plugin.json +1 -1
  64. package/plugins/todo/collections/todos/schema.json +1 -1
  65. package/plugins/todo/plugin.js +3 -9
  66. package/plugins/todo/plugin.json +13 -3
  67. package/public/css/site.css +1 -1
  68. package/public/js/site.js +1 -1
  69. package/scripts/build.js +48 -0
  70. package/scripts/create-plugin.js +113 -0
  71. package/scripts/fresh.js +6 -7
  72. package/scripts/gen-instance-secret.js +46 -0
  73. package/scripts/reset.js +3 -3
  74. package/scripts/setup.js +31 -13
  75. package/server/middleware/auth.js +48 -0
  76. package/server/middleware/managerAuth.js +36 -0
  77. package/server/routes/api/actions.js +1 -1
  78. package/server/routes/api/auth.js +4 -3
  79. package/server/routes/api/layouts.js +173 -49
  80. package/server/routes/api/notifications.js +155 -0
  81. package/server/routes/api/plugin-marketplace.js +75 -0
  82. package/server/routes/api/users.js +1 -1
  83. package/server/routes/api/views.js +1 -1
  84. package/server/routes/public.js +4 -9
  85. package/server/server.js +32 -3
  86. package/server/services/actions.js +1 -1
  87. package/server/services/managerClient.js +182 -0
  88. package/server/services/markdown.js +76 -9
  89. package/server/services/permissionRegistry.js +245 -173
  90. package/server/services/pluginInstaller.js +301 -0
  91. package/server/services/plugins.js +117 -10
  92. package/server/services/presetCollections.js +66 -251
  93. package/server/services/renderer.js +99 -0
  94. package/server/services/roles.js +191 -39
  95. package/server/services/users.js +1 -1
  96. package/server/services/views.js +1 -1
  97. package/server/templates/page.html +2 -2
  98. package/plugins/docs/admin/templates/docs.html +0 -69
  99. package/plugins/docs/admin/views/docs.js +0 -276
  100. package/plugins/docs/config.js +0 -8
  101. package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
  102. package/plugins/docs/data/folders.json +0 -9
  103. package/plugins/docs/data/templates.json +0 -1
  104. package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
  105. package/plugins/docs/plugin.js +0 -375
  106. package/plugins/docs/plugin.json +0 -23
  107. package/plugins/form-builder/data/forms/contacts.json +0 -66
  108. package/plugins/form-builder/data/forms/enquiries.json +0 -103
  109. package/plugins/form-builder/data/forms/feedback.json +0 -131
  110. package/plugins/form-builder/data/forms/notes.json +0 -79
  111. package/plugins/form-builder/data/forms/to-do.json +0 -100
  112. package/plugins/form-builder/data/submissions/contacts.json +0 -1
  113. package/plugins/form-builder/data/submissions/enquiries.json +0 -1
  114. package/plugins/form-builder/data/submissions/feedback.json +0 -1
  115. package/plugins/form-builder/data/submissions/notes.json +0 -1
  116. package/plugins/form-builder/data/submissions/to-do.json +0 -1
  117. package/plugins/garage/admin/templates/garage.html +0 -111
  118. package/plugins/garage/admin/views/garage.js +0 -622
  119. package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
  120. package/plugins/garage/config.js +0 -18
  121. package/plugins/garage/data/vehicles.json +0 -70
  122. package/plugins/garage/plugin.js +0 -398
  123. package/plugins/garage/plugin.json +0 -33
  124. package/scripts/seed.js +0 -1996
  125. package/server/services/userTypes.js +0 -227
@@ -0,0 +1 @@
1
+ import{api as i}from"../api.js";export const pluginMarketplaceView={templateUrl:"/admin/js/templates/plugin-marketplace.html",async onMount(e){const a=E.loader(e.get(0),{type:"dots"});let t;try{t=await i.marketplace.catalogue()}catch(n){E.toast(`Failed to load marketplace: ${n.message}`,{type:"error"}),a.destroy();return}if(a.destroy(),!t.available){e.find("#marketplace-unavailable").show();return}e.find("#marketplace-available").show(),b(e,t.catalogue),Domma.icons.scan()}};function b(e,a){const t=e.find("#marketplace-grid").get(0);if(t){for(;t.firstChild;)t.removeChild(t.firstChild);if(!a||a.length===0){const n=document.createElement("p");n.className="text-muted",n.textContent="No plugins available in the marketplace.",t.appendChild(n);return}a.forEach(n=>{const u=$(n,e);t.appendChild(u)})}}function U(e){if(!e||e===0||e==="0")return"Free";const a=parseFloat(e);return isNaN(a)||a===0?"Free":`\xA3${a.toFixed(2)}/mo`}function $(e,a){const{slug:t,version:n,displayName:u,description:w,price:D,entitled:x,installed:F}=e,s=document.createElement("div");s.className="card plugin-card",s.dataset.slug=t;const o=document.createElement("div");o.className="card-body";const c=document.createElement("div");c.className="plugin-header";const h=document.createElement("div");h.className="plugin-icon";const k=document.createElement("span");k.dataset.icon="package",h.appendChild(k);const d=document.createElement("div");d.className="plugin-meta";const g=document.createElement("div");g.className="plugin-name",g.textContent=u||t;const f=document.createElement("div");f.className="plugin-version",f.textContent=`v${n}`,d.appendChild(g),d.appendChild(f);const r=document.createElement("div");r.style.cssText="display:flex;gap:.35rem;align-items:center;";const C=document.createElement("span");if(C.className="badge badge-outline",C.textContent=U(D),r.appendChild(C),x){const N=document.createElement("span");N.className="badge badge-success",N.textContent="Entitled",r.appendChild(N)}c.appendChild(h),c.appendChild(d),c.appendChild(r);const v=document.createElement("p");v.className="plugin-desc",v.textContent=w||"",o.appendChild(c),o.appendChild(v);const m=document.createElement("div");m.className="plugin-footer";const p=document.createElement("span");p.className="text-muted",p.style.fontSize=".8rem",p.textContent="Restart server after changes",m.appendChild(p);const y=document.createElement("div");y.className="plugin-footer-actions";const l=document.createElement("button");return l.className="btn btn-sm",F?(l.className+=" btn-ghost",l.textContent="Uninstall",l.onclick=()=>R(t,a)):x?(l.className+=" btn-primary",l.textContent="Install",l.onclick=()=>I(t,n,a)):(l.className+=" btn-ghost",l.textContent="Not entitled",l.disabled=!0),y.appendChild(l),m.appendChild(y),s.appendChild(o),s.appendChild(m),s}async function I(e,a,t){if(await E.confirm(`Install ${e} v${a}?`))try{await i.marketplace.install(e,a),E.toast(`${e} installed. Restart the server to activate.`,{type:"success"});const n=await i.marketplace.catalogue();b(t,n.catalogue),Domma.icons.scan()}catch(n){E.toast(`Install failed: ${n.message}`,{type:"error"})}}async function R(e,a){if(await E.confirm(`Uninstall ${e}? This will remove its files.`))try{await i.marketplace.uninstall(e),E.toast(`${e} uninstalled. Restart to apply.`,{type:"success"});const t=await i.marketplace.catalogue();b(a,t.catalogue),Domma.icons.scan()}catch(t){E.toast(`Uninstall failed: ${t.message}`,{type:"error"})}}
@@ -1,38 +1,38 @@
1
- import{api as r}from"../api.js";export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(o){const m=E.loader(o.get(0),{type:"dots"});let i=[];try{i=await r.plugins.list()}catch(e){E.toast(`Failed to load plugins: ${e.message}`,{type:"error"})}if(m.destroy(),!i.length){o.find("#plugins-empty").show();return}g(o,i),Domma.icons.scan()}};function g(o,m){const i=o.find("#plugins-grid").empty();m.forEach(e=>{const n=e.enabled?"badge-success":"badge-secondary",l=e.enabled?"Enabled":"Disabled",s=e.enabled?"Disable":"Enable",v=e.enabled?"btn-ghost":"btn-primary",c=e.bundled?"var(--dm-info,#2563eb)":"var(--dm-secondary,#6b7280)",u=e.bundled?"Bundled \u2014 click to unbundle":"Not bundled \u2014 click to bundle",t=`<button class="badge badge-outline btn-bundle-plugin" data-name="${e.name}" data-bundled="${e.bundled?"1":"0"}" title="${u}" style="font-size:0.65rem;padding:1px 6px;cursor:pointer;background:none;border:1px solid;color:${c};border-color:${c};">${e.bundled?"Bundled":"Unbundled"}</button>`,d=e.author?`by ${a(e.author)}`:"",p=e.date?a(e.date):"",b=`
1
+ import{api as i}from"../api.js";import{pluginMarketplaceView as f}from"./plugin-marketplace.js";export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(l){let c=!1;l.find('[data-tab="marketplace"]').on("click",async function(){if(c)return;const a=l.find("#marketplace-mount");try{const s=await H.get(f.templateUrl),u=new DOMParser().parseFromString(s,"text/html");for(;u.body.firstChild;)a.get(0).appendChild(u.body.firstChild);await f.onMount(a),c=!0}catch{E.toast("Failed to load marketplace.",{type:"error"})}}),E.tabs(l.find("#plugin-tabs").get(0));const r=E.loader(l.get(0),{type:"dots"});let e=[];try{e=await i.plugins.list()}catch(a){E.toast(`Failed to load plugins: ${a.message}`,{type:"error"})}if(r.destroy(),!e.length){l.find("#plugins-empty").show();return}y(l,e),Domma.icons.scan()}};function y(l,c){const r=l.find("#plugins-grid").empty();c.forEach(e=>{const a=e.enabled?"badge-success":"badge-secondary",s=e.enabled?"Enabled":"Disabled",o=e.enabled?"Disable":"Enable",u=e.enabled?"btn-ghost":"btn-primary",m=e.bundled?"var(--dm-info,#2563eb)":"var(--dm-secondary,#6b7280)",p=e.bundled?"Bundled \u2014 click to unbundle":"Not bundled \u2014 click to bundle",t=`<button class="badge badge-outline btn-bundle-plugin" data-name="${e.name}" data-bundled="${e.bundled?"1":"0"}" title="${p}" style="font-size:0.65rem;padding:1px 6px;cursor:pointer;background:none;border:1px solid;color:${m};border-color:${m};">${e.bundled?"Bundled":"Unbundled"}</button>`,d=e.author?`by ${n(e.author)}`:"",g=e.date?n(e.date):"",b=`
2
2
  <div class="card plugin-card" data-plugin="${e.name}">
3
3
  <div class="card-body">
4
4
  <div class="plugin-header">
5
5
  <div class="plugin-icon"><span data-icon="${e.icon||"package"}"></span></div>
6
6
  <div class="plugin-meta">
7
- <div class="plugin-name">${a(e.displayName)}</div>
8
- <div class="plugin-version">v${a(e.version)}${d?` &middot; ${d}`:""}</div>
7
+ <div class="plugin-name">${n(e.displayName)}</div>
8
+ <div class="plugin-version">v${n(e.version)}${d?` &middot; ${d}`:""}</div>
9
9
  </div>
10
10
  <div style="display:flex;gap:.35rem;align-items:center;">
11
11
  ${t}
12
- <span class="badge ${n}">${l}</span>
12
+ <span class="badge ${a}">${s}</span>
13
13
  </div>
14
14
  </div>
15
- <p class="plugin-desc">${a(e.description)}</p>
16
- ${p?`<p class="plugin-date text-muted" style="font-size:.8rem;margin:0">Released ${p}</p>`:""}
15
+ <p class="plugin-desc">${n(e.description)}</p>
16
+ ${g?`<p class="plugin-date text-muted" style="font-size:.8rem;margin:0">Released ${g}</p>`:""}
17
17
  </div>
18
18
  <div class="plugin-footer">
19
19
  <span class="text-muted" style="font-size:.8rem">Restart server after changes</span>
20
20
  <div class="plugin-footer-actions">
21
21
  ${e.settingsSchema?`<button class="btn btn-sm btn-ghost btn-plugin-settings" data-name="${e.name}">Settings</button>`:""}
22
- <button class="btn btn-sm ${v} btn-toggle-plugin"
22
+ <button class="btn btn-sm ${u} btn-toggle-plugin"
23
23
  data-name="${e.name}"
24
24
  data-enabled="${e.enabled?"1":"0"}">
25
- ${s}
25
+ ${o}
26
26
  </button>
27
27
  </div>
28
28
  </div>
29
- </div>`;i.append(b)}),Domma.icons.scan(),Domma.effects.reveal(".plugin-card",{animation:"fade",stagger:60,duration:400}),Domma.effects.ripple(".btn-toggle-plugin"),i.on("click",".btn-plugin-settings",async function(){const e=$(this).data("name"),n=m.find(t=>t.name===e);if(!n?.settingsSchema)return;const s=`<form id="plugin-settings-form">${n.settingsSchema.map(t=>{const d=n.settings?.[t.key]??t.default??"";if(t.type==="select"){const p=t.options.map(b=>`<option value="${a(b.value)}"${String(d)===String(b.value)?" selected":""}>${a(b.label)}</option>`).join("");return`<div class="form-group">
30
- <label class="form-label">${a(t.label)}</label>
31
- <select class="form-control" name="${a(t.key)}">${p}</select>
29
+ </div>`;r.append(b)}),Domma.icons.scan(),Domma.effects.reveal(".plugin-card",{animation:"fade",stagger:60,duration:400}),Domma.effects.ripple(".btn-toggle-plugin"),r.on("click",".btn-plugin-settings",async function(){const e=$(this).data("name"),a=c.find(t=>t.name===e);if(!a?.settingsSchema)return;const o=`<form id="plugin-settings-form">${a.settingsSchema.map(t=>{const d=a.settings?.[t.key]??t.default??"";if(t.type==="select"){const g=t.options.map(b=>`<option value="${n(b.value)}"${String(d)===String(b.value)?" selected":""}>${n(b.label)}</option>`).join("");return`<div class="form-group">
30
+ <label class="form-label">${n(t.label)}</label>
31
+ <select class="form-control" name="${n(t.key)}">${g}</select>
32
32
  </div>`}return t.type==="number"?`<div class="form-group">
33
- <label class="form-label">${a(t.label)}</label>
34
- <input class="form-control" type="number" name="${a(t.key)}" value="${a(String(d))}"${t.min!=null?` min="${t.min}"`:""}${t.max!=null?` max="${t.max}"`:""}>
33
+ <label class="form-label">${n(t.label)}</label>
34
+ <input class="form-control" type="number" name="${n(t.key)}" value="${n(String(d))}"${t.min!=null?` min="${t.min}"`:""}${t.max!=null?` max="${t.max}"`:""}>
35
35
  </div>`:`<div class="form-group">
36
- <label class="form-label">${a(t.label)}</label>
37
- <input class="form-control" type="text" name="${a(t.key)}" value="${a(String(d))}">
38
- </div>`}).join("")}</form>`;if(!await E.confirm(s,{title:`${a(n.displayName)} Settings`,confirmText:"Save",cancelText:"Cancel",html:!0}))return;const c=document.getElementById("plugin-settings-form");if(!c)return;const u={};n.settingsSchema.forEach(t=>{const d=c.elements[t.key];d&&(u[t.key]=t.type==="number"?Number(d.value):d.value)});try{await r.plugins.update(e,{settings:u}),n.settings={...n.settings,...u},E.toast("Settings saved.",{type:"success"})}catch(t){E.toast(`Failed to save: ${t.message}`,{type:"error"})}}),i.on("click",".btn-bundle-plugin",async function(){const e=$(this),n=e.data("name"),l=e.data("bundled")!==1&&e.data("bundled")!=="1";e.prop("disabled",!0).text(l?"Bundling\u2026":"Unbundling\u2026");try{await r.plugins.update(n,{bundled:l}),E.toast(`Plugin ${l?"marked as bundled":"unbundled"}.`,{type:"success"});const s=await r.plugins.list();g(o,s)}catch(s){E.toast(`Failed: ${s.message}`,{type:"error"}),e.prop("disabled",!1)}}),i.on("click",".btn-toggle-plugin",async function(){const e=$(this),n=e.data("name"),l=e.data("enabled")!==1&&e.data("enabled")!=="1";e.prop("disabled",!0).text(l?"Enabling\u2026":"Disabling\u2026");try{await r.plugins.update(n,{enabled:l}),E.toast(`Plugin ${l?"enabled":"disabled"}. Restart the server to apply changes.`,{type:"success"});const s=await r.plugins.list();g(o,s)}catch(s){E.toast(`Failed: ${s.message}`,{type:"error"}),e.prop("disabled",!1)}})}function a(o){return String(o).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
36
+ <label class="form-label">${n(t.label)}</label>
37
+ <input class="form-control" type="text" name="${n(t.key)}" value="${n(String(d))}">
38
+ </div>`}).join("")}</form>`;if(!await E.confirm(o,{title:`${n(a.displayName)} Settings`,confirmText:"Save",cancelText:"Cancel",html:!0}))return;const m=document.getElementById("plugin-settings-form");if(!m)return;const p={};a.settingsSchema.forEach(t=>{const d=m.elements[t.key];d&&(p[t.key]=t.type==="number"?Number(d.value):d.value)});try{await i.plugins.update(e,{settings:p}),a.settings={...a.settings,...p},E.toast("Settings saved.",{type:"success"})}catch(t){E.toast(`Failed to save: ${t.message}`,{type:"error"})}}),r.on("click",".btn-bundle-plugin",async function(){const e=$(this),a=e.data("name"),s=e.data("bundled")!==1&&e.data("bundled")!=="1";e.prop("disabled",!0).text(s?"Bundling\u2026":"Unbundling\u2026");try{await i.plugins.update(a,{bundled:s}),E.toast(`Plugin ${s?"marked as bundled":"unbundled"}.`,{type:"success"});const o=await i.plugins.list();y(l,o)}catch(o){E.toast(`Failed: ${o.message}`,{type:"error"}),e.prop("disabled",!1)}}),r.on("click",".btn-toggle-plugin",async function(){const e=$(this),a=e.data("name"),s=e.data("enabled")!==1&&e.data("enabled")!=="1";e.prop("disabled",!0).text(s?"Enabling\u2026":"Disabling\u2026");try{await i.plugins.update(a,{enabled:s}),E.toast(`Plugin ${s?"enabled":"disabled"}. Restart the server to apply changes.`,{type:"success"});const o=await i.plugins.list();y(l,o)}catch(o){E.toast(`Failed: ${o.message}`,{type:"error"}),e.prop("disabled",!1)}})}function n(l){return String(l).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "brand": {
3
- "text": "Domma CMS",
4
- "url": "/",
5
- "icon": "home",
6
- "size": "lg"
3
+ "text": "My Bloggy Thing",
4
+ "logo": null,
5
+ "url": "/"
7
6
  },
8
7
  "items": [
9
8
  {
@@ -16,78 +15,12 @@
16
15
  "url": "/about",
17
16
  "icon": "info"
18
17
  },
19
- {
20
- "text": "Blog",
21
- "url": "/blog",
22
- "icon": "book-open"
23
- },
24
18
  {
25
19
  "text": "Contact",
26
20
  "url": "/contact",
27
21
  "icon": "mail"
28
- },
29
- {
30
- "text": "Feedback",
31
- "url": "/feedback",
32
- "icon": "message-square"
33
- },
34
- {
35
- "text": "Resources",
36
- "url": "/resources",
37
- "icon": "book-open",
38
- "hidden": true,
39
- "items": [
40
- {
41
- "text": "Overview",
42
- "url": "/resources",
43
- "icon": "grid"
44
- },
45
- {
46
- "text": "Typography",
47
- "url": "/resources/typography",
48
- "icon": "type"
49
- },
50
- {
51
- "text": "Grid System",
52
- "url": "/resources/grid",
53
- "icon": "layout"
54
- },
55
- {
56
- "text": "Cards",
57
- "url": "/resources/cards",
58
- "icon": "credit-card"
59
- },
60
- {
61
- "text": "Shortcode Reference",
62
- "url": "/resources/shortcodes",
63
- "icon": "code"
64
- },
65
- {
66
- "text": "Effects",
67
- "url": "/resources/effects",
68
- "icon": "zap"
69
- },
70
- {
71
- "text": "Interactive",
72
- "url": "/resources/interactive",
73
- "icon": "mouse-pointer"
74
- }
75
- ]
76
- },
77
- {
78
- "text": "Scratch",
79
- "url": "/scratch",
80
- "icon": "home",
81
- "hidden": true
82
22
  }
83
23
  ],
84
- "variant": "transparent",
85
- "position": "sticky",
86
- "style": {
87
- "fontFamily": "Roboto",
88
- "fontSize": "16px",
89
- "fontWeight": "400",
90
- "letterSpacing": "0em",
91
- "iconSize": "16px"
92
- }
24
+ "variant": "dark",
25
+ "position": "sticky"
93
26
  }
@@ -1,31 +1,27 @@
1
1
  {
2
- "theme-switcher": {
3
- "enabled": false,
4
- "settings": {},
5
- "bundled": true
6
- },
7
- "contacts": {
2
+ "todo": {
8
3
  "enabled": true,
4
+ "bundled": true,
9
5
  "settings": {}
10
6
  },
11
- "garage": {
7
+ "notes": {
12
8
  "enabled": true,
9
+ "bundled": true,
13
10
  "settings": {}
14
11
  },
15
- "site-search": {
16
- "enabled": false,
17
- "settings": {}
18
- },
19
- "notes": {
12
+ "contacts": {
20
13
  "enabled": true,
14
+ "bundled": true,
21
15
  "settings": {}
22
16
  },
23
- "todo": {
17
+ "site-search": {
24
18
  "enabled": true,
19
+ "bundled": true,
25
20
  "settings": {}
26
21
  },
27
- "docs": {
22
+ "blog": {
28
23
  "enabled": true,
24
+ "bundled": false,
29
25
  "settings": {}
30
26
  }
31
27
  }
@@ -1,30 +1,67 @@
1
1
  {
2
2
  "default": {
3
+ "key": "default",
3
4
  "label": "Default",
4
- "description": "Standard page with navbar and footer",
5
+ "description": "Standard page with navbar and footer.",
6
+ "builtin": true,
5
7
  "navbar": true,
6
8
  "footer": true,
7
- "sidebar": false
9
+ "sidebar": false,
10
+ "width": "normal",
11
+ "bgColor": "",
12
+ "bgImage": "",
13
+ "class": ""
14
+ },
15
+ "landing": {
16
+ "key": "landing",
17
+ "label": "Landing Page",
18
+ "description": "Full-width landing page with navbar, no footer.",
19
+ "builtin": true,
20
+ "navbar": true,
21
+ "footer": false,
22
+ "sidebar": false,
23
+ "width": "full",
24
+ "bgColor": "",
25
+ "bgImage": "",
26
+ "class": ""
27
+ },
28
+ "blank": {
29
+ "key": "blank",
30
+ "label": "Blank",
31
+ "description": "Minimal page with no navbar or footer.",
32
+ "builtin": true,
33
+ "navbar": false,
34
+ "footer": false,
35
+ "sidebar": false,
36
+ "width": "normal",
37
+ "bgColor": "",
38
+ "bgImage": "",
39
+ "class": ""
8
40
  },
9
41
  "with-sidebar": {
42
+ "key": "with-sidebar",
10
43
  "label": "With Sidebar",
11
- "description": "Page with navbar, sidebar, and footer",
44
+ "description": "Page with navbar, sidebar, and footer.",
45
+ "builtin": false,
12
46
  "navbar": true,
13
47
  "footer": true,
14
- "sidebar": true
48
+ "sidebar": true,
49
+ "width": "normal",
50
+ "bgColor": "",
51
+ "bgImage": "",
52
+ "class": ""
15
53
  },
16
54
  "minimal": {
55
+ "key": "minimal",
17
56
  "label": "Minimal",
18
- "description": "Clean page with no navbar or footer",
57
+ "description": "Clean page with no navbar or footer.",
58
+ "builtin": false,
19
59
  "navbar": false,
20
60
  "footer": false,
21
- "sidebar": false
22
- },
23
- "landing": {
24
- "label": "Landing Page",
25
- "description": "Full-width landing page layout",
26
- "navbar": true,
27
- "footer": true,
28
- "sidebar": false
61
+ "sidebar": false,
62
+ "width": "normal",
63
+ "bgColor": "",
64
+ "bgImage": "",
65
+ "class": ""
29
66
  }
30
67
  }
package/config/site.json CHANGED
@@ -1,27 +1,14 @@
1
1
  {
2
- "title": "My Nice Little Super Blog!",
3
- "tagline": "A nice little bloggy type thingy.",
4
- "fontFamily": "Roboto",
5
- "fontSize": 16,
6
- "theme": "lemon-light",
7
- "autoTheme": {
8
- "enabled": false,
9
- "dayTheme": "charcoal-light",
10
- "nightTheme": "charcoal-dark",
11
- "dayStart": "07:00",
12
- "nightStart": "19:00"
13
- },
14
- "adminTheme": "charcoal-dark",
15
- "layoutOptions": {
16
- "spacerSize": 40
17
- },
2
+ "title": "My Bloggy Thing",
3
+ "tagline": "This is the Domma CMS Testbed",
4
+ "theme": "charcoal-dark",
18
5
  "seo": {
19
- "defaultTitle": "My Nice Little Super Blog!",
6
+ "defaultTitle": "My Bloggy Thing",
20
7
  "titleSeparator": " | ",
21
8
  "defaultDescription": "A site built with Domma CMS"
22
9
  },
23
10
  "footer": {
24
- "copyright": "© 2026 DCBW Consulting Ltd",
11
+ "copyright": "© My Site. All rights reserved.",
25
12
  "links": [
26
13
  {
27
14
  "text": "Privacy Policy",
@@ -30,59 +17,20 @@
30
17
  {
31
18
  "text": "Contact",
32
19
  "url": "/contact"
33
- },
34
- {
35
- "text": "GDPR",
36
- "url": "/gdpr"
37
20
  }
38
21
  ]
39
22
  },
40
- "social": {
41
- "twitter": "",
42
- "facebook": "",
43
- "instagram": "",
44
- "linkedin": "",
45
- "github": "https://github.com/pinpointzero73/",
46
- "youtube": ""
47
- },
23
+ "social": {},
48
24
  "smtp": {
49
- "host": "192.168.1.27",
50
- "port": 1025,
25
+ "host": "",
26
+ "port": 587,
51
27
  "user": "",
52
28
  "pass": "",
53
29
  "secure": false,
54
- "fromAddress": "noreply@dwit.uk",
55
- "fromName": "My Big Boss Website"
56
- },
57
- "backToTop": {
58
- "enabled": true,
59
- "scrollThreshold": 180,
60
- "position": "bottom-right",
61
- "offset": 16,
62
- "bottomOffset": 16,
63
- "label": "",
64
- "smooth": true
65
- },
66
- "cookieConsent": {
67
- "enabled": true,
68
- "message": "We use cookies to enhance your browsing experience, serve personalised content, and analyse our traffic. By clicking \"Accept All\", you consent to our use of cookies.",
69
- "acceptAllText": "Accept All",
70
- "rejectAllText": "Reject All",
71
- "customizeText": "Customize",
72
- "savePreferencesText": "Save Preferences",
73
- "privacyPolicyText": "Privacy Policy",
74
- "privacyPolicyUrl": "/privacy-policy",
75
- "cookiePolicyText": "Cookie Policy",
76
- "cookiePolicyUrl": "",
77
- "position": "bottom",
78
- "layout": "bar",
79
- "theme": "dark",
80
- "showFunctional": true,
81
- "showAnalytics": true,
82
- "showMarketing": true,
83
- "consentVersion": "1.0"
30
+ "fromAddress": "",
31
+ "fromName": ""
84
32
  },
85
33
  "adminBrand": {
86
- "title": "Domma Admin"
34
+ "title": "Domma CMS"
87
35
  }
88
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.9.10",
3
+ "version": "0.12.0",
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",
@@ -27,6 +27,7 @@
27
27
  ],
28
28
  "scripts": {
29
29
  "build": "node scripts/build.js",
30
+ "create-plugin": "node scripts/create-plugin.js",
30
31
  "delete-users": "node -e \"import('fs').then(({readdirSync,rmSync})=>{const d='content/users';readdirSync(d).filter(f=>f.endsWith('.json')).forEach(f=>{rmSync(d+'/'+f);console.log('deleted',f)})})\"",
31
32
  "start": "node server/server.js",
32
33
  "pm2:start": "pm2 start ecosystem.config.cjs --env production",
@@ -0,0 +1,17 @@
1
+ <div class="view-header d-flex justify-content-between align-items-center mb-4">
2
+ <div>
3
+ <h1 class="view-title">PLUGIN_DISPLAY_NAME</h1>
4
+ <p class="view-subtitle text-muted">Manage PLUGIN_DISPLAY_NAME.</p>
5
+ </div>
6
+ <div class="d-flex gap-2">
7
+ <button id="btn-new-item" class="btn btn-primary">
8
+ <span data-icon="plus"></span> New Item
9
+ </button>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="card">
14
+ <div class="card-body">
15
+ <div id="items-table"></div>
16
+ </div>
17
+ </div>
@@ -0,0 +1,19 @@
1
+ /**
2
+ * PLUGIN_DISPLAY_NAME — admin view.
3
+ * Route: #/plugins/PLUGIN_SLUG
4
+ */
5
+ export const indexView = {
6
+ templateUrl: '/plugins/PLUGIN_SLUG/admin/templates/index.html',
7
+
8
+ async onMount($container) {
9
+ // Your view logic here.
10
+ // Available globals: $, _, M, D, S, H, F, E, I, T, R
11
+ //
12
+ // Fetch data: const data = await H.get('/api/plugins/PLUGIN_SLUG/items');
13
+ // Show toast: E.toast('Saved!', { type: 'success' });
14
+ // Confirm: if (!await E.confirm('Are you sure?')) return;
15
+ // Create table: T.create(el, { data, columns: [...] });
16
+ // Create form: F.create({ fields }, defaults, { showSubmitButton: false }).renderTo(el);
17
+ I.scan();
18
+ }
19
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * PLUGIN_DISPLAY_NAME plugin — default configuration.
3
+ * Merged with user overrides from config/plugins.json.
4
+ */
5
+ export default {
6
+ // Add your config keys and defaults here.
7
+ // Example: itemsPerPage: 20
8
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * PLUGIN_DISPLAY_NAME plugin — server-side routes and lifecycle hooks.
3
+ */
4
+
5
+ export default async function plugin(fastify, options) {
6
+ const { authenticate, requireAdmin } = options.auth;
7
+ const BASE = `/api/plugins/PLUGIN_SLUG`;
8
+
9
+ // Add your API routes here.
10
+ // Example:
11
+ // fastify.get(`${BASE}/items`, { preHandler: [authenticate] }, async (request, reply) => {
12
+ // return { items: [] };
13
+ // });
14
+ }
15
+
16
+ export async function onEnable({ fastify, services }) {
17
+ // Called when the plugin is enabled via admin.
18
+ // Create collections, pages, or forms here if needed.
19
+ }
20
+
21
+ export async function onDisable({ fastify, services }) {
22
+ // Called when the plugin is disabled via admin.
23
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "PLUGIN_SLUG",
3
+ "displayName": "PLUGIN_DISPLAY_NAME",
4
+ "version": "1.0.0",
5
+ "description": "PLUGIN_DESCRIPTION",
6
+ "author": "PLUGIN_AUTHOR",
7
+ "date": "PLUGIN_DATE",
8
+ "icon": "box",
9
+ "admin": {
10
+ "css": ["admin/css/index.css"],
11
+ "sidebar": [
12
+ {
13
+ "id": "PLUGIN_SLUG",
14
+ "text": "PLUGIN_DISPLAY_NAME",
15
+ "icon": "box",
16
+ "url": "#/plugins/PLUGIN_SLUG",
17
+ "section": "#/plugins/PLUGIN_SLUG"
18
+ }
19
+ ],
20
+ "routes": [
21
+ {
22
+ "path": "/plugins/PLUGIN_SLUG",
23
+ "view": "plugin-PLUGIN_SLUG",
24
+ "title": "PLUGIN_DISPLAY_NAME - Domma CMS"
25
+ }
26
+ ],
27
+ "views": {
28
+ "plugin-PLUGIN_SLUG": {
29
+ "entry": "PLUGIN_SLUG/admin/views/index.js",
30
+ "exportName": "indexView"
31
+ }
32
+ }
33
+ }
34
+ }
@@ -1,35 +1,45 @@
1
1
  {
2
2
  "name": "analytics",
3
- "displayName": "Analytics",
4
- "version": "1.0.0",
5
- "description": "Basic page view analytics. Tracks hits per page using a simple JSON store.",
6
- "author": "Darryl Waterhouse",
7
- "date": "2026-03-01",
8
- "icon": "chart-bar",
9
- "admin": {
10
- "sidebar": [
11
- { "id": "analytics", "text": "Analytics", "icon": "chart-bar", "url": "#/plugins/analytics", "section": "#/plugins/analytics" }
12
- ],
13
- "routes": [
14
- { "path": "/plugins/analytics", "view": "plugin-analytics", "title": "Analytics - Domma CMS" }
15
- ],
16
- "views": {
17
- "plugin-analytics": {
18
- "entry": "analytics/admin/views/analytics.js",
19
- "exportName": "analyticsView"
20
- }
21
- }
22
- },
23
- "inject": {
24
- "head": "public/inject-head.html",
25
- "bodyEnd": "public/inject-body.html"
26
- },
27
- "scaffold": {
28
- "reset": [
29
- {
30
- "path": "stats.json",
31
- "content": "{}"
32
- }
33
- ]
3
+ "displayName": "Analytics",
4
+ "version": "1.0.0",
5
+ "description": "Basic page view analytics. Tracks hits per page using a simple JSON store.",
6
+ "author": "Darryl Waterhouse",
7
+ "date": "2026-03-01",
8
+ "icon": "chart-bar",
9
+ "admin": {
10
+ "sidebar": [
11
+ {
12
+ "id": "analytics",
13
+ "text": "Analytics",
14
+ "icon": "chart-bar",
15
+ "url": "#/plugins/analytics",
16
+ "section": "#/plugins/analytics"
17
+ }
18
+ ],
19
+ "routes": [
20
+ {
21
+ "path": "/plugins/analytics",
22
+ "view": "plugin-analytics",
23
+ "title": "Analytics - Domma CMS"
24
+ }
25
+ ],
26
+ "views": {
27
+ "plugin-analytics": {
28
+ "entry": "analytics/admin/views/analytics.js?v=80339cdc",
29
+ "exportName": "analyticsView"
30
+ }
34
31
  }
32
+ },
33
+ "inject": {
34
+ "head": "public/inject-head.html",
35
+ "bodyEnd": "public/inject-body.html"
36
+ },
37
+ "scaffold": {
38
+ "reset": [
39
+ {
40
+ "path": "stats.json",
41
+ "content": "{}"
42
+ }
43
+ ]
44
+ }
35
45
  }
@@ -0,0 +1,22 @@
1
+ <div class="view-header d-flex align-items-center justify-content-between mb-4">
2
+ <h1 class="h3 mb-0">Blog Posts</h1>
3
+ <button id="btn-new-post" class="btn btn-primary">
4
+ <span data-icon="plus"></span> New Post
5
+ </button>
6
+ </div>
7
+
8
+ <div class="card mb-3">
9
+ <div class="card-body">
10
+ <div class="d-flex gap-2 flex-wrap">
11
+ <select id="filter-status" class="form-select" style="width:auto;">
12
+ <option value="">All statuses</option>
13
+ <option value="draft">Draft</option>
14
+ <option value="scheduled">Scheduled</option>
15
+ <option value="published">Published</option>
16
+ </select>
17
+ <input id="filter-search" type="text" class="form-control" style="width:200px;" placeholder="Search posts…">
18
+ </div>
19
+ </div>
20
+ </div>
21
+
22
+ <div id="posts-table"></div>
@@ -0,0 +1,7 @@
1
+ <div class="view-header d-flex align-items-center justify-content-between mb-4">
2
+ <h1 class="h3 mb-0">Blog Categories</h1>
3
+ <button id="btn-new-category" class="btn btn-primary">
4
+ <span data-icon="plus"></span> New Category
5
+ </button>
6
+ </div>
7
+ <div id="categories-table"></div>
@@ -0,0 +1,11 @@
1
+ <div class="view-header mb-4">
2
+ <h1 class="h3 mb-0">Blog Comments</h1>
3
+ </div>
4
+
5
+ <div class="tab-list mb-3">
6
+ <button class="tab-item active" data-tab="pending">Pending</button>
7
+ <button class="tab-item" data-tab="approved">Approved</button>
8
+ <button class="tab-item" data-tab="spam">Spam</button>
9
+ </div>
10
+
11
+ <div id="comments-table"></div>