domma-cms 0.89.1 → 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.
- package/admin/js/app.js +2 -2
- package/admin/js/lib/seo-analyse.js +1 -0
- package/admin/js/lib/seo-redirects.js +1 -0
- package/admin/js/lib/seo-schema.js +2 -0
- package/admin/js/lib/seo-settings.js +2 -0
- package/{plugins/seo/admin → admin/js}/templates/seo.html +1 -2
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/seo.js +144 -0
- package/package.json +1 -1
- package/plugins/free-tier.lock.json +0 -19
- package/server/routes/api/seo.js +201 -0
- package/server/server.js +12 -1
- package/server/services/permissionRegistry.js +11 -0
- package/server/services/plugins.js +12 -2
- package/{plugins/seo/server → server/services/seo}/audit.js +2 -2
- package/server/services/seo/index.js +345 -0
- package/{plugins/seo/server → server/services/seo}/registry.js +5 -3
- package/server/services/seo/store.js +110 -0
- package/server/services/siteGitignore.js +4 -1
- package/server/services/tools.js +2 -1
- package/plugins/seo/CLAUDE.md +0 -48
- package/plugins/seo/admin/lib/analyse.js +0 -286
- package/plugins/seo/admin/lib/redirects.js +0 -163
- package/plugins/seo/admin/lib/schema.js +0 -113
- package/plugins/seo/admin/lib/settings.js +0 -141
- package/plugins/seo/admin/views/seo.js +0 -912
- package/plugins/seo/config.js +0 -7
- package/plugins/seo/plugin.js +0 -477
- package/plugins/seo/plugin.json +0 -64
- package/plugins/seo/server/store.js +0 -76
- package/plugins/seo/tests/seo.test.js +0 -192
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}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(),
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}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}})})()});
|
|
@@ -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})}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
<!-- SEO. Bound with M.applyBindings - admin/views/seo.js inserts the item templates (the router renders a
|
|
1
|
+
<!-- SEO. Bound with M.applyBindings - admin/js/views/seo.js inserts the item templates (the router renders a
|
|
2
2
|
templateUrl through _.render, eating any {{…}}). Refresh and Settings are banner actions. -->
|
|
3
3
|
<div class="sox">
|
|
4
4
|
<p class="sox-note is-bad" data-bind-hidden="!error.value" data-bind-text="error.value"></p>
|
|
5
|
-
<p class="sox-note is-warn" data-bind-hidden="hooksOk.value">This version of Domma CMS cannot take SEO's changes to the sitemap, robots.txt and page tags - they are core's own until it is updated (0.89 or later). The audit, the editor and redirects work.</p>
|
|
6
5
|
|
|
7
6
|
<nav class="sox-tabs" role="tablist" aria-label="SEO">
|
|
8
7
|
<button type="button" role="tab" data-on-click="tab_overview" data-bind-class="tab.value === 'overview' && 'is-on'"><span data-icon="search" data-icon-size="14"></span> Pages <small class="sox-count" data-bind-hidden="!attention.value" data-bind-text="attention.value"></small></button>
|
package/admin/js/views/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView as
|
|
1
|
+
import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as n}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{pluginCodeView as V}from"./plugin-code.js";import{documentationView as a}from"./documentation.js";import{tutorialsView as d}from"./tutorials.js";import{docViews as l}from"./doc-pages.js";import{apiReferenceView as u}from"./api-reference.js";import{collectionsView as E}from"./collections.js";import{collectionEditorView as g}from"./collection-editor.js";import{collectionEntriesView as v}from"./collection-entries.js";import{formsView as b}from"./forms.js";import{formEditorView as h}from"./form-editor.js";import{formSubmissionsView as y}from"./form-submissions.js";import{viewsListView as j}from"./views-list.js";import{viewEditorView as k}from"./view-editor.js";import{viewPreviewView as L}from"./view-preview.js";import{actionsListView as x}from"./actions-list.js";import{actionEditorView as M}from"./action-editor.js";import{proDocsView as D}from"./pro-docs.js";import{blocksView as P}from"./blocks.js";import{blockEditorView as R}from"./block-editor.js";import{componentsView as S}from"./components.js";import{componentEditorView as C}from"./component-editor.js";import{myProfileView as T}from"./my-profile.js";import{rolesView as U}from"./roles.js";import{roleEditorView as q}from"./role-editor.js";import{effectsView as z}from"./effects.js";import{searchView as A}from"./search.js";import{notesView as B}from"./notes.js";import{todoView as F}from"./todo.js";import{analyticsView as G}from"./analytics.js";import{seoView as H}from"./seo.js";import{contactsView as I}from"./contacts.js";import{menusView as J}from"./menus.js";import{contextMenusView as K}from"./context-menus.js";import{contextMenuEditorView as N}from"./context-menu-editor.js";import{menuEditorView as O}from"./menu-editor.js";import{menuLocationsView as Q}from"./menu-locations.js";import{projectsView as W}from"./projects.js";import{projectDetailView as X}from"./project-detail.js";import{projectSettingsView as Y}from"./project-settings.js";import{apiTokensView as Z}from"./api-tokens.js";import{apiEndpointsView as _}from"./api-endpoints.js";import{apiEndpointEditorView as $}from"./api-endpoint-editor.js";const oo={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:W,projectDetail:X,projectSettings:Y,apiTokens:Z,apiEndpoints:_,apiEndpointEditor:$,dashboard:o,pages:i,pageEditor:r,settings:t,theme:e,layouts:p,media:s,login:n,users:f,userEditor:w,plugins:c,pluginCode:V,documentation:a,tutorials:d,apiReference:u,collections:E,collectionEditor:g,collectionEntries:v,forms:b,formEditor:h,formSubmissions:y,viewsList:j,viewEditor:k,viewPreview:L,actionsList:x,actionEditor:M,proDocs:D,blocks:P,blockEditor:R,components:S,componentEditor:C,myProfile:T,roles:U,roleEditor:q,effects:z,search:A,notes:B,todo:F,analytics:G,seo:H,contacts:I,notifications:m,menus:J,contextMenus:K,contextMenuEditor:N,menuEditor:O,menuLocations:Q,menusRedirect:oo,...l};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import{apiRequest as Gt}from"../api.js";import{addBannerAction as Jt}from"../lib/plugin-chrome.js";import{helpPopovers as Kt}from"../lib/help-popover.js";import{onceClosed as zt,openMenuAt as Ft,viewLifetime as Yt}from"../lib/tool-kit.js";import{snippet as Qt}from"../lib/seo-analyse.js";import{checkRule as Qe,matchRedirect as Xt}from"../lib/seo-redirects.js";import{checkSettings as Vt,mergeDefaults as Xe,ORG_TYPES as Zt}from"../lib/seo-settings.js";const es="/seo",Ae=/^#\/seo(\/[a-z0-9-]+)?\/?(\?.*)?$/,Ve=["overview","redirects","sitemap"],ts="#/seo",ss={pass:"\u2713",warn:"!",fail:"\u2715",info:"i"},as={301:"301 moved",302:"302 for now",307:"307 for now",308:"308 moved",410:"410 gone"},os={apiRequest:Gt,addBannerAction:Jt,helpPopovers:Kt,onceClosed:zt,openMenuAt:Ft,viewLifetime:Yt},ns=async()=>os,y=l=>String(l??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function Pe(l){const o=document.createElement("template");return o.innerHTML=l.trim(),o.content.firstElementChild}async function oe(l,o){try{await navigator.clipboard.writeText(l),E.toast(`Copied ${o}.`,{type:"success"})}catch{E.toast(`Could not copy - ${l}`,{type:"warning",duration:6e3})}}function Ze(){try{window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:ts}}))}catch{}}const et=l=>typeof l!="number"?"":l>=85?"good":l>=60?"poor":"bad",tt=l=>l?new Date(l).toLocaleDateString("en-GB",{day:"numeric",month:"short",year:"numeric"}):"",st=l=>{if(!l)return"";const o=Math.round((Date.now()-Date.parse(l))/6e4);if(o<1)return"just now";if(o<60)return`${o} min ago`;const $=Math.round(o/60);return $<48?`${$} hour${$===1?"":"s"} ago`:tt(l)};function is(l){const o=d=>{let r=String(d??"");return/^[=+\-@\t\r]/.test(r)&&(r=`'${r}`),/[",\n\r]/.test(r)?`"${r.replace(/"/g,'""')}"`:r},$=Object.keys(l[0]||{});return`\uFEFF${[$.join(","),...l.map(d=>$.map(r=>o(d[r])).join(","))].join(`\r
|
|
2
|
+
`)}\r
|
|
3
|
+
`}function ls(l,o,$="text/csv;charset=utf-8"){const d=URL.createObjectURL(new Blob([o],{type:$})),r=Object.assign(document.createElement("a"),{href:d,download:l});document.body.appendChild(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(d),1e4)}const rs=`
|
|
4
|
+
<div class="sox-row" data-bind-data-row="id" tabindex="0" data-bind-title="path">
|
|
5
|
+
<span class="sox-pill" data-bind-data-tone="tone" data-bind-text="scoreText"></span>
|
|
6
|
+
<span class="sox-cell"><span data-bind-text="path"></span><small data-bind-text="sourceText"></small></span>
|
|
7
|
+
<span class="sox-cell sox-hide-sm" data-bind-text="title" data-bind-title="title"></span>
|
|
8
|
+
<span class="sox-cell sox-muted sox-hide-sm" data-bind-text="problems" data-bind-title="problems"></span>
|
|
9
|
+
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
10
|
+
</div>`,ds=`
|
|
11
|
+
<div class="sox-row" data-bind-data-row="id" tabindex="0">
|
|
12
|
+
<span class="sox-cell"><span data-bind-text="text" data-bind-title="text"></span><small data-bind-text="detail" data-bind-title="detail"></small></span>
|
|
13
|
+
<span class="sox-pill" data-bind-data-tone="tone" data-bind-text="badge"></span>
|
|
14
|
+
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
15
|
+
</div>`,cs=`
|
|
16
|
+
<div class="sox-row" data-bind-data-row="id" tabindex="0" data-bind-title="note">
|
|
17
|
+
<span class="sox-cell sox-mono" data-bind-text="from" data-bind-title="from"></span>
|
|
18
|
+
<span class="sox-arrow" aria-hidden="true">\u2192</span>
|
|
19
|
+
<span class="sox-cell sox-mono"><span data-bind-text="to" data-bind-title="to"></span><small data-bind-text="note"></small></span>
|
|
20
|
+
<span class="sox-pill sox-hide-sm" data-bind-data-tone="tone" data-bind-text="code"></span>
|
|
21
|
+
<span class="sox-cell sox-muted sox-hide-sm" data-bind-text="hits" data-bind-title="lastHit"></span>
|
|
22
|
+
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
23
|
+
</div>`,us=`
|
|
24
|
+
<div class="sox-row" data-bind-data-row="id" tabindex="0">
|
|
25
|
+
<span class="sox-cell"><span data-bind-text="path" data-bind-title="path"></span><small data-bind-text="title"></small></span>
|
|
26
|
+
<span class="sox-cell sox-muted sox-hide-sm" data-bind-text="source"></span>
|
|
27
|
+
<span class="sox-cell sox-muted sox-hide-sm" data-bind-text="note" data-bind-title="note"></span>
|
|
28
|
+
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
29
|
+
</div>`,ps=`
|
|
30
|
+
<div class="sox-row" data-bind-data-row="id" tabindex="0">
|
|
31
|
+
<span class="sox-cell"><span data-bind-text="label"></span><small data-bind-text="detail"></small></span>
|
|
32
|
+
<span class="sox-pill" data-bind-data-tone="tone" data-bind-text="state"></span>
|
|
33
|
+
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
34
|
+
</div>`,hs=`
|
|
35
|
+
<div>
|
|
36
|
+
<div class="sox-group" data-bind-hidden="!head" data-bind-text="head"></div>
|
|
37
|
+
<div class="sox-check" data-bind-hidden="!title">
|
|
38
|
+
<span class="sox-status" data-bind-data-status="status" data-bind-text="glyph"></span>
|
|
39
|
+
<span class="sox-check-main"><strong data-bind-text="title"></strong><small data-bind-text="detail"></small><small data-bind-text="fix" data-bind-hidden="!fix"></small></span>
|
|
40
|
+
</div>
|
|
41
|
+
</div>`;export const seoView={templateUrl:"/admin/js/templates/seo.html",async onMount(l){const o=await ns();if(!Ae.test(location.hash))return;const $=seoView._lifetime??(seoView._lifetime=o.viewLifetime(Ae));$.reset();const d=l.get(0).querySelector(".sox")||l.get(0),r=()=>Ae.test(location.hash)&&d.isConnected,f=t=>($.add(t),t),h=(t,e)=>o.apiRequest(`${es}${t}`,e),at=location.hash.match(/^#\/seo\/([a-z0-9-]+)/)?.[1]||"overview",N=M.observable(at),he=M.observable(!0),me=M.observable(""),S=M.observable(!0),m=M.observable(null),be=M.observable({results:[]}),J=M.observable("all"),ve=M.observable(""),fe=M.observable(""),x=M.observable({rules:[],chains:[],onRename:!0}),ge=M.observable(""),ne=M.observable(""),ie=M.observable(""),Ie=M.observable("301"),j=M.observable({entries:[],left:[],sources:[],labels:{},robots:""}),X=M.observable("in"),ye=M.observable(""),Ne=M.observable([]),ot=M.computed(()=>!Ve.includes(N.value));let q=null;const K=t=>j.value.labels?.[t]||(t==="pages"?"Pages":t),nt=M.computed(()=>(m.value?.score??null)===null?"\u2014":`${m.value.score}%`),it=M.computed(()=>`${m.value?.score??0}%`),lt=M.computed(()=>et(m.value?.score)),rt=M.computed(()=>{const t=m.value;return t?{pages:String(t.pages),failing:String(t.failing),duplicates:String(t.duplicates),redirects:String(t.redirects)}:{pages:"\u2014",failing:"\u2014",duplicates:"\u2014",redirects:"\u2014"}}),dt=M.computed(()=>m.value?.failing?String(m.value.failing):""),ct=M.computed(()=>{const t=m.value;return t?.running?`Checking\u2026 ${t.progress?.done||0} of ${t.progress?.total||"?"}`:t?.at?`Checked ${st(t.at)}`:"Not checked yet"}),je=M.computed(()=>[...new Set((be.value.results||[]).map(t=>t.source))]),qe=M.observable([]);let V=[];f(M.effect(()=>{const t=fe.value.trim().toLowerCase(),e=J.value,s=ve.value;V=(be.value.results||[]).filter(a=>(e==="all"||(e==="hidden"?a.noindex||a.unlisted:!a.noindex&&(a.error||a.failing>0)))&&(!s||a.source===s)&&(!t||[a.urlPath,a.title,a.label].some(n=>String(n||"").toLowerCase().includes(t)))),V.sort((a,n)=>(a.error?-1:0)-(n.error?-1:0)||(a.score??101)-(n.score??101)),qe.value=V.slice(0,1e3).map((a,n)=>({id:`p${n}`,path:a.urlPath,sourceText:[K(a.source),a.noindex?"hidden from search engines":a.unlisted?"not in the sitemap":""].filter(Boolean).join(" \xB7 "),title:a.title||a.label||"",scoreText:a.error?"!":typeof a.score=="number"?String(a.score):"\u2014",tone:a.error?"bad":a.noindex?"info":et(a.score),problems:a.error||(a.problems?.length?a.problems.join(" \xB7 "):"Nothing to fix")}))}));const ut=M.computed(()=>m.value?.at?fe.value.trim()?"No page matches.":J.value==="problems"?"Nothing needs attention.":"No pages.":m.value?.running?"Checking every page - this takes a moment\u2026":"Not checked yet. Check now with the button top right."),De=M.observable([]);let O=[];f(M.effect(()=>{const t=m.value;if(O=[],t){const e=t.siteSeo||{};for(const s of t.issues?.duplicateTitles||[])O.push({kind:"dupe",text:`Title on ${s.paths.length} pages: "${s.text}"`,detail:s.paths.join(", "),paths:s.paths,tone:"poor",badge:"duplicate"});for(const s of t.issues?.duplicateDescriptions||[]){const a=e.defaultDescription&&s.text===e.defaultDescription;O.push({kind:"dupe",text:a?`${s.paths.length} pages use the site's default description`:`Description on ${s.paths.length} pages: "${s.text}"`,detail:s.paths.join(", "),paths:s.paths,tone:"poor",badge:a?"default":"duplicate"})}e.defaultImage||O.push({kind:"setting",text:"No default share image",detail:"Pages without their own image share with none. Set one in Settings \u203A SEO.",link:"#/settings",tone:"poor",badge:"setting"}),(!t.origin||/localhost|127\.0\.0\.1/.test(t.origin))&&O.push({kind:"setting",text:"The site address is not set",detail:"Canonical links and the sitemap use whatever address a visitor came in on. Set the site address in Settings.",link:"#/settings",tone:"poor",badge:"setting"}),t.chains&&O.push({kind:"chains",text:`${t.chains} redirect${t.chains===1?" leads":"s lead"} to another redirect`,detail:"Each hop is slower and loses a little standing. The Redirects tab shows which.",tone:"poor",badge:"redirects"})}De.value=O.map((e,s)=>({id:`s${s}`,text:e.text,detail:e.detail||"",tone:e.tone,badge:e.badge}))}));const xe=M.observable([]);let Z=[];f(M.effect(()=>{const t=ge.value.trim().toLowerCase(),e=new Set(x.value.chains.map(s=>s.id));Z=x.value.rules.filter(s=>!t||`${s.from} ${s.to} ${s.note}`.toLowerCase().includes(t)),xe.value=Z.map((s,a)=>({id:`r${a}`,from:s.from,to:s.code===410?"(gone)":s.to,note:[s.auto?"Added when the page moved":s.note,e.has(s.id)?"leads to another redirect":""].filter(Boolean).join(" \xB7 "),code:String(s.code),tone:s.code===410?"bad":e.has(s.id)?"poor":s.code===301||s.code===308?"good":"info",hits:s.hits?`${s.hits.toLocaleString("en-GB")} use${s.hits===1?"":"s"}`:"unused",lastHit:s.lastHit?`Last used ${st(s.lastHit)}`:"Not used yet"}))}));const pt=M.computed(()=>x.value.rules.length?String(x.value.rules.length):""),ht=M.computed(()=>`${Z.length+0*xe.value.length} of ${x.value.rules.length}`),mt=M.computed(()=>ge.value.trim()?"No redirect matches.":"No redirects yet."),bt=M.computed(()=>{const t=x.value.chains.length;return t?`${t} redirect${t===1?" leads":"s lead"} to another. Right-click one and choose "Go straight there" to make it one hop.`:""}),vt=M.computed(()=>x.value.onRename?"When a page is renamed, a redirect from its old address is added for you.":"Redirects are not added when a page is renamed (see Settings)."),Be=M.observable([]);f(M.effect(()=>{Be.value=j.value.sources.map((t,e)=>({id:`x${e}`,label:K(t.id),detail:`${t.listed} of ${t.count} listed`,state:t.on?"on":"off",tone:t.on?"good":""}))}));const we=M.observable([]);let z=[];f(M.effect(()=>{const t=ye.value.trim().toLowerCase();z=(X.value==="in"?j.value.entries:j.value.left).filter(s=>!t||`${s.urlPath} ${s.title}`.toLowerCase().includes(t)),we.value=z.slice(0,2e3).map((s,a)=>({id:`e${a}`,path:s.urlPath,title:s.title||"",source:K(s.source),note:s.why||[s.lastmod?`Changed ${tt(s.lastmod)}`:"",s.changefreq||"",s.priority!==void 0?`priority ${s.priority}`:""].filter(Boolean).join(" \xB7 ")}))}));const ft=M.computed(()=>j.value.robots||""),gt=M.computed(()=>`${z.length+0*we.value.length} address${z.length===1?"":"es"}`),yt=M.computed(()=>j.value.hooks?ye.value.trim()?"No address matches.":X.value==="in"?"The sitemap is empty.":"Nothing is left out.":"This version of Domma CMS lists only its own pages in the sitemap."),le=t=>/403|forbidden/i.test(String(t?.message));async function ee(){try{const t=await h("/overview");if(!r()||!t)return;m.value=t,me.value="",t.running&&Ue()}catch(t){r()&&(me.value=le(t)?"You do not have access to SEO.":`Could not load SEO: ${t.message||t}`)}}async function ke(){try{const t=await h("/audit");r()&&t&&(be.value=t)}catch{}}async function te(){try{const t=await h("/redirects");r()&&t&&(x.value=t)}catch{}}async function Me(){try{const t=await h("/sitemap");r()&&t&&(j.value=t)}catch(t){r()&&!le(t)&&E.toast(`Could not load the sitemap: ${t.message||t}`,{type:"error"})}}async function $e(){he.value=!0,await Promise.all([ee(),ke(),te(),Me()]),r()&&(he.value=!1)}function Ue(){q||(q=setInterval(async()=>{if(!r()){clearInterval(q),q=null;return}await ee(),m.peek()?.running||(clearInterval(q),q=null,await ke(),Ze())},1500))}f(()=>{q&&clearInterval(q)});async function _e(){try{const t=await h("/audit",{method:"POST",body:JSON.stringify({})});r()&&(m.value={...m.peek(),...t,running:!0}),Ue()}catch(t){E.toast(`Could not start the check: ${t.message||t}`,{type:"error"})}}async function Se(t,e,s=te){try{const a=await t();return e&&E.toast(e,{type:"success"}),await Promise.all([s(),ee()]),a}catch(a){return le(a)?(S.value=!1,E.toast("Only people who may manage SEO can change this.",{type:"warning"})):E.toast(`Not done: ${a.message||a}`,{type:"error"}),null}}const xt=(t,e=`Redirect from ${t.from} added.`)=>Se(()=>h("/redirects",{method:"POST",body:JSON.stringify(t)}),e),wt=(t,e)=>Se(()=>h(`/redirects/${encodeURIComponent(t.join(","))}`,{method:"DELETE"}),`${e} removed.`);async function kt(t){try{const s=(await h(`/redirects/test?path=${encodeURIComponent(t.replace(/\/\*$/,"/example"))}`)).hops||[];E.toast(s.length?s.map(a=>`${a.from} \u2192 ${a.code===410?"gone":a.to} (${a.code})`).join(" \xB7 "):"No redirect applies.",{type:s.length>1?"warning":"info",duration:7e3})}catch(e){E.toast(`Could not test: ${e.message||e}`,{type:"error"})}}function He(t){const e={from:M.observable(t.from),to:M.observable(t.code===410?"":t.to),code:M.observable(String(t.code)),note:M.observable(t.note||"")};We(`Redirect from ${t.from}`,e,`
|
|
42
|
+
<div class="sox-form-grid">
|
|
43
|
+
<div class="sox-wide"><label class="form-label" for="sox-r-from">From</label><input id="sox-r-from" class="form-input sox-mono" data-model="from.value"></div>
|
|
44
|
+
<div class="sox-wide"><label class="form-label" for="sox-r-to">To</label><input id="sox-r-to" class="form-input sox-mono" data-model="to.value" data-bind-disabled="code.value === '410'"></div>
|
|
45
|
+
<div><label class="form-label" for="sox-r-code">Kind</label><select id="sox-r-code" class="form-input" data-model="code.value">
|
|
46
|
+
${Object.entries(as).map(([s,a])=>`<option value="${s}">${a}</option>`).join("")}</select></div>
|
|
47
|
+
<div><label class="form-label" for="sox-r-note">Note</label><input id="sox-r-note" class="form-input" data-model="note.value"></div>
|
|
48
|
+
</div>`,()=>{const s={from:e.from.value,to:e.to.value,code:Number(e.code.value),note:e.note.value},a=x.peek().rules.filter(n=>n.id!==t.id);return{body:s,errors:Qe(s,a).errors}},async s=>{await h(`/redirects/${encodeURIComponent(t.id)}`,{method:"PUT",body:JSON.stringify(s)}),E.toast("Redirect saved.",{type:"success"}),await te()})}function Mt(){const t={csv:M.observable("")};We("Import redirects",t,`
|
|
49
|
+
<p class="sox-note">One redirect per line: <span class="sox-mono">from, to</span>, and optionally the kind (301, 302, 307, 308, 410) and a note. A header line is skipped. Paste from a spreadsheet, or another CMS's export.</p>
|
|
50
|
+
<textarea class="form-input sox-mono" rows="14" data-model="csv.value" placeholder="/old-page, /new-page /news/*, /blog/*, 301 /retired, , 410"></textarea>`,()=>({body:{csv:t.csv.value},errors:t.csv.value.trim()?{}:{csv:"Paste some redirects first."}}),async e=>{const s=await h("/redirects/import",{method:"POST",body:JSON.stringify(e)}),a=s.skipped||[];E.toast(`${s.added} added${a.length?`, ${a.length} skipped - line ${a[0].line}: ${a[0].why}`:"."}`,{type:a.length?"warning":"success",duration:a.length?9e3:4e3}),await Promise.all([te(),ee()])},"Import")}function We(t,e,s,a,n,u="Save"){const i=M.observable(!1),b=M.computed(()=>a()),R=M.computed(()=>Object.values(b.value.errors).join(" ")),w={...e,saving:i,errText:R,cannot:M.computed(()=>i.value||!!R.value)},v=E.slideover({title:t,size:"md",position:"right"}),g=Pe(`<div class="sox-form">${s}
|
|
51
|
+
<p class="sox-err" data-bind-hidden="!errText.value" data-bind-text="errText.value"></p>
|
|
52
|
+
<div class="sox-actions"><small class="sox-muted">Ctrl+Enter ${u.toLowerCase()}s.</small><span class="sox-spacer"></span>
|
|
53
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
54
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">${y(u)}</button></div></div>`);async function k(){if(!w.cannot.peek()){i.value=!0;try{await n(b.peek().body),v.close()}catch(T){E.toast(`Not saved: ${T.message||T}`,{type:"error"})}finally{i.value=!1}}}v.element.appendChild(g);const C=o.helpPopovers(g),F=M.applyBindings(w,g,{methods:{save:k,cancel:()=>v.close()}});g.addEventListener("keydown",T=>{(T.ctrlKey||T.metaKey)&&T.key==="Enter"&&(T.preventDefault(),k())}),o.onceClosed(v,()=>{F.dispose(),C()}),f(()=>{try{v.close()}catch{}}),I.scan(g),v.open(),g.querySelector("input, textarea")?.focus()}let re=!1;async function W(t){if(re)return;re=!0;let e;try{e=await h(`/item?path=${encodeURIComponent(t)}`)}catch(p){re=!1,E.toast(`Could not open ${t}: ${p.message||p}`,{type:"error"});return}const s={title:M.observable(e.fields.title),description:M.observable(e.fields.description),focus:M.observable(e.fields.focus),image:M.observable(e.fields.image),noindex:M.observable(e.fields.noindex)},a=e.limits||{},n=e.page?.title||e.live?.h1||"",u=e.kind==="page"?`${n}${e.defaults.separator}${e.defaults.siteTitle}`:e.live?.title||"",i=e.kind==="page"?e.defaults.description||"":e.live?.description||"",b=e.kind==="page"?e.defaults.image:e.live?.image||"",R=M.computed(()=>s.title.value.trim()||u),w=M.computed(()=>s.description.value.trim()||i),v=M.computed(()=>s.image.value.trim()||b),g=e.origin||location.origin,k=M.computed(()=>Qt({title:R.value,description:w.value,url:`${g}${t}`,limits:a})),C=(p,ae,Q)=>{const H=p.length;return{text:`${H} / ${Q}`,width:`${Math.min(100,H/Q*100)}%`,tone:H?H<ae||H>Q?"poor":"good":"bad"}},F=M.computed(()=>C(R.value,a.titleMin||30,a.titleMax||60)),T=M.computed(()=>C(w.value,a.descMin||70,a.descMax||160)),Oe=(()=>{try{return new URL(g).host}catch{return location.host}})(),c=M.computed(()=>v.value?`url("${v.value.replace(/["\\\n\r]/g,"")}")`:"none"),B=M.observable([]),A=p=>{const ae=[],Q=p?.checks||[],H={fail:0,warn:1,pass:2,info:3};for(const Le of[...new Set(Q.map(P=>P.group))]){ae.push({id:`g-${Le}`,head:Le,title:"",detail:"",fix:"",status:"",glyph:""});for(const P of Q.filter(Re=>Re.group===Le).sort((Re,Wt)=>H[Re.status]-H[Wt.status]))ae.push({id:`c-${P.id}`,head:"",title:P.title,detail:P.detail,fix:P.status==="pass"?"":P.fix,status:P.status,glyph:ss[P.status]||"?"})}B.value=ae};A(e.analysis);const G=M.observable(e.analysis?`Score ${e.analysis.score}% \xB7 ${e.analysis.failing} failing, ${e.analysis.warnings} could be better`:e.status===200?"":`Not live (answers ${e.status||"nothing"}) - the checks run once it is published.`),U=()=>({path:t,title:s.title.value,description:s.description.value,focus:s.focus.value,image:s.image.value,noindex:s.noindex.value});let ue=JSON.stringify(U());const pe=M.observable(!1),ze=M.computed(()=>s.image.value.trim()&&!/^(https?:\/\/|\/)/i.test(s.image.value.trim())?"The image: a path like /media/photo.jpg, or a full web address.":""),Fe={...s,saving:pe,errText:ze,checks:B,scoreLine:G,serp:k,titleMeter:F,descMeter:T,shareBg:c,effTitle:R,effDesc:w,host:Oe,hasImage:M.computed(()=>!!v.value),defTitle:u,defDesc:i,canManage:S,cannot:M.computed(()=>pe.value||!!ze.value||!S.value)},Y=E.slideover({title:`SEO \xB7 ${t}`,size:"lg",position:"right"}),qt=e.kind==="url"?'<p class="sox-note is-warn">A plugin serves this address (a blog post, a listing). What you set here takes the place of its own SEO fields.</p>':e.page?.status!=="published"?`<p class="sox-note is-warn">This page is a ${y(e.page?.status||"draft")} - not live yet.</p>`:"",Dt=e.redirect?`<p class="sox-note is-bad">A redirect sends visitors from here to ${y(e.redirect.to||"(gone)")} - they never see this page.</p>`:"",_=Pe(`
|
|
55
|
+
<div class="sox-form">
|
|
56
|
+
${qt}${Dt}
|
|
57
|
+
<section><h4>As it will look <span data-help="The search result is drawn the way Google draws it, cut where it cuts (roughly - it measures pixels). The share card is what Facebook, LinkedIn, WhatsApp and others show when someone shares the link."></span>
|
|
58
|
+
<span class="sox-spacer"></span><a class="btn btn-xs btn-ghost" href="${y(t)}" target="_blank" rel="noopener"><span data-icon="external-link" data-icon-size="12"></span> Open</a></h4>
|
|
59
|
+
<div class="sox-previews">
|
|
60
|
+
<div class="sox-serp" aria-label="Search result preview">
|
|
61
|
+
<div class="sox-serp-crumbs" data-bind-text="serp.value.crumbs"></div>
|
|
62
|
+
<div class="sox-serp-title" data-bind-text="serp.value.title"></div>
|
|
63
|
+
<div class="sox-serp-desc" data-bind-text="serp.value.description"></div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="sox-share" aria-label="Share card preview">
|
|
66
|
+
<div class="sox-share-img"><span data-bind-hidden="hasImage.value">No image</span></div>
|
|
67
|
+
<div class="sox-share-body"><small data-bind-text="host"></small><strong data-bind-text="effTitle.value"></strong><span data-bind-text="effDesc.value"></span></div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</section>
|
|
71
|
+
<section><h4>Search and sharing</h4><div class="sox-form-grid">
|
|
72
|
+
<div class="sox-wide"><label class="form-label" for="sox-i-title">Title <span data-help="The blue link in search results and the headline of a share. Say what the page is about, words that matter first. Empty uses the page title and the site name."></span></label>
|
|
73
|
+
<input id="sox-i-title" class="form-input" data-model="title.value" placeholder="${y(u)}" maxlength="200">
|
|
74
|
+
<div class="sox-meter" data-bind-data-tone="titleMeter.value.tone"><span class="sox-bar"><span data-bind-style-width="titleMeter.value.width"></span></span><span data-bind-text="titleMeter.value.text"></span></div></div>
|
|
75
|
+
<div class="sox-wide"><label class="form-label" for="sox-i-desc">Description <span data-help="The grey text under the link. One or two sentences that make someone want to click. Empty uses the site's default - which every other such page shares."></span></label>
|
|
76
|
+
<textarea id="sox-i-desc" rows="3" class="form-input" data-model="description.value" placeholder="${y(i)}" maxlength="400"></textarea>
|
|
77
|
+
<div class="sox-meter" data-bind-data-tone="descMeter.value.tone"><span class="sox-bar"><span data-bind-style-width="descMeter.value.width"></span></span><span data-bind-text="descMeter.value.text"></span></div></div>
|
|
78
|
+
<div><label class="form-label" for="sox-i-focus">Focus keyphrase <span data-help="The search this page should be found for - 'garden tools', 'plumber in Leeds'. The checks then look for it in the title, description, heading, address and opening. It is not shown anywhere."></span></label>
|
|
79
|
+
<input id="sox-i-focus" class="form-input" data-model="focus.value" maxlength="100"></div>
|
|
80
|
+
<div><label class="form-label" for="sox-i-image">Share image <span data-help="Shown when the page is shared. 1200 \xD7 630 pixels is ideal. A path from Media (/media/photo.jpg) or a full address. Empty uses the site's default image."></span></label>
|
|
81
|
+
<input id="sox-i-image" class="form-input sox-mono" data-model="image.value" list="sox-media" placeholder="${y(b||"/media/\u2026")}"></div>
|
|
82
|
+
<label class="sox-wide" style="display:flex;align-items:center;gap:.5rem"><input type="checkbox" class="form-switch" data-model="noindex.value"> Hide from search engines
|
|
83
|
+
<span data-help="Asks search engines not to list this page, and leaves it out of the sitemap. For thank-you pages, private landing pages and the like. It stays reachable by its address."></span></label>
|
|
84
|
+
</div><datalist id="sox-media"></datalist></section>
|
|
85
|
+
<section><h4>Checks <small class="sox-muted" data-bind-text="scoreLine.value"></small></h4>
|
|
86
|
+
<div class="sox-checks" data-each="checks.value key=id"></div>
|
|
87
|
+
</section>
|
|
88
|
+
<p class="sox-err" data-bind-hidden="!errText.value" data-bind-text="errText.value"></p>
|
|
89
|
+
<div class="sox-actions">
|
|
90
|
+
<small class="sox-muted">Ctrl+Enter saves.</small><span class="sox-spacer"></span>
|
|
91
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Close</button>
|
|
92
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value"><span data-icon="save" data-icon-size="13"></span> Save and check</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>`);_.querySelector(".sox-checks").innerHTML=hs,$t(_.querySelector("#sox-media"));async function Ye(){if(!Fe.cannot.peek()){pe.value=!0;try{await h("/item",{method:"PUT",body:JSON.stringify(U())}),ue=JSON.stringify(U()),E.toast("Saved.",{type:"success"});const p=await h(`/item?path=${encodeURIComponent(t)}`).catch(()=>null);p?.analysis&&(A(p.analysis),G.value=`Score ${p.analysis.score}% \xB7 ${p.analysis.failing} failing, ${p.analysis.warnings} could be better`),ke(),ee(),Ze()}catch(p){le(p)&&(S.value=!1),E.toast(`Not saved: ${p.message||p}`,{type:"error"})}finally{pe.value=!1}}}Y.element.appendChild(_);const Bt=o.helpPopovers(_),Ut=M.applyBindings(Fe,_,{methods:{save:Ye,cancel:()=>Y.close()}}),_t=_.querySelector(".sox-share-img"),Ht=M.effect(()=>{_t.style.backgroundImage=c.value});_.addEventListener("keydown",p=>{(p.ctrlKey||p.metaKey)&&p.key==="Enter"&&(p.preventDefault(),Ye())}),o.onceClosed(Y,()=>{Ut.dispose(),Bt(),Ht?.(),re=!1}),Ge(Y,()=>JSON.stringify(U())!==ue),f(()=>{try{Y.close(!0)}catch{}}),I.scan(_),Y.open()}let Ee=null;async function $t(t){try{Ee??(Ee=await o.apiRequest("/media").then(e=>Array.isArray(e)?e:e?.files||e?.items||[]));for(const e of Ee){const s=e.url||e.path||(e.filename?`/media/${e.filename}`:"");s&&/\.(jpe?g|png|webp|gif|avif|svg)$/i.test(s)&&t.appendChild(Object.assign(document.createElement("option"),{value:s}))}}catch{Ee=[]}}function Ge(t,e){const s=t.close.bind(t);let a=!1;t.close=async n=>{if(n!==!0&&e()){if(a)return;a=!0;const u=await E.confirm("Discard the changes you have not saved?");if(a=!1,!u)return}return s()}}let de=!1;async function St(){if(de)return;de=!0;let t;try{const c=await h("/settings");t=Xe(c.settings)}catch(c){de=!1,E.toast(`Could not load the settings: ${c.message||c}`,{type:"error"});return}const e=t.organisation,s={orgOn:M.observable(e.enabled),orgType:M.observable(e.type),orgName:M.observable(e.name),orgUrl:M.observable(e.url),orgLogo:M.observable(e.logo),orgEmail:M.observable(e.email),orgPhone:M.observable(e.phone),orgSame:M.observable(e.sameAs.join(`
|
|
95
|
+
`)),street:M.observable(e.address.street),locality:M.observable(e.address.locality),region:M.observable(e.address.region),postcode:M.observable(e.address.postcode),country:M.observable(e.address.country),articles:M.observable(t.articles),exclude:M.observable(t.sitemap.exclude.join(`
|
|
96
|
+
`)),blockAll:M.observable(t.robots.blockAll),robotsExtra:M.observable(t.robots.extra),onRename:M.observable(t.redirects.onRename),every:M.observable(String(t.audit.everyHours)),tMin:M.observable(String(t.audit.titleMin)),tMax:M.observable(String(t.audit.titleMax)),dMin:M.observable(String(t.audit.descMin)),dMax:M.observable(String(t.audit.descMax)),words:M.observable(String(t.audit.minWords))},a=()=>({organisation:{enabled:s.orgOn.value,type:s.orgType.value,name:s.orgName.value,url:s.orgUrl.value,logo:s.orgLogo.value,email:s.orgEmail.value,phone:s.orgPhone.value,sameAs:s.orgSame.value,address:{street:s.street.value,locality:s.locality.value,region:s.region.value,postcode:s.postcode.value,country:s.country.value}},articles:s.articles.value,sitemap:{exclude:s.exclude.value,sources:t.sitemap.sources},robots:{blockAll:s.blockAll.value,extra:s.robotsExtra.value},redirects:{onRename:s.onRename.value},audit:{everyHours:s.every.value,titleMin:s.tMin.value,titleMax:s.tMax.value,descMin:s.dMin.value,descMax:s.dMax.value,minWords:s.words.value}}),n=JSON.stringify(a()),u=M.observable(!1),i=M.computed(()=>Vt(a(),t)),b=M.computed(()=>Object.values(i.value.errors).join(" ")),R={...s,saving:u,errText:b,cannot:M.computed(()=>u.value||!!b.value),isPerson:M.computed(()=>s.orgType.value==="Person")},w=(c,B,A,G="",U="")=>`<div><label class="form-label" for="sox-s-${c}">${A}${G?` <span data-help="${y(G)}"></span>`:""}</label>
|
|
97
|
+
<input id="sox-s-${c}" class="form-input" data-model="${B}.value" ${U}></div>`,v=(c,B,A,G,U,ue="")=>w(c,B,A,ue,`type="number" min="${G}" max="${U}"`),g=(c,B,A="")=>`<label class="sox-wide" style="display:flex;align-items:center;gap:.5rem"><input type="checkbox" class="form-switch" data-model="${c}.value"> ${B}${A?` <span data-help="${y(A)}"></span>`:""}</label>`,k=E.slideover({title:"SEO settings",size:"md",position:"right"}),C=Pe(`
|
|
98
|
+
<div class="sox-form">
|
|
99
|
+
<p class="sox-note">The site's default title, description and share image are in <a href="#/settings">Settings \u203A SEO</a>.</p>
|
|
100
|
+
<section><h4>Who is behind the site <span data-help="Structured data on the home page, so search engines can show the name, logo and contact details - a knowledge panel, or a local business in maps."></span></h4><div class="sox-form-grid">
|
|
101
|
+
${g("orgOn","Tell search engines who runs the site")}
|
|
102
|
+
<div><label class="form-label" for="sox-s-type">It is</label><select id="sox-s-type" class="form-input" data-model="orgType.value">
|
|
103
|
+
${Zt.map(c=>`<option value="${c}">${{Organization:"An organisation",LocalBusiness:"A local business",Person:"A person"}[c]}</option>`).join("")}</select></div>
|
|
104
|
+
${w("name","orgName","Name")}
|
|
105
|
+
${w("url","orgUrl","Web address","Empty uses the site address.",'placeholder="https://\u2026"')}
|
|
106
|
+
${w("logo","orgLogo","Logo (or photo)","A square image works best - a path from Media or a full address.",'placeholder="/media/logo.png"')}
|
|
107
|
+
${w("email","orgEmail","Email","",'type="email"')}
|
|
108
|
+
${w("phone","orgPhone","Phone")}
|
|
109
|
+
<div class="sox-wide" data-bind-hidden="isPerson.value"><label class="form-label">Address <span data-help="For a local business, the address customers visit. Leave empty for an organisation without one."></span></label>
|
|
110
|
+
<div class="sox-form-grid"><input class="form-input sox-wide" placeholder="Street" aria-label="Street" data-model="street.value">
|
|
111
|
+
<input class="form-input" placeholder="Town or city" aria-label="Town or city" data-model="locality.value"><input class="form-input" placeholder="County or region" aria-label="County or region" data-model="region.value">
|
|
112
|
+
<input class="form-input" placeholder="Postcode" aria-label="Postcode" data-model="postcode.value"><input class="form-input" placeholder="Country (GB)" aria-label="Country" data-model="country.value"></div></div>
|
|
113
|
+
<div class="sox-wide"><label class="form-label" for="sox-s-same">Profiles elsewhere <span data-help="Your pages on Facebook, LinkedIn, Instagram, X and so on - one address per line. Search engines use them to know they are you."></span></label>
|
|
114
|
+
<textarea id="sox-s-same" rows="3" class="form-input sox-mono" data-model="orgSame.value" placeholder="https://www.linkedin.com/company/\u2026"></textarea></div>
|
|
115
|
+
${g("articles","Mark blog posts as articles","Adds BlogPosting structured data to each post - headline, image, publisher - which search engines use for article results.")}
|
|
116
|
+
</div></section>
|
|
117
|
+
<section><h4>Sitemap and robots.txt</h4><div class="sox-form-grid">
|
|
118
|
+
<div class="sox-wide"><label class="form-label" for="sox-s-ex">Leave out of the sitemap <span data-help="One address per line. Ending with / leaves out everything under it: /members/ covers /members/area. To hide one page from search engines altogether, open it on the Pages tab instead."></span></label>
|
|
119
|
+
<textarea id="sox-s-ex" rows="3" class="form-input sox-mono" data-model="exclude.value" placeholder="/thank-you /members/"></textarea></div>
|
|
120
|
+
${g("blockAll","Ask every search engine to stay away","robots.txt then says Disallow: / to everyone. For a site that is not ready yet - remember to switch it off at launch. It is a request: well-behaved crawlers honour it.")}
|
|
121
|
+
<div class="sox-wide"><label class="form-label" for="sox-s-rob">More robots.txt rules <span data-help="Lines like Disallow: /private/ join the rules for every crawler. Start with User-agent: to add rules for one crawler, e.g. User-agent: GPTBot then Disallow: / to keep an AI crawler out."></span></label>
|
|
122
|
+
<textarea id="sox-s-rob" rows="3" class="form-input sox-mono" data-model="robotsExtra.value" placeholder="Disallow: /private/"></textarea></div>
|
|
123
|
+
</div></section>
|
|
124
|
+
<section><h4>Redirects</h4><div class="sox-form-grid">
|
|
125
|
+
${g("onRename","Add a redirect when a page is renamed","The old address keeps working, and any redirect that pointed at it is re-aimed at the new one - so there are never chains.")}
|
|
126
|
+
</div></section>
|
|
127
|
+
<section><h4>The checks</h4><div class="sox-form-grid">
|
|
128
|
+
${v("every","every","Check every page every (hours)",0,168,"0 checks only when you ask.")}
|
|
129
|
+
${v("words","words","Words a page should have",0,5e3)}
|
|
130
|
+
${v("tmin","tMin","Shortest title",10,70)}
|
|
131
|
+
${v("tmax","tMax","Longest title",30,100,"Google cuts titles at about 60 characters.")}
|
|
132
|
+
${v("dmin","dMin","Shortest description",30,160)}
|
|
133
|
+
${v("dmax","dMax","Longest description",80,320,"Google cuts descriptions at about 155-160 characters.")}
|
|
134
|
+
</div></section>
|
|
135
|
+
<p class="sox-err" data-bind-hidden="!errText.value" data-bind-text="errText.value"></p>
|
|
136
|
+
<div class="sox-actions">
|
|
137
|
+
<small class="sox-muted">Ctrl+Enter saves.</small><span class="sox-spacer"></span>
|
|
138
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
139
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value"><span data-icon="save" data-icon-size="13"></span> Save</button>
|
|
140
|
+
</div>
|
|
141
|
+
</div>`);async function F(){if(!R.cannot.peek()){u.value=!0;try{await h("/settings",{method:"PUT",body:JSON.stringify(i.peek().settings)}),E.toast("Settings saved.",{type:"success"}),k.close(!0),$e()}catch(c){E.toast(`Not saved: ${c.message||c}`,{type:"error"})}finally{u.value=!1}}}k.element.appendChild(C);const T=o.helpPopovers(C),Oe=M.applyBindings(R,C,{methods:{save:F,cancel:()=>k.close()}});C.addEventListener("keydown",c=>{(c.ctrlKey||c.metaKey)&&c.key==="Enter"&&(c.preventDefault(),F())}),o.onceClosed(k,()=>{Oe.dispose(),T(),de=!1}),Ge(k,()=>JSON.stringify(a())!==n),f(()=>{try{k.close(!0)}catch{}}),I.scan(C),k.open()}async function Et(t,e){try{const s=await h("/settings"),a=Xe(s.settings);a.sitemap.sources[t]=e,await h("/settings",{method:"PUT",body:JSON.stringify(a)}),E.toast(`${K(t)} ${e?"back in":"left out of"} the sitemap.`,{type:"success"}),Me()}catch(s){E.toast(`Not changed: ${s.message||s}`,{type:"error"})}}const L=t=>Number(String(t?.dataset?.row||"").slice(1));function Tt(t){const e=V[L(t)];return e?[{header:e.urlPath},{label:"Title, description and checks\u2026",icon:"edit-3",action:()=>W(e.urlPath)},{label:"Open the page",icon:"external-link",action:()=>window.open(e.urlPath,"_blank","noopener")},{label:"Redirect it elsewhere\u2026",icon:"corner-up-right",disabled:!S.peek(),action:()=>{D("redirects"),ne.value=e.urlPath,ie.value=""}},{type:"divider"},{label:"Copy the address",icon:"copy",action:()=>oe(e.urlPath,"the address")},{label:"Copy the problems",icon:"clipboard",disabled:!e.problems?.length&&!e.error,action:()=>oe(`${e.urlPath}
|
|
142
|
+
${e.error||e.problems.join(`
|
|
143
|
+
`)}`,"the problems")}]:[]}function Ct(t){const e=O[L(t)];return e?e.kind==="dupe"?[{header:"Open one of them"},...e.paths.slice(0,12).map(s=>({label:s,icon:"edit-3",action:()=>W(s)})),{type:"divider"},{label:"Copy the addresses",icon:"copy",action:()=>oe(e.paths.join(`
|
|
144
|
+
`),"the addresses")}]:e.kind==="chains"?[{label:"Show the redirects",icon:"corner-up-right",action:()=>D("redirects")}]:[{label:"Go to Settings",icon:"settings",action:()=>{location.hash=e.link}}]:[]}function Ot(t){const e=O[L(t)];e?.kind==="dupe"?W(e.paths[0]):e?.kind==="chains"?D("redirects"):e?.link&&(location.hash=e.link)}function Lt(t){const e=Z[L(t)];if(!e)return[];const s=x.peek().rules,a=x.peek().chains.some(u=>u.id===e.id);let n=null;if(a){n=e.to;for(let u=0;u<10;u++){const i=Xt(s,n);if(!i)break;if(i.code===410){n=null;break}if(n=i.location,/^https?:/i.test(n))break}}return[{header:`${e.from} \u2192 ${e.code===410?"gone":e.to}`},{label:"Edit\u2026",icon:"edit-3",disabled:!S.peek(),action:()=>He(e)},{label:"Test it",icon:"play",action:()=>kt(e.from)},{label:"Go straight there",icon:"fast-forward",disabled:!n||!S.peek(),action:()=>Se(()=>h(`/redirects/${encodeURIComponent(e.id)}`,{method:"PUT",body:JSON.stringify({to:n})}),`${e.from} now goes straight to ${n}.`)},{label:"Open the old address",icon:"external-link",disabled:e.from.endsWith("/*"),action:()=>window.open(e.from,"_blank","noopener")},{type:"divider"},{label:"Copy",icon:"copy",action:()=>oe(`${e.from} ${e.to} ${e.code}`,"the redirect")},{label:"Remove",icon:"trash-2",danger:!0,disabled:!S.peek(),action:async()=>{await E.confirm(`Remove the redirect from ${y(e.from)}? Visitors to it will get "not found".`)&&wt([e.id],"Redirect")}}]}function Rt(t){const e=z[L(t)];return e?[{header:e.urlPath},{label:"Title, description and checks\u2026",icon:"edit-3",action:()=>W(e.urlPath)},{label:"Open",icon:"external-link",action:()=>window.open(e.urlPath,"_blank","noopener")},{type:"divider"},{label:"Copy the address",icon:"copy",action:()=>oe(e.urlPath,"the address")}]:[]}function At(t){const e=j.peek().sources[L(t)];return e?[{header:K(e.id)},{label:e.on?"Leave all of it out":"List it again",icon:e.on?"eye-off":"eye",disabled:!S.peek(),action:()=>Et(e.id,!e.on)}]:[]}function se(t,e,s){const a=E.contextMenu(t,{cursor:"glyph",match:"[data-row]",inherit:!1,density:"compact",items:i=>e(i)});f(()=>a?.destroy?.());const n=i=>{const b=i.target.closest("[data-row]");if(!(!b||i.target.closest("a"))){if(i.target.closest(".sox-kebab")){o.openMenuAt(b);return}s?.(b)}},u=i=>{const b=i.target.closest("[data-row]");b&&(i.key==="F10"&&i.shiftKey?(i.preventDefault(),o.openMenuAt(b)):i.key==="Enter"&&i.target===b&&s?.(b))};t.addEventListener("click",n),t.addEventListener("keydown",u),f(()=>{t.removeEventListener("click",n),t.removeEventListener("keydown",u)})}const Te=d.querySelector(".sox-ext-body"),Ce=d.querySelector(".sox-ext-tabs");let ce=null;async function Pt(){let t=[];try{t=(await h("/extensions"))?.tabs||[]}catch{}r()&&(Ne.value=t,Ce.replaceChildren(...t.map(e=>{const s=document.createElement("button");return s.type="button",s.setAttribute("role","tab"),s.dataset.ext=e.key,s.innerHTML=`<span data-icon="${y(e.icon||"box")}" data-icon-size="14"></span> ${y(e.label)} <span class="sox-pro">PRO</span>`,e.help&&(s.title=e.help),s.addEventListener("click",()=>D(e.key)),s})),I.scan(Ce),Je())}async function Je(){for(const e of Ce.querySelectorAll("button"))e.classList.toggle("is-on",e.dataset.ext===N.peek());const t=Ne.peek().find(e=>e.key===N.peek());if(!t){Ve.includes(N.peek())||D("overview");return}if(ce?.key!==t.key){try{ce?.dispose?.()}catch{}Te.replaceChildren();try{const e=await import(t.module);if(!r()||N.peek()!==t.key)return;const s=await e.mount(Te,{kit:o,api:o.apiRequest,scope:f,here:r,openItem:W,reload:$e,E,M,I});ce={key:t.key,dispose:s}}catch(e){Te.innerHTML=`<p class="sox-note is-bad">${y(t.label)} could not be shown: ${y(e.message||e)}</p>`}}}f(()=>{try{ce?.dispose?.()}catch{}}),d.querySelector(".sox-pages").innerHTML=rs,d.querySelector(".sox-site").innerHTML=ds,d.querySelector(".sox-rules").innerHTML=cs,d.querySelector(".sox-entries").innerHTML=us,d.querySelector(".sox-sources").innerHTML=ps;const Ke=d.querySelector(".sox-select");f(M.effect(()=>{const t=je.value;Ke.replaceChildren(new Option("Everything",""),...t.map(e=>new Option(K(e),e))),Ke.value=ve.peek()}));function D(t){N.peek()!==t&&(N.value=t,history.replaceState(null,"",`#/seo${t==="overview"?"":`/${t}`}`),t==="redirects"&&te(),t==="sitemap"&&Me()),Je()}const It={tab:N,busy:he,error:me,canManage:S,scoreText:nt,scoreWidth:it,scoreTone:lt,ov:rt,attention:dt,asOf:ct,sourceOptions:je,source:ve,show:J,query:fe,pageRows:qe,pagesEmpty:ut,siteRows:De,ruleRows:xe,ruleCount:pt,ruleText:ht,rulesEmpty:mt,chainText:bt,renameText:vt,ruleQuery:ge,newFrom:ne,newTo:ie,newCode:Ie,sourceRows:Be,smRows:we,smShow:X,smQuery:ye,smText:gt,smEmpty:yt,robots:ft,extOn:ot},Nt={tab_overview:()=>D("overview"),tab_redirects:()=>D("redirects"),tab_sitemap:()=>D("sitemap"),show_all:()=>{J.value="all"},show_problems:()=>{J.value="problems"},show_hidden:()=>{J.value="hidden"},sm_in:()=>{X.value="in"},sm_out:()=>{X.value="out"},importCsv:Mt,exportRules:()=>{const t=x.peek().rules;if(!t.length){E.toast("Nothing to export.",{type:"info"});return}ls(`redirects-${location.hostname}-${new Date().toISOString().slice(0,10)}.csv`,is(t.map(e=>({from:e.from,to:e.to,code:e.code,note:e.note||"",hits:e.hits||0,last_used:e.lastHit||""}))))},addRule:()=>{const t={from:ne.peek(),to:ie.peek(),code:Number(Ie.peek())},{errors:e}=Qe(t,x.peek().rules);return Object.keys(e).length?(E.toast(Object.values(e).join(" "),{type:"warning"}),!1):(xt(t).then(s=>{s&&(ne.value="",ie.value="")}),!1)}},jt=M.applyBindings(It,d,{methods:Nt});f(()=>jt.dispose()),f(o.helpPopovers(d,{position:"bottom"})),se(d.querySelector(".sox-pages"),Tt,t=>{const e=V[L(t)];e&&W(e.urlPath)}),se(d.querySelector(".sox-site"),Ct,Ot),se(d.querySelector(".sox-rules"),Lt,t=>{const e=Z[L(t)];e&&S.peek()&&He(e)}),se(d.querySelector(".sox-entries"),Rt,t=>{const e=z[L(t)];e&&W(e.urlPath)}),se(d.querySelector(".sox-sources"),At),o.addBannerAction(l,{id:"seo-check",icon:"refresh-cw",title:"Check every page again",onClick:_e}),o.addBannerAction(l,{id:"seo-settings",icon:"settings",title:"SEO settings",onClick:St}),I.scan(d),await Promise.all([$e(),Pt()]),r()&&I.scan(d),r()&&!m.peek()?.at&&!m.peek()?.running&&_e()}};
|
package/package.json
CHANGED
|
@@ -106,25 +106,6 @@
|
|
|
106
106
|
"tests/security.test.js": "f0cb70dcb28bc8b3de8d3b39064616296382c5b0f97bfd615ee8e24fe3b031bc"
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
|
-
"seo": {
|
|
110
|
-
"version": "1.0.0",
|
|
111
|
-
"files": {
|
|
112
|
-
"CLAUDE.md": "c4b326afeacded36d076f96ec3554b6cd875087ea5c84b29dffb144059292ff7",
|
|
113
|
-
"admin/lib/analyse.js": "368b9e20bc1eac957870272ee6909b5f7c09ecf0a2fc2c344fb5cd85473f18cd",
|
|
114
|
-
"admin/lib/redirects.js": "a506ca40b712c3d8087d6698397b900fe5005ae6094f80dbc3f3091d5d9b6819",
|
|
115
|
-
"admin/lib/schema.js": "4313e0ed528562ee8b85c18479663c8216101f892848b44426baefc8d7c1c7e8",
|
|
116
|
-
"admin/lib/settings.js": "94e76c2960376f0cc0570128a7226af177f5de7ee115357e37baba45b1e05e19",
|
|
117
|
-
"admin/templates/seo.html": "e8684d98bd0649c001d7395b75b2bc3c74234f947d3dbda508ff4ecb91e20d1c",
|
|
118
|
-
"admin/views/seo.js": "b37774a5df642bdbdb6f7fb89b12dbc6d45d06d2b70bd2de7faf4c3098c26189",
|
|
119
|
-
"config.js": "d90f4cfb10b2f44b528de37522d31fd01351a6ebbab95556b5dfa2820d69e1a8",
|
|
120
|
-
"plugin.js": "3dd88bec75535349e6a748bae90fc49e54e2781c4765b9c8ff02898795ef476d",
|
|
121
|
-
"plugin.json": "6fe2fb5d50d37cc9749cf84db7ed59c5557a52c1b38f63c8e7bc1744065cd7d6",
|
|
122
|
-
"server/audit.js": "f51fcf9403ad40fde9e9ba5f31e58ed95fc2a5c8514c921146f23ee1ca2e0bd3",
|
|
123
|
-
"server/registry.js": "9336614bff3ab8b4fe9d50de074a9633307ab41c46fa62113ca82379cfad90a6",
|
|
124
|
-
"server/store.js": "3584ec3efd8a2d0ba8fd5be7e56d97cca76b5b2358a7dda99e379c42be67253c",
|
|
125
|
-
"tests/seo.test.js": "bdc52f2a9f8f02aaa0cb907f76f1acf8ff887034172e18d9ae108d3675b68f1a"
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
109
|
"shopping-cart": {
|
|
129
110
|
"version": "1.0.0",
|
|
130
111
|
"files": {
|