domma-cms 1.15.0 → 1.16.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/CHANGELOG.md +8 -0
- package/admin/css/admin.css +1 -1
- package/admin/js/lib/admin-brand.js +1 -1
- package/admin/js/lib/google-analytics-panel.js +9 -0
- package/admin/js/lib/google-business-callback.js +1 -0
- package/admin/js/lib/google-business-import.js +3 -0
- package/admin/js/lib/google-business-panel.js +32 -0
- package/admin/js/lib/seo-defaults.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +5 -5
- package/admin/js/lib/site-search-section.js +1 -0
- package/admin/js/lib/site-settings-arrange.js +3 -3
- package/admin/js/lib/topbar-account.js +1 -1
- package/admin/js/lib/topbar-cache.js +6 -0
- package/admin/js/lib/topbar-notifications.js +4 -4
- package/admin/js/templates/docs/usage-site-settings.html +7 -5
- package/admin/js/templates/settings.html +8 -41
- package/admin/js/views/analytics.js +8 -6
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +50 -42
- package/admin/js/views/settings.js +3 -3
- package/docs/DommaFeatures.md +1 -1
- package/docs/google-business-profile.md +44 -0
- package/docs/screenshots/google-business-profile-review.png +0 -0
- package/docs/screenshots/notifications-cache.png +0 -0
- package/docs/screenshots/site-settings-search.png +0 -0
- package/package.json +1 -1
- package/public/js/site.js +1 -1
- package/server/routes/api/analytics.js +19 -0
- package/server/routes/api/google-business.js +64 -0
- package/server/routes/api/seo.js +14 -3
- package/server/services/adminBrand.js +1 -0
- package/server/services/googleBusiness.js +225 -0
- package/server/services/renderer.js +2 -0
- package/server/templates/page.html +1 -1
package/admin/js/views/search.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{apiRequest as
|
|
1
|
+
import{apiRequest as A}from"../api.js";import{helpPopovers as ge}from"../lib/help-popover.js";import{openMenuAt as oe,sidebarCount as ye}from"../lib/tool-kit.js";import{POSITION_LABEL as xe,clampNotes as Se,draftQuestion as we,formFromSettings as D,keptDraft as ke,searchDraftKey as Me,liveChips as Ee,maxRadius as Te,payloadFromForm as l,previewState as ne,radiusForSize as Le,resultRow as Re,snapshot as d}from"../lib/search-settings-arrange.js";import{fmtDateTime as Pe}from"../lib/dates.js";import{SHAPES as ze,whileLoading as qe}from"../lib/loading.js";const Oe="#/search",Ae='<span class="ssx-chip" data-bind-class="cls">{{text}}</span>',De=`
|
|
2
2
|
<div class="ssx-row" data-url="{{url}}" tabindex="0">
|
|
3
3
|
<span class="ssx-rank">{{rank}}</span>
|
|
4
4
|
<span class="ssx-main"><strong>{{title}}</strong><small>{{meta}}</small><span class="ssx-snippet">{{snippet}}</span></span>
|
|
5
5
|
<span class="ssx-chip">{{score}}</span>
|
|
6
6
|
<button type="button" class="ssx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
|
|
7
|
-
</div>`;
|
|
7
|
+
</div>`;export async function mountSearchSettings(H,{onDirty:ae=()=>{},onReady:re=()=>{}}={}){const K=[],r=e=>(K.push(e),e);let B=!1;const a=H.get(0).querySelector(".ssx")||H.get(0),T=()=>!B&&a.isConnected;let L="";try{L=S.get("auth_user")?.id||""}catch{}const c=Me(L),b=a.querySelector(".ssx-loading"),Q=a.querySelector(".ssx-body");let $,C=null;try{[$,C]=await qe(b,Promise.all([A("/search/settings"),A("/search/_count").catch(()=>null)]),ze.rows)}catch(e){if(!a.isConnected)return;b&&(b.hidden=!0);const t=a.querySelector(".ssx-failed");t&&(t.textContent=`Could not load the search settings: ${e.message||e}. Reload the page to try again.`,t.hidden=!1);return}if(!a.isConnected)return;b&&(b.hidden=!0),Q&&(Q.hidden=!1);const R=M.observable($),V=D($),s={};for(const[e,t]of Object.entries(V))s[e]=M.observable(t);const h=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),p=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),g=M.observable(d(V)),f=M.computed(()=>d(p())!==g.value);r(M.effect(()=>ae(f.value)));const P=M.observable(!1);let z=g.peek();const J=M.observable(C?{searchable:C.count}:null),Y=M.observable([]);r(M.effect(()=>{Y.value=Ee(R.value,J.value).map(e=>({text:e.text,cls:e.tone?`is-${e.tone}`:""}))}));let m=Number(s.iconSize.peek())||52;r(M.effect(()=>{const e=Number(s.iconSize.value)||52;if(e===m)return;const t=Le(s.iconRadius.peek(),m,e);m=e,String(t)!==String(s.iconRadius.peek())&&(s.iconRadius.value=String(t))}));const ie=M.computed(()=>Te(s.iconSize.value)),G=a.querySelector("#ssx-radius");r(M.effect(()=>{G&&(G.max=String(ie.value))}));const y=M.observable(""),x=M.observable([]),q=M.observable(""),O=M.observable(""),ce=M.computed(()=>ne(y.value,s.minQueryLength.value).note);let N=0,_=null;async function le(){const e=ne(y.peek(),s.minQueryLength.peek()),t=++N;if(!e.run){x.value=[],O.value="",q.value="";return}const n=l(h()).maxResults;try{const o=await A(`/search/preview?q=${encodeURIComponent(y.peek().trim())}&max=${n}`);if(t!==N||!T())return;x.value=(o.results||[]).map(Re),q.value="";const i=x.peek().length;O.value=i?`${i} result${i===1?"":"s"} (showing up to ${o.maxResults}) from ${o.coverage?.searchable??"?"} searchable pages.`:`Nothing found in ${o.coverage?.searchable??"?"} searchable pages.`,o.coverage&&(J.value=o.coverage)}catch(o){if(t!==N)return;x.value=[],O.value="",q.value=`Search failed: ${o.message||o}`}}r(M.effect(()=>{y.value,s.maxResults.value,clearTimeout(_),_=setTimeout(le,l(h()).debounceMs)})),r(()=>clearTimeout(_));async function W(){if(P.peek())return;P.value=!0;const e=h(),t=d(e);try{const o=(await A("/search/settings",{method:"PUT",body:JSON.stringify(l(e))}))?.settings||l(e),i=D(o);if(!T()){ue(d(i),t);return}if(R.value=o,d(h())===t){m=Number(i.iconSize)||52;for(const[be,he]of Object.entries(i))s[be].value=he}return g.value=d(i),z=g.peek(),f.peek()||w(),ye(Oe),E.toast(o.enabled?"Search settings saved.":"Saved - search is off on the public site.",{type:"success"}),!0}catch(n){return E.toast(`Search not saved: ${n.message||n}`,{type:"error"}),!1}finally{P.value=!1}}function w(){if(c)try{S.remove?S.remove(c):S.set(c,null)}catch{}}function ue(...e){if(!c)return;let t=null;try{t=S.get(c)}catch{return}if(t?.form)try{e.includes(d(t.form))&&w()}catch{}}function X(){const e=D(R.peek());m=Number(e.iconSize)||52;for(const[t,n]of Object.entries(e))s[t].value=n}a.querySelector(".ssx-chips").innerHTML=Ae;const v=a.querySelector(".ssx-list");v.innerHTML=De;const k=e=>`${e}px`,pe={...s,dirty:f,saving:P,chips:Y,query:y,results:x,previewNote:ce,previewError:q,previewMeta:O,clampText:M.computed(()=>Se(p()).join(" ")),sizeText:M.computed(()=>k(l(p()).iconSize)),radiusText:M.computed(()=>k(l(p()).iconRadius)),sizePx:M.computed(()=>k(l(p()).iconSize)),radiusPx:M.computed(()=>k(l(p()).iconRadius)),glyphPx:M.computed(()=>k(Math.round(l(p()).iconSize*20/52))),positionText:M.computed(()=>xe[s.position.value]||s.position.value)},de=E.tabs(a.querySelector(".ssx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"});r(()=>de.destroy?.());const fe=M.applyBindings(pe,a,{methods:{save:()=>W(),discard:()=>X(),useNavbar:()=>{s.displayMode.value="navbar"},useFloating:()=>{s.displayMode.value="floating"},setPos:e=>{s.position.value=e}}});r(()=>fe.dispose()),r(ge(a,{position:"bottom"}));const me=e=>[{header:e},{label:"Open on the site",icon:"external-link",action:()=>window.open(e,"_blank","noopener")},{label:"Edit the page",icon:"edit",action:()=>{location.hash=`#/pages/edit${e}`}},{label:"Copy address",icon:"copy",action:()=>{const t=new URL(e,location.origin).href;navigator.clipboard?.writeText(t).then(()=>E.toast("Address copied.",{type:"success"}),()=>E.toast(t,{type:"info",duration:8e3}))}}],ve=E.contextMenu(v,{cursor:"glyph",match:".ssx-row",inherit:!1,density:"compact",items:e=>me(e.dataset.url)});r(()=>ve?.destroy?.());const Z=e=>{const t=e.target.closest(".ssx-row");if(!t)return;const n=e.target.closest(".ssx-kebab");if(n){oe(n);return}window.open(t.dataset.url,"_blank","noopener")},ee=e=>{const t=e.target.closest(".ssx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),oe(t)):e.key==="Enter"&&e.target===t&&(e.preventDefault(),e.stopPropagation(),window.open(t.dataset.url,"_blank","noopener")))};v.addEventListener("click",Z),v.addEventListener("keydown",ee),r(()=>{v.removeEventListener("click",Z),v.removeEventListener("keydown",ee)});let j=null;try{j=c?S.get(c):null}catch{}const u=ke(j,g.peek(),L);let F=!u;const te=()=>{if(c)if(f.peek())try{S.set(c,{owner:L,form:h(),was:z,at:new Date().toISOString()})}catch{}else F&&w()},se=e=>{f.peek()&&(te(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",se),r(()=>window.removeEventListener("beforeunload",se));const U={save:W,discard:X,dirty:()=>f.peek(),dispose(){B=!0,te(),K.splice(0).reverse().forEach(e=>{try{e()}catch{}})}};if(re(U),!T())return U;if(I.scan(a),!u&&j&&w(),u){const e=we({when:u.at?Pe(u.at):"",stale:u.stale});if(await new Promise(n=>setTimeout(n,0)),await E.confirm(e.text,{title:e.title,confirmText:e.confirmText,cancelText:e.cancelText}))w(),F=!0;else if(T()){F=!0;const n={...D(R.peek()),...u.form};m=Number(n.iconSize)||52;for(const[o,i]of Object.entries(n))s[o]&&(s[o].value=i);z=u.was||z}}return U}export const searchView={template:"",onMount(){location.replace("#/settings?tab=search")}};
|
package/admin/js/views/seo.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import{apiRequest as ys}from"../api.js";import{addBannerAction as xs}from"../lib/plugin-chrome.js";import{helpPopovers as ws}from"../lib/help-popover.js";import{onceClosed as ks,openMenuAt as Ms,viewLifetime as $s}from"../lib/tool-kit.js";import{snippet as Ss}from"../lib/seo-analyse.js";import{checkRule as
|
|
1
|
+
import{apiRequest as ys}from"../api.js";import{addBannerAction as xs}from"../lib/plugin-chrome.js";import{helpPopovers as ws}from"../lib/help-popover.js";import{onceClosed as ks,openMenuAt as Ms,viewLifetime as $s}from"../lib/tool-kit.js";import{snippet as Ss}from"../lib/seo-analyse.js";import{checkRule as xt,matchRedirect as Es}from"../lib/seo-redirects.js";import{checkSettings as Ts,mergeDefaults as wt,ORG_TYPES as Cs}from"../lib/seo-settings.js";import{pickMedia as ze}from"../lib/media-picker.js";import{checkSeoDefaults as Os,seoDefaults as Ls}from"../lib/seo-defaults.js";import{mountGoogleBusiness as Rs}from"../lib/google-business-panel.js";import{fmtDate as Is,fmtDateTime as Fe}from"../lib/dates.js";import{SHAPES as As,whileLoading as Ps}from"../lib/loading.js";import{countText as ve,draftAction as js,draftKey as Je,filterPages as Ds,fingerprint as J,pageCounts as Ns,restoreQuestion as Ye,usableDraft as kt}from"../lib/seo-arrange.js";const qs="/seo",Qe=/^#\/seo(\/[a-z0-9-]+)?\/?(\?.*)?$/,Mt=["overview","redirects","sitemap"],Us="#/seo",Bs={pass:"\u2713",warn:"!",fail:"\u2715",info:"i"},_s={301:"301 moved",302:"302 for now",307:"307 for now",308:"308 moved",410:"410 gone"},Hs={apiRequest:ys,addBannerAction:xs,helpPopovers:ws,onceClosed:ks,openMenuAt:Ms,viewLifetime:$s},Ws=async()=>Hs,L=i=>String(i??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function Ve(i){const d=document.createElement("template");return d.innerHTML=i.trim(),d.content.firstElementChild}async function ge(i,d){try{await navigator.clipboard.writeText(i),E.toast(`Copied ${d}.`,{type:"success"})}catch{E.toast(`Could not copy - ${i}`,{type:"warning",duration:6e3})}}const R={user(){try{return S.get("auth_user")?.id||""}catch{return""}},get(i){try{return i?S.get(i)??null:null}catch{return null}},set(i,d){try{i&&S.set(i,d)}catch{}},drop(i){try{i&&(S.remove?S.remove(i):S.set(i,null))}catch{}},keys(i){try{return(S.keys?.()||[]).filter(d=>d.startsWith(i))}catch{return[]}}},Ks=()=>new Promise(i=>setTimeout(i,0)),Xe=async(i,d)=>(await Ks(),E.confirm(L(i),d)),Ze=i=>Xe(i.text,{title:i.title,confirmText:i.confirmText,cancelText:i.cancelText});function $t(){try{window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:Us}}))}catch{}}const St=i=>typeof i!="number"?"":i>=85?"good":i>=60?"poor":"bad",Et=i=>i?Is(i):"",Tt=i=>{if(!i)return"";const d=Math.round((Date.now()-Date.parse(i))/6e4);if(d<1)return"just now";if(d<60)return`${d} min ago`;const U=Math.round(d/60);return U<48?`${U} hour${U===1?"":"s"} ago`:Et(i)};function Gs(i){const d=m=>{let n=String(m??"");return/^[=+\-@\t\r]/.test(n)&&(n=`'${n}`),/[",\n\r]/.test(n)?`"${n.replace(/"/g,'""')}"`:n},U=Object.keys(i[0]||{});return`\uFEFF${[U.join(","),...i.map(m=>U.map(n=>d(m[n])).join(","))].join(`\r
|
|
2
2
|
`)}\r
|
|
3
|
-
`}function
|
|
3
|
+
`}function zs(i,d,U="text/csv;charset=utf-8"){const m=URL.createObjectURL(new Blob([d],{type:U})),n=Object.assign(document.createElement("a"),{href:m,download:i});document.body.appendChild(n),n.click(),n.remove(),setTimeout(()=>URL.revokeObjectURL(m),1e4)}const Fs=`
|
|
4
4
|
<div class="sox-row" data-bind-data-row="id" tabindex="0" data-bind-title="path">
|
|
5
5
|
<span class="sox-pill" data-bind-data-tone="tone" data-bind-text="scoreText"></span>
|
|
6
6
|
<span class="sox-cell"><span data-bind-text="path"></span><small data-bind-text="sourceText"></small></span>
|
|
7
7
|
<span class="sox-cell sox-hide-sm" data-bind-text="title" data-bind-title="title"></span>
|
|
8
8
|
<span class="sox-cell sox-muted sox-hide-sm" data-bind-text="problems" data-bind-title="problems"></span>
|
|
9
9
|
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
10
|
-
</div>`,
|
|
10
|
+
</div>`,Js=`
|
|
11
11
|
<div class="sox-row" data-bind-data-row="id" tabindex="0">
|
|
12
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
13
|
<span class="sox-pill" data-bind-data-tone="tone" data-bind-text="badge"></span>
|
|
14
14
|
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
15
|
-
</div>`,
|
|
15
|
+
</div>`,Ys=`
|
|
16
16
|
<div class="sox-row" data-bind-data-row="id" tabindex="0" data-bind-title="note">
|
|
17
17
|
<span class="sox-cell sox-mono" data-bind-text="from" data-bind-title="from"></span>
|
|
18
18
|
<span class="sox-arrow" aria-hidden="true">\u2192</span>
|
|
@@ -20,42 +20,42 @@ import{apiRequest as ys}from"../api.js";import{addBannerAction as xs}from"../lib
|
|
|
20
20
|
<span class="sox-pill sox-hide-sm" data-bind-data-tone="tone" data-bind-text="code"></span>
|
|
21
21
|
<span class="sox-cell sox-muted sox-hide-sm" data-bind-text="hits" data-bind-title="lastHit"></span>
|
|
22
22
|
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
23
|
-
</div>`,
|
|
23
|
+
</div>`,Qs=`
|
|
24
24
|
<div class="sox-row" data-bind-data-row="id" tabindex="0">
|
|
25
25
|
<span class="sox-cell"><span data-bind-text="path" data-bind-title="path"></span><small data-bind-text="title"></small></span>
|
|
26
26
|
<span class="sox-cell sox-muted sox-hide-sm" data-bind-text="source"></span>
|
|
27
27
|
<span class="sox-cell sox-muted sox-hide-sm" data-bind-text="note" data-bind-title="note"></span>
|
|
28
28
|
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
29
|
-
</div>`,
|
|
29
|
+
</div>`,Vs=`
|
|
30
30
|
<div class="sox-row" data-bind-data-row="id" tabindex="0">
|
|
31
31
|
<span class="sox-cell"><span data-bind-text="label"></span><small data-bind-text="detail"></small></span>
|
|
32
32
|
<span class="sox-pill" data-bind-data-tone="tone" data-bind-text="state"></span>
|
|
33
33
|
<button type="button" class="sox-kebab" aria-label="Actions" title="Actions">\u22EE</button>
|
|
34
|
-
</div>`,
|
|
34
|
+
</div>`,Xs=`
|
|
35
35
|
<div>
|
|
36
36
|
<div class="sox-group" data-bind-hidden="!head" data-bind-text="head"></div>
|
|
37
37
|
<div class="sox-check" data-bind-hidden="!title">
|
|
38
38
|
<span class="sox-status" data-bind-data-status="status" data-bind-text="glyph"></span>
|
|
39
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
40
|
</div>
|
|
41
|
-
</div>`;export const seoView={templateUrl:"/admin/js/templates/seo.html",async onMount(i){const
|
|
41
|
+
</div>`;export const seoView={templateUrl:"/admin/js/templates/seo.html",async onMount(i){const d=await Ws();if(!Qe.test(location.hash))return;const U=seoView._lifetime??(seoView._lifetime=d.viewLifetime(Qe));U.reset();const m=i.get(0).querySelector(".sox")||i.get(0),n=()=>Qe.test(location.hash)&&m.isConnected,A=e=>(U.add(e),e),g=(e,t)=>d.apiRequest(`${qs}${e}`,t),Ct=location.hash.match(/^#\/seo\/([a-z0-9-]+)/)?.[1]||"overview",Y=M.observable(Ct),Ce=M.observable(!0),Oe=M.observable(""),B=M.observable(!0),w=M.observable(null),ye=M.observable({results:[]}),te=M.observable("all"),xe=M.observable(""),Le=M.observable(""),N=M.observable({rules:[],chains:[],onRename:!0}),Re=M.observable(""),we=M.observable(""),ke=M.observable(""),et=M.observable("301"),W=M.observable({entries:[],left:[],sources:[],labels:{},robots:""}),de=M.observable("in"),Ie=M.observable(""),tt=M.observable([]),Ot=M.computed(()=>!Mt.includes(Y.value)),st=M.observable(!0),at=M.observable(!0),ot=M.observable(!0);let Q=null;const se=e=>W.value.labels?.[e]||(e==="pages"?"Pages":e),Lt=M.computed(()=>(w.value?.score??null)===null?"\u2014":`${w.value.score}%`),Rt=M.computed(()=>`${w.value?.score??0}%`),It=M.computed(()=>St(w.value?.score)),At=M.computed(()=>{const e=w.value;return e?{pages:String(e.pages),failing:String(e.failing),duplicates:String(e.duplicates),redirects:String(e.redirects)}:{pages:"\u2014",failing:"\u2014",duplicates:"\u2014",redirects:"\u2014"}}),Pt=M.computed(()=>w.value?.failing?String(w.value.failing):""),jt=M.computed(()=>{const e=w.value;return e?.running?`Checking\u2026 ${e.progress?.done||0} of ${e.progress?.total||"?"}`:e?.at?`Checked ${Tt(e.at)}`:"Not checked yet"}),nt=M.computed(()=>[...new Set((ye.value.results||[]).map(e=>e.source))]),it=M.observable([]);let Me=[];const Dt=M.computed(()=>{const e=Ns(ye.value.results,{source:xe.value});return{all:ve(e.all),problems:ve(e.problems),hidden:ve(e.hidden)}});A(M.effect(()=>{Me=Ds(ye.value.results,{show:te.value,source:xe.value,query:Le.value}),it.value=Me.slice(0,1e3).map((e,t)=>({id:`p${t}`,path:e.urlPath,sourceText:[se(e.source),e.noindex?"hidden from search engines":e.unlisted?"not in the sitemap":""].filter(Boolean).join(" \xB7 "),title:e.title||e.label||"",scoreText:e.error?"!":typeof e.score=="number"?String(e.score):"\u2014",tone:e.error?"bad":e.noindex?"info":St(e.score),problems:e.error||(e.problems?.length?e.problems.join(" \xB7 "):"Nothing to fix")}))}));const Nt=M.computed(()=>w.value?.at?Le.value.trim()?"No page matches.":te.value==="problems"?"Nothing needs attention.":"No pages.":w.value?.running?"Checking every page - this takes a moment\u2026":"Not checked yet. Check now with the button top right."),lt=M.observable([]);let G=[];A(M.effect(()=>{const e=w.value;if(G=[],e){const t=e.siteSeo||{};for(const s of e.issues?.duplicateTitles||[])G.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 e.issues?.duplicateDescriptions||[]){const a=t.defaultDescription&&s.text===t.defaultDescription;G.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"})}t.defaultImage||G.push({kind:"setting",text:"No default share image",detail:"Pages without their own image share with none. Set one in SEO settings.",link:"#/seo?settings=1",tone:"poor",badge:"setting"}),(!e.origin||/localhost|127\.0\.0\.1/.test(e.origin))&&G.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"}),e.chains&&G.push({kind:"chains",text:`${e.chains} redirect${e.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"})}lt.value=G.map((t,s)=>({id:`s${s}`,text:t.text,detail:t.detail||"",tone:t.tone,badge:t.badge}))}));const Ae=M.observable([]);let ue=[];A(M.effect(()=>{const e=Re.value.trim().toLowerCase(),t=new Set(N.value.chains.map(s=>s.id));ue=N.value.rules.filter(s=>!e||`${s.from} ${s.to} ${s.note}`.toLowerCase().includes(e)),Ae.value=ue.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,t.has(s.id)?"leads to another redirect":""].filter(Boolean).join(" \xB7 "),code:String(s.code),tone:s.code===410?"bad":t.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 ${Tt(s.lastHit)}`:"Not used yet"}))}));const qt=M.computed(()=>N.value.rules.length?String(N.value.rules.length):""),Ut=M.computed(()=>`${ue.length+0*Ae.value.length} of ${N.value.rules.length}`),Bt=M.computed(()=>Re.value.trim()?"No redirect matches.":"No redirects yet."),_t=M.computed(()=>{const e=N.value.chains.length;return e?`${e} redirect${e===1?" leads":"s lead"} to another. Right-click one and choose "Go straight there" to make it one hop.`:""}),Ht=M.computed(()=>N.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)."),rt=M.observable([]);A(M.effect(()=>{rt.value=W.value.sources.map((e,t)=>({id:`x${t}`,label:se(e.id),detail:`${e.listed} of ${e.count} listed`,state:e.on?"on":"off",tone:e.on?"good":""}))}));const Pe=M.observable([]);let ae=[];A(M.effect(()=>{const e=Ie.value.trim().toLowerCase();ae=(de.value==="in"?W.value.entries:W.value.left).filter(s=>!e||`${s.urlPath} ${s.title}`.toLowerCase().includes(e)),Pe.value=ae.slice(0,2e3).map((s,a)=>({id:`e${a}`,path:s.urlPath,title:s.title||"",source:se(s.source),note:s.why||[s.lastmod?`Changed ${Et(s.lastmod)}`:"",s.changefreq||"",s.priority!==void 0?`priority ${s.priority}`:""].filter(Boolean).join(" \xB7 ")}))}));const Wt=M.computed(()=>W.value.robots||""),Kt=M.computed(()=>({in:ve((W.value.entries||[]).length),out:ve((W.value.left||[]).length)})),Gt=M.computed(()=>`${ae.length+0*Pe.value.length} address${ae.length===1?"":"es"}`),zt=M.computed(()=>W.value.hooks?Ie.value.trim()?"No address matches.":de.value==="in"?"The sitemap is empty.":"Nothing is left out.":"This version of Domma CMS lists only its own pages in the sitemap."),$e=e=>/403|forbidden/i.test(String(e?.message));async function oe(){try{const e=await g("/overview");if(!n()||!e)return;w.value=e,Oe.value="",e.running&&ct()}catch(e){n()&&(Oe.value=$e(e)?"You do not have access to SEO.":`Could not load SEO: ${e.message||e}`)}}const je=(e,t,s,a=As.rows)=>e.peek()?Ps(m.querySelector(t),s,a).finally(()=>{n()&&(e.value=!1)}):s;async function De(){try{const e=await je(st,".sox-pages-loading",g("/audit"));n()&&e&&(ye.value=e)}catch{}}async function pe(){try{const e=await je(at,".sox-rules-loading",g("/redirects"));if(!n()||!e)return;N.value=e,Vt(e.rules||[])}catch{}}async function Ne(){try{const e=await je(ot,".sox-entries-loading",g("/sitemap"));n()&&e&&(W.value=e)}catch(e){n()&&!$e(e)&&E.toast(`Could not load the sitemap: ${e.message||e}`,{type:"error"})}}async function qe(){Ce.value=!0,await Promise.all([oe(),De(),pe(),Ne()]),n()&&(Ce.value=!1)}function ct(){Q||(Q=setInterval(async()=>{if(!n()){clearInterval(Q),Q=null;return}await oe(),w.peek()?.running||(clearInterval(Q),Q=null,await De(),$t())},1500))}A(()=>{Q&&clearInterval(Q)});async function dt(){try{const e=await g("/audit",{method:"POST",body:JSON.stringify({})});n()&&(w.value={...w.peek(),...e,running:!0}),ct()}catch(e){n()&&E.toast(`Could not start the check: ${e.message||e}`,{type:"error"})}}async function Ue(e,t,s=pe){try{const a=await e();return n()&&(t&&E.toast(t,{type:"success"}),await Promise.all([s(),oe()])),a}catch(a){return n()&&($e(a)?(B.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 Ft=(e,t=`Redirect from ${e.from} added.`)=>Ue(()=>g("/redirects",{method:"POST",body:JSON.stringify(e)}),t),Jt=(e,t)=>Ue(()=>g(`/redirects/${encodeURIComponent(e.join(","))}`,{method:"DELETE"}),`${t} removed.`);async function Yt(e){try{const t=await g(`/redirects/test?path=${encodeURIComponent(e.replace(/\/\*$/,"/example"))}`);if(!n())return;const s=t.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(t){n()&&E.toast(`Could not test: ${t.message||t}`,{type:"error"})}}const ut=e=>({from:e.from||"",to:e.code===410?"":e.to||"",code:String(e.code||301),note:e.note||""});function Be(e,{asNew:t=!1,adopt:s=null}={}){const a=t?{from:"",to:"",code:"301",note:""}:ut(e),l={from:M.observable(a.from),to:M.observable(a.to),code:M.observable(a.code),note:M.observable(a.note)},o=t?"":J(ut(e));mt(t?"New redirect":`Redirect from ${e.from}`,l,`
|
|
42
42
|
<div class="sox-form-grid">
|
|
43
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
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
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(
|
|
46
|
+
${Object.entries(_s).map(([r,c])=>`<option value="${r}">${c}</option>`).join("")}</select></div>
|
|
47
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 r={from:
|
|
48
|
+
</div>`,()=>{const r={from:l.from.value,to:l.to.value,code:Number(l.code.value),note:l.note.value},c=N.peek().rules.filter(y=>t||y.id!==e.id);return{body:r,errors:xt(r,c).errors}},async r=>{t?await g("/redirects",{method:"POST",body:JSON.stringify(r)}):await g(`/redirects/${encodeURIComponent(e.id)}`,{method:"PUT",body:JSON.stringify(r)}),n()&&(E.toast(t?`Redirect from ${r.from} added.`:"Redirect saved.",{type:"success"}),await Promise.all([pe(),oe()]))},t?"Add":"Save",{kind:"redirect",id:e.id,what:t?`the redirect from ${s?.from||e.from||"?"}`:`the redirect from ${e.from}`,was:o,adopt:s})}function Qt(){const e={csv:M.observable("")};mt("Import redirects",e,`
|
|
49
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:e.csv.value},errors:e.csv.value.trim()?{}:{csv:"Paste some redirects first."}}),async t=>{const s=await g("/redirects/import",{method:"POST",body:JSON.stringify(t)});if(!n())return;const
|
|
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:e.csv.value},errors:e.csv.value.trim()?{}:{csv:"Paste some redirects first."}}),async t=>{const s=await g("/redirects/import",{method:"POST",body:JSON.stringify(t)});if(!n())return;const 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([pe(),oe()])},"Import",{kind:"import",what:"the redirects you were importing"})}let pt=!1;async function Vt(e){if(pt||!B.peek())return;pt=!0;const t=Je(R.user(),"redirect","");if(!t)return;const s=new Set(e.map(a=>String(a.id)));for(const a of R.keys(`${t}.`)){let l;try{l=decodeURIComponent(a.slice(t.length+1))}catch{continue}if(s.has(l))continue;const o=kt(R.get(a),{from:"",to:"",code:"301",note:""});if(!o){R.drop(a);continue}if(await Ze(Ye({what:`the redirect from ${o.values.from||"?"}`,when:o.at?Fe(o.at):"",gone:"Keep them to add it again as a new redirect."}))){R.drop(a);continue}n()&&Be({id:l},{asNew:!0,adopt:o.values});return}}function _e(e,{kind:t,id:s="",what:a,read:l,write:o,was:r=""}){const c=Je(R.user(),t,s);let y=J(l()),$=r,k=!0,O=!1,_=!1;const H=()=>J(l())!==y,f=()=>{const u=js({dirty:H(),owns:k});u==="write"?R.set(c,{values:l(),was:$,at:new Date().toISOString()}):u==="remove"&&R.drop(c)},T=u=>{O||(f(),H()&&(u.preventDefault(),u.returnValue=""))};window.addEventListener("beforeunload",T);const P=e.close.bind(e);let j=()=>{};const C=()=>{O=!0,window.removeEventListener("beforeunload",T),j()};return e.close=async u=>{if(O)return P(u);if(!(u!==!0&&!k)){if(u!==!0&&H()){if(_)return;_=!0;const h=await Xe("Throw away the changes you have not saved?",{title:"Unsaved changes",confirmText:"Throw away",cancelText:"Keep editing"});if(_=!1,!h||O)return;k&&R.drop(c)}else u!==!0&&f();return C(),P(u)}},j=U.add(()=>{if(!O){f(),C();try{P(!0)}catch{}}}),{saved(u,h=$){y=J(u),$=h,k=!0,f()},adopt(u,h=""){o(u),$=h,k=!0},async offer(u=r){const h=R.get(c),X=t==="settings"?Object.fromEntries(["defaultTitle","titleSeparator","defaultDescription","defaultImage"].map(b=>[b,l()[b]])):{},fe=h?.values?{...h,values:{...X,...h.values}}:h,q=kt(fe,l());return q?(k=!1,await Ze(Ye({what:a,when:q.at?Fe(q.at):"",stale:q.was!==u}))?(R.drop(c),k=!0,!1):O?!1:(k=!0,o(q.values),$=q.was,!0)):(h&&R.drop(c),!1)}}}let Se=!1;function mt(e,t,s,a,l,o="Save",r=null){if(Se)return;Se=!0;let c=null;try{const y=M.observable(!1),$=M.computed(()=>a()),k=M.computed(()=>Object.values($.value.errors).join(" ")),O={...t,saving:y,errText:k,cannot:M.computed(()=>y.value||!!k.value)},_=()=>Object.fromEntries(Object.entries(t).map(([u,h])=>[u,h.peek()])),H=u=>{for(const[h,X]of Object.entries(t))h in u&&(X.value=u[h])};c=E.slideover({title:e,size:"md",position:"right"});const f=Ve(`<div class="sox-form">${s}
|
|
51
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 ${
|
|
52
|
+
<div class="sox-actions"><small class="sox-muted">Ctrl+Enter ${o.toLowerCase()}s.</small><span class="sox-spacer"></span>
|
|
53
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">${
|
|
54
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">${L(o)}</button></div></div>`);let T=null;async function P(){if(O.cannot.peek())return;y.value=!0;const u=_();f.inert=!0;try{await l($.peek().body),T?.saved(u,""),c.close(!0)}catch(h){f.inert=!1,n()&&E.toast(`Not saved: ${h.message||h}`,{type:"error"})}finally{y.value=!1}}c.element.appendChild(f);const j=d.helpPopovers(f),C=M.applyBindings(O,f,{methods:{save:P,cancel:()=>c.close()}});f.addEventListener("keydown",u=>{(u.ctrlKey||u.metaKey)&&u.key==="Enter"&&(u.preventDefault(),P())}),d.onceClosed(c,()=>{C.dispose(),j(),Se=!1}),r?(T=_e(c,{...r,read:_,write:H}),r.adopt?T.adopt(r.adopt,""):T.offer(r.was)):A(()=>{try{c.close(!0)}catch{}}),I.scan(f),c.open(),f.querySelector("input, textarea")?.focus()}catch(y){Se=!1;try{c?.close(!0)}catch{}E.toast(`Could not open ${e}: ${y.message||y}`,{type:"error"})}}let ne=!1;const Xt=e=>({title:String(e.title??"").trim().slice(0,200),description:String(e.description??"").trim().slice(0,400),image:String(e.image??"").trim().slice(0,500),noindex:e.noindex===!0,focus:String(e.focus??"").trim().slice(0,100)});async function ee(e){if(ne)return;ne=!0;let t;try{t=await g(`/item?path=${encodeURIComponent(e)}`)}catch(s){if(ne=!1,!n())return;const a=s?.status===404?Je(R.user(),"item",e):null,l=a?R.get(a):null;if(l?.values){await Ze(Ye({what:e,when:l.at?Fe(l.at):"",gone:"Keep them and they are offered again if the address is served again."}))&&R.drop(a);return}E.toast(`Could not open ${e}: ${s.message||s}`,{type:"error"});return}if(!n()){ne=!1;return}try{Zt(e,t)}catch(s){ne=!1,E.toast(`Could not open ${e}: ${s.message||s}`,{type:"error"})}}function Zt(e,t){const s={title:M.observable(t.fields.title),description:M.observable(t.fields.description),focus:M.observable(t.fields.focus),image:M.observable(t.fields.image),noindex:M.observable(t.fields.noindex)},a=t.limits||{},l=t.page?.title||t.live?.h1||"",o=t.kind==="page"?`${l}${t.defaults.separator}${t.defaults.siteTitle}`:t.live?.title||"",r=t.kind==="page"?t.defaults.description||"":t.live?.description||"",c=t.kind==="page"?t.defaults.image:t.live?.image||"",y=M.computed(()=>s.title.value.trim()||o),$=M.computed(()=>s.description.value.trim()||r),k=M.computed(()=>s.image.value.trim()||c),O=t.origin||location.origin,_=M.computed(()=>Ss({title:y.value,description:$.value,url:`${O}${e}`,limits:a})),H=(x,v,ce)=>{const Z=x.length;return{text:`${Z} / ${ce}`,width:`${Math.min(100,Z/ce*100)}%`,tone:Z?Z<v||Z>ce?"poor":"good":"bad"}},f=M.computed(()=>H(y.value,a.titleMin||30,a.titleMax||60)),T=M.computed(()=>H($.value,a.descMin||70,a.descMax||160)),P=(()=>{try{return new URL(O).host}catch{return location.host}})(),j=M.computed(()=>k.value?`url("${k.value.replace(/["\\\n\r]/g,"")}")`:"none"),C=M.observable([]),u=x=>{const v=[],ce=x?.checks||[],Z={fail:0,warn:1,pass:2,info:3};for(const Ke of[...new Set(ce.map(F=>F.group))]){v.push({id:`g-${Ke}`,head:Ke,title:"",detail:"",fix:"",status:"",glyph:""});for(const F of ce.filter(Ge=>Ge.group===Ke).sort((Ge,gs)=>Z[Ge.status]-Z[gs.status]))v.push({id:`c-${F.id}`,head:"",title:F.title,detail:F.detail,fix:F.status==="pass"?"":F.fix,status:F.status,glyph:Bs[F.status]||"?"})}C.value=v};u(t.analysis);const h=M.observable(t.analysis?`Score ${t.analysis.score}% \xB7 ${t.analysis.failing} failing, ${t.analysis.warnings} could be better`:t.status===200?"":`Not live (answers ${t.status||"nothing"}) - the checks run once it is published.`),X=()=>({title:s.title.peek(),description:s.description.peek(),focus:s.focus.peek(),image:s.image.peek(),noindex:s.noindex.peek()}),fe=x=>{for(const v of Object.keys(s))v in x&&(s[v].value=x[v])},q=M.observable(!1),p=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.":""),b={...s,saving:q,errText:p,checks:C,scoreLine:h,serp:_,titleMeter:f,descMeter:T,shareBg:j,effTitle:y,effDesc:$,host:P,hasImage:M.computed(()=>!!k.value),defTitle:o,defDesc:r,canManage:B,cannot:M.computed(()=>q.value||!!p.value||!B.value)},D=E.slideover({title:`SEO \xB7 ${e}`,size:"lg",position:"right"}),re=t.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>':t.page?.status!=="published"?`<p class="sox-note is-warn">This page is a ${L(t.page?.status||"draft")} - not live yet.</p>`:"",be=t.redirect?`<p class="sox-note is-bad">A redirect sends visitors from here to ${L(t.redirect.to||"(gone)")} - they never see this page.</p>`:"",K=Ve(`
|
|
55
55
|
<div class="sox-form">
|
|
56
|
-
${
|
|
56
|
+
${re}${be}
|
|
57
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="${
|
|
58
|
+
<span class="sox-spacer"></span><a class="btn btn-xs btn-ghost" href="${L(e)}" target="_blank" rel="noopener"><span data-icon="external-link" data-icon-size="12"></span> Open</a></h4>
|
|
59
59
|
<div class="sox-previews">
|
|
60
60
|
<div class="sox-serp" aria-label="Search result preview">
|
|
61
61
|
<div class="sox-serp-crumbs" data-bind-text="serp.value.crumbs"></div>
|
|
@@ -70,15 +70,15 @@ import{apiRequest as ys}from"../api.js";import{addBannerAction as xs}from"../lib
|
|
|
70
70
|
</section>
|
|
71
71
|
<section><h4>Search and sharing</h4><div class="sox-form-grid">
|
|
72
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="${
|
|
73
|
+
<input id="sox-i-title" class="form-input" data-model="title.value" placeholder="${L(o)}" maxlength="200">
|
|
74
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
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="${
|
|
76
|
+
<textarea id="sox-i-desc" rows="3" class="form-input" data-model="description.value" placeholder="${L(r)}" maxlength="400"></textarea>
|
|
77
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
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
79
|
<input id="sox-i-focus" class="form-input" data-model="focus.value" maxlength="100"></div>
|
|
80
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. Choose it from Media (or upload one), or type a full address. Empty uses the site's default image."></span></label>
|
|
81
|
-
<div class="sox-pick"><input id="sox-i-image" class="form-input sox-mono" data-model="image.value" list="sox-media" placeholder="${
|
|
81
|
+
<div class="sox-pick"><input id="sox-i-image" class="form-input sox-mono" data-model="image.value" list="sox-media" placeholder="${L(c||"/media/\u2026")}">
|
|
82
82
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="pickImage"><span data-icon="image"></span> Choose\u2026</button></div></div>
|
|
83
83
|
<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
|
|
84
84
|
<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>
|
|
@@ -92,49 +92,57 @@ import{apiRequest as ys}from"../api.js";import{addBannerAction as xs}from"../lib
|
|
|
92
92
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Close</button>
|
|
93
93
|
<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>
|
|
94
94
|
</div>
|
|
95
|
-
</div>`);
|
|
96
|
-
`)),street:M.observable(
|
|
97
|
-
`)),blockAll:M.observable(e.robots.blockAll),llms:M.observable(e.llms.enabled),robotsExtra:M.observable(e.robots.extra),onRename:M.observable(e.redirects.onRename),every:M.observable(String(e.audit.everyHours)),tMin:M.observable(String(e.audit.titleMin)),tMax:M.observable(String(e.audit.titleMax)),dMin:M.observable(String(e.audit.descMin)),dMax:M.observable(String(e.audit.descMax)),words:M.observable(String(e.audit.minWords))},
|
|
98
|
-
<input id="sox-s-${
|
|
95
|
+
</div>`);K.querySelector(".sox-checks").innerHTML=Xs,es(K.querySelector("#sox-media"));let Te=null;async function gt(){if(b.cannot.peek())return;q.value=!0;const x=X();try{if(await g("/item",{method:"PUT",body:JSON.stringify({path:e,...x})}),Te?.saved(x,J(Xt(x))),!n())return;E.toast("Saved.",{type:"success"});const v=await g(`/item?path=${encodeURIComponent(e)}`).catch(()=>null);if(!n())return;v?.fields&&Te?.saved(x,J(v.fields)),v?.analysis&&(u(v.analysis),h.value=`Score ${v.analysis.score}% \xB7 ${v.analysis.failing} failing, ${v.analysis.warnings} could be better`),De(),oe(),$t()}catch(v){if(!n())return;$e(v)&&(B.value=!1),E.toast(`Not saved: ${v.message||v}`,{type:"error"})}finally{q.value=!1}}D.element.appendChild(K);const hs=d.helpPopovers(K),fs=M.applyBindings(b,K,{methods:{save:gt,cancel:()=>D.close(),pickImage:async()=>{const x=await ze({title:"Choose a share image",current:s.image.peek()});x&&(s.image.value=x)}}}),bs=K.querySelector(".sox-share-img"),vs=M.effect(()=>{bs.style.backgroundImage=j.value});K.addEventListener("keydown",x=>{(x.ctrlKey||x.metaKey)&&x.key==="Enter"&&(x.preventDefault(),gt())}),d.onceClosed(D,()=>{fs.dispose(),hs(),vs?.(),ne=!1});const yt=J(t.fields);Te=_e(D,{kind:"item",id:e,what:e,read:X,write:fe,was:yt}),I.scan(K),D.open(),Te.offer(yt)}let He=null;async function es(e){try{He??(He=await d.apiRequest("/media").then(t=>Array.isArray(t)?t:t?.files||t?.items||[]));for(const t of He){const s=t.url||t.path||(t.filename?`/media/${t.filename}`:"");s&&/\.(jpe?g|png|webp|gif|avif|svg)$/i.test(s)&&e.appendChild(Object.assign(document.createElement("option"),{value:s}))}}catch{He=[]}}let ie=!1;async function ht(){if(ie)return;ie=!0;let e,t,s;try{const a=await g("/settings");e=wt(a.settings),t=a.defaults||Ls(),s=J({settings:a.settings,defaults:t})}catch(a){ie=!1,n()&&E.toast(`Could not load the settings: ${a.message||a}`,{type:"error"});return}if(!n()){ie=!1;return}try{ts(e,s,t)}catch(a){ie=!1,E.toast(`Could not open the settings: ${a.message||a}`,{type:"error"})}}function ts(e,t,s){const a=e.organisation,l=w.peek()?.siteSeo?.defaultTitle||"The site's name",o={defaultTitle:M.observable(s.defaultTitle),titleSeparator:M.observable(s.titleSeparator),defaultDescription:M.observable(s.defaultDescription),defaultImage:M.observable(s.defaultImage),orgOn:M.observable(a.enabled),orgType:M.observable(a.type),orgName:M.observable(a.name),orgUrl:M.observable(a.url),orgLogo:M.observable(a.logo),orgEmail:M.observable(a.email),orgPhone:M.observable(a.phone),orgSame:M.observable(a.sameAs.join(`
|
|
96
|
+
`)),street:M.observable(a.address.street),locality:M.observable(a.address.locality),region:M.observable(a.address.region),postcode:M.observable(a.address.postcode),country:M.observable(a.address.country),articles:M.observable(e.articles),exclude:M.observable(e.sitemap.exclude.join(`
|
|
97
|
+
`)),blockAll:M.observable(e.robots.blockAll),llms:M.observable(e.llms.enabled),robotsExtra:M.observable(e.robots.extra),onRename:M.observable(e.redirects.onRename),every:M.observable(String(e.audit.everyHours)),tMin:M.observable(String(e.audit.titleMin)),tMax:M.observable(String(e.audit.titleMax)),dMin:M.observable(String(e.audit.descMin)),dMax:M.observable(String(e.audit.descMax)),words:M.observable(String(e.audit.minWords))},r=()=>({organisation:{enabled:o.orgOn.value,type:o.orgType.value,name:o.orgName.value,url:o.orgUrl.value,logo:o.orgLogo.value,email:o.orgEmail.value,phone:o.orgPhone.value,sameAs:o.orgSame.value,address:{street:o.street.value,locality:o.locality.value,region:o.region.value,postcode:o.postcode.value,country:o.country.value}},articles:o.articles.value,sitemap:{exclude:o.exclude.value,sources:e.sitemap.sources},robots:{blockAll:o.blockAll.value,extra:o.robotsExtra.value},llms:{enabled:o.llms.value},redirects:{onRename:o.onRename.value},audit:{everyHours:o.every.value,titleMin:o.tMin.value,titleMax:o.tMax.value,descMin:o.dMin.value,descMax:o.dMax.value,minWords:o.words.value}}),c=()=>Object.fromEntries(Object.entries(o).map(([p,b])=>[p,b.peek()])),y=p=>{for(const[b,D]of Object.entries(o))b in p&&(D.value=p[b])},$=M.observable(!1),k=M.computed(()=>Ts(r(),e)),O=M.computed(()=>Os(Object.fromEntries(Object.keys(s).map(p=>[p,o[p].value])))),_=M.computed(()=>[Object.values(k.value.errors).join(" "),O.value.error].filter(Boolean).join(" ")),H={...o,saving:$,errText:_,cannot:M.computed(()=>$.value||!!_.value),isPerson:M.computed(()=>o.orgType.value==="Person")},f=(p,b,D,re="",be="")=>`<div><label class="form-label" for="sox-s-${p}">${D}${re?` <span data-help="${L(re)}"></span>`:""}</label>
|
|
98
|
+
<input id="sox-s-${p}" class="form-input" data-model="${b}.value" ${be}></div>`,T=(p,b,D,re,be,K="")=>f(p,b,D,K,`type="number" min="${re}" max="${be}"`),P=(p,b,D="")=>`<label class="sox-wide" style="display:flex;align-items:center;gap:.5rem"><input type="checkbox" class="form-switch" data-model="${p}.value"> ${b}${D?` <span data-help="${L(D)}"></span>`:""}</label>`,j=E.slideover({title:"SEO settings",size:"md",position:"right"}),C=Ve(`
|
|
99
99
|
<div class="sox-form">
|
|
100
|
-
<p class="sox-note">
|
|
100
|
+
<section><h4>Site defaults</h4><p class="sox-note">Used by pages without their own SEO values.</p><div class="sox-form-grid">
|
|
101
|
+
${f("default-title","defaultTitle","Default site title")}
|
|
102
|
+
${f("separator","titleSeparator","Title separator")}
|
|
103
|
+
${f("default-description","defaultDescription","Default meta description")}
|
|
104
|
+
<div><label class="form-label" for="sox-default-image">Default share image</label>
|
|
105
|
+
<div class="sox-pick"><input id="sox-default-image" class="form-input" data-model="defaultImage.value" placeholder="/media/share.jpg">
|
|
106
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="pickDefaultImage">Choose image</button></div></div>
|
|
107
|
+
</div></section>
|
|
108
|
+
<section data-google-business></section>
|
|
101
109
|
<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">
|
|
102
|
-
${
|
|
110
|
+
${P("orgOn","Tell search engines who runs the site")}
|
|
103
111
|
<div><label class="form-label" for="sox-s-type">It is</label><select id="sox-s-type" class="form-input" data-model="orgType.value">
|
|
104
|
-
${Cs.map(
|
|
105
|
-
${
|
|
106
|
-
${
|
|
112
|
+
${Cs.map(p=>`<option value="${p}">${{Organization:"An organisation",LocalBusiness:"A local business",Person:"A person"}[p]}</option>`).join("")}</select></div>
|
|
113
|
+
${f("name","orgName","Name","Empty uses the site's name.",`placeholder="${L(l)}"`)}
|
|
114
|
+
${f("url","orgUrl","Web address","Empty uses the site address.",'placeholder="https://\u2026"')}
|
|
107
115
|
<div><label class="form-label" for="sox-s-logo">Logo (or photo) <span data-help="A square image works best. Choose it from Media (or upload one), or type a full address."></span></label>
|
|
108
116
|
<div class="sox-pick"><input id="sox-s-logo" class="form-input sox-mono" data-model="orgLogo.value" placeholder="/media/logo.png">
|
|
109
117
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="pickLogo"><span data-icon="image"></span> Choose\u2026</button></div></div>
|
|
110
|
-
${
|
|
111
|
-
${
|
|
118
|
+
${f("email","orgEmail","Email","",'type="email"')}
|
|
119
|
+
${f("phone","orgPhone","Phone")}
|
|
112
120
|
<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>
|
|
113
121
|
<div class="sox-form-grid"><input class="form-input sox-wide" placeholder="Street" aria-label="Street" data-model="street.value">
|
|
114
122
|
<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">
|
|
115
123
|
<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>
|
|
116
124
|
<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>
|
|
117
125
|
<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>
|
|
118
|
-
${
|
|
126
|
+
${P("articles","Mark blog posts as articles","Adds BlogPosting structured data to each post - headline, image, publisher - which search engines use for article results.")}
|
|
119
127
|
</div></section>
|
|
120
128
|
<section><h4>Sitemap, robots.txt and llms.txt</h4><div class="sox-form-grid">
|
|
121
129
|
<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>
|
|
122
130
|
<textarea id="sox-s-ex" rows="3" class="form-input sox-mono" data-model="exclude.value" placeholder="/thank-you /members/"></textarea></div>
|
|
123
|
-
${
|
|
131
|
+
${P("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.")}
|
|
124
132
|
<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>
|
|
125
133
|
<textarea id="sox-s-rob" rows="3" class="form-input sox-mono" data-model="robotsExtra.value" placeholder="Disallow: /private/"></textarea></div>
|
|
126
|
-
${
|
|
134
|
+
${P("llms","Publish /llms.txt for AI assistants","A plain list of the pages in the sitemap, with their descriptions, at /llms.txt - the format AI assistants look for. Nothing kept out of the sitemap appears in it, and it is withdrawn while search engines are asked to stay away.")}
|
|
127
135
|
</div></section>
|
|
128
136
|
<section><h4>Redirects</h4><div class="sox-form-grid">
|
|
129
|
-
${
|
|
137
|
+
${P("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.")}
|
|
130
138
|
</div></section>
|
|
131
139
|
<section><h4>The checks</h4><div class="sox-form-grid">
|
|
132
|
-
${
|
|
133
|
-
${
|
|
134
|
-
${
|
|
135
|
-
${
|
|
136
|
-
${
|
|
137
|
-
${
|
|
140
|
+
${T("every","every","Check every page every (hours)",0,168,"0 checks only when you ask.")}
|
|
141
|
+
${T("words","words","Words a page should have",0,5e3)}
|
|
142
|
+
${T("tmin","tMin","Shortest title",10,70)}
|
|
143
|
+
${T("tmax","tMax","Longest title",30,100,"Google cuts titles at about 60 characters.")}
|
|
144
|
+
${T("dmin","dMin","Shortest description",30,160)}
|
|
145
|
+
${T("dmax","dMax","Longest description",80,320,"Google cuts descriptions at about 155-160 characters.")}
|
|
138
146
|
</div></section>
|
|
139
147
|
<p class="sox-err" data-bind-hidden="!errText.value" data-bind-text="errText.value"></p>
|
|
140
148
|
<div class="sox-actions">
|
|
@@ -142,7 +150,7 @@ import{apiRequest as ys}from"../api.js";import{addBannerAction as xs}from"../lib
|
|
|
142
150
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
143
151
|
<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>
|
|
144
152
|
</div>
|
|
145
|
-
</div>`);let
|
|
153
|
+
</div>`);let u=null;async function h(){if(H.cannot.peek())return;$.value=!0;const p=c();C.inert=!0;try{const b=await g("/settings",{method:"PUT",body:JSON.stringify({...k.peek().settings,siteDefaults:O.peek().defaults})});if(u?.saved(p,J({settings:b?.settings,defaults:b?.defaults})),j.close(!0),!n())return;E.toast("Settings saved.",{type:"success"}),qe()}catch(b){C.inert=!1,n()&&E.toast(`Not saved: ${b.message||b}`,{type:"error"})}finally{$.value=!1}}j.element.appendChild(C);const X=d.helpPopovers(C),fe=M.applyBindings(H,C,{methods:{save:h,cancel:()=>j.close(),pickDefaultImage:async()=>{const p=await ze({title:"Choose a default share image",current:o.defaultImage.peek()});p&&(o.defaultImage.value=p)},pickLogo:async()=>{const p=await ze({title:"Choose a logo",current:o.orgLogo.peek()});p&&(o.orgLogo.value=p)}}}),q=Rs(C.querySelector("[data-google-business]"),{read:c,apply:y});C.addEventListener("keydown",p=>{(p.ctrlKey||p.metaKey)&&p.key==="Enter"&&(p.preventDefault(),h())}),d.onceClosed(j,()=>{q(),fe.dispose(),X(),ie=!1}),u=_e(j,{kind:"settings",what:"the SEO settings",read:c,write:y,was:t}),I.scan(C),j.open(),u.offer(t)}async function ss(e,t){try{const s=await g("/settings"),a=wt(s.settings);if(a.sitemap.sources[e]=t,await g("/settings",{method:"PUT",body:JSON.stringify(a)}),!n())return;E.toast(`${se(e)} ${t?"back in":"left out of"} the sitemap.`,{type:"success"}),Ne()}catch(s){n()&&E.toast(`Not changed: ${s.message||s}`,{type:"error"})}}const z=e=>Number(String(e?.dataset?.row||"").slice(1));function as(e){const t=Me[z(e)];return t?[{header:t.urlPath},{label:"Title, description and checks\u2026",icon:"edit-3",action:()=>ee(t.urlPath)},{label:"Open the page",icon:"external-link",action:()=>window.open(t.urlPath,"_blank","noopener")},{label:"Redirect it elsewhere\u2026",icon:"corner-up-right",disabled:!B.peek(),action:()=>{V("redirects"),we.value=t.urlPath,ke.value=""}},{type:"divider"},{label:"Copy the address",icon:"copy",action:()=>ge(t.urlPath,"the address")},{label:"Copy the problems",icon:"clipboard",disabled:!t.problems?.length&&!t.error,action:()=>ge(`${t.urlPath}
|
|
146
154
|
${t.error||t.problems.join(`
|
|
147
|
-
`)}`,"the problems")}]:[]}function
|
|
148
|
-
`),"the addresses")}]:t.kind==="chains"?[{label:"Show the redirects",icon:"corner-up-right",action:()=>
|
|
155
|
+
`)}`,"the problems")}]:[]}function os(e){const t=G[z(e)];return t?t.kind==="dupe"?[{header:"Open one of them"},...t.paths.slice(0,12).map(s=>({label:s,icon:"edit-3",action:()=>ee(s)})),{type:"divider"},{label:"Copy the addresses",icon:"copy",action:()=>ge(t.paths.join(`
|
|
156
|
+
`),"the addresses")}]:t.kind==="chains"?[{label:"Show the redirects",icon:"corner-up-right",action:()=>V("redirects")}]:[{label:"Go to Settings",icon:"settings",action:()=>{location.hash=t.link}}]:[]}function ns(e){const t=G[z(e)];t?.kind==="dupe"?ee(t.paths[0]):t?.kind==="chains"?V("redirects"):t?.link&&(location.hash=t.link)}function is(e){const t=ue[z(e)];if(!t)return[];const s=N.peek().rules,a=N.peek().chains.some(o=>o.id===t.id);let l=null;if(a){l=t.to;for(let o=0;o<10;o++){const r=Es(s,l);if(!r)break;if(r.code===410){l=null;break}if(l=r.location,/^https?:/i.test(l))break}}return[{header:`${t.from} \u2192 ${t.code===410?"gone":t.to}`},{label:"Edit\u2026",icon:"edit-3",disabled:!B.peek(),action:()=>Be(t)},{label:"Test it",icon:"play",action:()=>Yt(t.from)},{label:"Go straight there",icon:"fast-forward",disabled:!l||!B.peek(),action:()=>Ue(()=>g(`/redirects/${encodeURIComponent(t.id)}`,{method:"PUT",body:JSON.stringify({to:l})}),`${t.from} now goes straight to ${l}.`)},{label:"Open the old address",icon:"external-link",disabled:t.from.endsWith("/*"),action:()=>window.open(t.from,"_blank","noopener")},{type:"divider"},{label:"Copy",icon:"copy",action:()=>ge(`${t.from} ${t.to} ${t.code}`,"the redirect")},{label:"Remove",icon:"trash-2",danger:!0,disabled:!B.peek(),action:async()=>{await Xe(`Remove the redirect from ${t.from}? Visitors to it will get "not found".`)&&Jt([t.id],"Redirect")}}]}function ls(e){const t=ae[z(e)];return t?[{header:t.urlPath},{label:"Title, description and checks\u2026",icon:"edit-3",action:()=>ee(t.urlPath)},{label:"Open",icon:"external-link",action:()=>window.open(t.urlPath,"_blank","noopener")},{type:"divider"},{label:"Copy the address",icon:"copy",action:()=>ge(t.urlPath,"the address")}]:[]}function rs(e){const t=W.peek().sources[z(e)];return t?[{header:se(t.id)},{label:t.on?"Leave all of it out":"List it again",icon:t.on?"eye-off":"eye",disabled:!B.peek(),action:()=>ss(t.id,!t.on)}]:[]}function me(e,t,s){const a=E.contextMenu(e,{cursor:"glyph",match:"[data-row]",inherit:!1,density:"compact",items:r=>t(r)});A(()=>a?.destroy?.());const l=r=>{const c=r.target.closest("[data-row]");if(!(!c||r.target.closest("a"))){if(r.target.closest(".sox-kebab")){d.openMenuAt(c);return}s?.(c)}},o=r=>{const c=r.target.closest("[data-row]");c&&(r.key==="F10"&&r.shiftKey?(r.preventDefault(),d.openMenuAt(c)):r.key==="Enter"&&r.target===c&&(r.preventDefault(),r.stopPropagation(),setTimeout(()=>{n()&&c.isConnected&&s?.(c)},0)))};e.addEventListener("click",l),e.addEventListener("keydown",o),A(()=>{e.removeEventListener("click",l),e.removeEventListener("keydown",o)})}const cs=m.querySelector(".sox-ext"),We=m.querySelector(".sox-ext-tabs");let le=null,Ee="",he=0;const ft=()=>{const e=le;le=null;try{e?.dispose?.()}catch{}e?.box?.remove()};async function ds(){let e=[];try{e=(await g("/extensions"))?.tabs||[]}catch{}n()&&(tt.value=e,We.replaceChildren(...e.map(t=>{const s=document.createElement("button");return s.type="button",s.setAttribute("role","tab"),s.dataset.ext=t.key,s.innerHTML=`<span data-icon="${L(t.icon||"box")}" data-icon-size="14"></span> ${L(t.label)} <span class="sox-pro">PRO</span>`,t.help&&(s.title=t.help),s.addEventListener("click",()=>V(t.key)),s})),I.scan(We),bt())}async function bt(){if(!n())return;for(const l of We.querySelectorAll("button"))l.classList.toggle("is-on",l.dataset.ext===Y.peek());const e=tt.peek().find(l=>l.key===Y.peek());if(le&&le.key!==e?.key&&ft(),!e){Ee="",he++,Mt.includes(Y.peek())||V("overview");return}if(le?.key===e.key||Ee===e.key)return;const t=++he;Ee=e.key;const s=document.createElement("div");s.className="sox-panel sox-ext-body",cs.replaceChildren(s);const a=()=>t===he&&n()&&Y.peek()===e.key;try{const l=await import(e.module);if(!a())return;const o=await l.mount(s,{kit:d,api:d.apiRequest,scope:A,here:n,openItem:ee,reload:qe,E,M,I});if(!a()){try{o?.()}catch{}s.remove();return}le={key:e.key,dispose:o,box:s}}catch(l){a()&&(s.innerHTML=`<p class="sox-note is-bad">${L(e.label)} could not be shown: ${L(l.message||l)}</p>`)}finally{t===he&&(Ee="")}}A(()=>{he++,ft()}),m.querySelector(".sox-pages").innerHTML=Fs,m.querySelector(".sox-site").innerHTML=Js,m.querySelector(".sox-rules").innerHTML=Ys,m.querySelector(".sox-entries").innerHTML=Qs,m.querySelector(".sox-sources").innerHTML=Vs;const vt=m.querySelector(".sox-select");A(M.effect(()=>{const e=nt.value;vt.replaceChildren(new Option("Everything",""),...e.map(t=>new Option(se(t),t))),vt.value=xe.peek()}));function V(e){Y.peek()!==e&&(Y.value=e,history.replaceState(null,"",`#/seo${e==="overview"?"":`/${e}`}`),e==="redirects"&&pe(),e==="sitemap"&&Ne()),bt()}const us={tab:Y,busy:Ce,error:Oe,canManage:B,scoreText:Lt,scoreWidth:Rt,scoreTone:It,ov:At,attention:Pt,asOf:jt,sourceOptions:nt,source:xe,show:te,query:Le,pageRows:it,pagesEmpty:Nt,siteRows:lt,ruleRows:Ae,ruleCount:qt,ruleText:Ut,rulesEmpty:Bt,chainText:_t,renameText:Ht,ruleQuery:Re,newFrom:we,newTo:ke,newCode:et,sourceRows:rt,smRows:Pe,smShow:de,smQuery:Ie,smText:Gt,smEmpty:zt,robots:Wt,extOn:Ot,counts:Dt,smCounts:Kt,pagesLoading:st,rulesLoading:at,smLoading:ot},ps={tab_overview:()=>V("overview"),tab_redirects:()=>V("redirects"),tab_sitemap:()=>V("sitemap"),show_all:()=>{te.value="all"},show_problems:()=>{te.value="problems"},show_hidden:()=>{te.value="hidden"},sm_in:()=>{de.value="in"},sm_out:()=>{de.value="out"},importCsv:Qt,exportRules:()=>{const e=N.peek().rules;if(!e.length){E.toast("Nothing to export.",{type:"info"});return}zs(`redirects-${location.hostname}-${new Date().toISOString().slice(0,10)}.csv`,Gs(e.map(t=>({from:t.from,to:t.to,code:t.code,note:t.note||"",hits:t.hits||0,last_used:t.lastHit||""}))))},addRule:()=>{const e={from:we.peek(),to:ke.peek(),code:Number(et.peek())},{errors:t}=xt(e,N.peek().rules);return Object.keys(t).length?(E.toast(Object.values(t).join(" "),{type:"warning"}),!1):(Ft(e).then(s=>{s&&(we.value="",ke.value="")}),!1)}},ms=M.applyBindings(us,m,{methods:ps});A(()=>ms.dispose()),A(d.helpPopovers(m,{position:"bottom"})),me(m.querySelector(".sox-pages"),as,e=>{const t=Me[z(e)];t&&ee(t.urlPath)}),me(m.querySelector(".sox-site"),os,ns),me(m.querySelector(".sox-rules"),is,e=>{const t=ue[z(e)];t&&B.peek()&&Be(t)}),me(m.querySelector(".sox-entries"),ls,e=>{const t=ae[z(e)];t&&ee(t.urlPath)}),me(m.querySelector(".sox-sources"),rs),d.addBannerAction(i,{id:"seo-check",icon:"refresh-cw",title:"Check every page again",onClick:dt}),d.addBannerAction(i,{id:"seo-settings",icon:"settings",title:"SEO settings",onClick:ht}),I.scan(m),await Promise.all([qe(),ds()]),n()&&new URLSearchParams(location.hash.split("?")[1]||"").get("settings")==="1"&&ht(),n()&&I.scan(m),n()&&!w.peek()?.at&&!w.peek()?.running&&dt()}};
|