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,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools CSS v0.44.6
2
+ * Domma Tools CSS v0.44.7
3
3
  * Dynamic Object Manipulation & Modeling API
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-09-26T15:34:30.010Z
6
- * Commit: b3ed216
5
+ * Built: 2026-09-27T08:47:51.458Z
6
+ * Commit: 91af217
7
7
  */
8
8
 
9
9
  /*!
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools v0.44.6
2
+ * Domma Tools v0.44.7
3
3
  * Developer tools: Theme Roller & Page Roller
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-09-26T15:34:15.956Z
6
- * Commit: b3ed216
5
+ * Built: 2026-09-27T08:47:41.655Z
6
+ * Commit: 91af217
7
7
  *
8
8
  * Requires: domma.min.js
9
9
  */
package/admin/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import{views as q}from"./views/index.js";import{withPluginChrome as B}from"./lib/plugin-chrome.js";import{api as l,getUser as O,isAuthenticated as u,logout as z,sendToLogin as K}from"./api.js";import{isAuthScreen as C,loginHashFor as J}from"./lib/login-arrange.js";import{installHttpInterceptor as Y}from"./http-interceptor.js";import{installResizableSlideovers as G}from"./lib/slideover-resizable.js";import{installActivityThrobber as Q}from"./lib/activity-throbber.js";import{isPathAllowed as X,redirectFor as Z}from"./lib/admin-scope.js";import{deliverDesktop as tt,desktopActive as et,notificationSettings as it}from"./lib/desktop-notify.js";import{avatarUrl as ot}from"./lib/avatar.js";$(()=>{Y(),Q(),G(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:e}=await import("./lib/admin-theme.js");u()?await e(l,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let c=null,r="/",h=!1,w=null;function b(t){if(y[t])return E.toast(`${y[t]} is switched off on this site.`,{type:"info"}),"/";const e=D[t]||Z(c,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?D[r]||r:null)}const D={},y={};R.use(async(t,e,i)=>{if(C(t.path))return i();if(!u()){K();return}const o=b(t.path);if(o){R.navigate(o);return}i()});async function g(){try{const t=await l.get("/auth/permissions");c=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return h=X(c,"/settings")&&e.some(i=>i==="settings"||i.startsWith("settings.")),w=e.some(i=>i==="notifications"||i.startsWith("notifications.")),e}catch{return c=null,r="/",h=!1,w=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{g()});let N=null,P=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(N){P=!0;return}N=(async()=>{do{P=!1;try{await j(await g())}catch{}}while(P)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:c})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(d&&(clearInterval(d),d=null),c=null,r="/",h=!1,w=null)});const V=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Plugin - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(e?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(l)),await x();const i=await g(),o=b(t.path);if(o&&R.navigate(o),j(i),it.read().signInToasts)try{const a=await l.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const f=p(s.data?.title||""),k=p((s.data?.body||"").slice(0,120));E.toast(`${f} - ${k}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}T()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const F=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function W(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const e=t?String(t.path).split("?")[0]:"";if(!t||!W(e))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,f=()=>{a+=n;for(const k of F){const v=document.querySelector(k);if(!v)continue;if(Array.from(v.options||[]).find(H=>H.value===o)){v.value=o,v.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(f,n)};setTimeout(f,n)});const I="cms_card_states",A=S.get(I)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&A[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const i=$(this).find("h2, h3").first().text().trim();i&&(A[i]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(I,A))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const m={...q},_={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},dashboard:{displayName:"Dashboard",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,e]of Object.entries(_))m[t]&&(m[t]=B(m[t],{plugin:t,licence:"Built in",...e}));const U=[...V];async function x(){if(u())try{const t=await l.plugins.adminConfig();t.routes?.length&&U.push(...t.routes);for(const[e,i]of Object.entries(t.views||{}))try{const o=await import(`/plugins/${i.entry}`);m[e]=B(o[i.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:i}]of Object.entries(t.takeovers||{}))i&&(D[`/${e}`]=i);for(const[e,{enabled:i,displayName:o}]of Object.entries(t.tools||{}))i===!1&&(y[`/${e}`]=o);for(const{id:e,href:i}of t.css||[])if(!document.getElementById(e)){const o=document.createElement("link");o.id=e,o.rel="stylesheet",o.href=i,document.head.appendChild(o)}}catch{}}function T(){const t=O();if(!t||$("#topbar-user-name").length)return;const i={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ot(t);$("#topbar-user").html(`
1
+ import{views as q}from"./views/index.js";import{withPluginChrome as O}from"./lib/plugin-chrome.js";import{api as l,getUser as B,isAuthenticated as u,logout as z,sendToLogin as K}from"./api.js";import{isAuthScreen as C,loginHashFor as J}from"./lib/login-arrange.js";import{installHttpInterceptor as Y}from"./http-interceptor.js";import{installResizableSlideovers as G}from"./lib/slideover-resizable.js";import{installActivityThrobber as Q}from"./lib/activity-throbber.js";import{isPathAllowed as X,redirectFor as Z}from"./lib/admin-scope.js";import{deliverDesktop as tt,desktopActive as et,notificationSettings as it}from"./lib/desktop-notify.js";import{avatarUrl as ot}from"./lib/avatar.js";$(()=>{Y(),Q(),G(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:e}=await import("./lib/admin-theme.js");u()?await e(l,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let c=null,r="/",h=!1,w=null;function b(t){if(y[t])return E.toast(`${y[t]} is switched off on this site.`,{type:"info"}),"/";const e=D[t]||Z(c,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?D[r]||r:null)}const D={},y={};R.use(async(t,e,i)=>{if(C(t.path))return i();if(!u()){K();return}const o=b(t.path);if(o){R.navigate(o);return}i()});async function g(){try{const t=await l.get("/auth/permissions");c=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return h=X(c,"/settings")&&e.some(i=>i==="settings"||i.startsWith("settings.")),w=e.some(i=>i==="notifications"||i.startsWith("notifications.")),e}catch{return c=null,r="/",h=!1,w=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{g()});let N=null,P=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(N){P=!0;return}N=(async()=>{do{P=!1;try{await j(await g())}catch{}}while(P)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:c})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(d&&(clearInterval(d),d=null),c=null,r="/",h=!1,w=null)});const V=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Plugin - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(e?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(l)),await x();const i=await g(),o=b(t.path);if(o&&R.navigate(o),j(i),it.read().signInToasts)try{const a=await l.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const f=p(s.data?.title||""),k=p((s.data?.body||"").slice(0,120));E.toast(`${f} - ${k}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}T()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const F=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function W(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const e=t?String(t.path).split("?")[0]:"";if(!t||!W(e))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,f=()=>{a+=n;for(const k of F){const v=document.querySelector(k);if(!v)continue;if(Array.from(v.options||[]).find(H=>H.value===o)){v.value=o,v.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(f,n)};setTimeout(f,n)});const I="cms_card_states",A=S.get(I)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&A[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const i=$(this).find("h2, h3").first().text().trim();i&&(A[i]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(I,A))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const m={...q},_={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},dashboard:{displayName:"Dashboard",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,e]of Object.entries(_))m[t]&&(m[t]=O(m[t],{plugin:t,licence:"Built in",...e}));const U=[...V];async function x(){if(u())try{const t=await l.plugins.adminConfig();t.routes?.length&&U.push(...t.routes);for(const[e,i]of Object.entries(t.views||{}))try{const o=await import(`/plugins/${i.entry}`);m[e]=O(o[i.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:i}]of Object.entries(t.takeovers||{}))i&&(D[`/${e}`]=i);for(const[e,{enabled:i,displayName:o}]of Object.entries(t.tools||{}))i===!1&&(y[`/${e}`]=o);for(const{id:e,href:i}of t.css||[])if(!document.getElementById(e)){const o=document.createElement("link");o.id=e,o.rel="stylesheet",o.href=i,document.head.appendChild(o)}}catch{}}function T(){const t=B();if(!t||$("#topbar-user-name").length)return;const i={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ot(t);$("#topbar-user").html(`
2
2
  ${o?`<img id="topbar-avatar" class="topbar-avatar" src="${p(o)}" alt="">`:""}
3
3
  <span id="topbar-user-name" class="topbar-user-name">${p(t.name)}</span>
4
4
  <span class="topbar-role-badge topbar-role-badge--${p(t.role)}">${i}</span>
@@ -19,4 +19,4 @@ import{views as q}from"./views/index.js";import{withPluginChrome as B}from"./lib
19
19
  <span data-icon="log-out"></span>
20
20
  <span>Sign out</span>
21
21
  </a>
22
- `);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!h),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),z()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),L(),d||(d=setInterval(L,6e4))}function p(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let d=null;async function L(){if(!(!u()||w===!1))try{const{count:t}=await l.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const e=$("#topbar-bell-badge");t>0?e.text(t>99?"99+":String(t)).removeAttr("hidden"):e.attr("hidden","").text(""),t>0&&et()&&tt(await l.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!u()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=J(window.location.hash));else if(await x(),O()){const i=await g(),o=(window.location.hash||"#/").slice(1)||"/",a=b(o);a&&(window.location.hash=`#${a}`),await j(i),T()}const t=R._extractParams.bind(R);R._extractParams=function(e,i){const o=i.indexOf("?");if(o!==-1&&(i=i.slice(0,o)),e.endsWith("/*")){const a=e.slice(0,-2);return i.startsWith(a+"/")?{}:null}return t(e,i)},R.init({container:"#view-container",routes:U,views:m,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
22
+ `);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!h),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),z()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),L(),d||(d=setInterval(L,6e4))}function p(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let d=null;async function L(){if(!(!u()||w===!1))try{const{count:t}=await l.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const e=$("#topbar-bell-badge");t>0?e.text(t>99?"99+":String(t)).removeAttr("hidden"):e.attr("hidden","").text(""),t>0&&et()&&tt(await l.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!u()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=J(window.location.hash));else if(await x(),B()){const i=await g(),o=(window.location.hash||"#/").slice(1)||"/",a=b(o);a&&(window.location.hash=`#${a}`),await j(i),T()}const t=R._extractParams.bind(R);R._extractParams=function(e,i){const o=i.indexOf("?");if(o!==-1&&(i=i.slice(0,o)),e.endsWith("/*")){const a=e.slice(0,-2);return i.startsWith(a+"/")?{}:null}return t(e,i)},R.init({container:"#view-container",routes:U,views:m,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
@@ -1 +1 @@
1
- export const SCREEN=/^\/pages\/(new|edit\/.*?)(\?.*)?$/;export function pageOfPath(t){const e=String(t||"").match(/^\/pages\/edit(\/[^?#]*)/);return e?e[1]:""}export const draftKey=t=>`page-editor.draft.${t||"new"}`,FIELDS=["urlPath","title","description","layout","status","sortOrder","showInNav","sidebar","breadcrumbs","bundled","tags","category","visibility","project","theme","seoTitle","seoDesc","dconfig","body","css"];export function snapshot(t={}){const e=t||{},s={};for(const n of FIELDS){const o=e[n];n==="tags"?s[n]=Array.isArray(o)?o.map(String):[]:typeof o=="boolean"?s[n]=o:n==="css"?s[n]=o==null?null:String(o):s[n]=o==null?"":String(o)}return JSON.stringify(s)}export function makeDraft(t,e,s=new Date().toISOString()){return{state:JSON.parse(snapshot(t)),base:e,at:s}}export function readDraft(t){return!t||typeof t!="object"||!t.state||typeof t.state!="object"?null:{state:JSON.parse(snapshot(t.state)),base:typeof t.base=="string"?t.base:"",at:typeof t.at=="string"?t.at:""}}export const draftIsStale=(t,e)=>!!(t?.base&&e&&t.base!==e);export function problems(t={}){const e=[],s=String(t.urlPath||"").trim();s?s.startsWith("/")?/\s/.test(s)&&e.push({field:"urlPath",text:"The address cannot contain spaces."}):e.push({field:"urlPath",text:"The address starts with / - for example /about."}):e.push({field:"urlPath",text:"The page needs an address - its URL path."});const n=String(t.dconfig||"").trim();if(n)try{const o=JSON.parse(n);(!o||typeof o!="object"||Array.isArray(o))&&e.push({field:"dconfig",text:"DConfig must be a JSON object ({ \u2026 })."})}catch{e.push({field:"dconfig",text:"DConfig is not valid JSON."})}return e}export function bannerSection(t,e,s){if(!t)return"New page";const n=String(e||"").trim();return n?`Edit \u201C${n}\u201D`:`Edit ${s||""}`.trim()}
1
+ export const SCREEN=/^\/pages\/(new|edit\/.*?)(\?.*)?$/;export function pageOfPath(t){const e=String(t||"").match(/^\/pages\/edit(\/[^?#]*)/);return e?e[1]:""}export const draftKey=t=>`page-editor.draft.${t||"new"}`,FIELDS=["urlPath","title","description","layout","status","sortOrder","showInNav","sidebar","breadcrumbs","bundled","tags","category","visibility","project","theme","seoTitle","seoDesc","dconfig","body","css"];export function snapshot(t={}){const e=t||{},n={};for(const r of FIELDS){const o=e[r];r==="tags"?n[r]=Array.isArray(o)?o.map(String):[]:typeof o=="boolean"?n[r]=o:r==="css"?n[r]=o==null?null:String(o):n[r]=o==null?"":String(o)}return JSON.stringify(n)}export function makeDraft(t,e,n=new Date().toISOString()){return{state:JSON.parse(snapshot(t)),base:e,at:n}}export function readDraft(t){return!t||typeof t!="object"||!t.state||typeof t.state!="object"?null:{state:JSON.parse(snapshot(t.state)),base:typeof t.base=="string"?t.base:"",at:typeof t.at=="string"?t.at:""}}export const draftIsStale=(t,e)=>!!(t?.base&&e&&t.base!==e);export function problems(t={}){const e=[],n=String(t.urlPath||"").trim();n?n.startsWith("/")?/\s/.test(n)&&e.push({field:"urlPath",text:"The address cannot contain spaces."}):e.push({field:"urlPath",text:"The address starts with / - for example /about."}):e.push({field:"urlPath",text:"The page needs an address - its URL path."});const r=String(t.dconfig||"").trim();if(r)try{const o=JSON.parse(r);(!o||typeof o!="object"||Array.isArray(o))&&e.push({field:"dconfig",text:"DConfig must be a JSON object ({ \u2026 })."})}catch{e.push({field:"dconfig",text:"DConfig is not valid JSON."})}return e}export function bannerSection(t,e,n){if(!t)return"New page";const r=String(e||"").trim();return r?`Edit \u201C${r}\u201D`:`Edit ${n||""}`.trim()}export function initialProject({isEdit:t,page:e=null,projects:n=[],fromHash:r="",urlPath:o=""}){if(!t&&r)return r;if(!e||e.project===null)return"";if(typeof e.project=="string"&&e.project)return e.project;if(!o)return"";let i="",a=-1;for(const s of n||[])s?.rootUrl&&(o===s.rootUrl||o.startsWith(s.rootUrl+"/"))&&s.rootUrl.length>a&&(i=s.slug,a=s.rootUrl.length);return i}
@@ -0,0 +1,153 @@
1
+ import{onceClosed as p}from"./tool-kit.js";const m=([o,a,i])=>i===void 0?{code:o,where:"",text:a}:{code:o,where:a,text:i},e=(o,a,i="")=>({title:o,code:a,note:i||""}),t=(o,{intro:a="",head:i=null,rows:s=[],block:l="",examples:n=[],outro:r=""}={})=>({title:o,intro:a,head:(i||[]).map((d,c)=>({i:c,text:d})),rows:s.map(m),block:l,examples:n,outro:r}),u=[t("Markdown Basics",{head:["Syntax","Result"],rows:[["**bold**","bold (rendered bold)"],["_italic_","italic (rendered italic)"],["## Heading","Heading (h2)"],["[text](url)","Link"],["![alt](url)","Image"],["- item","Bullet list"],["`code`","Inline code"],["---","Divider"]]}),t("Keyboard Shortcuts",{rows:[["Ctrl + B","Bold selected text"],["Ctrl + I","Italic selected text"],["Ctrl + K","Insert link dialog"],["Tab","Indent 2 spaces at cursor"],["Shift + Tab","Dedent 2 spaces on current line"],["Ctrl + X","Cut current line (no selection) or cut selection"],["Ctrl + C","Copy current line (no selection) or copy selection"],["Ctrl + V","Paste from clipboard"],["Ctrl + Z","Undo"],["Ctrl + Y","Redo"]]}),t("Tips",{rows:[['class="text-center"',"Centre text and inline elements on any shortcode"],['class="mx-auto"',"Centre a block element horizontally"],["[center]...[/center]","Shorthand wrapper for centred content"]],outro:"Most shortcodes accept a class attribute for custom styling."})],g=[t("Grid & Columns",{rows:[['cols="N"',"[grid] only","Number of columns (1-12)"],['gap="N"',"[grid], [row]","Gap between columns/rows (1-6)"],['span="N"',"[col] only","How many columns this cell spans"],['fullwidth="true"',"[grid] only","Break out of page container to span full viewport"],['class="x"',"all","Add extra CSS classes"]],examples:[e("Equal columns",`[grid cols="3" gap="4"]
2
+ [col]One[/col]
3
+ [col]Two[/col]
4
+ [col]Three[/col]
5
+ [/grid]`,"cols sets the track count; each [col] fills one track."),e("Column spanning (12-col base)",`[grid cols="12" gap="3"]
6
+ [col span="8"]Main content[/col]
7
+ [col span="4"]Sidebar[/col]
8
+ [/grid]`,"span values must add up to cols. Common splits: 8/4, 6/6, 9/3."),e("Row (flexbox, equal-width)",`[row gap="4"]
9
+ [col]Left[/col]
10
+ [col]Right[/col]
11
+ [/row]`,"Use [row] when you want equal-width columns without specifying a count."),e("Row with staggered reveal",`[row reveal reveal-mode="stagger" reveal-animation="slide-up" reveal-duration="400" reveal-stagger="60" reveal-delay="0" reveal-direction="ltr"]
12
+ [col]First[/col]
13
+ [col]Second[/col]
14
+ [col]Third[/col]
15
+ [/row]`,'Columns reveal one by one as the row scrolls into view. reveal-direction="rtl" reverses the order. Animations: slide-up, slide-down, slide-left, slide-right, fade, zoom, flip.'),e("Card in a grid",`[grid cols="3" gap="4"]
16
+ [col]
17
+ [card title="One"]Content[/card]
18
+ [/col]
19
+ [col]
20
+ [card title="Two"]Content[/card]
21
+ [/col]
22
+ [col]
23
+ [card title="Three"]Content[/card]
24
+ [/col]
25
+ [/grid]`)]}),t("Spacer",{intro:"Self-closing shortcode that inserts a fixed-height blank gap.",rows:[['size="N"',"Height in px (default: 8)"],['class="..."',"Extra CSS class on the spacer div"]],examples:[e("32px gap",'[spacer size="32" /]')]}),t("Center",{intro:"Wraps content in a centred div. Works with any content including cards, grids, and text.",examples:[e("Example","[center]Centred content here[/center]",'Accepts an optional class="..." attribute for extra styling.')]}),t("Hero",{intro:"Full-width hero sections - no plugin required. Uses Domma's built-in Hero CSS component.",rows:[['title="..."',"Heading text"],['tagline="..."',"Subtitle text"],['size="sm|lg|full"',"Height variant (default: normal)"],['variant="dark|primary|gradient-blue|gradient-purple|gradient-sunset|gradient-ocean"',"Colour / gradient preset"],['image="url"',"Background image URL (adds cover mode)"],['overlay="light|dark|darker|gradient|gradient-reverse"',"Image overlay style"],['align="center|left"',"Content alignment (default: center)"],['fullwidth="true"',"Break out of the page container to span full viewport width"],['bg="..."',"Background colour CSS value"],["twinkle","Flag: adds particle overlay (requires Effects plugin)"],['twinkle-count="N"',"Number of particles"],['twinkle-colour="..."',"Particle colour CSS value"],["blobs","Flag: adds ambient blob background"],['blobs-type="..."',"Blob animation type (default: float-blobs)"],['class="..."',"Extra CSS classes"],['id="..."',"Element id"]],examples:[e("Basic hero",'[hero title="Welcome" tagline="Build something great"][/hero]'),e("Gradient with body content",`[hero title="Get Started" tagline="Everything you need." size="lg" variant="gradient-blue" align="center"]
26
+ Some introductory **Markdown** content here.
27
+ [/hero]`),e("Background image with overlay",'[hero title="Our Story" image="/media/hero.jpg" overlay="dark" size="full"][/hero]',"Combine image + overlay for text legibility over photos.")]})],h=[t("Cards",{examples:[e("Basic card",`[card title="Optional Title"]
28
+ Markdown **works** here.
29
+ [/card]`,"Omit title for a card with no header."),e("Icon - inline (default)",`[card title="Feature" icon="star"]
30
+ Icon sits left of the title in a flex row.
31
+ [/card]`,"icon accepts any Domma icon name. Default layout when icon is set."),e("Icon - stacked (centred)",`[card title="Feature" icon="star" icon-layout="stacked" hover]
32
+ Icon is centred above the title. Great for feature tiles.
33
+ [/card]`,'icon-layout="stacked" centres the icon above the title. Combine with hover for a lift effect.'),e("Icon with subtitle",`[card title="Feature" subtitle="Tagline here" icon="star" icon-layout="stacked"]
34
+ Body content.
35
+ [/card]`,"subtitle adds a secondary line beneath the title in both layouts."),e("Collapsible card",`[card title="Click to expand" collapsible="true"]
36
+ Hidden by default.
37
+ [/card]`),e("Full attribute reference",`[card title="Title" subtitle="Sub" icon="star" icon-layout="stacked"
38
+ variant="primary" hover footer="Footer text"
39
+ collapsible="true" class="extra" id="my-card"]
40
+ Body content.
41
+ [/card]`,'All attributes are optional. variant accepts "primary". icon-layout accepts "inline" (default) or "stacked".'),e("Rich header / footer sub-tags",`[card]
42
+ [header]
43
+ ### Custom Title with **bold** and [a link](/about)
44
+ [/header]
45
+
46
+ Body content here.
47
+
48
+ [footer]
49
+ [Read more](/about) | [icon name="arrow-right" /]
50
+ [/footer]
51
+ [/card]`,"Use [header] and [footer] sub-tags for Markdown-rendered header/footer regions. Sub-tags override the title/subtitle/icon and footer attributes when both are present.")]}),t("Badge",{intro:"Inline badge/label elements. Self-closing renders an empty coloured dot.",rows:[['variant="..."',"primary (default), secondary, success, danger, warning, info, light, dark"],["pill","Flag: rounded pill shape (.badge-pill)"],["outline","Flag: outlined style (.badge-outline)"],['size="small|large"',"Reduced or enlarged badge"]],examples:[e("Basic badge",'[badge variant="success"]New[/badge]'),e("Pill outline badge",'[badge variant="danger" outline pill]Deprecated[/badge]')]}),t("Button",{intro:"Renders a styled anchor as a Domma button. Use the Insert \u2192 Button toolbar item for a guided dialog.",rows:[['href="..."',"Link destination (required)"],['label="..."',"Button text (self-closing form)"],['variant="..."',"primary (default), secondary, success, danger, warning, info, outline, ghost, link, outline-*"],['size="sm|lg"',"Button size modifier"],['icon="..."',"Domma icon name before the label"],['icon-after="..."',"Domma icon name after the label"],['target="..."','Link target, e.g. "_blank"'],['class="..."',"Extra CSS classes"],['id="..."',"Element id"]],examples:[e("Primary button",'[button href="/contact" variant="primary"]Get in touch[/button]'),e("Self-closing with icon",'[button href="/download" variant="success" icon="download" size="sm" label="Download" /]'),e("Opens in new tab",'[button href="https://example.com" variant="outline" target="_blank"]Visit site[/button]')]}),t("Link (shortcode)",{intro:"Renders a plain anchor with optional icon, target, and class. Use Insert \u2192 Link for a guided dialog. For simple inline links, standard Markdown [text](url) is fine.",rows:[['href="..."',"Link destination (required)"],['label="..."',"Link text (self-closing form)"],['target="..."','Link target, e.g. "_blank"'],['class="..."',"CSS classes on the <a>"],['icon="..."',"Domma icon name before the text"],['icon-after="..."',"Domma icon name after the text"],['id="..."',"Element id"]],examples:[e("External link in new tab",'[link href="https://example.com" target="_blank" icon-after="external-link"]Visit site[/link]'),e("Self-closing with icon",'[link href="/about" icon="arrow-right" label="About us" /]')]}),t("Icon",{intro:"Self-closing shortcode that renders any Domma icon inline.",rows:[['name="..."',"Domma icon name (required). Also accepted as src=."],['size="N"',"Width and height in px"],['color="..."',"CSS colour applied via style"],['class="..."',"Extra CSS classes"]],examples:[e("Coloured icon",'[icon name="star" size="24" color="#f5a623" /]')]}),t("Interactive Components",{intro:"Tabs, accordion, carousel, and countdown - no plugin required.",examples:[e("Tabs",`[tabs]
52
+ [tab title="First"]Content **A**[/tab]
53
+ [tab title="Second"]Content **B**[/tab]
54
+ [/tabs]`,'Add style="pills" for pill-style navigation.'),e("Accordion",`[accordion]
55
+ [item title="Question 1"]Answer here.[/item]
56
+ [item title="Question 2"]Answer here.[/item]
57
+ [/accordion]`,'Add multiple="true" to allow several panels open at once.'),e("Accordion - collection-bound",'[collection slug="faq" display="accordion" title-field="title" body-field="description" /]',"Renders collection entries as accordion items. Use Insert \u2192 Accordion for a guided dialog."),e("Carousel",`[carousel autoplay="true" interval="5000"]
58
+ [slide title="Slide 1"]Description[/slide]
59
+ [slide image="/media/photo.jpg" title="Slide 2"]Caption[/slide]
60
+ [/carousel]`,'animation="slide|fade|crossfade" (default slide). Use Insert \u2192 Carousel for a guided dialog.'),e("Carousel - fade / crossfade",`[carousel animation="crossfade" animation-duration="800" animation-easing="ease-in-out" autoplay="true"]
61
+ [slide image="/media/one.jpg" title="One"][/slide]
62
+ [slide image="/media/two.jpg" title="Two"][/slide]
63
+ [/carousel]`,"fade swaps opacity per slide; crossfade overlaps the two. animation-easing accepts any CSS timing function (ease, linear, cubic-bezier(...))."),e("Countdown (to date)",'[countdown to="2026-12-31" format="DD:HH:mm:ss" /]'),e("Countdown (duration)",'[countdown duration="300" format="mm:ss" /]',"duration is in seconds. format options: mm:ss \xB7 HH:mm:ss \xB7 DD:HH:mm:ss")]}),t("Timeline",{intro:"Renders a Domma Progression component with event items.",rows:[['layout="vertical|centred|horizontal"',"[timeline] - layout orientation (default: vertical)"],['theme="minimal|corporate|modern"',"[timeline] - visual theme (default: minimal)"],['mode="timeline|roadmap"',"[timeline] - display mode (default: timeline)"],['class="..."',"[timeline] - extra CSS classes"],['id="..."',"[timeline] - element id"],['title="..."',"[event] - event heading"],['date="..."',"[event] - display date string"],['status="planned|in-progress|completed|blocked"',"[event] - progress status"],['icon="..."',"[event] - Domma icon name"]],examples:[e("Vertical timeline",`[timeline layout="vertical" theme="modern"]
64
+ [event title="Kickoff" date="Jan 2025" status="completed" icon="flag"]
65
+ Project launched.
66
+ [/event]
67
+ [event title="Beta" date="Jun 2025" status="in-progress" icon="rocket"]
68
+ In active development.
69
+ [/event]
70
+ [/timeline]`)]}),t("Tables",{intro:"Wrap a standard Markdown (GFM) table with Domma CSS classes and responsive horizontal scrolling.",rows:[['striped="true"',"Alternate row background (.table-striped)"],['bordered="true"',"Borders on all cells (.table-bordered)"],['compact="true"',"Reduced cell padding (.table-compact)"],['caption="..."',"Caption text above the table"],['class="..."',"Extra CSS classes appended to .table"],['id="..."',"id attribute on the <table> element"]],examples:[e("Striped + bordered table",`[table striped="true" bordered="true" caption="Product Pricing"]
71
+ | Product | Price |
72
+ | ------- | ----: |
73
+ | Widget | $9.99 |
74
+ | Gadget | $14.99 |
75
+ [/table]`,"The inner content must be a valid GFM Markdown table. The shortcode adds Domma CSS classes and wraps in a responsive scroll container.")]}),t("Slideover",{intro:"A trigger button that opens a slide-in panel with Markdown content. No plugin required.",rows:[['title="..."',"Panel header text"],['trigger="..."','Button label (default: "Open")'],['size="sm|md|lg"',"Panel width (default: md)"],['position="right|left"',"Slide direction (default: right)"]],examples:[e("Example",`[slideover title="More Info" trigger="Read more" size="md"]
76
+ ## Details
77
+ Markdown content here.
78
+ [/slideover]`,"Nested [card] and [grid] shortcodes work inside the slideover body.")]})],b=[t("Embedding a Form",{rows:[['name="..."',"Form slug (required). Also accepted as slug=."],['class="..."',"Extra CSS classes on the wrapper div"],['id="..."',"id attribute on the wrapper div"]],examples:[e("Basic",'[form name="contact" /]',"Forms are managed under Forms in the sidebar."),e("With wrapper styling",'[form name="newsletter" class="mt-4" id="newsletter-form" /]')]}),t("Displaying a Collection",{block:'[collection slug="enquiries" display="table" /]',rows:[["slug","Required. The collection slug."],["display","table (default), cards, or list."],["fields","Comma-separated field keys to show. Defaults to all."],["limit","Maximum number of entries to display."],["sort","Field key to sort by."],["order","asc or desc (default asc)."],["title-field","Field used as the card/list title (cards + list)."],["columns","Grid columns for cards display (2-4, default 3)."],["search","true/false - enable search in table mode (default true)."],["sortable","true/false - enable column sorting in table mode (default true)."],["exportable","true/false - show CSV export button in table mode (default false)."],["page-size","Rows per page in table mode (default 25)."],["empty","Text shown when there are no entries."],["cta","Action slug: enables per-entry CTA buttons."],["cta-label",'CTA button label (default: "Run").'],["cta-icon","CTA button icon name."],["cta-style","CTA button variant (default: primary)."],["cta-confirm","CTA confirmation prompt text."],["class","Extra CSS classes on the wrapper div."],["id","id attribute on the wrapper div."]],outro:"Table display uses Domma Table - search, sort, pagination, column selector, and CSV export are all built in. Cards and lists are static."}),t("View - Pro",{intro:"Renders a Collection with full filtering, pagination, and display options. Requires a Pro MongoDB collection.",rows:[["slug","Required. The collection slug."],["display","table (default), cards, or list."],["limit","Maximum number of entries to display."],["fields","Comma-separated field keys to show."],["title-field","Field used as the card/list title."],["columns","Grid columns for cards display (2-4, default 3)."],["empty","Text shown when there are no entries."],["class","Extra CSS classes on the wrapper div."],["id","id attribute on the wrapper div."]],examples:[e("Table display",'[view slug="products" display="table" limit="50" /]'),e("Cards display",'[view slug="team" display="cards" columns="3" title-field="name" /]')]}),t("CTA - Pro",{intro:"Renders an action button that triggers a Pro Action against an entry. Requires the Pro Actions feature.",rows:[["action","Required. The action slug."],["entry","Required. The entry id to act on."],["style","Button variant (default: primary)."],["icon","Domma icon name for the button."],["size","Button size (sm, lg)."],["confirm","Confirmation prompt text before running."],["label","Self-closing only: button label text."]],examples:[e("Wrapping form",'[cta action="approve" entry="abc123" style="success" confirm="Approve this entry?"]Approve[/cta]'),e("Self-closing",'[cta action="archive" entry="abc123" icon="archive" label="Archive" /]')]})],f=[t("Effects",{intro:"Shortcodes for scroll-triggered animations, counters, typewriter text, and more. Requires the Domma Effects plugin.",rows:[['[reveal animation="fade"]...[/reveal]',"Fade/slide/zoom on scroll"],["[breathe]...[/breathe]","Continuous gentle scale loop"],["[pulse]...[/pulse]","Repeating scale pulse"],["[shake]...[/shake]","One-shot shake"],['[scribe speed="50"]...[/scribe]',"Typewriter - simple mode (text typed letter by letter)"],['[scribe loop="true"][render]Text[/render][wait]1500[/wait][undo /][/scribe]',"Typewriter - script mode (sequenced render/wait/undo actions)"],["[scramble]...[/scramble]","Character-scramble reveal"],['[counter to="100" /]',"Animated number counter (self-closing)"],["[ripple]...[/ripple]","Click-triggered ripple"],['[twinkle count="50"]...[/twinkle]',"Particle/star overlay"],['[animate type="fade-in-up"]...[/animate]',"CSS-only animation (no plugin needed)"],['[ambient type="aurora"]...[/ambient]',"CSS-only animated background"],['[firework type="burst" colour="rainbow" /]',"CSS firework (self-closing)"],["[fireworks]...[/fireworks]","Fireworks display container"],['[celebrate theme="auto" /]',"Seasonal canvas celebration (auto-detects date)"],["[christmas /]","Christmas - snowflakes, trees, snowmen, sleigh"],["[halloween /]","Halloween - pumpkins, ghosts, bats, spiders"],["[st-patricks /]","St Patrick's Day - shamrocks, coins, rainbows, leprechaun"],["[valentines /]","Valentine's Day - hearts, roses"],["[guy-fawkes /]","Guy Fawkes - fireworks, sparkles"],["[st-andrews /]","St Andrew's Day - heather, thistles"],["[st-davids /]","St David's Day - daffodils, dragons"],["[st-georges /]","St George's Day - roses, flags"]],examples:[e("Scribe script - looping multi-phrase typewriter",`[scribe loop="true" loop-delay="2000" delete-speed="20"]
79
+ [render]We build things that matter.[/render]
80
+ [wait]1200[/wait]
81
+ [undo all="true" /]
82
+ [render]We design for humans.[/render]
83
+ [wait]1200[/wait]
84
+ [undo all="true" /]
85
+ [/scribe]`,'Use [render]...[/render] to type text, [wait]Ms[/wait] to pause, and [undo /] to delete. loop="true" replays the sequence. Only [render], [wait], and [undo] shortcodes are parsed - plain text between actions is ignored.'),e("Christmas",'[christmas intensity="medium" snowmen="off" trees="off" wreaths="off" northStars="off" sleigh="off" train="off" elf="off" robin="off" firework="off" /]',"intensity: light | medium | heavy. Count-based (off disables entirely): snowmen, trees, wreaths, northStars. Probability-based specials (off prevents spawning): sleigh, train, elf, robin, firework. Snowflakes always fall."),e("Halloween",'[halloween intensity="medium" gravestones="off" jackOLanterns="off" spiders="off" scarecrows="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): gravestones, jackOLanterns, spiders, scarecrows, twinklingStars. Particles: bats, ghosts, pumpkins (always on). Fixed decorations: haunted-house, cauldron (always present)."),e("St Patrick's Day",'[st-patricks intensity="medium" pots="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): pots (pots of gold), twinklingStars. Particles: shamrocks, clover petals, gold coins, sparkles (always on). Fixed decorations: rainbow, harp, moon. Leprechaun appears randomly - more likely at higher intensity."),e("Valentine's Day",'[valentines intensity="medium" garlands="off" butterflies="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): garlands (heart garlands), butterflies. Particles: hearts, rose petals, sparkles, lips (always on). Fixed decorations: envelopes (always present). Cupid appears randomly - more likely at higher intensity."),e("Guy Fawkes Night",'[guy-fawkes intensity="medium" bonfires="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): bonfires. Particles: embers, fireworks, sparks, rockets, bursts, trails (always on). Fixed decorations: guy-effigy, catherine-wheel, roman-candle, sparkler-bundle, moon. Starts at 25% particle density and builds up gradually."),e("St Andrew's Day",'[st-andrews intensity="medium" thistles="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): thistles, twinklingStars. Particles: heather petals, saltire sparkles (always on). Fixed decorations: bagpiper, saltire-flag, tartan-pattern, highland-scene (always present). Bagpiper probability increases with intensity."),e("St David's Day",'[st-davids intensity="medium" daffodilFields="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): daffodilFields, twinklingStars. Particles: daffodil petals, spring sparkles (always on). Fixed decorations: welsh-dragon, harp, leek-bundle, flag (always present). Flying dragon appears randomly - more likely at higher intensity."),e("St George's Day",'[st-georges intensity="medium" roses="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): roses (English roses), twinklingStars. Particles: rose petals, Tudor roses, oak leaves, sparkles (always on). Fixed decorations: st-georges-cross, dragon, shield, castle (always present). Knight appears randomly - more likely at higher intensity.")],outro:"Enable the Domma Effects plugin to activate these shortcodes on your site."}),t("DConfig - Declarative Behaviour",{intro:"Define click handlers and class toggles without writing JavaScript. Use the DConfig section above or embed inline with [dconfig]...[/dconfig] in the content body. Inline shortcodes win on selector conflict.",examples:[e("Toggle a class on click",'{ "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }','Selector keys use standard CSS selectors. "target" is optional - defaults to the element itself.'),e("Inline shortcode syntax",`[dconfig]
86
+ { "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }
87
+ [/dconfig]`)]}),t("CSS Editor",{intro:"The toolbar's </> button (left of the Split View icon) swaps the left pane between Markdown and Custom CSS. Edits are saved together with the page - one Save, one toast.",rows:[["</> button","Toggle between Markdown editor and Custom CSS editor"],["Split / Write / Preview","Standard view mode buttons to the right of </>"],["Save","Saves both page content and CSS in a single operation"]]}),t("Colour Picker",{intro:"Insert \u2192 Colour Picker opens a centred colour picker. Pick a colour, then use Copy to copy the hex code to the clipboard or Insert to drop it at the cursor. Click outside the panel to dismiss."})];export const REFERENCE=[{id:"basics",label:"Basics",sections:u},{id:"layout",label:"Layout",sections:g},{id:"components",label:"Components",sections:h},{id:"data",label:"Data",sections:b},{id:"advanced",label:"Advanced",sections:f}];const y=`
88
+ <div class="per">
89
+ <div class="per-tabs" role="tablist" data-each="tabs key=id">
90
+ <button type="button" class="per-tab" role="tab" data-on-click="$root.show(id)"
91
+ data-bind-class="$root.tab.value === id && 'is-on'" data-bind-aria-selected="$root.tab.value === id"
92
+ data-bind-text="label"></button>
93
+ </div>
94
+ <div data-each="tabs key=id">
95
+ <div class="per-panel" data-bind-hidden="$root.tab.value !== id">
96
+ {{#each sections key=title}}
97
+ <section class="per-section">
98
+ <h3 data-bind-text="title"></h3>
99
+ <p class="per-note" data-bind-hidden="!intro" data-bind-text="intro"></p>
100
+ <pre class="per-code per-block" data-bind-hidden="!block" data-bind-text="block"></pre>
101
+ <div class="per-table" role="table" data-bind-hidden="!rows.length">
102
+ <div class="per-tr per-thead" role="row" data-bind-hidden="!head.length">
103
+ {{#each head key=i}}<div class="per-th" role="columnheader" data-bind-text="text"></div>{{/each}}
104
+ </div>
105
+ {{#each rows key=code}}
106
+ <div class="per-tr" role="row">
107
+ <div class="per-td per-attr" role="cell"><code class="per-code" data-bind-text="code"></code></div>
108
+ <div class="per-td per-where" role="cell" data-bind-hidden="!where" data-bind-text="where"></div>
109
+ <div class="per-td per-desc" role="cell" data-bind-text="text"></div>
110
+ </div>
111
+ {{/each}}
112
+ </div>
113
+ {{#each examples key=title}}
114
+ <div class="per-example">
115
+ <p class="per-example-title" data-bind-text="title"></p>
116
+ <div class="per-example-code">
117
+ <pre class="per-code" data-bind-text="code"></pre>
118
+ <button type="button" class="per-copy" title="Copy code" aria-label="Copy code" data-on-click="$root.copy(code)">
119
+ <span data-icon="copy"></span></button>
120
+ </div>
121
+ <p class="per-note" data-bind-hidden="!note" data-bind-text="note"></p>
122
+ </div>
123
+ {{/each}}
124
+ <p class="per-note" data-bind-hidden="!outro" data-bind-text="outro"></p>
125
+ </section>
126
+ {{/each}}
127
+ </div>
128
+ </div>
129
+ </div>
130
+ <style>
131
+ .per { padding: 1rem; }
132
+ .per [hidden] { display: none !important; }
133
+ .per-tabs { display: flex; flex-wrap: wrap; gap: .25rem; border-bottom: 1px solid var(--dm-border); margin-bottom: 1rem; }
134
+ .per-tab { border: 0; background: none; color: var(--dm-text-muted); padding: .45rem .75rem; cursor: pointer; font: inherit;
135
+ border-bottom: 2px solid transparent; margin-bottom: -1px; }
136
+ .per-tab.is-on { color: var(--dm-primary); border-bottom-color: var(--dm-primary); }
137
+ .per-panel { display: flex; flex-direction: column; gap: 1rem; }
138
+ .per-section h3 { margin: 1rem 0 .5rem; font-size: 1rem; }
139
+ .per-code { background: var(--dm-background-alt); padding: .2rem .4rem; border-radius: 3px; font-size: .85em; font-family: monospace; }
140
+ pre.per-code { display: block; margin: 0; padding: .5rem 2rem .5rem .75rem; white-space: pre; overflow-x: auto; }
141
+ pre.per-block { padding-right: .75rem; margin-bottom: .5rem; }
142
+ .per-table { display: table; width: 100%; border-collapse: collapse; font-size: .85em; margin-bottom: .5rem; }
143
+ .per-tr { display: table-row; }
144
+ .per-th { display: table-cell; text-align: left; font-weight: 600; padding: .4rem .5rem; border-bottom: 1px solid var(--dm-border); }
145
+ .per-td { display: table-cell; padding: .3rem .4rem; border-bottom: 1px solid var(--dm-border); vertical-align: top; }
146
+ .per-where, .per-desc { color: var(--dm-text-muted); }
147
+ .per-note { margin: .3rem 0 .6rem; font-size: .82em; color: var(--dm-text-muted); }
148
+ .per-example-title { margin: .75rem 0 .25rem; font-size: .85em; font-weight: 600; }
149
+ .per-example-code { position: relative; }
150
+ .per-copy { position: absolute; top: .3rem; right: .3rem; background: none; border: none; cursor: pointer; opacity: .45; padding: .15rem;
151
+ line-height: 1; color: inherit; }
152
+ .per-copy:hover, .per-copy:focus-visible { opacity: 1; }
153
+ </style>`;async function w(o){try{return await navigator.clipboard.writeText(o),!0}catch{const a=document.createElement("textarea");a.value=o,a.style.cssText="position:fixed;opacity:0;",document.body.appendChild(a),a.select();let i=!1;try{i=document.execCommand("copy")}catch{}return a.remove(),i}}export function openEditorReference(){const o=E.slideover({title:"Editor Reference",size:"md",position:"right"}),a=document.createElement("div");a.innerHTML=y;const i=M.observable("basics"),s=M.applyBindings({tabs:REFERENCE,tab:i,show:n=>{i.value=n},copy:async n=>{const r=await w(n);E.toast(r?"Copied!":"Could not copy - select the code and copy it.",{type:r?"success":"warning",duration:1200})}},a);o.element.appendChild(a);const l=M.effect(()=>{i.value,requestAnimationFrame(()=>I.scan(a))});p(o,()=>{try{l()}catch{}try{s.dispose()}catch{}}),o.open()}
@@ -0,0 +1 @@
1
+ export const LIMITS={titleMin:30,titleMax:60,descMin:70,descMax:160,minWords:300};const A={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" ",ndash:"\u2013",mdash:"\u2014",hellip:"\u2026",lsquo:"\u2018",rsquo:"\u2019",ldquo:"\u201C",rdquo:"\u201D",pound:"\xA3",euro:"\u20AC",copy:"\xA9"};export function decode(e){return String(e??"").replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi,(s,n)=>{if(n[0]==="#"){const r=n[1]==="x"||n[1]==="X"?parseInt(n.slice(2),16):parseInt(n.slice(1),10);return Number.isFinite(r)&&r>0&&r<1114112?String.fromCodePoint(r):s}return A[n.toLowerCase()]??s})}const M=e=>decode(String(e??"").replace(/<[^>]*>/g," ")).replace(/\s+/g," ").trim();export function attrs(e){const s={},n=String(e).replace(/^<\s*[a-z0-9-]+/i,"").replace(/\/?>$/,""),r=/([^\s"'=<>`/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;let i;for(;i=r.exec(n);)s[i[1].toLowerCase()]=decode(i[2]??i[3]??i[4]??"");return s}export function scanHtml(e){const s=String(e??""),n=s.search(/<\/head\s*>/i),r=n>=0?s.slice(0,n):s,i=[];for(const l of s.matchAll(/<script\b[^>]*type\s*=\s*["']?application\/ld\+json["']?[^>]*>([\s\S]*?)<\/script>/gi))try{const c=JSON.parse(l[1]);for(const h of[c,...Array.isArray(c)?c:[],...Array.isArray(c?.["@graph"])?c["@graph"]:[]]){const o=h?.["@type"];o&&i.push(...(Array.isArray(o)?o:[o]).map(String))}}catch{i.push("(invalid)")}const a=s.replace(/<!--[\s\S]*?-->/g," ").replace(/<(script|style|template|noscript|svg)\b[\s\S]*?<\/\1\s*>/gi," "),p=[...r.matchAll(/<meta\b[^>]*>/gi)].map(l=>attrs(l[0])),t=(l,c)=>p.find(h=>(h[l]||"").toLowerCase()===c)?.content,$=[...r.matchAll(/<link\b[^>]*>/gi)].map(l=>attrs(l[0])),d=a.search(/<body\b/i),u=d>=0?a.slice(d):a,f=[...u.matchAll(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1\s*>/gi)].map(l=>({level:Number(l[1]),text:M(l[2])})),w=[...u.matchAll(/<img\b[^>]*>/gi)].map(l=>{const c=attrs(l[0]);return{src:c.src||c["data-src"]||"",alt:"alt"in c?c.alt:null,role:c.role||""}}),g=[...u.matchAll(/<a\b([^>]*)>([\s\S]*?)<\/a\s*>/gi)].map(l=>{const c=attrs(`<a ${l[1]}>`);return{href:c.href||"",text:M(l[2]),rel:c.rel||"",title:c.title||""}}),y=M(u),x=M(u.match(/<p\b[^>]*>([\s\S]*?)<\/p\s*>/i)?.[1]||"");return{title:M(r.match(/<title\b[^>]*>([\s\S]*?)<\/title\s*>/i)?.[1]||""),description:t("name","description")??null,robots:t("name","robots")||"",viewport:!!t("name","viewport"),canonical:$.find(l=>(l.rel||"").toLowerCase()==="canonical")?.href||"",lang:attrs(s.match(/<html\b[^>]*>/i)?.[0]||"<html>").lang||"",og:{title:t("property","og:title")||"",description:t("property","og:description")||"",image:t("property","og:image")||"",type:t("property","og:type")||""},twitter:{card:t("name","twitter:card")||"",image:t("name","twitter:image")||""},headings:f,images:w,links:g,words:y?y.split(/\s+/).filter(l=>/[\p{L}\p{N}]/u.test(l)).length:0,text:y.slice(0,2e4),firstPara:x.slice(0,1e3),jsonLdTypes:[...new Set(i)]}}const m=(e,s,n=`${s}s`)=>`${e} ${e===1?s:n}`,T=(e,s)=>!!s&&String(e||"").toLowerCase().includes(s),C=e=>String(e||"").toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");export function analyse(e,{urlPath:s="/",focus:n="",limits:r={},origin:i=""}={}){const a={...LIMITS,...r},p=[],t=(o,b,k,L,S,v="")=>p.push({id:o,group:b,status:k,title:L,detail:S,fix:v}),$=/noindex/i.test(e.robots),d=e.title.length;d?d<a.titleMin?t("title","Search result","warn","Title",`${d} characters - short. Search engines may write their own.`,`Aim for ${a.titleMin}-${a.titleMax} characters that say what the page is about.`):d>a.titleMax?t("title","Search result","warn","Title",`${d} characters - it will be cut off after about ${a.titleMax}.`,"Put the words that matter first, or shorten it."):t("title","Search result","pass","Title",`${d} characters.`):t("title","Search result","fail","Title","The page has no title.","Give it one: it is the blue link people click in the results.");const u=e.description,f=(u||"").length;f?f<a.descMin?t("description","Search result","warn","Description",`${f} characters - short.`,`Aim for ${a.descMin}-${a.descMax} characters.`):f>a.descMax?t("description","Search result","warn","Description",`${f} characters - it will be cut off after about ${a.descMax}.`,"Shorten it, keeping the point in the first half."):t("description","Search result","pass","Description",`${f} characters.`):t("description","Search result","fail","Description","No description: search engines pick any sentence from the page.","Write one or two sentences that make someone want to click.");const w=s.split("/").filter(Boolean).pop()||"";s!=="/"&&(w.length>60||/[A-Z_ %]/.test(w)||/\d{5,}/.test(w))?t("url","Search result","warn","Address",`"${w}" is long or hard to read.`,"Short, lower-case words joined by hyphens read best - rename the page (a redirect is added for you)."):t("url","Search result","pass","Address",s==="/"?"The home page.":`"${w}" reads well.`);const g=e.headings.filter(o=>o.level===1);g.length?g.length>1?t("h1","Content","warn","Main heading",`${g.length} H1 headings: "${g.slice(0,3).map(o=>o.text).join('", "')}".`,"Keep one H1; make the others H2."):g[0].text?t("h1","Content","pass","Main heading",`"${g[0].text.slice(0,80)}".`):t("h1","Content","warn","Main heading","The H1 heading is empty.","Give it words."):t("h1","Content","fail","Main heading","There is no H1 heading.","Start the page with one main heading (# in Markdown) saying what it is about.");const y=[];for(let o=1;o<e.headings.length;o++)e.headings[o].level>e.headings[o-1].level+1&&y.push(`H${e.headings[o-1].level} \u2192 H${e.headings[o].level}`);y.length?t("headings","Content","warn","Heading order",`Levels are skipped: ${[...new Set(y)].slice(0,3).join(", ")}.`,"Go down one level at a time (H2, then H3) - screen readers and search engines read them as an outline."):e.headings.length>1&&t("headings","Content","pass","Heading order",`${m(e.headings.length,"heading")}, in order.`),e.words<Math.round(a.minWords/3)?t("words","Content","warn","Length",`${m(e.words,"word")}. Very little for a search engine to go on.`,`Pages that rank usually have ${a.minWords}+ words - unless the page is a form or a contact page.`):e.words<a.minWords?t("words","Content","info","Length",`${m(e.words,"word")}.`,`Pages that rank usually have ${a.minWords}+ words.`):t("words","Content","pass","Length",`${e.words.toLocaleString("en-GB")} words.`);const x=e.images.filter(o=>o.role!=="presentation"),l=x.filter(o=>o.alt===null);l.length?t("alt","Content","fail","Image descriptions",`${m(l.length,"image")} of ${x.length} ${l.length===1?"has":"have"} no alt text.`,'Describe each image in a few words (empty alt="" only for decoration). It is how search and screen readers see them.'):x.length&&t("alt","Content","pass","Image descriptions",`All ${m(x.length,"image")} described.`);const c=e.links.filter(o=>N(o.href,i));!c.length&&e.links.length<50?t("links","Content","warn","Links to your other pages","None in the page itself.","Link to related pages: it helps visitors, and search engines find pages through links."):t("links","Content","pass","Links to your other pages",`${m(c.length,"link")}.`);const h=String(n||"").trim().toLowerCase();if(h){const b=[["title",T(e.title,h)],["description",T(u,h)],["main heading",g.some(S=>T(S.text,h))],["address",C(s).includes(C(h))],["first paragraph",T(e.firstPara,h)]].filter(([,S])=>!S).map(([S])=>S),k=j(e.text,h),L=e.words?k*h.split(/\s+/).length*100/e.words:0;b.length>=3?t("focus","Keyphrase","fail",`"${n}"`,`Missing from the ${b.join(", ")}.`,"Use the phrase people search for where they look first: the title, the heading and the opening."):b.length?t("focus","Keyphrase","warn",`"${n}"`,`Missing from the ${b.join(", ")}.`,"Work it in naturally - never at the cost of reading well."):t("focus","Keyphrase","pass",`"${n}"`,"In the title, description, heading, address and opening."),e.words<100||(L>3?t("density","Keyphrase","warn","Repetition",`The phrase is ${L.toFixed(1)}% of the words (${m(k,"time")}).`,"Over about 3% reads as stuffing - use other words for the same thing."):k&&t("density","Keyphrase","pass","Repetition",`Used ${m(k,"time")} (${L.toFixed(1)}%).`))}else t("focus","Keyphrase","info","Focus keyphrase","None set.","Say which search this page should be found for, and the checks will look for it.");return $?t("index","Technical","info","Search engines","Hidden from search engines (noindex).","On purpose for thank-you pages and the like; otherwise switch it off."):t("index","Technical","pass","Search engines","May be indexed."),e.canonical?e.canonical.replace(/\/$/,"").endsWith(s==="/"?"":s)?t("canonical","Technical","pass","Canonical address",e.canonical):t("canonical","Technical","info","Canonical address",`Points elsewhere: ${e.canonical}.`,"Right when this page is a copy of that one; otherwise it hides this page."):t("canonical","Technical","warn","Canonical address","No canonical link.","Set the site address in Settings so every page names its one true address."),e.og.image?t("share-image","Sharing","pass","Share image",e.og.image):t("share-image","Sharing","warn","Share image","No image when the page is shared.","Choose an image (1200 \xD7 630 is ideal), or set a default one for the site."),e.jsonLdTypes.includes("(invalid)")?t("schema","Technical","fail","Structured data","A JSON-LD block is not valid JSON.","Fix or remove it - search engines ignore the lot."):e.jsonLdTypes.length?t("schema","Technical","pass","Structured data",e.jsonLdTypes.join(", ")):t("schema","Technical","info","Structured data","None.","Structured data lets search engines show rich results."),e.lang||t("lang","Technical","warn","Language","The page does not say what language it is in.",'The site template sets <html lang="\u2026">.'),{...score(p),checks:p}}function N(e,s){const n=String(e||"").trim();if(!n||n.startsWith("#")||/^(mailto|tel|javascript|data):/i.test(n))return!1;if(n.startsWith("/")&&!n.startsWith("//"))return!0;if(!s)return!1;try{return new URL(n).origin===new URL(s).origin}catch{return!1}}function j(e,s){if(!s)return 0;const n=s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/\s+/g,"\\s+");return(String(e).match(new RegExp(`(^|[^\\p{L}\\p{N}])${n}(?=$|[^\\p{L}\\p{N}])`,"giu"))||[]).length}export function score(e){const s=e.filter(r=>r.status!=="info"),n=s.reduce((r,i)=>r+(i.status==="pass"?1:i.status==="warn"?.5:0),0);return{score:s.length?Math.round(n/s.length*100):100,failing:e.filter(r=>r.status==="fail").length,warnings:e.filter(r=>r.status==="warn").length}}export function siteIssues(e){const s=n=>{const r=new Map;for(const i of e){const a=String(i[n]||"").trim().toLowerCase();!a||i.noindex||i.error||(r.has(a)||r.set(a,{text:i[n],paths:[]}),r.get(a).paths.push(i.urlPath))}return[...r.values()].filter(i=>i.paths.length>1).sort((i,a)=>a.paths.length-i.paths.length)};return{duplicateTitles:s("title"),duplicateDescriptions:s("description")}}export function snippet({title:e="",description:s="",url:n="",limits:r={}}={}){const i={...LIMITS,...r},a=(t,$)=>t.length>$?`${t.slice(0,$-1).replace(/\s+\S*$/,"")} \u2026`:t;let p=n;try{const t=new URL(n);p=[t.hostname,...t.pathname.split("/").filter(Boolean)].join(" \u203A ")}catch{}return{title:a(String(e),i.titleMax),description:a(String(s),i.descMax),crumbs:p,titleCut:String(e).length>i.titleMax,descriptionCut:String(s).length>i.descMax}}
@@ -0,0 +1 @@
1
+ export const CODES=[301,302,307,308,410],MAX_RULES=5e3;export function normalisePath(n){let e=String(n??"").trim();if(!e)return"";try{e=decodeURI(e)}catch{}return e=e.split("#")[0].split("?")[0],e.startsWith("/")||(e=`/${e}`),e=e.replace(/\/{2,}/g,"/"),e.length>1&&(e=e.replace(/\/+$/,"")),e.toLowerCase()}const u=n=>String(n).endsWith("/*"),f=n=>/^https?:\/\//i.test(String(n));export function checkRule(n={},e=[]){const t={},l=Number(n.code??301),s=String(n.from??"").trim(),o=u(s),r=o?`${normalisePath(s.slice(0,-2))==="/"?"":normalisePath(s.slice(0,-2))}/*`:normalisePath(s);let i=String(n.to??"").trim();if(CODES.includes(l)||(t.code="Choose 301, 302, 307, 308 or 410."),s?/^https?:\/\//i.test(s)?t.from="The address to redirect is a path on this site, like /old-page.":/^\/(admin|api)(\/|$)/.test(r)?t.from="The admin and the API cannot be redirected.":r==="/*"&&(t.from="That would redirect the whole site."):t.from="Say which address to redirect.",l===410)i="";else if(!i)t.to="Say where it goes.";else if(f(i))try{new URL(i.replace("*","x"))}catch{t.to="Not a web address."}else if(/^[a-z][a-z0-9+.-]*:/i.test(i)||i.startsWith("//"))t.to="A path on this site (/new-page) or an http(s) address.";else{const[a,c]=i.split("?");i=normalisePath(a)+(c?`?${c}`:""),i.includes("*")&&!o&&(t.to='A "*" in the target needs a "/*" rule to fill it.')}if(!t.from&&!t.to&&l!==410&&!f(i)){const a=normalisePath(i.split("?")[0]);!o&&a===r?t.to="It would redirect to itself.":!o&&h(r,a,e)&&(t.to="That makes a loop with another redirect.")}return!t.from&&e.some(a=>a.from===r)&&(t.from=`There is already a redirect from ${r}.`),{errors:t,rule:{from:r,to:i,code:l,note:String(n.note??"").trim().slice(0,200),...n.auto===!0&&{auto:!0}}}}function h(n,e,t){const l=new Set([n]);let s=e;for(let o=0;o<20;o++){if(l.has(s))return!0;l.add(s);const r=matchRedirect(t,s);if(!r||!r.location||f(r.location))return!1;s=normalisePath(r.location.split("?")[0])}return!0}export function matchRedirect(n,e){const[t,l]=String(e??"").split("?"),s=normalisePath(t);if(!s)return null;let o=null,r="";for(const a of n)if(a.from===s){o=a,r="";break}if(!o){let a=-1;for(const c of n){if(!u(c.from))continue;const d=c.from.slice(0,-2);(s===d||s.startsWith(`${d}/`))&&d.length>a&&(a=d.length,o=c,r=s.slice(d.length+1))}}if(!o)return null;if(o.code===410)return{rule:o,location:"",code:410};let i=o.to.includes("*")?o.to.replace("*",r):o.to;return f(i)||(i=i.replace(/\/{2,}/g,"/").replace(/(.)\/$/,"$1")),l&&!i.includes("?")&&(i+=`?${l}`),{rule:o,location:i,code:o.code}}export function onMoved(n,e,t,{now:l=new Date().toISOString(),id:s}={}){const o=normalisePath(e),r=normalisePath(t);if(!o||!r||o===r)return n;const i=n.filter(c=>c.from!==r).map(c=>normalisePath(String(c.to).split("?")[0])===o&&!f(c.to)?{...c,to:r}:c),a=i.find(c=>c.from===o);return a?i.map(c=>c===a?{...c,to:r,code:301}:c):[...i,{id:s||`r${Date.now().toString(36)}`,from:o,to:r,code:301,note:"The page moved",auto:!0,hits:0,lastHit:null,at:l}]}export function chains(n){const e=[];for(const t of n){if(t.code===410||f(t.to)||u(t.from))continue;const l=matchRedirect(n,t.to);l&&l.rule!==t&&e.push({rule:t,via:l.rule})}return e}
@@ -0,0 +1,2 @@
1
+ const p=(t,o)=>{const a=String(t||"").trim();return!a||/^https?:\/\//i.test(a)||!o?a:`${o}${a.startsWith("/")?"":"/"}${a}`};export function organisationLd(t,{origin:o="",siteTitle:a=""}={}){if(!t?.enabled)return null;const i=t.name||a;if(!i)return null;const s={"@context":"https://schema.org","@type":t.type||"Organization",name:i},l=t.url||o;l&&(s.url=p(l,o)),t.logo&&(s[t.type==="Person"?"image":"logo"]=p(t.logo,o)),t.email&&(s.email=t.email),t.phone&&(s.telephone=t.phone),t.sameAs?.length&&(s.sameAs=t.sameAs);const e=t.address||{};return t.type!=="Person"&&(e.street||e.locality||e.postcode)&&(s.address={"@type":"PostalAddress",...e.street&&{streetAddress:e.street},...e.locality&&{addressLocality:e.locality},...e.region&&{addressRegion:e.region},...e.postcode&&{postalCode:e.postcode},...e.country&&{addressCountry:e.country}}),s}export function articleLd(t,{origin:o="",publisher:a=""}={}){const i={"@context":"https://schema.org","@type":"BlogPosting",headline:String(t.title||"").slice(0,110)};return t.description&&(i.description=t.description),t.image&&(i.image=p(t.image,o)),t.canonical&&(i.url=t.canonical,i.mainEntityOfPage=t.canonical),a&&(i.publisher={"@type":"Organization",name:a}),i}export function applySeo(t,{page:o,kind:a,origin:i="",siteTitle:s="",settings:l,override:e=null}){const n={...t,jsonLd:[...t.jsonLd||[]],extra:[...t.extra||[]]};if(e){e.title&&(n.title=e.title),e.description&&(n.description=e.description),e.image&&(n.image=p(e.image,i)),e.noindex===!0&&(n.noindex=!0);const c=n.jsonLd.find(d=>d?.["@type"]==="WebPage");if(c){const d=n.jsonLd.indexOf(c);n.jsonLd[d]={...c,...e.description&&{description:e.description},...n.image&&{image:n.image}}}}if((o?.urlPath||"/")==="/"){const c=organisationLd(l?.organisation,{origin:i,siteTitle:s});c&&n.jsonLd.push(c)}return l?.articles!==!1&&a==="plugin"&&o?.item?.type==="blog:post"&&(n.ogType="article",n.jsonLd.push(articleLd(n,{origin:i,publisher:l?.organisation?.enabled?l.organisation.name:s}))),n}export function robotsLines(t,{blockAll:o=!1,extra:a=""}={},i="/sitemap.xml"){if(o)return["# This site asks search engines to stay away (SEO settings).","User-agent: *","Disallow: /"];const s=String(a||"").split(`
2
+ `).map(e=>e.trimEnd()).filter((e,n,r)=>e||n>0&&r[n-1]);if(!s.length)return t;let l;if(s.some(e=>/^user-agent\s*:/i.test(e))){const e=t.findIndex(n=>/^sitemap:/i.test(n));l=e>=0?[...t.slice(0,e),...s,"",...t.slice(e)]:[...t,"",...s]}else{const e=t.findIndex(c=>/^user-agent\s*:\s*\*/i.test(c)),n=e>=0?t.findIndex((c,d)=>d>e&&!c.trim()):-1,r=n>=0?n:e>=0?t.length:-1;l=r>=0?[...t.slice(0,r),...s.filter(Boolean),...t.slice(r)]:["User-agent: *",...s.filter(Boolean),"",...t]}return l.some(e=>/^sitemap:/i.test(e))||l.push("",`Sitemap: ${i}`),l}
@@ -0,0 +1,2 @@
1
+ import{LIMITS as h}from"./seo-analyse.js";export const ORG_TYPES=["Organization","LocalBusiness","Person"],DEFAULT_SETTINGS={organisation:{enabled:!1,type:"Organization",name:"",logo:"",url:"",email:"",phone:"",sameAs:[],address:{street:"",locality:"",region:"",postcode:"",country:""}},articles:!0,sitemap:{exclude:[],sources:{}},robots:{blockAll:!1,extra:""},redirects:{onRename:!0},audit:{everyHours:24,...h}};const f=(e,i=300)=>String(e??"").trim().slice(0,i),c=(e,i)=>e===void 0?i:e===!0||e==="true",m=e=>(Array.isArray(e)?e:String(e??"").split(/[\n,]+/)).map(i=>String(i).trim()).filter(Boolean),p=(e,i,n)=>{const r=Number(e);return Number.isInteger(r)&&r>=i&&r<=n?r:null},g=e=>{try{return["http:","https:"].includes(new URL(e).protocol)}catch{return!1}};export function checkSettings(e={},i=DEFAULT_SETTINGS){const n={},r=JSON.parse(JSON.stringify(mergeDefaults(i))),o=e.organisation;if(o&&typeof o=="object"){const s=r.organisation;o.enabled!==void 0&&(s.enabled=c(o.enabled,!1)),o.type!==void 0&&(ORG_TYPES.includes(o.type)?s.type=o.type:n["organisation.type"]="Organisation, local business or person.");for(const t of["name","email","phone"])o[t]!==void 0&&(s[t]=f(o[t],200));for(const t of["logo","url"]){if(o[t]===void 0)continue;const a=f(o[t],500);a&&!g(a)&&!a.startsWith("/")?n[`organisation.${t}`]=`${t==="logo"?"The logo":"The address"}: a full web address, or a path like /media/logo.png.`:s[t]=a}if(o.sameAs!==void 0){const t=m(o.sameAs),a=t.filter(l=>!g(l));a.length?n["organisation.sameAs"]=`Not web addresses: ${a.slice(0,2).join(", ")}`:s.sameAs=[...new Set(t)].slice(0,20)}if(o.address&&typeof o.address=="object")for(const t of Object.keys(DEFAULT_SETTINGS.organisation.address))o.address[t]!==void 0&&(s.address[t]=f(o.address[t],200));s.enabled&&!s.name&&(n["organisation.name"]="Give the name search engines should show.")}if(e.articles!==void 0&&(r.articles=c(e.articles,!0)),e.sitemap&&typeof e.sitemap=="object"){if(e.sitemap.exclude!==void 0){const s=m(e.sitemap.exclude),t=s.filter(a=>!a.startsWith("/"));t.length?n["sitemap.exclude"]=`Each starts with "/": ${t.slice(0,3).join(", ")}`:r.sitemap.exclude=[...new Set(s)].slice(0,200)}if(e.sitemap.sources&&typeof e.sitemap.sources=="object")for(const[s,t]of Object.entries(e.sitemap.sources))/^[a-z0-9][a-z0-9:-]*$/.test(s)&&(r.sitemap.sources[s]=c(t,!0))}if(e.robots&&typeof e.robots=="object"&&(e.robots.blockAll!==void 0&&(r.robots.blockAll=c(e.robots.blockAll,!1)),e.robots.extra!==void 0)){const s=String(e.robots.extra??"").replace(/\r/g,"").slice(0,5e3),t=s.split(`
2
+ `).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#")&&!/^[A-Za-z-]+\s*:/.test(a));t.length?n["robots.extra"]=`Each line is "Field: value" (or a # comment): ${t[0].slice(0,40)}`:r.robots.extra=s.trim()}e.redirects&&e.redirects.onRename!==void 0&&(r.redirects.onRename=c(e.redirects.onRename,!0));const d=e.audit;if(d&&typeof d=="object"){const s=(t,a,l,b)=>{if(d[t]===void 0)return;const u=p(d[t],a,l);u===null?n[`audit.${t}`]=`${b}: a whole number, ${a} to ${l}.`:r.audit[t]=u};s("everyHours",0,168,"Check every (hours)"),s("titleMin",10,70,"Shortest title"),s("titleMax",30,100,"Longest title"),s("descMin",30,160,"Shortest description"),s("descMax",80,320,"Longest description"),s("minWords",0,5e3,"Words a page should have"),r.audit.titleMax<r.audit.titleMin&&(n["audit.titleMax"]="The longest title cannot be shorter than the shortest."),r.audit.descMax<r.audit.descMin&&(n["audit.descMax"]="The longest description cannot be shorter than the shortest.")}return{errors:n,settings:r}}export function mergeDefaults(e={}){const i=DEFAULT_SETTINGS;return{organisation:{...i.organisation,...e.organisation||{},address:{...i.organisation.address,...e.organisation?.address||{}},sameAs:Array.isArray(e.organisation?.sameAs)?e.organisation.sameAs:[]},articles:e.articles??i.articles,sitemap:{exclude:Array.isArray(e.sitemap?.exclude)?e.sitemap.exclude:[],sources:{...e.sitemap?.sources||{}}},robots:{...i.robots,...e.robots||{}},redirects:{...i.redirects,...e.redirects||{}},audit:{...i.audit,...e.audit||{}}}}export function excluded(e,i=[]){const n=String(e||"").toLowerCase();return i.some(r=>{const o=String(r).toLowerCase().replace(/\*$/,"");return o.endsWith("/")?n===o.slice(0,-1)||n.startsWith(o):n===o})}