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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import{api as D,apiRequest as
|
|
1
|
+
import{api as D,apiRequest as x}from"../api.js";import{placeTools as Ae}from"../lib/sidebar-grouping.js";import{addBannerAction as be}from"../lib/plugin-chrome.js";import{helpPopovers as $e}from"../lib/help-popover.js";import{openMenuAt as G,sidebarCount as ye}from"../lib/tool-kit.js";import{changedTabs as Be,describeCacheKey as J,draftOf as De,expiresIn as He,footerShowcaseErrors as je,formFromDraft as qe,formFromSettings as Ue,homeValue as Fe,payloadFromForm as _e,siteUrlError as ge,settingsProblems as Ke,snapshot as Q,TAB_FIELDS as ze}from"../lib/site-settings-arrange.js";import{pickMedia as Ne}from"../lib/media-picker.js";import{faviconGenerator as Ve}from"../lib/favicon-generator.js";import{makeIconInput as We}from"../lib/shortcode-modal.js";import{fmtDateTime as Ye}from"../lib/dates.js";import{siteSearchSection as Ge}from"../lib/site-search-section.js";const X=/^\/settings(\?.*)?$/,we="#/settings",w="settings.draft",Je=`
|
|
2
2
|
<div class="stx-problem">
|
|
3
3
|
<span class="stx-chip" data-bind-class="tone === 'danger' ? 'is-bad' : 'is-warn'">{{label}}</span>
|
|
4
4
|
<span>{{text}}</span>
|
|
5
5
|
<button type="button" class="btn btn-sm btn-ghost" data-go="{{tab}}">Go there</button>
|
|
6
|
-
</div>`,
|
|
6
|
+
</div>`,Qe=`
|
|
7
7
|
<div class="stx-row" data-id="{{id}}" tabindex="0">
|
|
8
8
|
<span class="stx-main"><strong>{{path}}</strong><small>{{meta}}</small></span>
|
|
9
9
|
<span class="stx-chip">{{expires}}</span>
|
|
10
10
|
<button type="button" class="stx-kebab" aria-label="Actions for {{path}}" title="Actions">\u22EE</button>
|
|
11
|
-
</div>`,we=["general","branding","favicon","seo","footer","email","pageNavigation","cookies","analytics","breadcrumbs","motion","cache"];async function Qe(c,u){const[o,w]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),s=new Map([["/","Dashboard (built in)"]]),i=(v,d)=>{for(const r of v||[]){if(!r||r.hidden||r.restricted)continue;const p=typeof r.url=="string"?r.url:"";p.startsWith("#/")&&p.length>2&&!s.has(p.slice(1))&&s.set(p.slice(1),`${d?`${d} \u203A `:""}${r.text||p}`),i(r.items,r.url?d:r.text||d)}};i($e(o?.items||[],w||[]),""),s.has(u)||s.set(u,`${u} (not in the sidebar)`),c.replaceChildren();for(const[v,d]of s){const r=document.createElement("option");r.value=v,r.textContent=d,c.appendChild(r)}}let f=null,ke=!1;function Xe(){ke||!window.R?.use||(ke=!0,R.use((c,u,o)=>!(Y.test(u?.path||"")&&!Y.test(c?.path||""))||!f?.dirty()?o():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(s=>{s?o():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:c}={})=>{if(f&&!Y.test(c?.path||"")){const u=f;f=null,u.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(c){if(Xe(),f){const e=f;f=null,e.dispose()}const u=[],o=e=>(u.push(e),e);let w=!1;const s=c.get(0).querySelector(".stx")||c.get(0),i=()=>!w&&s.isConnected,v=E.loader(c.get(0),{type:"dots"});let d;try{d=await D.settings.get()}catch(e){v.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(v.destroy(),!s.isConnected)return;const r=M.observable(d||{}),p=Fe(d||{}),n={};for(const[e,t]of Object.entries(p))n[e]=M.observable(t);const B=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),J=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),b=M.observable(p),j=M.computed(()=>ge(b.value)),x=M.computed(()=>ge(B())!==j.value),C=M.observable(!1),T=M.observable(""),Q=M.observable(!1);D.plugins.policy().then(e=>{i()&&(Q.value=e?.signedOnly===!1)}).catch(()=>{});const q=M.observable(!1),X=M.computed(()=>{const e=new Set(De(B(),b.value));return Object.fromEntries(Object.keys(ze).map(t=>[t,e.has(t)]))}),F=M.observable([]);o(M.effect(()=>{F.value=Ke(B()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const Z=M.observable(""),L=M.observable(!1),O=M.observable(""),P=M.observable(""),m=M.observable(null),U=M.observable([]),_=M.observable(""),K=M.observable(!0),z=M.observable(""),ee=M.observable([]);o(M.effect(()=>{const e=_.value.trim().toLowerCase(),t=Date.now();ee.value=[...U.value].reverse().filter(a=>!e||String(a.key).toLowerCase().includes(e)||(a.tags||[]).some(l=>String(l).toLowerCase().includes(e))).map(a=>{const l=W(a.key);return{id:a.key,path:l.path,meta:[l.page?`as ${l.role}`:"not a page",(a.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:He(a.expiresAt,t)}})}));const te=Ge(n.favicon,n.brandLogo,i),Ee={...te.data,...n,saving:C,dirty:x,tabs:X,problems:F,testTo:Z,testing:L,testResult:O,testTone:P,cacheQuery:_,cacheLoading:K,cacheError:z,cacheRows:ee,errorText:T,problemsText:M.computed(()=>F.value.length?"yes":""),siteUrlError:M.computed(()=>be(n.siteUrl.value)),gaIdError:M.computed(()=>ve(n.gaId.value)),gaForcesBanner:M.computed(()=>!!String(n.gaId.value||"").trim()&&n.ccEnabled.value!==!0),noSmtp:M.computed(()=>!String(n.smtpHost.value||"").trim()),mtSizeText:M.computed(()=>`${n.mtIconSize.value} px`),mtRadiusText:M.computed(()=>`${n.mtIconRadius.value} px`),mayAllowInsecureTLS:Q,dirtyEmail:M.computed(()=>X.value.email),seoLength:M.computed(()=>{const e=String(n.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cacheOn:M.computed(()=>m.value?.enabled===!0),cacheState:M.computed(()=>{const e=m.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=m.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=m.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>m.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>_.value.trim()?"Nothing matches.":m.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function N(){if(C.peek())return;const e=J(),t=b.peek(),a=be(e.siteUrl);if(a){E.toast(`Site URL: ${a}`,{type:"error"});return}const l=ve(e.gaId);if(l){E.toast(`Google Analytics: ${l}`,{type:"error"});return}const me=je(e)[0];if(me){E.toast(me,{type:"error"});return}C.value=!0,T.value="";const Oe=String(e.title).trim()!==String(t.title).trim();try{await D.settings.save(_e(e,r.peek(),{adminHomeReady:q.peek()}));const A=await D.settings.get().catch(()=>null);if(!i())return;A&&(r.value=A);const Pe={...e,smtpPass:""};n.smtpPass.value="",b.value=Pe;try{S.remove?S.remove(y):S.set(y,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(Oe||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(pe=>e[pe]!==t[pe]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),he(ye),E.toast("Settings saved.",{type:"success"})}catch(A){T.value=A.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{C.value=!1}}async function Se(){await E.confirm("Throw away your unsaved settings?")&&(ae(b.peek()),T.value="")}function ae(e){for(const[t,a]of Object.entries(e))n[t]&&(n[t].value=a)}async function Me(){if(!L.peek()){L.value=!0,P.value="",O.value="Sending\u2026";try{const e=await k("/settings/test-email",{method:"POST",body:JSON.stringify({to:Z.peek().trim()||void 0})});if(!i())return;O.value=e?.message||"Test email sent.",P.value="is-ok"}catch(e){if(!i())return;O.value=e.message||"Could not send the test email.",P.value="is-bad"}finally{L.value=!1}}}async function $(){K.value=!0;try{const[e,t]=await Promise.all([k("/cache/status").catch(()=>null),k("/cache/keys")]);if(!i())return;m.value=e,U.value=Array.isArray(t?.entries)?t.entries:[],z.value=""}catch(e){if(!i())return;z.value=`Could not read the cache: ${e.message||e}`}finally{K.value=!1}}async function xe(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await k("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await $()}}async function Ce(){const e=m.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{m.value=await k("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),he(ye)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await $()}}const ne=e=>U.peek().find(t=>t.key===e);function Te(e){const t=ne(e);if(!t)return[];const a=W(t.key);return[{header:a.path},{label:"Open the page",icon:"external-link",disabled:!a.page,action:()=>window.open(a.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...a.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(a.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}s.querySelector(".stx-problems").innerHTML=Ye;const g=s.querySelector(".stx-list");g.innerHTML=Je;const se=E.tabs(s.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Ie=M.applyBindings(Ee,s,{methods:{...te.methods,save:N,discard:Se,sendTest:Me,refreshCache:$,clearCache:xe,toggleCache:Ce,pickBrandImage:async e=>{const t=await Ne({title:"Choose a branding image",current:n[e].peek()});t&&i()&&(n[e].value=t)},clearBrandImage:e=>{n[e].value=""},corner:e=>{n.bcPosition.value=e},motionCorner:e=>{n.mtPosition.value=e},motionPlacement:e=>{n.mtPlacement.value=e},goTab:e=>{const t=we.indexOf(e);t>=0&&se.activate(t)}}}),oe=s.querySelector("#stx-mt-radius");o(M.effect(()=>{const e=Math.floor((Number.parseInt(n.mtIconSize.value,10)||52)/2);oe.max=String(e);const t=Number.parseInt(n.mtIconRadius.peek(),10)||0;t>e?n.mtIconRadius.value=String(e):oe.value=String(t)})),o(()=>Ie.dispose());for(const e of s.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,a=Ve("Choose an icon",n[t].peek());a.input.id=`stx-${t}`,a.input.classList.add("form-input");const l=()=>{n[t].value=a.input.value};a.input.addEventListener("input",l),o(()=>a.input.removeEventListener("input",l)),o(M.effect(()=>{a.input.value=n[t].value})),e.appendChild(a.el)}o(Ae(s,{position:"bottom"}));const Re=e=>{const t=e.target.closest("[data-go]");if(!t)return;const a=we.indexOf(t.dataset.go);a>=0&&se.activate(a)};s.querySelector(".stx-problems").addEventListener("click",Re);const G=s.querySelector("#stx-admin-home");Qe(G,Ue(n.adminHome.peek())).then(()=>{i()&&(G.value=n.adminHome.peek(),q.value=!0)}).catch(()=>{});const Le=E.contextMenu(g,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Te(e.dataset.id)});o(()=>Le?.destroy?.());const re=e=>{const t=ne(e.dataset.id),a=t&&W(t.key);a?.page?window.open(a.path,"_blank","noopener"):V(e)},ie=e=>{const t=e.target.closest(".stx-row");if(!t)return;const a=e.target.closest(".stx-kebab");if(a){V(a);return}re(t)},ce=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),V(t)):e.key==="Enter"&&e.target===t&&re(t))};g.addEventListener("click",ie),g.addEventListener("keydown",ce),o(()=>{g.removeEventListener("click",ie),g.removeEventListener("keydown",ce)}),fe(c,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),fe(c,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>N()});const le=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),N())};document.addEventListener("keydown",le,!0),o(()=>document.removeEventListener("keydown",le,!0));const ue=()=>{if(x.peek())try{S.set(y,{form:Be(J()),base:j.peek(),at:new Date().toISOString()})}catch{}},de=e=>{x.peek()&&(ue(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",de),o(()=>window.removeEventListener("beforeunload",de)),f={dirty:()=>x.peek(),dispose(){w=!0,ue(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s),$();let h=null;try{h=S.get(y)}catch{}if(h?.form){try{S.remove?S.remove(y):S.set(y,null)}catch{}const e=h.base&&h.base!==j.peek(),t=h.at?` (${We(h.at)})`:"",a=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;await E.confirm(a)&&i()&&(ae(qe(b.peek(),h.form)),q.peek()&&(G.value=n.adminHome.peek()))}}};
|
|
11
|
+
</div>`,j=["general","branding","favicon","footer","email","pageNavigation","cookies","breadcrumbs","motion","cache","search"];async function Xe(c,d){const[o,k]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),n=new Map([["/","Dashboard (built in)"]]),i=(l,m)=>{for(const r of l||[]){if(!r||r.hidden||r.restricted)continue;const p=typeof r.url=="string"?r.url:"";p.startsWith("#/")&&p.length>2&&!n.has(p.slice(1))&&n.set(p.slice(1),`${m?`${m} \u203A `:""}${r.text||p}`),i(r.items,r.url?m:r.text||m)}};i(Ae(o?.items||[],k||[]),""),n.has(d)||n.set(d,`${d} (not in the sidebar)`),c.replaceChildren();for(const[l,m]of n){const r=document.createElement("option");r.value=l,r.textContent=m,c.appendChild(r)}}let h=null,ke=!1;function Ze(){ke||!window.R?.use||(ke=!0,R.use((c,d,o)=>!(X.test(d?.path||"")&&!X.test(c?.path||""))||!h?.dirty()?o():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(n=>{n?o():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:c}={})=>{if(h&&!X.test(c?.path||"")){const d=h;h=null,d.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(c){if(Ze(),h){const e=h;h=null,e.dispose()}const d=[],o=e=>(d.push(e),e);let k=!1;const n=c.get(0).querySelector(".stx")||c.get(0),i=()=>!k&&n.isConnected;let l=null;const m=E.loader(c.get(0),{type:"dots"});let r;try{r=await D.settings.get()}catch(e){m.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(m.destroy(),!n.isConnected)return;const p=M.observable(r||{}),Z=Ue(r||{}),s={};for(const[e,t]of Object.entries(Z))s[e]=M.observable(t);const q=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),ee=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),b=M.observable(Z),C=M.computed(()=>Q(b.value)),T=M.computed(()=>Q(q())!==C.value),y=M.observable(!1),L=M.observable(""),te=M.observable(!1);D.plugins.policy().then(e=>{i()&&(te.value=e?.signedOnly===!1)}).catch(()=>{});const U=M.observable(!1),ae=M.computed(()=>{const e=new Set(Be(q(),b.value));return Object.fromEntries(Object.keys(ze).map(t=>[t,e.has(t)]))}),F=M.observable([]);o(M.effect(()=>{F.value=Ke(q(),p.value).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const se=M.observable(""),O=M.observable(!1),P=M.observable(""),A=M.observable(""),f=M.observable(null),_=M.observable([]),K=M.observable(""),z=M.observable(!0),N=M.observable(""),ne=M.observable([]);o(M.effect(()=>{const e=K.value.trim().toLowerCase(),t=Date.now();ne.value=[..._.value].reverse().filter(a=>!e||String(a.key).toLowerCase().includes(e)||(a.tags||[]).some(u=>String(u).toLowerCase().includes(e))).map(a=>{const u=J(a.key);return{id:a.key,path:u.path,meta:[u.page?`as ${u.role}`:"not a page",(a.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:He(a.expiresAt,t)}})}));const oe=Ve(s.favicon,s.brandLogo,i),Se={...oe.data,...s,saving:y,dirty:T,tabs:ae,problems:F,testTo:se,testing:O,testResult:P,testTone:A,cacheQuery:K,cacheLoading:z,cacheError:N,cacheRows:ne,errorText:L,problemsText:M.computed(()=>F.value.length?"yes":""),siteUrlError:M.computed(()=>ge(s.siteUrl.value)),noSmtp:M.computed(()=>!String(s.smtpHost.value||"").trim()),mtSizeText:M.computed(()=>`${s.mtIconSize.value} px`),mtRadiusText:M.computed(()=>`${s.mtIconRadius.value} px`),mayAllowInsecureTLS:te,dirtyEmail:M.computed(()=>ae.value.email),cacheOn:M.computed(()=>f.value?.enabled===!0),cacheState:M.computed(()=>{const e=f.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=f.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=f.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>f.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>K.value.trim()?"Nothing matches.":f.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function V(){if(y.peek())return;const e=ee(),t=b.peek();if(l?.dirty()&&Q(e)===C.peek()){y.value=!0;try{await l.save()}finally{y.value=!1}return}const a=ge(e.siteUrl);if(a){E.toast(`Site URL: ${a}`,{type:"error"});return}const u=je(e)[0];if(u){E.toast(u,{type:"error"});return}y.value=!0,L.value="";const Ie=String(e.title).trim()!==String(t.title).trim();try{await D.settings.save(_e(e,p.peek(),{adminHomeReady:U.peek()}));const B=await D.settings.get().catch(()=>null);if(!i())return;B&&(p.value=B);const Pe={...e,smtpPass:""};s.smtpPass.value="",b.value=Pe;try{S.remove?S.remove(w):S.set(w,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(Ie||["adminBrandEnabled","adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(ve=>e[ve]!==t[ve]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),ye(we),E.toast("Settings saved.",{type:"success"}),l?.dirty()&&await l.save()}catch(B){L.value=B.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{y.value=!1}}async function Ee(){await E.confirm("Throw away your unsaved settings?")&&(re(b.peek()),l?.discard(),L.value="")}function re(e){for(const[t,a]of Object.entries(e))s[t]&&(s[t].value=a)}async function Me(){if(!O.peek()){O.value=!0,A.value="",P.value="Sending\u2026";try{const e=await x("/settings/test-email",{method:"POST",body:JSON.stringify({to:se.peek().trim()||void 0})});if(!i())return;P.value=e?.message||"Test email sent.",A.value="is-ok"}catch(e){if(!i())return;P.value=e.message||"Could not send the test email.",A.value="is-bad"}finally{O.value=!1}}}async function $(){z.value=!0;try{const[e,t]=await Promise.all([x("/cache/status").catch(()=>null),x("/cache/keys")]);if(!i())return;f.value=e,_.value=Array.isArray(t?.entries)?t.entries:[],N.value=""}catch(e){if(!i())return;N.value=`Could not read the cache: ${e.message||e}`}finally{z.value=!1}}async function xe(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await x("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await $()}}async function Ce(){const e=f.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{f.value=await x("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),ye(we)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await $()}}const ie=e=>_.peek().find(t=>t.key===e);function Te(e){const t=ie(e);if(!t)return[];const a=J(t.key);return[{header:a.path},{label:"Open the page",icon:"external-link",disabled:!a.page,action:()=>window.open(a.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...a.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(a.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}n.querySelector(".stx-problems").innerHTML=Je;const g=n.querySelector(".stx-list");g.innerHTML=Qe;const W=E.tabs(n.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),ce=new URLSearchParams(location.hash.split("?")[1]||"").get("tab");j.includes(ce)&&W.activate(j.indexOf(ce));const Re=M.applyBindings(Se,n,{methods:{...oe.methods,save:V,discard:Ee,sendTest:Me,refreshCache:$,clearCache:xe,toggleCache:Ce,pickBrandImage:async e=>{const t=await Ne({title:"Choose a branding image",current:s[e].peek()});t&&i()&&(s[e].value=t)},clearBrandImage:e=>{s[e].value=""},corner:e=>{s.bcPosition.value=e},motionCorner:e=>{s.mtPosition.value=e},motionPlacement:e=>{s.mtPlacement.value=e},goTab:e=>{const t=j.indexOf(e);t>=0&&W.activate(t)}}}),le=n.querySelector("#stx-mt-radius");o(M.effect(()=>{const e=Math.floor((Number.parseInt(s.mtIconSize.value,10)||52)/2);le.max=String(e);const t=Number.parseInt(s.mtIconRadius.peek(),10)||0;t>e?s.mtIconRadius.value=String(e):le.value=String(t)})),o(()=>Re.dispose());for(const e of n.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,a=We("Choose an icon",s[t].peek());a.input.id=`stx-${t}`,a.input.classList.add("form-input");const u=()=>{s[t].value=a.input.value};a.input.addEventListener("input",u),o(()=>a.input.removeEventListener("input",u)),o(M.effect(()=>{a.input.value=s[t].value})),e.appendChild(a.el)}o($e(n,{position:"bottom"}));const Le=e=>{const t=e.target.closest("[data-go]");if(!t)return;const a=j.indexOf(t.dataset.go);a>=0&&W.activate(a)};n.querySelector(".stx-problems").addEventListener("click",Le);const Y=n.querySelector("#stx-admin-home");Xe(Y,Fe(s.adminHome.peek())).then(()=>{i()&&(Y.value=s.adminHome.peek(),U.value=!0)}).catch(()=>{});const Oe=E.contextMenu(g,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Te(e.dataset.id)});o(()=>Oe?.destroy?.());const ue=e=>{const t=ie(e.dataset.id),a=t&&J(t.key);a?.page?window.open(a.path,"_blank","noopener"):G(e)},de=e=>{const t=e.target.closest(".stx-row");if(!t)return;const a=e.target.closest(".stx-kebab");if(a){G(a);return}ue(t)},me=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),G(t)):e.key==="Enter"&&e.target===t&&ue(t))};g.addEventListener("click",de),g.addEventListener("keydown",me),o(()=>{g.removeEventListener("click",de),g.removeEventListener("keydown",me)}),be(c,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),be(c,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>V()});const pe=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),V())};document.addEventListener("keydown",pe,!0),o(()=>document.removeEventListener("keydown",pe,!0));const fe=()=>{if(T.peek())try{S.set(w,{form:De(ee()),base:C.peek(),at:new Date().toISOString()})}catch{}},he=e=>{T.peek()&&(fe(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",he),o(()=>window.removeEventListener("beforeunload",he)),h={dirty:()=>T.peek()||!!l?.dirty(),dispose(){k=!0,fe(),d.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(n),$();let v=null;try{v=S.get(w)}catch{}if(v?.form){try{S.remove?S.remove(w):S.set(w,null)}catch{}const e=v.base&&v.base!==C.peek(),t=v.at?` (${Ye(v.at)})`:"",a=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;await E.confirm(a)&&i()&&(re(qe(b.peek(),v.form)),U.peek()&&(Y.value=s.adminHome.peek()))}i()&&(l=Ge(n),o(()=>l.dispose()))}};
|
package/docs/DommaFeatures.md
CHANGED
|
@@ -396,7 +396,7 @@ headings (Layouts → Page contents settings, and `[contents]` per page).
|
|
|
396
396
|
- **robots.txt** - allows the site, disallows `/admin/` and `/api/`, points at the sitemap; extra lines and a "block all" switch in SEO settings. Ahead of the `/api/` block it allows the public read endpoints a page's scripts call while it renders (`/api/effects/settings`, `/api/forms/*/public`, `/api/components/*.js`, the `/api/collections/*/public` and `/api/views/*/public` listings), so Google, which renders pages with JavaScript, can fetch them; the longest matching rule wins. Those JSON answers carry `X-Robots-Tag: noindex`, so they are fetched for rendering but never listed. Nothing that needs a sign-in or writes is allowed (`PUBLIC_API_ALLOW` in `sitemap.js`).
|
|
397
397
|
- Where: `/robots.txt`; `server/services/sitemap.js`; checked by `tests/routes/llms-robots.test.js`
|
|
398
398
|
- Since: the Allow lines 1.8.36
|
|
399
|
-
- **llms.txt** - an automatic `/llms.txt` (the llmstxt.org format) for AI assistants: `# Site title`, `> description` (
|
|
399
|
+
- **llms.txt** - an automatic `/llms.txt` (the llmstxt.org format) for AI assistants: `# Site title`, `> description` (SEO settings default description), then `## Pages` and one section per plugin sitemap source (blog posts and so on), each line `- [Title](absolute url): description`. Built from the same published, public, indexable set as the sitemap (SEO exclusions included), with absolute links from the site address in Site Settings (else the request's origin). Cached and invalidated with the sitemap and with Site Settings saves. On by default with its own switch in SEO settings; answers 404 while "Ask every search engine to stay away" is on. With the SEO Tool switched off it is still served, as the sitemap is.
|
|
400
400
|
- Where: `/llms.txt`; **Tools > SEO > Settings** ("Publish /llms.txt for AI assistants"); `config/seo.json` `llms.enabled`; `buildLlmsTxt()` in `server/services/sitemap.js`
|
|
401
401
|
- Since: 1.8.36
|
|
402
402
|
- **Readable without JavaScript** - pages are rendered on the server, so robots that run no JavaScript (GPTBot, ClaudeBot and most AI crawlers) read the words, headings, body links, meta tags and JSON-LD. The navbar and footer (menus with their nested items, copyright, social links) are also written as plain links inside `#site-navbar` and `#site-footer`, built from the same per-viewer menu data the browser gets and visually hidden (`.sr-only.cms-nojs-links`, so nothing flashes or shifts); site.js removes them once it has drawn the real ones. An interactive `[collection]` or `[view]` lists every entry in its no-JS fallback, in client and server mode alike (or its `limit`; at most 1,000 - `FALLBACK_MAX` - with the count left out in `data-cb-more`, which the SEO audit warns about; server mode runs one extra query for it, kept by the page cache), and a `[form]` embed carries the form's title and description as hidden text. Scripts load at the end of the body; the ones nothing needs mid-parse carry `defer` (collection-browser, form-logic-engine, effects, search, analytics, back-to-top, cookie consent - the first two sit above site.js because deferred and module scripts run in one queue in document order). DOMPurify ships in `public/js/vendor/dompurify/` at a pinned version (admin and public pages), so no public page script comes from a CDN. Still built in the browser: form fields, search results and the Calendar month view.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Google Business Profile
|
|
2
|
+
|
|
3
|
+
Open SEO and choose its settings cog. Site defaults and the Google Business Profile connection are here. Google Analytics has its own section in the Analytics tool's settings.
|
|
4
|
+
|
|
5
|
+
## Set up Google access
|
|
6
|
+
|
|
7
|
+
Use a Google Cloud project approved for Business Profile API access. Approval is a Google process and must be completed before the connection can retrieve locations. Follow [Google's prerequisites](https://developers.google.com/my-business/content/prereqs).
|
|
8
|
+
|
|
9
|
+
1. Enable the My Business Account Management API and My Business Business Information API in the approved project.
|
|
10
|
+
2. Configure the OAuth consent screen. If the app is in testing, add the Google account that manages the business as a test user.
|
|
11
|
+
3. Create an OAuth client of type Web application.
|
|
12
|
+
4. Copy the exact return address shown under Google app setup in SEO settings into the client's Authorized redirect URIs. It ends with `/api/seo/google-business/callback`.
|
|
13
|
+
5. Enter the client ID and client secret, then choose Connect Google. Sign in with an owner or manager of the Business Profile and grant access.
|
|
14
|
+
|
|
15
|
+
Google requires the `business.manage` scope to read these resources. This version uses the API to read business details and revoke its own connection. See [Google's OAuth guide](https://developers.google.com/my-business/content/implement-oauth) and [basic setup](https://developers.google.com/my-business/content/basic-setup).
|
|
16
|
+
|
|
17
|
+
The return address uses the trusted Site URL. Open the admin at that address when connecting. HTTPS is required, with HTTP allowed for localhost development.
|
|
18
|
+
|
|
19
|
+
## Review and import
|
|
20
|
+
|
|
21
|
+
Choose the business account, then the location. More accounts and More locations load subsequent result pages. Choose Review details to fetch the selected location from Google.
|
|
22
|
+
|
|
23
|
+
Each proposed field appears beside its current value. Empty fields are selected by default. Select any replacements you want, then choose Apply selected details to form. Save SEO settings to publish the resulting structured data on the website.
|
|
24
|
+
|
|
25
|
+
The import can offer the business name, website, phone, public storefront address, Google Maps profile link, LocalBusiness type and structured data switch. Existing logo, email and profile links are retained. Missing Google fields do not clear existing website values. Review address fields before publishing them, especially for a service business.
|
|
26
|
+
|
|
27
|
+
Google listing edits, review replies, posts and automatic synchronisation are outside this version. Imported details are a reviewed copy and can be edited in SEO afterwards.
|
|
28
|
+
|
|
29
|
+
## Reconnect and disconnect
|
|
30
|
+
|
|
31
|
+
Reconnect Google renews authorization. An empty client secret field keeps the saved secret when the client ID is unchanged. Google may ask you to grant access again after an app in testing expires or access is revoked.
|
|
32
|
+
|
|
33
|
+
Disconnect removes this site's saved credentials and attempts to revoke the Google token. Previously imported details remain in SEO. If Google cannot be reached for revocation, the screen explains how to remove the app from your Google account permissions.
|
|
34
|
+
|
|
35
|
+
The encrypted connection and its encryption key live under `content/sessions/google-business/`, which is excluded from site repositories. Keep this private runtime directory in protected backups if you want connections to survive a restore. The key and credential files are readable only by their owner. Tokens and client secrets are never returned by the status API, placed in browser drafts, or stored in `site.json`.
|
|
36
|
+
|
|
37
|
+
## Troubleshooting
|
|
38
|
+
|
|
39
|
+
- Access refused: check Google's API approval, enable both APIs and confirm the Google account manages the business.
|
|
40
|
+
- No locations: select another account or use the Google account that owns or manages the listing.
|
|
41
|
+
- Redirect mismatch: use the exact return address shown in SEO and the Web application OAuth client type.
|
|
42
|
+
- Sign-in expired: choose Connect again in the same browser. Sign-in tickets last ten minutes and can be used once.
|
|
43
|
+
- Access revoked: reconnect to grant Google access again.
|
|
44
|
+
- Request limit: wait before retrying. Google applies project quotas.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/public/js/site.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{applyMobileMenu as oe,initCarouselPopup as ne}from"/public/js/mobile-ui.mjs?v=20261016-mobile-options";import{installSmoothCarets as se}from"/public/js/smooth-caret.js";import{decodeBase64Json as W}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as J,applyColour as U,applyPill as ae,applyFloat as ie,applyHighlight as V,applyItemHighlight as re,itemHighlightAttrs as ce,isCurrentItem as G,menuAlign as le,applyClasses as j,cssClasses as Y,applyEntrance as X}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{applyMenuLook as de}from"/public/js/menu-cards.mjs?v=20261029-glider-layout";import{initPageMotion as me}from"/public/js/page-motion.mjs?v=20261029-page-motion";import{applyFooterShowcase as ue}from"/public/js/footer-showcase.mjs?v=20261029-footer-showcase";import{initFloatingMenus as pe}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as K,markInvalidFields as Q,showValidationPopup as Z}from"/public/js/forms.js?v=20261025-forms-files-total";import{buildWizardSteps as fe}from"/public/js/form-steps.mjs";import{mountMotionControl as he}from"/public/js/motion-control.mjs?v=20261029-motion-under-switcher";import{applyFormClasses as ee,applySubmitButtons as ge,applyWizardButtons as ye}from"/public/js/form-submit.mjs";import{captureFileFields as te,destroyDropzones as ve,fileBlueprint as be,restoreFileFields as we,setFileLimits as Ce,splitFiles as Se,submitBody as xe,totalTooLarge as ke,wireFileFields as Le}from"/public/js/form-files.mjs";document.addEventListener("click",u=>{const r=u.target?.closest?.(".dm-banner__dismiss");r&&(u.preventDefault(),r.closest(".dm-banner")?.remove())}),$(()=>{se(document);const u=window.__CMS_NAV__||{},r=window.__CMS_SITE__||{};if(r.autoTheme?.enabled){let s=function(o){const c=(o||"07:00").split(":");return+c[0]*60+(+c[1]||0)},n=function(){const o=new Date,c=o.getHours()*60+o.getMinutes();return c>=s(t.dayStart)&&c<s(t.nightStart)?t.dayTheme:t.nightTheme},e=function(o){const c=t.apply&&t.apply[o]||{theme:o,custom:""};Domma.theme.set(c.theme);for(const f of[...document.body.classList])f.startsWith("dm-custom-")&&document.body.classList.remove(f);c.custom&&document.body.classList.add(c.custom)};const t=r.autoTheme;e(n()),setInterval(()=>e(n()),6e4)}const L=$("#site-navbar");if(L.length&&u.brand){let n=function(m,w=0){return Array.isArray(m)?m.filter(d=>!(d&&(d.type==="spacer"||d.type==="theme-switcher"))).filter(d=>!(d&&d.type==="separator"&&w===0)).map(d=>d&&d.type==="separator"?{divider:!0}:Array.isArray(d?.items)?{...d,items:n(d.items,w+1)}:d):m};const t={...u.brand},s=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let m="";t.logo?m+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(m+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(m+=`<span class="navbar-brand-text${s}">${t.text}</span>`),t.tagline&&(m+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=m}else s&&t.text&&(t.html=`<span class="navbar-brand-text${s}">${t.text}</span>`);const e=n(u.items||[]),o=u.position==="floating",c=u.orientation==="vertical";L.get(0).querySelector(":scope > .cms-nojs-links")?.remove(),Domma.elements.navbar("#site-navbar",{brand:t,items:e,variant:u.variant||"dark",position:o?"static":u.position||"sticky",collapsible:!0,appearOnHover:!!u.appearOnHover}),Domma.icons.scan("#site-navbar");const f=L.get(0);oe(f,u.mobile);const a=()=>{if(!c)return;const w=f.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");w&&!w.classList.contains("open")&&w.querySelector(":scope > .navbar-dropdown-toggle")?.click()};f.classList.add(`dm-nav-align-${le(u)||"centre"}`),c&&L.get(0).classList.add("dm-nav-vertical",u.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const h=u.dock||{};c&&!o&&$e(f,u.side==="right"),!c&&h.hideOnScroll&&Ee(f),V(f,u),X(f,u),j(f,u.class),o&&(ie(L.get(0),u),pe(L.get(0).parentNode||document));const l=f.querySelector(".navbar-toggle"),y=f.querySelector(".navbar-collapse");if(l&&y){let m=null;const w=()=>{y.classList.contains("show")&&l.click()},d=()=>{m&&(m.remove(),m=null),document.body.classList.remove("dm-drawer-open")},p=()=>{m||(m=document.createElement("div"),m.className="dm-navbar-backdrop",m.addEventListener("click",w),document.body.appendChild(m),document.body.classList.add("dm-drawer-open"))};l.addEventListener("click",()=>{requestAnimationFrame(()=>{y.classList.contains("show")?(a(),p()):d()})}),y.addEventListener("click",v=>{v.target.closest("a")&&requestAnimationFrame(d)}),document.addEventListener("keydown",v=>{v.key==="Escape"&&y.classList.contains("show")&&w()})}const C=f.querySelector(".navbar-nav");if(C){const m=Array.from(C.children);let w=0;for(const d of u.items||[])if(!(d&&d.type==="separator")){if(d&&d.type==="spacer"){const p=document.createElement("li"),v=["xs","sm","md","lg","xl","flex"].includes(d.size)?d.size:"md";p.className=`dm-menu-spacer dm-menu-spacer--${v}`,p.setAttribute("aria-hidden","true"),C.insertBefore(p,m[w]||null);continue}if(d&&d.type==="theme-switcher"){const p=document.createElement("li");p.className="dm-menu-theme-switcher";const v=document.createElement("span");v.dataset.themeSwitcher="menu",v.dataset.position="inline",p.appendChild(v),C.insertBefore(p,m[w]||null);continue}w++}window.CMSThemeSwitcher?.scan(C)}f.querySelectorAll("[data-path]").forEach(m=>{const w=m.dataset.path.split(".").map(Number),d=w.length>1,p=w.reduce((A,i)=>A?.items?.[i],{items:e});if(!p)return;p.colour&&U(m,p.colour),p.pill&&!d&&ae(m,p.pill),re(m,p),j(m.closest("li"),p.class);const v=J(p.badge);v&&m.appendChild(v),G(p.url,location.pathname)&&(m.classList.add("active"),m.setAttribute("aria-current","page"))}),de(f,u,e),a()}let k=null;const x=$("#site-footer");if(x.length){let o=function(i){return String(i??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},c=function(i){return o(i).replace(/'/g,"'")},f=function(i){if(i&&i.type==="separator")return'<li class="footer-sep"><hr></li>';if(i&&i.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(i.size)?i.size:"md"}" aria-hidden="true"></li>`;if(i&&i.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const M=c(i.url||"#"),q=o(i.text||""),g=Array.isArray(i.items)&&i.items.length?`<ul>${i.items.map(f).join("")}</ul>`:"",D=ce(i),N=[D.hover&&` data-menu-item-hover="${c(D.hover)}"`,D.active&&` data-menu-item-active="${c(D.active)}"`].filter(Boolean).join(""),O=D.accent?` style="--dm-menu-highlight:${c(D.accent)}"`:"",R=Y(i.class).join(" ");return`<li${R?` class="${c(R)}"`:""}><a href="${M}"${N}${O}>${q}</a>${g}</li>`},h=function(){if(!r.footer)return"";const i=Object.keys(s).filter(q=>t[q]);if(!i.length)return"";const M=Y(r.footer.socialClass).join(" ");return`<div class="footer-social">${i.map(q=>{const{label:g,svg:D}=s[q];return`<a href="${t[q]}" target="_blank" rel="noopener noreferrer" aria-label="${g}" class="footer-social-link${M?` ${c(M)}`:""}">${D}</a>`}).join("")}</div>`},m=function(i){if(!i||i.type==="separator"||i.type==="spacer")return"";if(i.type==="theme-switcher")return`<li class="cms-footer-col">${f(i).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const M=Array.isArray(i.items)&&i.items.length?`<ul>${i.items.map(f).join("")}</ul>`:"",q=C(i)?`<span class="cms-footer-col-title">${o(i.text||"")}</span>`:`<a class="cms-footer-col-title" href="${c(i.url||"#")}">${o(i.text||"")}</a>`,g=["cms-footer-col",...Y(i.class)].join(" ");return`<li class="${c(g)}">${q}${M}</li>`},p=function(i,M,q=!1){return(i||[]).forEach(g=>{g&&(g.type==="separator"||g.type==="spacer"||g.type==="theme-switcher")||(q&&C(g)||M.push(g),Array.isArray(g.items)&&g.items.length&&p(g.items,M))}),M};const t=r.social||{},s={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let n='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},a=(()=>{const i=r.cookieConsent;if(!i||i.showSettingsLink===!1)return"";const M=q=>(q||[]).some(g=>g&&(g.url==="#cookie-settings"||M(g.items)));return M(e.primary)||M(e.legal)?"":`<li class="cms-cookie-settings"><a href="#cookie-settings">${o(i.settingsLinkText||"Cookie settings")}</a></li>`})(),l=["columns","row"].includes(r.footer?.layout)?r.footer.layout:"auto",y=r.footer?.look==="showcase"||l==="columns"||l==="auto"&&e.primary.some(i=>Array.isArray(i?.items)&&i.items.length),C=i=>y&&!i.url&&Array.isArray(i.items)&&i.items.length>0;if(y){const i=u.brand||{},M=i.text||r.title||"",q=i.logo?`<img src="${c(i.logo)}" class="cms-footer-brand-logo" alt="">`:i.icon?'<span class="cms-footer-brand-icon"></span>':"",g=String(r.footer?.blurb||"").trim();n='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',n+=`<div class="cms-footer-brand">${M||q?`<a class="cms-footer-brand-name" href="${c(i.url||"/")}">${q}<span>${o(M)}</span></a>`:""}${g?`<p class="cms-footer-blurb">${o(g)}</p>`:""}${h()}</div>`,e.primary.length&&(n+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${e.primary.map(m).join("")}</ul></nav>`),n+='</div><div class="cms-footer-bottom">',e.copyright&&(n+=`<p class="cms-footer-copyright">${o(e.copyright)}</p>`),(e.legal.length||a)&&(n+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${e.legal.map(f).join("")}${a}</ul></nav>`),n+="</div>"}else e.copyright&&(n+=`<p>${o(e.copyright)}</p>`),e.primary.length&&(n+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(f).join("")}</ul></nav>`),(e.legal.length||a)&&(n+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(f).join("")}${a}</ul></nav>`),n+=h();n+="</div>",x.get(0).querySelector(":scope > .cms-nojs-links")?.remove();const w=x.get(0).querySelector(":scope > .cms-footer-blocks");w?.remove(),x.html(n),w&&x.get(0).prepend(w),window.CMSThemeSwitcher?.scan(x.get(0));const d=x.get(0).querySelector(".cms-footer-brand-icon");d&&u.brand?.icon&&d.setAttribute("data-icon",u.brand.icon),j(x.get(0),r.footer?.class),y&&ue(x.get(0),r);const v=[...p(e.primary,[],!0),...p(e.legal,[])];x.get(0).querySelectorAll(".footer-nav a").forEach((i,M)=>{const q=v[M];if(!q)return;q.colour&&U(i,q.colour);const g=J(q.badge);g&&i.appendChild(g),G(q.url,location.pathname)&&(i.classList.add("is-active"),i.setAttribute("aria-current","page"))}),V(x.get(0).querySelector(".footer-nav--primary"),{highlight:e.primaryHighlight}),V(x.get(0).querySelector(".footer-nav--legal"),{highlight:e.legalHighlight}),X(x.get(0).querySelector(".footer-nav--primary"),{entrance:e.primaryEntrance}),X(x.get(0).querySelector(".footer-nav--legal"),{entrance:e.legalEntrance}),j(x.get(0).querySelector(".footer-nav--primary"),e.primaryClass),j(x.get(0).querySelector(".footer-nav--legal"),e.legalClass),k=x.get(0).querySelector(".cms-footer-bottom")||x.get(0).querySelector(".footer-inner")}he(r.motion||{enabled:r.footer?.showMotionToggle!==!1,default:r.footer?.motionDefault,placement:"footer",control:r.footer?.motionControl,label:"Reduce motion"},{footer:k,navbar:document.querySelector("#site-navbar .navbar-container")?.closest("#site-navbar")||null});const I=document.getElementById("site-sidebar");I&&Ae(I),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const b=$(".page-body");if(b.length){b.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),b.find(".tabs").each(function(){Domma.elements.tabs(this)}),b.find(".carousel").each(function(){const s={primary:"--dm-primary",secondary:"--dm-secondary",accent:"--dm-primary",text:"--dm-text",surface:"--dm-surface",background:"--dm-background",success:"--dm-success",warning:"--dm-warning",danger:"--dm-danger",info:"--dm-info"},n=h=>{const l=String(h||"").trim();return l?s[l.toLowerCase()]?`var(${s[l.toLowerCase()]})`:CSS.supports("color",l)?l:"":""},e=n(this.dataset.background),o=n(this.dataset.arrowBackground),c=n(this.dataset.arrowColor);e&&(this.style.setProperty("--dm-carousel-background",e),this.classList.add("dm-carousel-custom-background")),o&&this.style.setProperty("--dm-carousel-arrow-background",o),c&&this.style.setProperty("--dm-carousel-arrow-color",c);const f=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${f}`);const a={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide",heightMode:this.dataset.heightMode==="average"?"average":"crop"};if(this.dataset.height){const h=Number(this.dataset.height);Number.isFinite(h)&&h>0&&(a.height=h)}if(this.dataset.animationDuration){const h=parseInt(this.dataset.animationDuration,10);Number.isFinite(h)&&(a.animationDuration=h)}this.dataset.animationEasing&&(a.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(h=>h.cloneNode(!0)),this._dmCarouselOpts=a),this._dmCarousel=Domma.elements.carousel(this,a),ne(this)}),b.find(".dm-countdown").each(function(){const s={autoStart:!0},n=String(this.dataset.format||"").trim(),e=String(this.dataset.to||"").trim(),o=/^(\d{4})-(\d{2})-(\d{2})$/.exec(e),c=o?new Date(+o[1],+o[2]-1,+o[3]).getTime():e?new Date(e).getTime():NaN;Number.isFinite(c)?s.duration=Math.max(0,c-Date.now()):this.dataset.duration&&(s.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const f=/^dd:/i.test(n);s.format=f?"hh:mm:ss":n||(s.duration>=36e5?"hh:mm:ss":"mm:ss");const a=this,h=y=>String(y).padStart(2,"0"),l=y=>{const C=Math.max(0,Math.floor(y/1e3)),m=a.querySelector(".dm-timer-time");m&&(m.textContent=`${h(Math.floor(C/86400))}:${h(Math.floor(C%86400/3600))}:${h(Math.floor(C%3600/60))}:${h(C%60)}`)};f&&(s.onTick=l),Domma.elements.timer(this,s),f&&l(s.duration||0)}),b.find("[data-tooltip]:not(.dm-range-slider)").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),b.find(".dm-progression").each(function(){const s=Array.from(this.querySelectorAll(".dm-progression-item")).map(o=>{const c={title:o.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:o.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return o.dataset.date&&(c.date=o.dataset.date),o.dataset.status&&(c.status=o.dataset.status),o.dataset.eventIcon&&(c.icon=o.dataset.eventIcon),o.dataset.entryId&&(c._entryId=o.dataset.entryId),c}),n=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",e=Domma.elements.progression(this,{layout:n,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...s.length?{items:s}:{}});if((this.dataset.mode||"timeline")==="timeline"){const o={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((c,f)=>{const a=s[f];if(!a)return;const h=c.querySelector(".dm-progression-title");if(a.icon&&h){const y=document.createElement("span");y.dataset.icon=a.icon,y.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",y.setAttribute("aria-hidden","true"),h.prepend(y)}const l=o[a.status];if(l&&h){const y=document.createElement("span");y.className=`badge badge-${l.tone} cms-timeline-status`,y.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const C=document.createElement("span");C.dataset.icon=l.icon,C.dataset.iconSize="14",C.setAttribute("aria-hidden","true"),y.append(C,document.createTextNode(l.label)),h.after(y)}})}Domma.icons.scan(this),s.length&&(this._dmCtx={instance:e,items:s})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}const t=matchMedia("(prefers-reduced-motion: reduce)").matches||document.documentElement.classList.contains("dm-reduced-motion");document.querySelectorAll(".page-body .row[data-reveal]").forEach(s=>{const n=s.dataset.revealMode||"stagger",e=s.dataset.revealAnimation||"slide-up",o=parseInt(s.dataset.revealDuration,10)||(e.startsWith("swoosh")?700:400),c=parseInt(s.dataset.revealStagger,10)||(e.startsWith("swoosh")?90:60),f=parseInt(s.dataset.revealDelay,10)||0,a=s.dataset.revealDirection==="rtl",h=s.dataset.revealReplay==="true",l=Array.from(s.children);if(t&&e.startsWith("swoosh"))return;const y={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)",swoosh:"translateY(36px) scale(0.96)"},C=p=>e!=="swoosh-sides"?y[e]||"":l.length>1&&p===0?"translateX(-64px) rotate(-1.5deg)":l.length>1&&p===l.length-1?"translateX(64px) rotate(1.5deg)":"translateY(40px) scale(0.94)",m=e.startsWith("swoosh"),w=m?"cubic-bezier(.2,.8,.2,1)":"ease",d=(p,v)=>{p.style.opacity="0",p.style.transform=C(v),m&&(p.style.filter="blur(10px)")};l.forEach((p,v)=>{d(p,v),p.style.transition=`opacity ${o}ms ${w}, transform ${o}ms ${w}`+(m?`, filter ${o}ms ${w}`:"");const A=a?l.length-1-v:v;p.style.transitionDelay=n==="stagger"?`${f+A*c}ms`:`${f}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const p=new IntersectionObserver(v=>{v.forEach(A=>{if(A.isIntersecting){if(A.target.offsetWidth,A.target.style.opacity="1",A.target.style.transform="",m&&(A.target.style.filter=""),!h){p.unobserve(A.target);const i=A.target;i.addEventListener("transitionend",function M(q){q.target!==i||q.propertyName!=="opacity"||(i.removeEventListener("transitionend",M),i.style.transition="",i.style.transitionDelay="")})}}else h&&A.boundingClientRect.top>0&&d(A.target,l.indexOf(A.target))})},{threshold:.1});l.forEach(v=>p.observe(v))}))}),me(document.querySelector(".page-body")||document),b.find(".card[data-collapsible]").each(function(){const s=this.querySelector(".card-header");s&&s.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),b.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const s=this.dataset.soTarget,n=document.getElementById(s);if(!n)return;const e=E.slideover({title:n.dataset.soTitle||"",size:n.dataset.soSize||"md",position:n.dataset.soPosition||"right"});n.style.display="",e.setContent(n),e.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(s=>{try{const n=W(s.dataset.config);Object.assign(t,n)}catch{}}),Object.keys(t).length>0){const s={};for(const[n,e]of Object.entries(t)){const o=e?.events?.click,{confirm:c,toast:f,alert:a,prompt:h,...l}=o||{};if(c||f||a||h){$(n).on("click",async function(R){if(R.preventDefault(),c&&!await E.confirm(c))return;let z=null;if(!(h&&(z=await E.prompt(h,{inputPlaceholder:l.promptPlaceholder||"",inputValue:l.promptDefault||""}),z===null))){{const B=$(l.target||this);l.toggleClass&&B.toggleClass(l.toggleClass),l.addClass&&B.addClass(l.addClass),l.removeClass&&B.removeClass(l.removeClass),z!==null&&(l.setText&&B.text(z),l.setVal&&B.val(z),l.setAttr&&B.attr(l.setAttr,z))}l.href&&(window.location.href=l.href),f&&E.toast(f,{type:l.toastType||"success"}),a&&E.alert(a)}});const{target:C,toggleClass:m,addClass:w,removeClass:d,setText:p,setVal:v,setAttr:A,href:i,toastType:M,promptPlaceholder:q,promptDefault:g,...D}=l,N={...e.events};Object.keys(D).length?N.click=C?{target:C,...D}:D:delete N.click;const O={...e,events:N};Object.keys(N).length||delete O.events,Object.keys(O).length&&(s[n]=O)}else s[n]=e}$.setup(s)}}b.length&&P(b.get(0))}),window.wireCTAButtons=P;function P(u){u.querySelectorAll(".dm-cta-trigger").forEach(r=>{r.__ctaWired||(r.__ctaWired=!0,r.addEventListener("click",async()=>{const L=r.dataset.action,k=r.dataset.entry,x=r.dataset.confirm;let I=S.get("auth_token");if(!I){E.toast("Please log in to perform this action.",{type:"warning"});return}if(x&&!await E.confirm(x))return;const b=Array.from(r.childNodes).map(s=>s.cloneNode(!0));r.disabled=!0,r.textContent="Running\u2026";const t=s=>fetch(`/api/actions/${L}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(r.dataset.scope?{entryId:k,scope:r.dataset.scope}:{entryId:k})});try{let s=await t(I);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const o=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(o.ok){const{token:c}=await o.json();S.set("auth_token",c),I=c,s=await t(I)}}}const n=await s.json().catch(()=>({}));if(!s.ok||n.success===!1)throw new Error(n.error||n.message||`Error ${s.status}`);E.toast(n.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{r.disabled=!1,r.textContent="",b.forEach(s=>r.appendChild(s)),Domma.icons.scan(r)}}))})}(function(){const r=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!r.length)return;const L=S.get("auth_token");function k(I,b){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(b){s.appendChild(document.createTextNode("Please "));const n=document.createElement("a");n.href="/login",n.textContent="sign in",s.appendChild(n),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=I;return t.appendChild(s),t}function x(I,b){const t=document.createElement("template");t.innerHTML=b||"",I.replaceWith(t.content)}r.forEach(async I=>{const b=I.dataset.collectionAttrs;if(!b)return;if(!L){I.replaceChildren(k("",!0));return}const t=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:b})});try{let s=L,n=await t(s);if(n.status===401){const o=S.get("auth_refresh_token");if(o){const c=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:o})});if(c.ok){const{token:f}=await c.json();S.set("auth_token",f),s=f,n=await t(s)}}}if(!n.ok){const{error:o}=await n.json().catch(()=>({}));I.replaceChildren(k(o||"Unable to load entries.",!1));return}const{html:e}=await n.json();x(I,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{I.replaceChildren(k("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),P(document.body)},100)})(),(function(){const r=document.querySelectorAll("[data-collection-table]");!r.length||typeof T>"u"||!T.create||r.forEach(L=>{let k;try{k=W(L.dataset.payload)}catch{return}const{columns:x,rows:I,search:b,sortable:t,exportable:s,pageSize:n,empty:e,ctaConfig:o,slug:c,exportCfg:f}=k;if(!x?.length)return;if(o){const l=x.findIndex(y=>y.key==="_cta");l!==-1&&(x[l]={key:"_cta",title:"",render:(y,C)=>{const m=document.createElement("button");if(m.className=`btn btn-${o.style||"primary"} dm-cta-trigger`,m.dataset.action=o.action||"",o.scope&&(m.dataset.scope=o.scope),m.dataset.entry=C._entryId||"",o.confirm&&(m.dataset.confirm=o.confirm),o.icon){const w=document.createElement("span");w.dataset.icon=o.icon,m.appendChild(w),m.appendChild(document.createTextNode(" "))}return m.appendChild(document.createTextNode(o.label||"Run")),m}})}const a="col-table-"+Math.random().toString(36).slice(2,7),h=document.createElement("div");h.id=a,L.replaceChildren(h),T.create("#"+a,{data:I,columns:x,search:b,sortable:t,exportable:s,pageSize:n,emptyMessage:e}),o&&P(h),L._dmTable={columns:x,rows:I,slug:c,exportCfg:f,render(l){T.create("#"+a,{data:l,columns:x,search:b,sortable:t,exportable:s,pageSize:n,emptyMessage:e}),o&&P(h)}}})})(),(function(){const r=document.querySelectorAll("[data-form-inline]");if(!r.length||typeof F>"u")return;function L(t){const s=t&&t.logic;return!!(s&&(s.visibility||s.requirement))}function k(t,s){const n={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const o=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),c=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":o?"chooser":e.type,f={...e.formConfig||{}};f.span==="full"&&s&&(f.span=s);const a={type:c,required:L(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...f}};e.minLength!==void 0&&(a.minLength=e.minLength),e.maxLength!==void 0&&(a.maxLength=e.maxLength),e.min!==void 0&&(a.min=e.min),e.max!==void 0&&(a.max=e.max),o&&(e.variant&&(a.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(a.multiple=!0),e.density&&(a.density=e.density),e.columns&&(a.columns=e.columns),e.accent&&(a.accent=e.accent),e.accentStyle&&(a.accentStyle=e.accentStyle),e.glow&&(a.glow=!0),e.glowColour&&(a.glowColour=e.glowColour),e.shadow&&(a.shadow=e.shadow),e.shadowColour&&(a.shadowColour=e.shadowColour)),n[e.name]=e.type==="file"?be(e,a):a}),n}function x(t){const s={};return(t||[]).forEach(n=>{if(!(!n.name||n.type==="page-break"||n.type==="spacer")&&(n.type==="select"||n.type==="multiselect")&&n.required){const e=(n.options||[])[0];e&&(s[n.name]=typeof e=="object"?e.value:e)}}),s}function I(t,s){(s||[]).forEach(n=>{if(n.type!=="date"||!n.name)return;const e=t.querySelector(`[name="${n.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function b(t,s,n){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=s,e.style.cssText=n?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}r.forEach(t=>{let s;try{s=W(t.dataset.formInline)}catch{return}t.querySelector(":scope > .cms-form-embed-summary")?.remove();const n=s.fields||[],e=s.settings||{},o=Number(e.columns)===3?3:2,c=e.layout||"grid",f=n.some(w=>w.type==="page-break"),a=new Map;Ce(t,s.fileLimits,a),Le(t,n);const h=async w=>{if(t.dataset.triggerBlocked==="true"){const d=t._formLogicRuntime,p=d&&d.submitBlock?d.submitBlock().reasons:[];b(t,p.join(" ")||"This form cannot be submitted yet.",!0);const v=new Error("Blocked by a form trigger");throw v.formSubmitFailed=!0,v}try{const d=t.querySelector(".fb-form-honeypot"),p=d?.querySelector('input[type="text"]')||null,v=d?.querySelector('[name="_t"]')||null,A=Object.assign({},w);delete A._t,d?.dataset.hpName&&delete A[d.dataset.hpName],p!==null&&(A._hp=p.value),v!==null&&(A._t=v.value);const i=new Map(a);te(t,i);const M=Se(A,n,i),q=ke(t,M.files);if(q){b(t,q,!0);const D=new Error(q);throw D.formSubmitFailed=!0,D}const g=await H.post(`/api/forms/submit/${s.slug}`,M.files.size?xe(M.values,M.files).body:M.values);if(g&&g.error){b(t,g.error,!0);const D=new Error(g.error);throw D.formSubmitFailed=!0,D}if(g?.redirect){window.location.href=g.redirect;return}for(ve(t);t.firstChild;)t.removeChild(t.firstChild);b(t,g?.message||e.successMessage||"Thank you for your submission.",!1)}catch(d){throw d.formSubmitFailed||b(t,d.message||"Submission failed. Please try again.",!0),d}},l=Date.now();function y(w){const d=w.querySelector("form");if(!d||d.querySelector(".fb-form-honeypot"))return;const p=document.createElement("div");p.className="fb-form-honeypot",p.setAttribute("aria-hidden","true");const v=document.createElement("input");v.name=d.querySelector('[name="website"]')?"_website":"website",p.dataset.hpName=v.name,v.type="text",v.tabIndex=-1,v.autocomplete="url",v.placeholder="https://",p.appendChild(v);const A=document.createElement("input");A.name="_t",A.type="hidden",A.value=l,p.appendChild(A),d.appendChild(p)}function C(w){if(!window.FormLogicEngine||!n.some(p=>p.logic||Array.isArray(p.triggers)&&p.triggers.length))return null;const d=new window.FormLogicEngine.FormLogicRuntime(s,t,w?{baseValues:w}:void 0);if(d.init(),t._formLogicRuntime=d,t._formLogicObserved)return d;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const p=new MutationObserver(function(v){for(const A of v)for(const i of A.removedNodes)if(i===t||i.nodeType===1&&i.contains&&i.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),p.disconnect();return}});p.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=p}return d}let m=!1;if(t.addEventListener("dm-form-end",w=>{const d=w.detail?.validation;if(d&&!d.valid){Q(t,d.missing),Z(d);return}m||(m=!0,h(w.detail?.values||{}).catch(()=>{}).finally(()=>{m=!1}))}),f&&F.wizard){const w=fe(n,s.title),d=w.map(g=>({title:g.title,description:g.description,fields:k(g.fields,o)})),p=w.map(g=>g.fields);let v=0,A=null,i=null;const M=g=>{const D=window.FormLogicEngine;if(!D||!D.validateFields)return;const N=D.validateFields(t,p[v]||[]);N.valid||(g.preventDefault(),g.stopImmediatePropagation(),Q(t,N.missing),Z(N))},q=()=>{const g=p[v]||n;ye(t,e),ee(t,e,g),I(t,g),K(t,g),we(t,a),e.honeypot!==!1&&y(t),A&&A.destroy(),A=C(i?.getStepData?i.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(D=>D.addEventListener("click",M,!0))};i=F.wizard(t,{schema:{steps:d},layout:c,columns:o,finishText:e.submitText||"Finish",onSubmit:h,onStepChange:g=>{te(t,a),v=g,setTimeout(q,0)}}),t.addEventListener("dm-form-trigger",g=>{if(g.detail?.type!=="goto-step"||!i?.goToStep)return;const D=Number(g.detail.action.step);!Number.isFinite(D)||D<0||D>=d.length||D!==v&&i.goToStep(D)}),Promise.resolve(i).then(q)}else if(F.render){const w=F.render(t,k(n,o),x(n),{submitText:e.submitText||"Submit",layout:c,columns:o,onSubmit:h});Promise.resolve(w).then(function(){ge(t,e,c),ee(t,e,n),I(t,n),K(t,n),e.honeypot!==!1&&y(t),C()})}})})();function $e(u,r){const L=document.body,k=L.classList,x=new Set((L.dataset.railSite||"").split(" ").filter(Boolean)),I=()=>window.matchMedia("(min-width: 993px)").matches;u.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(o=>{const c=o.textContent.trim();c&&(o.setAttribute("title",c),o.querySelector("svg, [data-icon]")||(o.dataset.initial=c.charAt(0).toUpperCase()))});const b=document.createElement("button");b.type="button",b.className="dm-nav-collapse-toggle";const t=()=>{const o=k.contains("dm-vnav-collapsed");b.setAttribute("aria-pressed",String(o)),b.setAttribute("aria-label",o?"Expand menu":"Collapse menu"),b.title=b.getAttribute("aria-label");const c=r?"chevrons-left":"chevrons-right",f=r?"chevrons-right":"chevrons-left";b.innerHTML=`<span data-icon="${o?c:f}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(b)},s=o=>{k.toggle("dm-vnav-collapsed",o),S.set("nav_collapsed",o),t()};b.addEventListener("click",()=>s(!k.contains("dm-vnav-collapsed"))),t();const n=u.querySelector(".navbar-container")||u;n.insertBefore(b,n.firstChild?.nextSibling||null);const e=o=>{k.toggle("dm-vnav-autohide",o),k.toggle("dm-vnav-collapsible",!o&&x.has("collapsible")),o?k.remove("dm-vnav-collapsed"):x.has("collapsible")&&S.get("nav_collapsed")===!0&&k.add("dm-vnav-collapsed"),u.classList.remove("dm-nav-revealed"),t()};u.addEventListener("pointerdown",o=>{k.contains("dm-vnav-autohide")&&o.pointerType!=="mouse"&&!u.classList.contains("dm-nav-revealed")&&(u.classList.add("dm-nav-revealed"),o.preventDefault())}),document.addEventListener("pointerdown",o=>{u.contains(o.target)||u.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const o=k.contains("dm-vnav-autohide"),c=k.contains("dm-vnav-collapsed"),f=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:c?"Expand menu":"Collapse to icons",icon:c?"maximize":"minimize",disabled:!I()||o||!x.has("collapsible"),action:()=>s(!c)},{label:o?"Keep on screen":"Hide when not in use",icon:o?"eye":"eye-off",disabled:!I(),action:()=>{S.set("nav_autohide",!o),e(!o)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!f,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(x.has("autohide")),k.toggle("dm-vnav-collapsed",!x.has("autohide")&&x.has("start-collapsed")),t()}}]}})}function Ee(u){u.classList.add("dm-nav-scroll-hide");let r=window.scrollY,L=!1;window.addEventListener("scroll",()=>{L||(L=!0,requestAnimationFrame(()=>{L=!1;const k=window.scrollY,x=u.matches(":focus-within")||u.querySelector(".navbar-dropdown.open, .navbar-collapse.show");k>r&&k>u.offsetHeight&&!x?u.classList.add("dm-nav-scrolled-away"):k<r&&u.classList.remove("dm-nav-scrolled-away"),r=k}))},{passive:!0})}function Ae(u){let r={};try{r=JSON.parse(u.dataset.contents||"{}")}catch{}const L=u.closest(".site-main"),k=document.querySelector(".site-content"),x=new Set(String(r.exclude||"").split(",").map(a=>a.trim().toLowerCase()).filter(Boolean)),I=a=>!a.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),b=a=>(k?[...k.querySelectorAll(a)]:[]).filter(I).filter(h=>!x.has(h.textContent.trim().toLowerCase())),t=r.headings==="h2"?b("h2"):r.headings==="h3"?b("h3"):r.headings==="h2h3"?b("h2, h3"):b("h2").length?b("h2"):b("h3");if(!t.length){L?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(a=>a.id)),n=document.createElement("nav");n.className="dm-contents",n.setAttribute("aria-label",r.title||"Page contents"),r.hover&&(n.dataset.menuHover=r.hover),r.selected&&(n.dataset.menuActive=r.selected),r.accent&&n.style.setProperty("--dm-menu-highlight",r.accent.startsWith("#")?r.accent:`var(--dm-${r.accent})`),r.font&&(n.style.fontFamily=r.font),r.fontSize&&(n.style.fontSize=r.fontSize),r.fontWeight&&(n.style.fontWeight=r.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const o=document.createElement("p");o.className="dm-contents-title",o.textContent=r.title||"",e.appendChild(o),n.appendChild(e);const c=document.createElement("ol");c.className="dm-contents-list"+(r.numbered?" is-numbered":"");const f=[];for(const a of t){if(!a.id){const y=(a.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let C=y;for(let m=2;s.has(C);m++)C=`${y}-${m}`;s.add(C),a.id=C}const h=document.createElement("li");h.className=`dm-contents-item dm-contents-${a.tagName.toLowerCase()}`;const l=document.createElement("a");l.href=`#${a.id}`,l.textContent=a.textContent.trim(),h.appendChild(l),c.appendChild(h),f.push([a,l])}if(n.appendChild(c),u.replaceChildren(n),r.scrollSpy!==!1&&"IntersectionObserver"in window){const a=new Map,h=()=>{const y=f.find(([C])=>a.get(C))||null;f.forEach(([,C])=>{const m=y&&C===y[1];C.classList.toggle("is-active",!!m),m?C.setAttribute("aria-current","location"):C.removeAttribute("aria-current")})},l=new IntersectionObserver(y=>{y.forEach(C=>a.set(C.target,C.isIntersecting)),h()},{rootMargin:"-80px 0px -60% 0px"});f.forEach(([y])=>l.observe(y))}if(L?.classList.contains("contents-collapsible")){const a=document.createElement("button");a.type="button",a.className="dm-contents-toggle";const h=()=>{const l=L.classList.contains("contents-collapsed");a.setAttribute("aria-expanded",String(!l)),a.setAttribute("aria-label",l?"Show the contents":"Hide the contents"),a.title=a.getAttribute("aria-label"),a.innerHTML=`<span data-icon="${l?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(a)};a.addEventListener("click",()=>{S.set("contents_collapsed",L.classList.toggle("contents-collapsed")),h()}),h(),e.insertBefore(a,e.firstChild)}if(L?.classList.contains("contents-floating")&&r.movable){n.classList.add("is-movable");const a=S.get("contents_position");a&&Number.isFinite(a.x)&&Object.assign(u.style,{left:`${a.x}px`,top:`${a.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",h=>{if(h.target.closest("button"))return;const l=u.getBoundingClientRect(),y=h.clientX-l.left,C=h.clientY-l.top,m=d=>{const p=Math.max(0,Math.min(window.innerWidth-l.width,d.clientX-y)),v=Math.max(0,Math.min(window.innerHeight-40,d.clientY-C));Object.assign(u.style,{left:`${p}px`,top:`${v}px`,right:"auto",bottom:"auto"})},w=()=>{document.removeEventListener("pointermove",m),S.set("contents_position",{x:parseInt(u.style.left,10),y:parseInt(u.style.top,10)})};document.addEventListener("pointermove",m),document.addEventListener("pointerup",w,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(u,{items:()=>{const a=L?.classList.contains("contents-collapsible"),h=L?.classList.contains("contents-collapsed"),l=f.find(([,y])=>y.classList.contains("is-active"));return[{header:"Contents"},...a?[{label:h?"Show the contents":"Collapse to a button",icon:h?"list":"minimize",action:()=>u.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!l,action:async()=>{const y=`${location.origin}${location.pathname}#${l[0].id}`;try{await _.copyToClipboard(y),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),L?.classList.contains("contents-autohide")&&(u.addEventListener("pointerdown",a=>{a.pointerType!=="mouse"&&!u.classList.contains("is-revealed")&&(u.classList.add("is-revealed"),a.preventDefault())}),document.addEventListener("pointerdown",a=>{u.contains(a.target)||u.classList.remove("is-revealed")}))}
|
|
1
|
+
import{applyMobileMenu as ne,initCarouselPopup as ae}from"/public/js/mobile-ui.mjs?v=20261016-mobile-options";import{installSmoothCarets as se}from"/public/js/smooth-caret.js";import{decodeBase64Json as V}from"/public/js/utf8-base64.mjs";import{makeBadgeEl as U,applyColour as G,applyPill as ie,applyFloat as re,applyHighlight as Y,applyItemHighlight as ce,itemHighlightAttrs as le,isCurrentItem as K,menuAlign as de,applyClasses as P,cssClasses as X,applyEntrance as J}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{applyMenuLook as me}from"/public/js/menu-cards.mjs?v=20261029-glider-layout";import{initPageMotion as ue}from"/public/js/page-motion.mjs?v=20261029-page-motion";import{applyFooterShowcase as pe}from"/public/js/footer-showcase.mjs?v=20261029-footer-showcase";import{initFloatingMenus as fe}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as Q,markInvalidFields as Z,showValidationPopup as ee}from"/public/js/forms.js?v=20261025-forms-files-total";import{buildWizardSteps as he}from"/public/js/form-steps.mjs";import{mountMotionControl as ge}from"/public/js/motion-control.mjs?v=20261029-motion-under-switcher";import{applyFormClasses as te,applySubmitButtons as ye,applyWizardButtons as ve}from"/public/js/form-submit.mjs";import{captureFileFields as oe,destroyDropzones as be,fileBlueprint as we,restoreFileFields as Ce,setFileLimits as Se,splitFiles as xe,submitBody as ke,totalTooLarge as Le,wireFileFields as $e}from"/public/js/form-files.mjs";document.addEventListener("click",m=>{const i=m.target?.closest?.(".dm-banner__dismiss");i&&(m.preventDefault(),i.closest(".dm-banner")?.remove())}),$(()=>{se(document);const m=window.__CMS_NAV__||{},i=window.__CMS_SITE__||{},M=i.brandingEnabled!==!1&&m.brand?.enabled!==!1;if(i.autoTheme?.enabled){let o=function(c){const d=(c||"07:00").split(":");return+d[0]*60+(+d[1]||0)},e=function(){const c=new Date,d=c.getHours()*60+c.getMinutes();return d>=o(s.dayStart)&&d<o(s.nightStart)?s.dayTheme:s.nightTheme},t=function(c){const d=s.apply&&s.apply[c]||{theme:c,custom:""};Domma.theme.set(d.theme);for(const n of[...document.body.classList])n.startsWith("dm-custom-")&&document.body.classList.remove(n);d.custom&&document.body.classList.add(d.custom)};const s=i.autoTheme;t(e()),setInterval(()=>t(e()),6e4)}const L=$("#site-navbar");if(L.length&&m.brand){let e=function(u,f=0){return Array.isArray(u)?u.filter(h=>!(h&&(h.type==="spacer"||h.type==="theme-switcher"))).filter(h=>!(h&&h.type==="separator"&&f===0)).map(h=>h&&h.type==="separator"?{divider:!0}:Array.isArray(h?.items)?{...h,items:e(h.items,f+1)}:h):u};const s={...m.brand},o=s.size&&s.size!=="md"?` navbar-brand-${s.size}`:"";if(s.logo||s.icon||s.tagline){let u="";s.logo?u+=`<img src="${s.logo}" class="navbar-brand-logo" alt="${s.text||""}">`:s.icon&&(u+=`<span data-icon="${s.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),s.text&&(u+=`<span class="navbar-brand-text${o}">${s.text}</span>`),s.tagline&&(u+=`<small class="navbar-brand-tagline">${s.tagline}</small>`),s.html=u}else o&&s.text&&(s.html=`<span class="navbar-brand-text${o}">${s.text}</span>`);const t=e(m.items||[]),c=m.position==="floating",d=m.orientation==="vertical";L.get(0).querySelector(":scope > .cms-nojs-links")?.remove(),Domma.elements.navbar("#site-navbar",{brand:M?s:null,items:t,variant:m.variant||"dark",position:c?"static":m.position||"sticky",collapsible:!0,appearOnHover:!!m.appearOnHover}),Domma.icons.scan("#site-navbar");const n=L.get(0);ne(n,m.mobile);const y=()=>{if(!d)return;const f=n.querySelector('a[aria-current="page"]')?.closest(".navbar-dropdown");f&&!f.classList.contains("open")&&f.querySelector(":scope > .navbar-dropdown-toggle")?.click()};n.classList.add(`dm-nav-align-${de(m)||"centre"}`),d&&L.get(0).classList.add("dm-nav-vertical",m.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left");const g=m.dock||{};d&&!c&&Ee(n,m.side==="right"),!d&&g.hideOnScroll&&Ae(n),Y(n,m),J(n,m),P(n,m.class),c&&(re(L.get(0),m),fe(L.get(0).parentNode||document));const l=n.querySelector(".navbar-toggle"),b=n.querySelector(".navbar-collapse");if(l&&b){let u=null;const f=()=>{b.classList.contains("show")&&l.click()},h=()=>{u&&(u.remove(),u=null),document.body.classList.remove("dm-drawer-open")},p=()=>{u||(u=document.createElement("div"),u.className="dm-navbar-backdrop",u.addEventListener("click",f),document.body.appendChild(u),document.body.classList.add("dm-drawer-open"))};l.addEventListener("click",()=>{requestAnimationFrame(()=>{b.classList.contains("show")?(y(),p()):h()})}),b.addEventListener("click",k=>{k.target.closest("a")&&requestAnimationFrame(h)}),document.addEventListener("keydown",k=>{k.key==="Escape"&&b.classList.contains("show")&&f()})}const w=n.querySelector(".navbar-nav");if(w){const u=Array.from(w.children);let f=0;for(const h of m.items||[])if(!(h&&h.type==="separator")){if(h&&h.type==="spacer"){const p=document.createElement("li"),k=["xs","sm","md","lg","xl","flex"].includes(h.size)?h.size:"md";p.className=`dm-menu-spacer dm-menu-spacer--${k}`,p.setAttribute("aria-hidden","true"),w.insertBefore(p,u[f]||null);continue}if(h&&h.type==="theme-switcher"){const p=document.createElement("li");p.className="dm-menu-theme-switcher";const k=document.createElement("span");k.dataset.themeSwitcher="menu",k.dataset.position="inline",p.appendChild(k),w.insertBefore(p,u[f]||null);continue}f++}window.CMSThemeSwitcher?.scan(w)}n.querySelectorAll("[data-path]").forEach(u=>{const f=u.dataset.path.split(".").map(Number),h=f.length>1,p=f.reduce((q,r)=>q?.items?.[r],{items:t});if(!p)return;p.colour&&G(u,p.colour),p.pill&&!h&&ie(u,p.pill),ce(u,p),P(u.closest("li"),p.class);const k=U(p.badge);k&&u.appendChild(k),K(p.url,location.pathname)&&(u.classList.add("active"),u.setAttribute("aria-current","page"))}),me(n,m,t),y()}let I=null;const C=$("#site-footer");if(C.length){let c=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},d=function(r){return c(r).replace(/'/g,"'")},n=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;if(r&&r.type==="theme-switcher")return'<li class="dm-menu-theme-switcher"><span data-theme-switcher="menu" data-position="inline"></span></li>';const D=d(r.url||"#"),v=c(r.text||""),x=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(n).join("")}</ul>`:"",N=le(r),O=[N.hover&&` data-menu-item-hover="${d(N.hover)}"`,N.active&&` data-menu-item-active="${d(N.active)}"`].filter(Boolean).join(""),j=N.accent?` style="--dm-menu-highlight:${d(N.accent)}"`:"",W=X(r.class).join(" ");return`<li${W?` class="${d(W)}"`:""}><a href="${D}"${O}${j}>${v}</a>${x}</li>`},g=function(){if(!i.footer)return"";const r=Object.keys(o).filter(v=>s[v]);if(!r.length)return"";const D=X(i.footer.socialClass).join(" ");return`<div class="footer-social">${r.map(v=>{const{label:x,svg:N}=o[v];return`<a href="${s[v]}" target="_blank" rel="noopener noreferrer" aria-label="${x}" class="footer-social-link${D?` ${d(D)}`:""}">${N}</a>`}).join("")}</div>`},u=function(r){if(!r||r.type==="separator"||r.type==="spacer")return"";if(r.type==="theme-switcher")return`<li class="cms-footer-col">${n(r).replace(/^<li[^>]*>|<\/li>$/g,"")}</li>`;const D=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(n).join("")}</ul>`:"",v=w(r)?`<span class="cms-footer-col-title">${c(r.text||"")}</span>`:`<a class="cms-footer-col-title" href="${d(r.url||"#")}">${c(r.text||"")}</a>`,x=["cms-footer-col",...X(r.class)].join(" ");return`<li class="${d(x)}">${v}${D}</li>`},p=function(r,D,v=!1){return(r||[]).forEach(x=>{x&&(x.type==="separator"||x.type==="spacer"||x.type==="theme-switcher")||(v&&w(x)||D.push(x),Array.isArray(x.items)&&x.items.length&&p(x.items,D))}),D};const s=i.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let e='<div class="footer-inner container">';const t=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""},y=(()=>{const r=i.cookieConsent;if(!r||r.showSettingsLink===!1)return"";const D=v=>(v||[]).some(x=>x&&(x.url==="#cookie-settings"||D(x.items)));return D(t.primary)||D(t.legal)?"":`<li class="cms-cookie-settings"><a href="#cookie-settings">${c(r.settingsLinkText||"Cookie settings")}</a></li>`})(),l=["columns","row"].includes(i.footer?.layout)?i.footer.layout:"auto",b=i.footer?.look==="showcase"||l==="columns"||l==="auto"&&t.primary.some(r=>Array.isArray(r?.items)&&r.items.length),w=r=>b&&!r.url&&Array.isArray(r.items)&&r.items.length>0;if(b){const r=m.brand||{},D=M&&(r.text||i.title)||"",v=M?r.logo?`<img src="${d(r.logo)}" class="cms-footer-brand-logo" alt="">`:r.icon?'<span class="cms-footer-brand-icon"></span>':"":"",x=String(i.footer?.blurb||"").trim();e='<div class="footer-inner footer-inner--columns container"><div class="cms-footer-top">',e+=`<div class="cms-footer-brand">${D||v?`<a class="cms-footer-brand-name" href="${d(r.url||"/")}">${v}<span>${c(D)}</span></a>`:""}${x?`<p class="cms-footer-blurb">${c(x)}</p>`:""}${g()}</div>`,t.primary.length&&(e+=`<nav class="footer-nav footer-nav--primary footer-nav--columns" aria-label="Footer"><ul>${t.primary.map(u).join("")}</ul></nav>`),e+='</div><div class="cms-footer-bottom">',t.copyright&&(e+=`<p class="cms-footer-copyright">${c(t.copyright)}</p>`),(t.legal.length||y)&&(e+=`<nav class="footer-nav footer-nav--legal" aria-label="Legal"><ul>${t.legal.map(n).join("")}${y}</ul></nav>`),e+="</div>"}else t.copyright&&(e+=`<p>${c(t.copyright)}</p>`),t.primary.length&&(e+=`<nav class="footer-nav footer-nav--primary"><ul>${t.primary.map(n).join("")}</ul></nav>`),(t.legal.length||y)&&(e+=`<nav class="footer-nav footer-nav--legal"><ul>${t.legal.map(n).join("")}${y}</ul></nav>`),e+=g();e+="</div>",C.get(0).querySelector(":scope > .cms-nojs-links")?.remove();const f=C.get(0).querySelector(":scope > .cms-footer-blocks");f?.remove(),C.html(e),f&&C.get(0).prepend(f),window.CMSThemeSwitcher?.scan(C.get(0));const h=C.get(0).querySelector(".cms-footer-brand-icon");h&&m.brand?.icon&&h.setAttribute("data-icon",m.brand.icon),P(C.get(0),i.footer?.class),b&&pe(C.get(0),i);const k=[...p(t.primary,[],!0),...p(t.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((r,D)=>{const v=k[D];if(!v)return;v.colour&&G(r,v.colour);const x=U(v.badge);x&&r.appendChild(x),K(v.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),Y(C.get(0).querySelector(".footer-nav--primary"),{highlight:t.primaryHighlight}),Y(C.get(0).querySelector(".footer-nav--legal"),{highlight:t.legalHighlight}),J(C.get(0).querySelector(".footer-nav--primary"),{entrance:t.primaryEntrance}),J(C.get(0).querySelector(".footer-nav--legal"),{entrance:t.legalEntrance}),P(C.get(0).querySelector(".footer-nav--primary"),t.primaryClass),P(C.get(0).querySelector(".footer-nav--legal"),t.legalClass),I=C.get(0).querySelector(".cms-footer-bottom")||C.get(0).querySelector(".footer-inner")}ge(i.motion||{enabled:i.footer?.showMotionToggle!==!1,default:i.footer?.motionDefault,placement:"footer",control:i.footer?.motionControl,label:"Reduce motion"},{footer:I,navbar:document.querySelector("#site-navbar .navbar-container")?.closest("#site-navbar")||null});const A=document.getElementById("site-sidebar");A&&Te(A),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const a=$(".page-body");if(a.length){a.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),a.find(".tabs").each(function(){Domma.elements.tabs(this)}),a.find(".carousel").each(function(){const o={primary:"--dm-primary",secondary:"--dm-secondary",accent:"--dm-primary",text:"--dm-text",surface:"--dm-surface",background:"--dm-background",success:"--dm-success",warning:"--dm-warning",danger:"--dm-danger",info:"--dm-info"},e=g=>{const l=String(g||"").trim();return l?o[l.toLowerCase()]?`var(${o[l.toLowerCase()]})`:CSS.supports("color",l)?l:"":""},t=e(this.dataset.background),c=e(this.dataset.arrowBackground),d=e(this.dataset.arrowColor);t&&(this.style.setProperty("--dm-carousel-background",t),this.classList.add("dm-carousel-custom-background")),c&&this.style.setProperty("--dm-carousel-arrow-background",c),d&&this.style.setProperty("--dm-carousel-arrow-color",d);const n=["top","middle","bottom"].includes(this.dataset.arrowPosition)?this.dataset.arrowPosition:"middle";this.classList.add(`dm-carousel-arrows-${n}`);const y={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide",heightMode:this.dataset.heightMode==="average"?"average":"crop"};if(this.dataset.height){const g=Number(this.dataset.height);Number.isFinite(g)&&g>0&&(y.height=g)}if(this.dataset.animationDuration){const g=parseInt(this.dataset.animationDuration,10);Number.isFinite(g)&&(y.animationDuration=g)}this.dataset.animationEasing&&(y.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(g=>g.cloneNode(!0)),this._dmCarouselOpts=y),this._dmCarousel=Domma.elements.carousel(this,y),ae(this)}),a.find(".dm-countdown").each(function(){const o={autoStart:!0},e=String(this.dataset.format||"").trim(),t=String(this.dataset.to||"").trim(),c=/^(\d{4})-(\d{2})-(\d{2})$/.exec(t),d=c?new Date(+c[1],+c[2]-1,+c[3]).getTime():t?new Date(t).getTime():NaN;Number.isFinite(d)?o.duration=Math.max(0,d-Date.now()):this.dataset.duration&&(o.duration=(parseInt(this.dataset.duration,10)||0)*1e3);const n=/^dd:/i.test(e);o.format=n?"hh:mm:ss":e||(o.duration>=36e5?"hh:mm:ss":"mm:ss");const y=this,g=b=>String(b).padStart(2,"0"),l=b=>{const w=Math.max(0,Math.floor(b/1e3)),u=y.querySelector(".dm-timer-time");u&&(u.textContent=`${g(Math.floor(w/86400))}:${g(Math.floor(w%86400/3600))}:${g(Math.floor(w%3600/60))}:${g(w%60)}`)};n&&(o.onTick=l),Domma.elements.timer(this,o),n&&l(o.duration||0)}),a.find("[data-tooltip]:not(.dm-range-slider)").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),a.find(".dm-progression").each(function(){const o=Array.from(this.querySelectorAll(".dm-progression-item")).map(c=>{const d={title:c.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:c.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return c.dataset.date&&(d.date=c.dataset.date),c.dataset.status&&(d.status=c.dataset.status),c.dataset.eventIcon&&(d.icon=c.dataset.eventIcon),c.dataset.entryId&&(d._entryId=c.dataset.entryId),d}),e=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",t=Domma.elements.progression(this,{layout:e,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...o.length?{items:o}:{}});if((this.dataset.mode||"timeline")==="timeline"){const c={completed:{label:"Completed",icon:"check-circle",tone:"success"},planned:{label:"Planned",icon:"circle",tone:"secondary"},"in-progress":{label:"In progress",icon:"clock",tone:"primary"},blocked:{label:"Blocked",icon:"alert-circle",tone:"danger"}};this.querySelectorAll(".dm-progression-item").forEach((d,n)=>{const y=o[n];if(!y)return;const g=d.querySelector(".dm-progression-title");if(y.icon&&g){const b=document.createElement("span");b.dataset.icon=y.icon,b.style.cssText="display:inline-flex;vertical-align:middle;margin-right:.4em",b.setAttribute("aria-hidden","true"),g.prepend(b)}const l=c[y.status];if(l&&g){const b=document.createElement("span");b.className=`badge badge-${l.tone} cms-timeline-status`,b.style.cssText="display:inline-flex;align-items:center;gap:.35em;margin:.4rem 0";const w=document.createElement("span");w.dataset.icon=l.icon,w.dataset.iconSize="14",w.setAttribute("aria-hidden","true"),b.append(w,document.createTextNode(l.label)),g.after(b)}})}Domma.icons.scan(this),o.length&&(this._dmCtx={instance:t,items:o})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}const s=matchMedia("(prefers-reduced-motion: reduce)").matches||document.documentElement.classList.contains("dm-reduced-motion");document.querySelectorAll(".page-body .row[data-reveal]").forEach(o=>{const e=o.dataset.revealMode||"stagger",t=o.dataset.revealAnimation||"slide-up",c=parseInt(o.dataset.revealDuration,10)||(t.startsWith("swoosh")?700:400),d=parseInt(o.dataset.revealStagger,10)||(t.startsWith("swoosh")?90:60),n=parseInt(o.dataset.revealDelay,10)||0,y=o.dataset.revealDirection==="rtl",g=o.dataset.revealReplay==="true",l=Array.from(o.children);if(s&&t.startsWith("swoosh"))return;const b={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)",swoosh:"translateY(36px) scale(0.96)"},w=p=>t!=="swoosh-sides"?b[t]||"":l.length>1&&p===0?"translateX(-64px) rotate(-1.5deg)":l.length>1&&p===l.length-1?"translateX(64px) rotate(1.5deg)":"translateY(40px) scale(0.94)",u=t.startsWith("swoosh"),f=u?"cubic-bezier(.2,.8,.2,1)":"ease",h=(p,k)=>{p.style.opacity="0",p.style.transform=w(k),u&&(p.style.filter="blur(10px)")};l.forEach((p,k)=>{h(p,k),p.style.transition=`opacity ${c}ms ${f}, transform ${c}ms ${f}`+(u?`, filter ${c}ms ${f}`:"");const q=y?l.length-1-k:k;p.style.transitionDelay=e==="stagger"?`${n+q*d}ms`:`${n}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const p=new IntersectionObserver(k=>{k.forEach(q=>{if(q.isIntersecting){if(q.target.offsetWidth,q.target.style.opacity="1",q.target.style.transform="",u&&(q.target.style.filter=""),!g){p.unobserve(q.target);const r=q.target;r.addEventListener("transitionend",function D(v){v.target!==r||v.propertyName!=="opacity"||(r.removeEventListener("transitionend",D),r.style.transition="",r.style.transitionDelay="")})}}else g&&q.boundingClientRect.top>0&&h(q.target,l.indexOf(q.target))})},{threshold:.1});l.forEach(k=>p.observe(k))}))}),ue(document.querySelector(".page-body")||document),a.find(".card[data-collapsible]").each(function(){const o=this.querySelector(".card-header");o&&o.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),a.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const o=this.dataset.soTarget,e=document.getElementById(o);if(!e)return;const t=E.slideover({title:e.dataset.soTitle||"",size:e.dataset.soSize||"md",position:e.dataset.soPosition||"right"});e.style.display="",t.setContent(e),t.open()})})}if(typeof $.setup=="function"){const s=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const e=V(o.dataset.config);Object.assign(s,e)}catch{}}),Object.keys(s).length>0){const o={};for(const[e,t]of Object.entries(s)){const c=t?.events?.click,{confirm:d,toast:n,alert:y,prompt:g,...l}=c||{};if(d||n||y||g){$(e).on("click",async function(W){if(W.preventDefault(),d&&!await E.confirm(d))return;let z=null;if(!(g&&(z=await E.prompt(g,{inputPlaceholder:l.promptPlaceholder||"",inputValue:l.promptDefault||""}),z===null))){{const B=$(l.target||this);l.toggleClass&&B.toggleClass(l.toggleClass),l.addClass&&B.addClass(l.addClass),l.removeClass&&B.removeClass(l.removeClass),z!==null&&(l.setText&&B.text(z),l.setVal&&B.val(z),l.setAttr&&B.attr(l.setAttr,z))}l.href&&(window.location.href=l.href),n&&E.toast(n,{type:l.toastType||"success"}),y&&E.alert(y)}});const{target:w,toggleClass:u,addClass:f,removeClass:h,setText:p,setVal:k,setAttr:q,href:r,toastType:D,promptPlaceholder:v,promptDefault:x,...N}=l,O={...t.events};Object.keys(N).length?O.click=w?{target:w,...N}:N:delete O.click;const j={...t,events:O};Object.keys(O).length||delete j.events,Object.keys(j).length&&(o[e]=j)}else o[e]=t}$.setup(o)}}a.length&&R(a.get(0))}),window.wireCTAButtons=R;function R(m){m.querySelectorAll(".dm-cta-trigger").forEach(i=>{i.__ctaWired||(i.__ctaWired=!0,i.addEventListener("click",async()=>{const M=i.dataset.action,L=i.dataset.entry,I=i.dataset.confirm;let C=S.get("auth_token");if(!C){E.toast("Please log in to perform this action.",{type:"warning"});return}if(I&&!await E.confirm(I))return;const A=Array.from(i.childNodes).map(s=>s.cloneNode(!0));i.disabled=!0,i.textContent="Running\u2026";const a=s=>fetch(`/api/actions/${M}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify(i.dataset.scope?{entryId:L,scope:i.dataset.scope}:{entryId:L})});try{let s=await a(C);if(s.status===401){const e=S.get("auth_refresh_token");if(e){const t=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(t.ok){const{token:c}=await t.json();S.set("auth_token",c),C=c,s=await a(C)}}}const o=await s.json().catch(()=>({}));if(!s.ok||o.success===!1)throw new Error(o.error||o.message||`Error ${s.status}`);E.toast(o.message||"Action completed.",{type:"success"})}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{i.disabled=!1,i.textContent="",A.forEach(s=>i.appendChild(s)),Domma.icons.scan(i)}}))})}(function(){const i=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!i.length)return;const M=S.get("auth_token");function L(C,A){const a=document.createElement("div");a.className="dm-collection-display dm-collection-empty";const s=document.createElement("p");if(A){s.appendChild(document.createTextNode("Please "));const o=document.createElement("a");o.href="/login",o.textContent="sign in",s.appendChild(o),s.appendChild(document.createTextNode(" to view your entries."))}else s.textContent=C;return a.appendChild(s),a}function I(C,A){const a=document.createElement("template");a.innerHTML=A||"",C.replaceWith(a.content)}i.forEach(async C=>{const A=C.dataset.collectionAttrs;if(!A)return;if(!M){C.replaceChildren(L("",!0));return}const a=s=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({attrs:A})});try{let s=M,o=await a(s);if(o.status===401){const t=S.get("auth_refresh_token");if(t){const c=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:t})});if(c.ok){const{token:d}=await c.json();S.set("auth_token",d),s=d,o=await a(s)}}}if(!o.ok){const{error:t}=await o.json().catch(()=>({}));C.replaceChildren(L(t||"Unable to load entries.",!1));return}const{html:e}=await o.json();I(C,e),window.DommaCollectionBrowser?.hydrate&&window.DommaCollectionBrowser.hydrate(document)}catch{C.replaceChildren(L("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),R(document.body)},100)})(),(function(){const i=document.querySelectorAll("[data-collection-table]");!i.length||typeof T>"u"||!T.create||i.forEach(M=>{let L;try{L=V(M.dataset.payload)}catch{return}const{columns:I,rows:C,search:A,sortable:a,exportable:s,pageSize:o,empty:e,ctaConfig:t,slug:c,exportCfg:d}=L;if(!I?.length)return;if(t){const g=I.findIndex(l=>l.key==="_cta");g!==-1&&(I[g]={key:"_cta",title:"",render:(l,b)=>{const w=document.createElement("button");if(w.className=`btn btn-${t.style||"primary"} dm-cta-trigger`,w.dataset.action=t.action||"",t.scope&&(w.dataset.scope=t.scope),w.dataset.entry=b._entryId||"",t.confirm&&(w.dataset.confirm=t.confirm),t.icon){const u=document.createElement("span");u.dataset.icon=t.icon,w.appendChild(u),w.appendChild(document.createTextNode(" "))}return w.appendChild(document.createTextNode(t.label||"Run")),w}})}const n="col-table-"+Math.random().toString(36).slice(2,7),y=document.createElement("div");y.id=n,M.replaceChildren(y),T.create("#"+n,{data:C,columns:I,search:A,sortable:a,exportable:s,pageSize:o,emptyMessage:e}),t&&R(y),M._dmTable={columns:I,rows:C,slug:c,exportCfg:d,render(g){T.create("#"+n,{data:g,columns:I,search:A,sortable:a,exportable:s,pageSize:o,emptyMessage:e}),t&&R(y)}}})})(),(function(){const i=document.querySelectorAll("[data-form-inline]");if(!i.length||typeof F>"u")return;function M(a){const s=a&&a.logic;return!!(s&&(s.visibility||s.requirement))}function L(a,s){const o={};return(a||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const t=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),c=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":t?"chooser":e.type,d={...e.formConfig||{}};d.span==="full"&&s&&(d.span=s);const n={type:c,required:M(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...e.tooltip&&{tooltip:e.tooltip},...d}};e.minLength!==void 0&&(n.minLength=e.minLength),e.maxLength!==void 0&&(n.maxLength=e.maxLength),e.min!==void 0&&(n.min=e.min),e.max!==void 0&&(n.max=e.max),t&&(e.variant&&(n.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(n.multiple=!0),e.density&&(n.density=e.density),e.columns&&(n.columns=e.columns),e.accent&&(n.accent=e.accent),e.accentStyle&&(n.accentStyle=e.accentStyle),e.glow&&(n.glow=!0),e.glowColour&&(n.glowColour=e.glowColour),e.shadow&&(n.shadow=e.shadow),e.shadowColour&&(n.shadowColour=e.shadowColour)),o[e.name]=e.type==="file"?we(e,n):n}),o}function I(a){const s={};return(a||[]).forEach(o=>{if(!(!o.name||o.type==="page-break"||o.type==="spacer")&&(o.type==="select"||o.type==="multiselect")&&o.required){const e=(o.options||[])[0];e&&(s[o.name]=typeof e=="object"?e.value:e)}}),s}function C(a,s){(s||[]).forEach(o=>{if(o.type!=="date"||!o.name)return;const e=a.querySelector(`[name="${o.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function A(a,s,o){let e=a.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",a.appendChild(e)),e.textContent=s,e.style.cssText=o?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}i.forEach(a=>{let s;try{s=V(a.dataset.formInline)}catch{return}a.querySelector(":scope > .cms-form-embed-summary")?.remove();const o=s.fields||[],e=s.settings||{},t=Number(e.columns)===3?3:2,c=e.layout||"grid",d=o.some(u=>u.type==="page-break"),n=new Map;Se(a,s.fileLimits,n),$e(a,o);const y=async u=>{if(a.dataset.triggerBlocked==="true"){const f=a._formLogicRuntime,h=f&&f.submitBlock?f.submitBlock().reasons:[];A(a,h.join(" ")||"This form cannot be submitted yet.",!0);const p=new Error("Blocked by a form trigger");throw p.formSubmitFailed=!0,p}try{const f=a.querySelector(".fb-form-honeypot"),h=f?.querySelector('input[type="text"]')||null,p=f?.querySelector('[name="_t"]')||null,k=Object.assign({},u);delete k._t,f?.dataset.hpName&&delete k[f.dataset.hpName],h!==null&&(k._hp=h.value),p!==null&&(k._t=p.value);const q=new Map(n);oe(a,q);const r=xe(k,o,q),D=Le(a,r.files);if(D){A(a,D,!0);const x=new Error(D);throw x.formSubmitFailed=!0,x}const v=await H.post(`/api/forms/submit/${s.slug}`,r.files.size?ke(r.values,r.files).body:r.values);if(v&&v.error){A(a,v.error,!0);const x=new Error(v.error);throw x.formSubmitFailed=!0,x}if(v?.redirect){window.location.href=v.redirect;return}for(be(a);a.firstChild;)a.removeChild(a.firstChild);A(a,v?.message||e.successMessage||"Thank you for your submission.",!1)}catch(f){throw f.formSubmitFailed||A(a,f.message||"Submission failed. Please try again.",!0),f}},g=Date.now();function l(u){const f=u.querySelector("form");if(!f||f.querySelector(".fb-form-honeypot"))return;const h=document.createElement("div");h.className="fb-form-honeypot",h.setAttribute("aria-hidden","true");const p=document.createElement("input");p.name=f.querySelector('[name="website"]')?"_website":"website",h.dataset.hpName=p.name,p.type="text",p.tabIndex=-1,p.autocomplete="url",p.placeholder="https://",h.appendChild(p);const k=document.createElement("input");k.name="_t",k.type="hidden",k.value=g,h.appendChild(k),f.appendChild(h)}function b(u){if(!window.FormLogicEngine||!o.some(h=>h.logic||Array.isArray(h.triggers)&&h.triggers.length))return null;const f=new window.FormLogicEngine.FormLogicRuntime(s,a,u?{baseValues:u}:void 0);if(f.init(),a._formLogicRuntime=f,a._formLogicObserved)return f;if(a._formLogicObserved=!0,a.parentNode&&typeof MutationObserver<"u"){const h=new MutationObserver(function(p){for(const k of p)for(const q of k.removedNodes)if(q===a||q.nodeType===1&&q.contains&&q.contains(a)){a._formLogicRuntime&&a._formLogicRuntime.destroy(),h.disconnect();return}});h.observe(a.parentNode,{childList:!0,subtree:!1}),a._formLogicObserver=h}return f}let w=!1;if(a.addEventListener("dm-form-end",u=>{const f=u.detail?.validation;if(f&&!f.valid){Z(a,f.missing),ee(f);return}w||(w=!0,y(u.detail?.values||{}).catch(()=>{}).finally(()=>{w=!1}))}),d&&F.wizard){const u=he(o,s.title),f=u.map(v=>({title:v.title,description:v.description,fields:L(v.fields,t)})),h=u.map(v=>v.fields);let p=0,k=null,q=null;const r=v=>{const x=window.FormLogicEngine;if(!x||!x.validateFields)return;const N=x.validateFields(a,h[p]||[]);N.valid||(v.preventDefault(),v.stopImmediatePropagation(),Z(a,N.missing),ee(N))},D=()=>{const v=h[p]||o;ve(a,e),te(a,e,v),C(a,v),Q(a,v),Ce(a,n),e.honeypot!==!1&&l(a),k&&k.destroy(),k=b(q?.getStepData?q.getStepData():void 0),a.querySelectorAll(".wizard-next, .wizard-submit").forEach(x=>x.addEventListener("click",r,!0))};q=F.wizard(a,{schema:{steps:f},layout:c,columns:t,finishText:e.submitText||"Finish",onSubmit:y,onStepChange:v=>{oe(a,n),p=v,setTimeout(D,0)}}),a.addEventListener("dm-form-trigger",v=>{if(v.detail?.type!=="goto-step"||!q?.goToStep)return;const x=Number(v.detail.action.step);!Number.isFinite(x)||x<0||x>=f.length||x!==p&&q.goToStep(x)}),Promise.resolve(q).then(D)}else if(F.render){const u=F.render(a,L(o,t),I(o),{submitText:e.submitText||"Submit",layout:c,columns:t,onSubmit:y});Promise.resolve(u).then(function(){ye(a,e,c),te(a,e,o),C(a,o),Q(a,o),e.honeypot!==!1&&l(a),b()})}})})();function Ee(m,i){const M=document.body,L=M.classList,I=new Set((M.dataset.railSite||"").split(" ").filter(Boolean)),C=()=>window.matchMedia("(min-width: 993px)").matches;m.querySelectorAll(".navbar-link, .navbar-dropdown-toggle").forEach(t=>{const c=t.textContent.trim();c&&(t.setAttribute("title",c),t.querySelector("svg, [data-icon]")||(t.dataset.initial=c.charAt(0).toUpperCase()))});const A=document.createElement("button");A.type="button",A.className="dm-nav-collapse-toggle";const a=()=>{const t=L.contains("dm-vnav-collapsed");A.setAttribute("aria-pressed",String(t)),A.setAttribute("aria-label",t?"Expand menu":"Collapse menu"),A.title=A.getAttribute("aria-label");const c=i?"chevrons-left":"chevrons-right",d=i?"chevrons-right":"chevrons-left";A.innerHTML=`<span data-icon="${t?c:d}"></span>`,window.Domma&&Domma.icons&&Domma.icons.scan(A)},s=t=>{L.toggle("dm-vnav-collapsed",t),S.set("nav_collapsed",t),a()};A.addEventListener("click",()=>s(!L.contains("dm-vnav-collapsed"))),a();const o=m.querySelector(".navbar-container")||m;o.insertBefore(A,o.firstChild?.nextSibling||null);const e=t=>{L.toggle("dm-vnav-autohide",t),L.toggle("dm-vnav-collapsible",!t&&I.has("collapsible")),t?L.remove("dm-vnav-collapsed"):I.has("collapsible")&&S.get("nav_collapsed")===!0&&L.add("dm-vnav-collapsed"),m.classList.remove("dm-nav-revealed"),a()};m.addEventListener("pointerdown",t=>{L.contains("dm-vnav-autohide")&&t.pointerType!=="mouse"&&!m.classList.contains("dm-nav-revealed")&&(m.classList.add("dm-nav-revealed"),t.preventDefault())}),document.addEventListener("pointerdown",t=>{m.contains(t.target)||m.classList.remove("dm-nav-revealed")}),window.E&&typeof E.contextMenu=="function"&&E.contextMenu(m,{items:()=>{const t=L.contains("dm-vnav-autohide"),c=L.contains("dm-vnav-collapsed"),d=S.has("nav_autohide")||S.has("nav_collapsed");return[{header:"Menu"},{label:c?"Expand menu":"Collapse to icons",icon:c?"maximize":"minimize",disabled:!C()||t||!I.has("collapsible"),action:()=>s(!c)},{label:t?"Keep on screen":"Hide when not in use",icon:t?"eye":"eye-off",disabled:!C(),action:()=>{S.set("nav_autohide",!t),e(!t)}},{type:"divider"},{label:"Reset to the site's setting",icon:"refresh",disabled:!d,action:()=>{S.remove("nav_autohide"),S.remove("nav_collapsed"),e(I.has("autohide")),L.toggle("dm-vnav-collapsed",!I.has("autohide")&&I.has("start-collapsed")),a()}}]}})}function Ae(m){m.classList.add("dm-nav-scroll-hide");let i=window.scrollY,M=!1;window.addEventListener("scroll",()=>{M||(M=!0,requestAnimationFrame(()=>{M=!1;const L=window.scrollY,I=m.matches(":focus-within")||m.querySelector(".navbar-dropdown.open, .navbar-collapse.show");L>i&&L>m.offsetHeight&&!I?m.classList.add("dm-nav-scrolled-away"):L<i&&m.classList.remove("dm-nav-scrolled-away"),i=L}))},{passive:!0})}function Te(m){let i={};try{i=JSON.parse(m.dataset.contents||"{}")}catch{}const M=m.closest(".site-main"),L=document.querySelector(".site-content"),I=new Set(String(i.exclude||"").split(",").map(n=>n.trim().toLowerCase()).filter(Boolean)),C=n=>!n.closest(".hero, .card, .accordion, .tabs, figure, #site-sidebar"),A=n=>(L?[...L.querySelectorAll(n)]:[]).filter(C).filter(y=>!I.has(y.textContent.trim().toLowerCase())),a=i.headings==="h2"?A("h2"):i.headings==="h3"?A("h3"):i.headings==="h2h3"?A("h2, h3"):A("h2").length?A("h2"):A("h3");if(!a.length){M?.classList.add("no-sidebar-items");return}const s=new Set([...document.querySelectorAll("[id]")].map(n=>n.id)),o=document.createElement("nav");o.className="dm-contents",o.setAttribute("aria-label",i.title||"Page contents"),i.hover&&(o.dataset.menuHover=i.hover),i.selected&&(o.dataset.menuActive=i.selected),i.accent&&o.style.setProperty("--dm-menu-highlight",i.accent.startsWith("#")?i.accent:`var(--dm-${i.accent})`),i.font&&(o.style.fontFamily=i.font),i.fontSize&&(o.style.fontSize=i.fontSize),i.fontWeight&&(o.style.fontWeight=i.fontWeight);const e=document.createElement("div");e.className="dm-contents-head";const t=document.createElement("p");t.className="dm-contents-title",t.textContent=i.title||"",e.appendChild(t),o.appendChild(e);const c=document.createElement("ol");c.className="dm-contents-list"+(i.numbered?" is-numbered":"");const d=[];for(const n of a){if(!n.id){const l=(n.textContent||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"section";let b=l;for(let w=2;s.has(b);w++)b=`${l}-${w}`;s.add(b),n.id=b}const y=document.createElement("li");y.className=`dm-contents-item dm-contents-${n.tagName.toLowerCase()}`;const g=document.createElement("a");g.href=`#${n.id}`,g.textContent=n.textContent.trim(),y.appendChild(g),c.appendChild(y),d.push([n,g])}if(o.appendChild(c),m.replaceChildren(o),i.scrollSpy!==!1&&"IntersectionObserver"in window){const n=new Map,y=()=>{const l=d.find(([b])=>n.get(b))||null;d.forEach(([,b])=>{const w=l&&b===l[1];b.classList.toggle("is-active",!!w),w?b.setAttribute("aria-current","location"):b.removeAttribute("aria-current")})},g=new IntersectionObserver(l=>{l.forEach(b=>n.set(b.target,b.isIntersecting)),y()},{rootMargin:"-80px 0px -60% 0px"});d.forEach(([l])=>g.observe(l))}if(M?.classList.contains("contents-collapsible")){const n=document.createElement("button");n.type="button",n.className="dm-contents-toggle";const y=()=>{const g=M.classList.contains("contents-collapsed");n.setAttribute("aria-expanded",String(!g)),n.setAttribute("aria-label",g?"Show the contents":"Hide the contents"),n.title=n.getAttribute("aria-label"),n.innerHTML=`<span data-icon="${g?"list":"x"}"></span>`,window.Domma?.icons&&Domma.icons.scan(n)};n.addEventListener("click",()=>{S.set("contents_collapsed",M.classList.toggle("contents-collapsed")),y()}),y(),e.insertBefore(n,e.firstChild)}if(M?.classList.contains("contents-floating")&&i.movable){o.classList.add("is-movable");const n=S.get("contents_position");n&&Number.isFinite(n.x)&&Object.assign(m.style,{left:`${n.x}px`,top:`${n.y}px`,right:"auto",bottom:"auto"}),e.addEventListener("pointerdown",y=>{if(y.target.closest("button"))return;const g=m.getBoundingClientRect(),l=y.clientX-g.left,b=y.clientY-g.top,w=f=>{const h=Math.max(0,Math.min(window.innerWidth-g.width,f.clientX-l)),p=Math.max(0,Math.min(window.innerHeight-40,f.clientY-b));Object.assign(m.style,{left:`${h}px`,top:`${p}px`,right:"auto",bottom:"auto"})},u=()=>{document.removeEventListener("pointermove",w),S.set("contents_position",{x:parseInt(m.style.left,10),y:parseInt(m.style.top,10)})};document.addEventListener("pointermove",w),document.addEventListener("pointerup",u,{once:!0})})}window.E&&typeof E.contextMenu=="function"&&E.contextMenu(m,{items:()=>{const n=M?.classList.contains("contents-collapsible"),y=M?.classList.contains("contents-collapsed"),g=d.find(([,l])=>l.classList.contains("is-active"));return[{header:"Contents"},...n?[{label:y?"Show the contents":"Collapse to a button",icon:y?"list":"minimize",action:()=>m.querySelector(".dm-contents-toggle")?.click()}]:[],{label:"Copy a link to this section",icon:"link",disabled:!g,action:async()=>{const l=`${location.origin}${location.pathname}#${g[0].id}`;try{await _.copyToClipboard(l),E.toast("Link copied.",{type:"success"})}catch{E.toast("Could not copy the link.",{type:"error"})}}},{label:"Back to the top",icon:"arrow-up",action:()=>window.scrollTo({top:0,behavior:"smooth"})}]}}),M?.classList.contains("contents-autohide")&&(m.addEventListener("pointerdown",n=>{n.pointerType!=="mouse"&&!m.classList.contains("is-revealed")&&(m.classList.add("is-revealed"),n.preventDefault())}),document.addEventListener("pointerdown",n=>{m.contains(n.target)||m.classList.remove("is-revealed")}))}
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
import {clip, plural} from '../../services/sidebarBadges.js';
|
|
30
30
|
import fs from 'fs/promises';
|
|
31
31
|
import path from 'path';
|
|
32
|
+
import {getConfig, saveConfig} from '../../config.js';
|
|
33
|
+
import {invalidateMeasurementId, measurementIdOf, normaliseMeasurementId} from '../../services/googleAnalytics.js';
|
|
34
|
+
import {invalidateTags} from '../../services/cache/index.js';
|
|
32
35
|
import {authenticate as defaultAuthenticate, requireAdmin as defaultRequireAdmin} from '../../middleware/auth.js';
|
|
33
36
|
|
|
34
37
|
/**
|
|
@@ -371,6 +374,22 @@ function shouldRecordJourney(dayEntries, cap = JOURNEY_DAILY_CAP) {
|
|
|
371
374
|
export async function analyticsRoutes(fastify, opts = {}) {
|
|
372
375
|
const authenticate = opts.authenticate || defaultAuthenticate;
|
|
373
376
|
const requireAdmin = opts.requireAdmin || defaultRequireAdmin;
|
|
377
|
+
fastify.get('/settings', {preHandler: [authenticate, requireAdmin]}, async () => {
|
|
378
|
+
const site = getConfig('site');
|
|
379
|
+
return {measurementId: measurementIdOf(site), cookieBannerEnabled: site.cookieConsent?.enabled !== false};
|
|
380
|
+
});
|
|
381
|
+
fastify.put('/settings', {preHandler: [authenticate, requireAdmin]}, async (request, reply) => {
|
|
382
|
+
const raw = request.body?.measurementId;
|
|
383
|
+
if (typeof raw !== 'string' || (raw.trim() && !normaliseMeasurementId(raw))) {
|
|
384
|
+
return reply.code(400).send({error: 'A Google Analytics measurement ID looks like G-XXXXXXXXXX.'});
|
|
385
|
+
}
|
|
386
|
+
const site = getConfig('site');
|
|
387
|
+
const measurementId = normaliseMeasurementId(raw);
|
|
388
|
+
saveConfig('site', {...site, googleAnalytics: {...site.googleAnalytics, measurementId}});
|
|
389
|
+
invalidateMeasurementId();
|
|
390
|
+
await invalidateTags(['site']);
|
|
391
|
+
return {measurementId, cookieBannerEnabled: site.cookieConsent?.enabled !== false};
|
|
392
|
+
});
|
|
374
393
|
|
|
375
394
|
await migrateLegacy();
|
|
376
395
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Site-wide Google Business Profile connection, managed through SEO. */
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {createGoogleBusiness} from '../../services/googleBusiness.js';
|
|
4
|
+
import {originOf, trustedOrigin} from '../../services/passwordReset.js';
|
|
5
|
+
import {getUserById} from '../../services/users.js';
|
|
6
|
+
import {getPermissionsFor} from '../../services/roles.js';
|
|
7
|
+
import {getEffectiveRoles} from '../../services/userRoles.js';
|
|
8
|
+
import {resolveViewer} from '../../services/viewerSession.js';
|
|
9
|
+
|
|
10
|
+
const CALLBACK = '/api/seo/google-business/callback';
|
|
11
|
+
const cookieName = state => `dm_business_${createHash('sha256').update(state).digest('hex').slice(0, 16)}`;
|
|
12
|
+
const cookieValue = (header, name) => String(header || '').split(';').map(s => s.trim()).find(s => s.startsWith(`${name}=`))?.slice(name.length + 1) || '';
|
|
13
|
+
const esc = value => String(value).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
|
|
14
|
+
|
|
15
|
+
export async function googleBusinessRoutes(fastify, opts) {
|
|
16
|
+
const service = opts.service || createGoogleBusiness();
|
|
17
|
+
const mayManage = {preHandler: [opts.authenticate, opts.requirePermission('seo', 'manage')]};
|
|
18
|
+
const redirectFor = opts.redirectFor || (async request => {
|
|
19
|
+
const origin = await trustedOrigin(originOf(request));
|
|
20
|
+
if (!origin) throw Object.assign(new Error('Set the Site URL in Site Settings before connecting Google.'), {statusCode: 400});
|
|
21
|
+
return `${origin}${CALLBACK}`;
|
|
22
|
+
});
|
|
23
|
+
const mayComplete = opts.mayComplete || (async id => {
|
|
24
|
+
const user = await getUserById(id);
|
|
25
|
+
return user?.isActive === true && getEffectiveRoles(user).some(r => getPermissionsFor('seo', 'manage').includes(r));
|
|
26
|
+
});
|
|
27
|
+
fastify.addHook('onSend', async (request, reply, payload) => { reply.header('Cache-Control', 'no-store'); return payload; });
|
|
28
|
+
// Google token failures are not CMS session failures: avoid the admin client's 401 login retry.
|
|
29
|
+
const safe = handler => async (request, reply) => {
|
|
30
|
+
try { return await handler(request, reply); }
|
|
31
|
+
catch (e) { return reply.code(e.statusCode === 401 ? 409 : e.statusCode || 500).send({error: e.statusCode ? e.message : 'The Google connection could not be saved. Try again.'}); }
|
|
32
|
+
};
|
|
33
|
+
fastify.get('/status', mayManage, safe(async request => ({...service.status(), redirectUri: await redirectFor(request)})));
|
|
34
|
+
fastify.post('/connect', {...mayManage, config: {rateLimit: {max: 10, timeWindow: '1 minute'}}}, safe(async (request, reply) => {
|
|
35
|
+
if (!request.user?.id) return reply.code(401).send({error: 'Sign in before connecting Google.'});
|
|
36
|
+
const redirectUri = await redirectFor(request);
|
|
37
|
+
const {url, state, binding} = service.begin({...request.body, redirectUri, userId: request.user.id});
|
|
38
|
+
reply.header('Set-Cookie', `${cookieName(state)}=${binding}; Path=${CALLBACK}; HttpOnly; SameSite=Lax; Max-Age=600${redirectUri.startsWith('https:') ? '; Secure' : ''}`);
|
|
39
|
+
return {url};
|
|
40
|
+
}));
|
|
41
|
+
fastify.get('/callback', {config: {rateLimit: {max: 30, timeWindow: '1 minute'}}}, async (request, reply) => {
|
|
42
|
+
const state = String(request.query?.state || '').slice(0, 200);
|
|
43
|
+
const name = cookieName(state);
|
|
44
|
+
reply.header('Set-Cookie', `${name}=; Path=${CALLBACK}; HttpOnly; SameSite=Lax; Max-Age=0`);
|
|
45
|
+
let ok = false;
|
|
46
|
+
let message;
|
|
47
|
+
try {
|
|
48
|
+
const ticket = service.take(state, cookieValue(request.headers.cookie, name));
|
|
49
|
+
const viewer = await resolveViewer(request).catch(() => null);
|
|
50
|
+
if (viewer?.id && viewer.id !== ticket.userId) throw new Error('Google sign-in was started by someone else. Press Connect yourself.');
|
|
51
|
+
if (!(await mayComplete(ticket.userId))) throw new Error('You no longer have permission to connect Google Business Profile.');
|
|
52
|
+
if (request.query?.error) throw new Error('Google sign-in was cancelled or access was declined. Press Connect to try again.');
|
|
53
|
+
await service.complete(ticket, request.query?.code);
|
|
54
|
+
ok = true;
|
|
55
|
+
message = 'Google Business Profile connected. Return to SEO to choose a location.';
|
|
56
|
+
} catch (e) { message = e.statusCode || e.message?.startsWith('Google sign-in') || e.message?.startsWith('You no longer') ? e.message : 'The Google connection could not be completed. Press Connect to try again.'; }
|
|
57
|
+
return reply.type('text/html; charset=utf-8').header('Referrer-Policy', 'no-referrer').header('X-Robots-Tag', 'noindex')
|
|
58
|
+
.send(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Google Business Profile</title><body><main style="font-family:system-ui;max-width:36rem;margin:15vh auto;padding:1rem"><h1>${ok ? 'Google connected' : 'Google not connected'}</h1><p id="business-result" data-ok="${ok}">${esc(message)}</p><p>You can close this window and return to SEO.</p><a href="/admin/#/seo">Return to SEO</a></main><script src="/admin/js/lib/google-business-callback.js" defer></script></body></html>`);
|
|
59
|
+
});
|
|
60
|
+
fastify.get('/accounts', mayManage, safe(request => service.accounts(request.query?.pageToken)));
|
|
61
|
+
fastify.get('/locations', mayManage, safe(request => service.locations(request.query?.account, request.query?.pageToken)));
|
|
62
|
+
fastify.put('/location', mayManage, safe(request => service.select(request.body?.name)));
|
|
63
|
+
fastify.delete('/connection', mayManage, safe(() => service.disconnect()));
|
|
64
|
+
}
|
package/server/routes/api/seo.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* Was /api/plugins/seo while SEO was a plugin (0.89.0).
|
|
10
10
|
*/
|
|
11
11
|
import {authenticate as defaultAuthenticate, requirePermission as defaultRequirePermission} from '../../middleware/auth.js';
|
|
12
|
-
import {getConfig} from '../../config.js';
|
|
12
|
+
import {getConfig, saveConfig} from '../../config.js';
|
|
13
13
|
import {getPage, updatePage} from '../../services/content.js';
|
|
14
14
|
import {getSitemapSources} from '../../services/hooks.js';
|
|
15
15
|
import {seoEngine} from '../../services/seo/index.js';
|
|
16
16
|
import {invalidateTags} from '../../services/cache/index.js';
|
|
17
17
|
import {excluded} from '../../../admin/js/lib/seo-settings.js';
|
|
18
|
+
import {checkSeoDefaults, seoDefaults} from '../../../admin/js/lib/seo-defaults.js';
|
|
19
|
+
import {googleBusinessRoutes} from './google-business.js';
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* @param {import('fastify').FastifyInstance} fastify
|
|
@@ -194,11 +196,20 @@ export async function seoRoutes(fastify, opts = {}) {
|
|
|
194
196
|
robots: buildRobotsTxt(origin)};
|
|
195
197
|
});
|
|
196
198
|
|
|
197
|
-
fastify.
|
|
199
|
+
await fastify.register(googleBusinessRoutes, {prefix: '/google-business', authenticate, requirePermission, ...opts.googleBusiness});
|
|
200
|
+
|
|
201
|
+
fastify.get('/settings', may('read'), async () => ({settings: seo().settings(), defaults: seoDefaults(site()), hooks: true}));
|
|
198
202
|
fastify.put('/settings', may('manage'), async (request, reply) => {
|
|
203
|
+
const defaults = request.body?.siteDefaults === undefined ? null : checkSeoDefaults(request.body.siteDefaults);
|
|
204
|
+
if (defaults?.error) return reply.code(400).send({error: defaults.error});
|
|
199
205
|
const r = seo().saveSettings(request.body ?? {});
|
|
200
206
|
if (r.errors) return reply.code(400).send({error: Object.values(r.errors).join(' '), errors: r.errors});
|
|
201
|
-
|
|
207
|
+
if (defaults) {
|
|
208
|
+
const current = site();
|
|
209
|
+
saveConfig('site', {...current, seo: {...current.seo, ...defaults.defaults}});
|
|
210
|
+
await invalidateTags(['site']);
|
|
211
|
+
}
|
|
212
|
+
return {settings: r.settings, defaults: seoDefaults(site()), hooks: true};
|
|
202
213
|
});
|
|
203
214
|
|
|
204
215
|
fastify.get('/extensions', may('read'), async () => ({
|
|
@@ -24,6 +24,7 @@ export function adminBrandFor(site = {}) {
|
|
|
24
24
|
const str = (v) => (typeof v === 'string' ? v.replace(/[ \t\n\f\r]+/g, ' ').trim() : '');
|
|
25
25
|
const logo = str(own.logo);
|
|
26
26
|
return {
|
|
27
|
+
enabled: own.enabled !== false,
|
|
27
28
|
title: (str(own.title) || str(site?.title) || DEFAULT_ADMIN_TITLE).slice(0, 120),
|
|
28
29
|
// Same rule as the sidebar header: a site path or an http(s) address, nothing else.
|
|
29
30
|
logo: /^(?:\/(?!\/)|https?:\/\/)/i.test(logo) ? logo : '',
|