domma-cms 1.8.35 → 1.8.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/admin/index.html +1 -1
- package/admin/js/app.js +1 -1
- package/admin/js/lib/seo-analyse.js +1 -1
- package/admin/js/lib/seo-settings.js +2 -2
- package/admin/js/templates/docs/usage-search-engines.html +84 -0
- package/admin/js/templates/documentation.html +4 -0
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/seo.js +15 -14
- package/docs/DommaFeatures.md +9 -2
- package/docs/plugin-development.md +1 -1
- package/package.json +12 -12
- package/plugins/booking/CLAUDE.md +5 -0
- package/plugins/booking/admin/views/settings.js +1 -1
- package/plugins/booking/docs/guide.md +2 -0
- package/plugins/booking/plugin.json +2 -2
- package/plugins/booking/plugin.public.js +15 -2
- package/plugins/booking/tests/seo.test.js +71 -0
- package/plugins/free-tier.lock.json +18 -16
- package/plugins/shopping-cart/CLAUDE.md +9 -0
- package/plugins/shopping-cart/admin/views/settings.js +1 -1
- package/plugins/shopping-cart/docs/guide.md +2 -1
- package/plugins/shopping-cart/plugin.js +18 -0
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/plugin.public.js +39 -4
- package/plugins/shopping-cart/tests/seo.test.js +95 -0
- package/public/js/site.js +1 -1
- package/public/js/vendor/dompurify/LICENSE +202 -0
- package/public/js/vendor/dompurify/LICENSE-MPL +373 -0
- package/public/js/vendor/dompurify/purify.min.js +7 -0
- package/scripts/build.js +3 -0
- package/server/routes/api/collections.js +2 -0
- package/server/routes/api/effects.js +5 -1
- package/server/routes/api/forms.js +2 -0
- package/server/routes/api/views.js +2 -0
- package/server/routes/public.js +20 -1
- package/server/server.js +5 -4
- package/server/services/docs.js +1 -0
- package/server/services/hooks.js +3 -2
- package/server/services/markdown.js +200 -120
- package/server/services/nojsLinks.js +130 -0
- package/server/services/renderer.js +23 -6
- package/server/services/sitemap.js +100 -2
- package/server/templates/page.html +17 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.37] - 2026-10-22
|
|
4
|
+
|
|
5
|
+
- **Security updates to the libraries the CMS runs on.** Fastify 5.12 (form validation could be bypassed, and forwarded-address headers spoofed), sanitize-html 2.18 (ways past the HTML cleaner), the sign-in token library (a token signed the wrong way could be accepted), @fastify/static 10 (a protected file could be reached through an oddly written address), Nodemailer 10 (header injection in outgoing mail), sharp 0.35 (flaws in the image libraries an uploaded picture could reach), and fixes deeper down for denial-of-service bugs in upload, YAML and URL handling. Nothing changes in how the CMS looks or works.
|
|
6
|
+
|
|
7
|
+
## [1.8.36] - 2026-10-22
|
|
8
|
+
|
|
9
|
+
- **Pages load their scripts after the words, and DOMPurify ships with the CMS.** The scripts that can wait now carry `defer`, so the browser finishes reading the page first, and DOMPurify (the library that cleans HTML in the browser) comes from your own site at a fixed version instead of a CDN. A new **Documentation > Usage > Search Engines & AI** page explains what search engines and AI crawlers see and how to check a page yourself.
|
|
10
|
+
- **AI crawlers can read your navbar, footer, collections and forms.** Robots that run no JavaScript (GPTBot, ClaudeBot and others) now find the navbar and footer links (sub-menus included), the copyright line and social links in the page itself, the entries of a searchable or paged collection (up to 200, not just the first page), the rows of an interactive `[view]`, and each form's title and description. Visitors see no change: the plain copies are hidden and give way to the real navbar, footer and forms as the page loads.
|
|
11
|
+
- **`/llms.txt` for AI assistants.** Your site's name and description, then every page in the sitemap (and your blog posts) as a link with its description - the format AI assistants look for. On by default; switch it off in **Tools > SEO > Settings**. It is withdrawn while "Ask every search engine to stay away" is on.
|
|
12
|
+
- **Google can draw pages that read public data.** robots.txt still keeps crawlers out of `/api/`, but now lets through the public parts a page reads while it loads (a form's questions, effects settings, components, public collection and view listings), so Google sees the page as a visitor does. Nothing that needs a sign-in is let through.
|
|
13
|
+
- **Booking 1.7.2 and Shopping Cart 1.7.2.** The booking and shop pages always have a meta description (from your intro, or a line made from the site's name). An empty shop asks search engines not to list it until something is on sale, and the shop and its products are now in the sitemap.
|
|
14
|
+
|
|
3
15
|
## [1.8.35] - 2026-10-21
|
|
4
16
|
|
|
5
17
|
- **Missing pages answer 404 again on sites without their own 404 page.** Since 1.8.29 such a site answered every missing address with a server error, because the built-in 404 and 403 pages called a helper that had been moved away. The address shown on those pages is escaped, as before.
|
package/admin/index.html
CHANGED
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
</div>
|
|
64
64
|
|
|
65
65
|
<!-- DOMPurify - must load before DommaJS -->
|
|
66
|
-
<script src="
|
|
66
|
+
<script src="/public/js/vendor/dompurify/purify.min.js?v=3.4.17"></script>
|
|
67
67
|
|
|
68
68
|
<!-- DommaJS -->
|
|
69
69
|
<script src="/dist/domma/domma.min.js"></script>
|
package/admin/js/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{installSmoothCarets as K}from"./lib/smooth-caret.js";import{views as J}from"./views/index.js";import{withPluginChrome as O}from"./lib/plugin-chrome.js";import{api as c,getUser as x,isAuthenticated as m,logout as Y,sendToLogin as Q}from"./api.js";import{claimDrafts as X}from"./lib/draft-owner.js";import{isAuthScreen as b,loginHashFor as Z}from"./lib/login-arrange.js";import{installHttpInterceptor as tt}from"./http-interceptor.js";import{installResizableSlideovers as et}from"./lib/slideover-resizable.js";import{installActivityThrobber as it}from"./lib/activity-throbber.js";import{installUpdateNotice as ot}from"./lib/update-notice.js";import{isPathAllowed as at,redirectFor as st}from"./lib/admin-scope.js";import{deliverDesktop as nt,desktopActive as rt,notificationSettings as lt}from"./lib/desktop-notify.js";import{avatarUrl as ct}from"./lib/avatar.js";import{installCalculator as mt}from"./lib/calculator.js";import{takeOverviewPass as pt,withDashboardStrip as _}from"./lib/dashboards-strip.js";import{installHotkeys as dt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as V}from"./lib/admin-brand.js";import{installRouterCatchUp as ut}from"./lib/router-catch-up.js";import{installSidebarDrawer as ht}from"./lib/sidebar-drawer.js";$(()=>{K(document),tt(),mt(),ot(),it(),et(),ht(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,l="/",w=!1,g=null;function D(t){if(P[t])return E.toast(`${P[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||st(p,t);return i||((t.split("?")[0]||"/")==="/"&&l&&l!=="/"&&!pt()?y[l]||l:null)}const y={},P={};R.use(async(t,i,e)=>{if(b(t.path))return e();if(!m()){Q();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function f(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,V(h),l=t.adminHome||"/";const i=t.permissions||[];return w=at(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,l="/",h||V(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let N=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(N){A=!0;return}N=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{b(t.path)&&(u&&(clearInterval(u),u=null),p=null,l="/",w=!1,g=null)});const W=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - 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/editor",view:"tutorialEditor",title:"The Editor - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - 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/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - 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:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - 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:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",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:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - 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:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - 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:i})=>{if(!b(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),b(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await U();const e=await f(),o=D(t.path);if(o&&R.navigate(o),j(e),lt.read().signInToasts)try{const a=await c.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 v=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}B()}}),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 H=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function q(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 i=t?String(t.path).split("?")[0]:"";if(!t||!q(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,v=()=>{a+=n;for(const T of H){const C=document.querySelector(T);if(!C)continue;if(Array.from(C.options||[]).find(z=>z.value===o)){C.value=o,C.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(v,n)};setTimeout(v,n)});const L="cms_card_states",k=S.get(L)||{};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&&k[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 i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(L,k))}),dt(),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 i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const r={...J},G={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site 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"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site 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"},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,i]of Object.entries(G))r[t]&&(r[t]=O(r[t],{plugin:t,licence:"Built in",...i}));r.dashboard=_(r.dashboard);const I=[...W];async function U(){if(m())try{const t=await c.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);r[e]=O(a[o.exportName],(t.meta||{})[e])}catch{}r.dashboard=_(r.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(y[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(P[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function B(){const t=x();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ct(t);$("#topbar-user").html(`
|
|
1
|
+
import{installSmoothCarets as K}from"./lib/smooth-caret.js";import{views as J}from"./views/index.js";import{withPluginChrome as O}from"./lib/plugin-chrome.js";import{api as c,getUser as x,isAuthenticated as m,logout as Y,sendToLogin as Q}from"./api.js";import{claimDrafts as X}from"./lib/draft-owner.js";import{isAuthScreen as b,loginHashFor as Z}from"./lib/login-arrange.js";import{installHttpInterceptor as tt}from"./http-interceptor.js";import{installResizableSlideovers as et}from"./lib/slideover-resizable.js";import{installActivityThrobber as it}from"./lib/activity-throbber.js";import{installUpdateNotice as ot}from"./lib/update-notice.js";import{isPathAllowed as at,redirectFor as st}from"./lib/admin-scope.js";import{deliverDesktop as nt,desktopActive as rt,notificationSettings as lt}from"./lib/desktop-notify.js";import{avatarUrl as ct}from"./lib/avatar.js";import{installCalculator as mt}from"./lib/calculator.js";import{takeOverviewPass as pt,withDashboardStrip as _}from"./lib/dashboards-strip.js";import{installHotkeys as dt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as V}from"./lib/admin-brand.js";import{installRouterCatchUp as ut}from"./lib/router-catch-up.js";import{installSidebarDrawer as ht}from"./lib/sidebar-drawer.js";$(()=>{K(document),tt(),mt(),ot(),it(),et(),ht(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,l="/",w=!1,g=null;function D(t){if(P[t])return E.toast(`${P[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||st(p,t);return i||((t.split("?")[0]||"/")==="/"&&l&&l!=="/"&&!pt()?y[l]||l:null)}const y={},P={};R.use(async(t,i,e)=>{if(b(t.path))return e();if(!m()){Q();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function f(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,V(h),l=t.adminHome||"/";const i=t.permissions||[];return w=at(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,l="/",h||V(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let N=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(N){A=!0;return}N=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{b(t.path)&&(u&&(clearInterval(u),u=null),p=null,l="/",w=!1,g=null)});const W=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - 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/editor",view:"tutorialEditor",title:"The Editor - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - 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/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/usage/search-engines",view:"usageSearchEngines",title:"Search Engines & AI - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - 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:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - 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:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",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:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - 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:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - 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:i})=>{if(!b(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),b(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await U();const e=await f(),o=D(t.path);if(o&&R.navigate(o),j(e),lt.read().signInToasts)try{const a=await c.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 v=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}B()}}),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 H=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function q(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 i=t?String(t.path).split("?")[0]:"";if(!t||!q(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,v=()=>{a+=n;for(const T of H){const C=document.querySelector(T);if(!C)continue;if(Array.from(C.options||[]).find(z=>z.value===o)){C.value=o,C.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(v,n)};setTimeout(v,n)});const L="cms_card_states",k=S.get(L)||{};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&&k[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 i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(L,k))}),dt(),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 i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const r={...J},G={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site 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"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site 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"},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,i]of Object.entries(G))r[t]&&(r[t]=O(r[t],{plugin:t,licence:"Built in",...i}));r.dashboard=_(r.dashboard);const I=[...W];async function U(){if(m())try{const t=await c.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);r[e]=O(a[o.exportName],(t.meta||{})[e])}catch{}r.dashboard=_(r.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(y[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(P[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function B(){const t=x();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ct(t);$("#topbar-user").html(`
|
|
2
2
|
${o?`<img id="topbar-avatar" class="topbar-avatar" src="${d(o)}" alt="">`:""}
|
|
3
3
|
<span id="topbar-user-name" class="topbar-user-name">${d(t.name)}</span>
|
|
4
4
|
<span class="topbar-role-badge topbar-role-badge--${d(t.role)}">${e}</span>
|
|
@@ -1 +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}}
|
|
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," ").replace(/<div\b[^>]*\bclass\s*=\s*["'][^"']*\b(?:cms-nojs-links|cms-form-embed-summary)\b[^>]*>[\s\S]*?<\/div\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,m=[...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:m,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 f=(e,s,n=`${s}s`)=>`${e} ${e===1?s:n}`,v=(e,s)=>!!s&&String(e||"").toLowerCase().includes(s),T=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,C="")=>p.push({id:o,group:b,status:k,title:L,detail:S,fix:C}),$=/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,m=(u||"").length;m?m<a.descMin?t("description","Search result","warn","Description",`${m} characters - short.`,`Aim for ${a.descMin}-${a.descMax} characters.`):m>a.descMax?t("description","Search result","warn","Description",`${m} 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",`${m} 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",`${f(e.headings.length,"heading")}, in order.`),e.words<Math.round(a.minWords/3)?t("words","Content","warn","Length",`${f(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",`${f(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",`${f(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 ${f(x.length,"image")} described.`);const c=e.links.filter(o=>j(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",`${f(c.length,"link")}.`);const h=String(n||"").trim().toLowerCase();if(h){const b=[["title",v(e.title,h)],["description",v(u,h)],["main heading",g.some(S=>v(S.text,h))],["address",T(s).includes(T(h))],["first paragraph",v(e.firstPara,h)]].filter(([,S])=>!S).map(([S])=>S),k=N(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 (${f(k,"time")}).`,"Over about 3% reads as stuffing - use other words for the same thing."):k&&t("density","Keyphrase","pass","Repetition",`Used ${f(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 j(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 N(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}}
|
|
@@ -1,2 +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),
|
|
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=o.trim()}e.redirects&&e.redirects.onRename!==void 0&&(r.redirects.onRename=
|
|
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:""},llms:{enabled:!0},redirects:{onRename:!0},audit:{everyHours:24,...h}};const f=(e,i=300)=>String(e??"").trim().slice(0,i),l=(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},b=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))),s=e.organisation;if(s&&typeof s=="object"){const o=r.organisation;s.enabled!==void 0&&(o.enabled=l(s.enabled,!1)),s.type!==void 0&&(ORG_TYPES.includes(s.type)?o.type=s.type:n["organisation.type"]="Organisation, local business or person.");for(const t of["name","email","phone"])s[t]!==void 0&&(o[t]=f(s[t],200));for(const t of["logo","url"]){if(s[t]===void 0)continue;const a=f(s[t],500);a&&!b(a)&&!a.startsWith("/")?n[`organisation.${t}`]=`${t==="logo"?"The logo":"The address"}: a full web address, or a path like /media/logo.png.`:o[t]=a}if(s.sameAs!==void 0){const t=m(s.sameAs),a=t.filter(c=>!b(c));a.length?n["organisation.sameAs"]=`Not web addresses: ${a.slice(0,2).join(", ")}`:o.sameAs=[...new Set(t)].slice(0,20)}if(s.address&&typeof s.address=="object")for(const t of Object.keys(DEFAULT_SETTINGS.organisation.address))s.address[t]!==void 0&&(o.address[t]=f(s.address[t],200))}if(e.articles!==void 0&&(r.articles=l(e.articles,!0)),e.sitemap&&typeof e.sitemap=="object"){if(e.sitemap.exclude!==void 0){const o=m(e.sitemap.exclude),t=o.filter(a=>!a.startsWith("/"));t.length?n["sitemap.exclude"]=`Each starts with "/": ${t.slice(0,3).join(", ")}`:r.sitemap.exclude=[...new Set(o)].slice(0,200)}if(e.sitemap.sources&&typeof e.sitemap.sources=="object")for(const[o,t]of Object.entries(e.sitemap.sources))/^[a-z0-9][a-z0-9:-]*$/.test(o)&&(r.sitemap.sources[o]=l(t,!0))}if(e.robots&&typeof e.robots=="object"&&(e.robots.blockAll!==void 0&&(r.robots.blockAll=l(e.robots.blockAll,!1)),e.robots.extra!==void 0)){const o=String(e.robots.extra??"").replace(/\r/g,"").slice(0,5e3),t=o.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=o.trim()}e.llms&&e.llms.enabled!==void 0&&(r.llms.enabled=l(e.llms.enabled,!0)),e.redirects&&e.redirects.onRename!==void 0&&(r.redirects.onRename=l(e.redirects.onRename,!0));const d=e.audit;if(d&&typeof d=="object"){const o=(t,a,c,g)=>{if(d[t]===void 0)return;const u=p(d[t],a,c);u===null?n[`audit.${t}`]=`${g}: a whole number, ${a} to ${c}.`:r.audit[t]=u};o("everyHours",0,168,"Check every (hours)"),o("titleMin",10,70,"Shortest title"),o("titleMax",30,100,"Longest title"),o("descMin",30,160,"Shortest description"),o("descMax",80,320,"Longest description"),o("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||{}},llms:{...i.llms,...e.llms||{}},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 s=String(r).toLowerCase().replace(/\*$/,"");return s.endsWith("/")?n===s.slice(0,-1)||n.startsWith(s):n===s})}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="search"></span> Search Engines & AI</h1>
|
|
3
|
+
<a href="#/documentation" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> All usage topics</a>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="row">
|
|
7
|
+
<div class="col-12">
|
|
8
|
+
<div class="docs-body">
|
|
9
|
+
|
|
10
|
+
<p>Google and Bing run a page's JavaScript before they read it, but many of the robots that collect pages for AI
|
|
11
|
+
tools (GPTBot, ClaudeBot and others) do not: they read the HTML the server sends and nothing
|
|
12
|
+
more. A site that builds its words in the browser is close to blank to them. This page explains
|
|
13
|
+
what Domma CMS sends, what still needs JavaScript, and how to check a page yourself.</p>
|
|
14
|
+
|
|
15
|
+
<h3>What every robot sees</h3>
|
|
16
|
+
<p>Every public page is built on the server. The HTML that arrives already holds:</p>
|
|
17
|
+
<ul>
|
|
18
|
+
<li>the page's words, headings, pictures and links, including blocks, cards, tabs, accordions,
|
|
19
|
+
collections, blog posts and the Docs and manual pages;</li>
|
|
20
|
+
<li>the title, description and canonical address, and the share card tags for Facebook, X and
|
|
21
|
+
LinkedIn (set them in the page editor's <strong>SEO</strong> tab or in <strong>Tools >
|
|
22
|
+
SEO</strong>);</li>
|
|
23
|
+
<li>structured data (JSON-LD) saying what the page is - a web page, a blog post, who runs the
|
|
24
|
+
site - plus the breadcrumb trail;</li>
|
|
25
|
+
<li>the links in the navbar and footer, with their sub-menus, the copyright line and your
|
|
26
|
+
social links - written as plain links that only robots and screen readers notice, and
|
|
27
|
+
replaced by the real navbar and footer as the page loads;</li>
|
|
28
|
+
<li>every entry of a collection with search, filters or paging, and what each form is for (its
|
|
29
|
+
title and description).</li>
|
|
30
|
+
</ul>
|
|
31
|
+
<p><code>/sitemap.xml</code> lists every published page, and <code>/robots.txt</code> lets every
|
|
32
|
+
robot in except to <code>/admin/</code> and <code>/api/</code>. Drafts, private pages and pages
|
|
33
|
+
marked noindex are left out of the sitemap. A few public parts of <code>/api/</code> are let
|
|
34
|
+
through - the ones a page reads while it loads, such as a form's questions - so Google, which
|
|
35
|
+
runs a page's JavaScript, can draw the page as a visitor sees it. Nothing that needs a sign-in
|
|
36
|
+
is.</p>
|
|
37
|
+
|
|
38
|
+
<h3>llms.txt for AI assistants</h3>
|
|
39
|
+
<p><code>/llms.txt</code> is a short Markdown list of the same pages as the sitemap (and your blog
|
|
40
|
+
posts), each with its title, full address and description, under your site's name and
|
|
41
|
+
description. It is the format AI assistants look for. Write a description for each page in the
|
|
42
|
+
page editor's <strong>SEO</strong> tab and it appears here too.</p>
|
|
43
|
+
<p>It is on by default. Switch it off in <strong>Tools > SEO > Settings</strong> ("Publish
|
|
44
|
+
/llms.txt for AI assistants"). While "Ask every search engine to stay away" is on, it is
|
|
45
|
+
withdrawn as well.</p>
|
|
46
|
+
|
|
47
|
+
<h3>Kept out of the way of the page</h3>
|
|
48
|
+
<ul>
|
|
49
|
+
<li>Scripts load at the end of the page, after the words. The ones that can wait carry
|
|
50
|
+
<code>defer</code>, so the browser finishes reading the page first.</li>
|
|
51
|
+
<li>The CMS's own scripts come from your own site, never another company's servers, so no
|
|
52
|
+
version changes under you without an update. DOMPurify, the library that cleans HTML in
|
|
53
|
+
the browser, ships with the CMS at a fixed version.</li>
|
|
54
|
+
<li>Pages are compressed and, in production, held in a cache, so a robot gets a quick answer.</li>
|
|
55
|
+
</ul>
|
|
56
|
+
|
|
57
|
+
<h3>What still needs JavaScript</h3>
|
|
58
|
+
<p>These parts are drawn in the browser, so a robot that runs no JavaScript does not see them:</p>
|
|
59
|
+
<ul>
|
|
60
|
+
<li><strong>Form questions</strong> - the form's title and description are in the page, but the
|
|
61
|
+
questions appear once the page has loaded. Say what the form is for in the text around it
|
|
62
|
+
too.</li>
|
|
63
|
+
<li><strong>Large interactive collections</strong> - a collection set to load its pages from the
|
|
64
|
+
server (<code>mode="server"</code>, or one too big to send whole) sends its first page of
|
|
65
|
+
entries; the rest arrive as the visitor pages through.</li>
|
|
66
|
+
<li><strong>Site search results</strong>, <strong>the Calendar's month view</strong> and other
|
|
67
|
+
Tool screens that load live data.</li>
|
|
68
|
+
</ul>
|
|
69
|
+
<p>Robots also find your pages through the sitemap and llms.txt, so a page left out of the menus
|
|
70
|
+
is still found.</p>
|
|
71
|
+
|
|
72
|
+
<h3>Check a page yourself</h3>
|
|
73
|
+
<ol>
|
|
74
|
+
<li>Open the page and choose <strong>View page source</strong> (Ctrl+U). That is what a robot
|
|
75
|
+
that runs no JavaScript reads.</li>
|
|
76
|
+
<li>Search it (Ctrl+F) for a sentence from the page. If it is there, every robot can read
|
|
77
|
+
it.</li>
|
|
78
|
+
<li>Run the audit in <strong>Tools > SEO</strong> for titles, descriptions, headings and
|
|
79
|
+
alt text across the whole site.</li>
|
|
80
|
+
</ol>
|
|
81
|
+
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
<div class="card-body"><h3 style="margin-top:0;"><span data-icon="home"></span> Dashboard</h3>
|
|
71
71
|
<p class="text-muted" style="margin-bottom:0;">The admin's home, and Dashboard Pro: widgets, boards and settings.</p></div>
|
|
72
72
|
</a>
|
|
73
|
+
<a href="#/docs/usage/search-engines" class="card" style="text-decoration:none;display:block;">
|
|
74
|
+
<div class="card-body"><h3 style="margin-top:0;"><span data-icon="search"></span> Search Engines & AI</h3>
|
|
75
|
+
<p class="text-muted" style="margin-bottom:0;">What Google and AI crawlers read on your pages, and how to check.</p></div>
|
|
76
|
+
</a>
|
|
73
77
|
<a href="#/docs/usage/plugins" class="card" style="text-decoration:none;display:block;">
|
|
74
78
|
<div class="card-body"><h3 style="margin-top:0;"><span data-icon="package"></span> Plugins</h3>
|
|
75
79
|
<p class="text-muted" style="margin-bottom:0;">The Marketplace: install, update, restarts, switches and dependencies.</p></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mountScaffolder as i}from"../lib/crud-tutorial.js";function t(o,s){return{templateUrl:`/admin/js/templates/docs/${o}.html`,async onMount(e){typeof s=="function"&&s(e),Domma.icons.scan(),Domma.syntax.scan()}}}export const docViews={tutorialCrud:t("tutorial-crud",o=>{const s=o.find("#tutorial-scaffolder-mount").get(0);s&&i(s,null,null)}),tutorialPlugin:t("tutorial-plugin"),tutorialEditor:t("tutorial-editor"),tutorialForms:t("tutorial-forms"),componentsReference:t("components-reference"),componentsHowto:t("components-howto"),componentsWalkthrough:t("components-walkthrough"),componentsRules:t("components-rules")};const a=["pages","media","navigation","dconfig","shortcodes","site-settings","plugins","users-roles","views","actions","cta-shortcode","editions","collections","blocks","tools","dashboard"],c=["authentication","pages","settings","layouts","navigation","media","users","plugins","collections","views","actions","forms","scaffold","external","content","builder"],n=o=>o.split("-").map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");for(const o of a)docViews["usage"+n(o)]=t(`usage-${o}`);for(const o of c)docViews["apiRef"+n(o)]=t(`api-${o}`);
|
|
1
|
+
import{mountScaffolder as i}from"../lib/crud-tutorial.js";function t(o,s){return{templateUrl:`/admin/js/templates/docs/${o}.html`,async onMount(e){typeof s=="function"&&s(e),Domma.icons.scan(),Domma.syntax.scan()}}}export const docViews={tutorialCrud:t("tutorial-crud",o=>{const s=o.find("#tutorial-scaffolder-mount").get(0);s&&i(s,null,null)}),tutorialPlugin:t("tutorial-plugin"),tutorialEditor:t("tutorial-editor"),tutorialForms:t("tutorial-forms"),componentsReference:t("components-reference"),componentsHowto:t("components-howto"),componentsWalkthrough:t("components-walkthrough"),componentsRules:t("components-rules")};const a=["pages","media","navigation","dconfig","shortcodes","site-settings","plugins","users-roles","views","actions","cta-shortcode","editions","collections","blocks","tools","dashboard","search-engines"],c=["authentication","pages","settings","layouts","navigation","media","users","plugins","collections","views","actions","forms","scaffold","external","content","builder"],n=o=>o.split("-").map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");for(const o of a)docViews["usage"+n(o)]=t(`usage-${o}`);for(const o of c)docViews["apiRef"+n(o)]=t(`api-${o}`);
|