@obelism/content 0.7.0 → 0.8.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/README.md CHANGED
@@ -22,6 +22,11 @@ const content = new ContentClient({
22
22
  })
23
23
 
24
24
  await content.getPage('blog/hello') // Entry | null
25
+ await content.getPage('blog/hallo', { locale: 'nl' }) // looked up in nl's paths
26
+ await content.findPage('blog/old') // { entry } | { redirect: { path } } | null
27
+ await content.getRedirects({ locale: 'nl' }) // { items: [{ locale, from, to }], total, limit, skip }
28
+ for await (const redirect of content.getAllRedirects()) {
29
+ }
25
30
  await content.getEntry('entry_…') // Entry | null
26
31
  await content.getEntries({ contentType: 'post', limit: 20, skip: 0 }) // { items, total, limit, skip }
27
32
  for await (const post of content.getAllEntries({ contentType: 'post' })) {
@@ -39,7 +44,17 @@ await content.getContentModel() // the model file
39
44
  ```
40
45
 
41
46
  Each method takes `{ locale, depth, richText, init }` to override the
42
- defaults. Missing pages and entries are `null`; any other failure throws a
47
+ defaults.
48
+
49
+ Pages and folders can have a slug per locale. `getPage` looks the path up
50
+ in the requested locale's paths, `entry.path` is the path in that locale and
51
+ `entry.paths` the path in every locale (`{ en: 'about-us', nl: 'over-ons' }`),
52
+ for hreflang links and language switchers. The locale is never part of a
53
+ path: whether it is `/nl/over-ons` or `nl.example.com/over-ons` is your
54
+ routing. A page that moved keeps its old path as a redirect in each locale:
55
+ `findPage` answers it with `{ redirect: { path } }`, so your route can send
56
+ a `301` instead of a 404, and `getAllRedirects` lists them for redirect rules
57
+ built ahead of time. Missing pages and entries are `null`; any other failure throws a
43
58
  `ContentApiError` with `status` and the API's `message`. Type your content
44
59
  with generics: `content.getPage<Entry<{ title: string }>>('home')`.
45
60
 
@@ -131,11 +146,12 @@ bridge.undo()
131
146
  On a site signed in to edit (`@obelism/content/site`), the box also has **New
132
147
  page**: a path (`/pricing`, or under an existing page) and what the page is
133
148
  for. The CMS writes the page like the one the editor is on and stores it as a
134
- draft, and the bridge opens it at `/` + its path. When your routes put CMS
135
- pages elsewhere, say where:
149
+ draft, and the bridge opens it at `/` + its path. The path is the one in
150
+ the page's language (`<html lang>`). When your routes put CMS pages
151
+ elsewhere, say where:
136
152
 
137
153
  ```ts
138
- getContentBridge({ pagePath: (path) => `/en/${path}` })
154
+ getContentBridge({ pagePath: (path, locale) => `/${locale}/${path}` })
139
155
  await bridge.createPage('/pricing', 'Our plans, for agencies') // or your own UI
140
156
  ```
141
157
 
@@ -196,7 +212,7 @@ const event = await verifyWebhook(request, process.env.CONTENT_WEBHOOK_SECRET!)
196
212
 
197
213
  `WebhookEvent` is a union on `type`: `entry.published`, `entry.unpublished`,
198
214
  `entry.deleted` and `entry.moved` (`data.entry`: `{ id, contentType, name,
199
- path }`, plus `previousPath` when moved), `asset.updated` (`data.asset`,
215
+ path, paths }`, plus `previousPath` and `previousPaths` when moved), `asset.updated` (`data.asset`,
200
216
  `data.entries`), `model.changed` (`data.contentTypes`: `{ apiId, action }`),
201
217
  `locales.changed` (`data.defaultLocale`, `data.locales`) and `webhook.test`.
202
218
 
package/dist/bridge.d.mts CHANGED
@@ -112,7 +112,7 @@ declare const mountPromptBox: (doc: Document, bridge: ContentBridge, options?: P
112
112
  type BridgeOptions = {
113
113
  url?: string | undefined;
114
114
  prompt?: boolean | PromptBoxOptions | undefined;
115
- pagePath?: ((path: string) => string) | undefined;
115
+ pagePath?: ((path: string, locale: string) => string) | undefined;
116
116
  };
117
117
  declare const peekContentBridge: () => ContentBridge | null;
118
118
  declare const getContentBridge: ({ url, prompt, pagePath, }?: BridgeOptions) => ContentBridge;
package/dist/bridge.d.ts CHANGED
@@ -112,7 +112,7 @@ declare const mountPromptBox: (doc: Document, bridge: ContentBridge, options?: P
112
112
  type BridgeOptions = {
113
113
  url?: string | undefined;
114
114
  prompt?: boolean | PromptBoxOptions | undefined;
115
- pagePath?: ((path: string) => string) | undefined;
115
+ pagePath?: ((path: string, locale: string) => string) | undefined;
116
116
  };
117
117
  declare const peekContentBridge: () => ContentBridge | null;
118
118
  declare const getContentBridge: ({ url, prompt, pagePath, }?: BridgeOptions) => ContentBridge;
@@ -179,4 +179,4 @@ button.chip:disabled { opacity: .5; }
179
179
  background: rgb(var(--theme-accent-rgb)); margin-right: 8px;
180
180
  }
181
181
  [hidden] { display: none !important; }
182
- `,x=(e,t,r={},...n)=>{let o=e.createElement(t);for(let t of(Object.assign(o,r),n))o.appendChild("string"==typeof t?e.createTextNode(t):t);return o},w=(e,t)=>{let r=x(e,"span",{className:"icon"});return r.setAttribute("aria-hidden","true"),r.innerHTML=`<svg viewBox="0 0 24 24">${v[t]}</svg>`,r},k=(e,t,r={})=>{let n={...f,...r.labels},o=x(e,"div");o.setAttribute("data-obelism-content-prompt","");let a=o.attachShadow({mode:"open"}),i="site"===t.mode,s=x(e,"span",{textContent:n.open}),l=x(e,"button",{className:"pill",type:"button"},s,x(e,"span",{className:"mark",textContent:"⁜"})),d=x(e,"span",{className:"pill compact"},`${n.viewingDrafts} \xb7 `),c=x(e,"button",{className:"link",type:"button",textContent:n.signOut});d.appendChild(c);let p=x(e,"p",{className:"status"});p.setAttribute("role","status");let g=x(e,"textarea",{placeholder:n.placeholder,rows:1});g.setAttribute("aria-label",n.open);let u=w(e,"page"),h=w(e,"block"),b=x(e,"span"),m=x(e,"button",{type:"button",title:n.clearBlock});m.setAttribute("aria-label",n.clearBlock),m.appendChild(w(e,"close"));let v=x(e,"span",{className:"chip scope"},u,h,b,m),k=x(e,"button",{className:"action chip",type:"button"},w(e,"undo"),n.undo),E=x(e,"button",{className:"action chip",type:"button"},w(e,"newPage"),n.newPage),S=x(e,"button",{type:"button",title:n.newPageCancel});S.setAttribute("aria-label",n.newPageCancel),S.appendChild(w(e,"close"));let C=x(e,"span",{className:"chip scope"},w(e,"newPage"),x(e,"span",{textContent:n.newPage}),S),N=x(e,"input",{type:"text",placeholder:n.pathPlaceholder,autocomplete:"off",spellcheck:!1}),L=x(e,"label",{className:"path"},x(e,"span",{textContent:n.path}),N),U=x(e,"button",{className:"close icon-button",type:"button",title:n.close},w(e,"close"));U.setAttribute("aria-label",n.close);let P=x(e,"button",{className:"action primary icon-button",type:"submit"},x(e,"span",{className:"visually-hidden",textContent:n.send}),w(e,"send"));P.title=n.send;let $=x(e,"span",{className:"unsaved"});$.appendChild(x(e,"span",{className:"dot"})),$.appendChild(e.createTextNode(n.unsaved));let A=x(e,"button",{className:"link",type:"button",textContent:n.signOut}),O=x(e,"button",{className:"action chip",type:"button",textContent:n.save}),I=x(e,"button",{className:"action chip",type:"button",textContent:n.publish}),M=x(e,"div",{className:"footer"},$,x(e,"span",{className:"spacer"}),A,O,I),j=x(e,"form",{className:"panel"},p,L,g,x(e,"div",{className:"toolbar"},v,C,k,E,x(e,"span",{className:"spacer"}),U,x(e,"span",{className:"divider"}),P),M),z=x(e,"div",{className:"root"},x(e,"div",{className:"bar"},l,d),j);a.append(x(e,"style",{textContent:y}),z);let R=!1,B=!1,D=null,T=e=>{let r=e.features.includes("prompt"),o=e.saving||e.publishing,a=e.prompting||o;z.hidden=!r&&!i,l.hidden=R||!r,d.hidden=r||!i,j.hidden=!R||!r;let c=i&&e.features.includes("create"),f=i&&null===t.entryId,y=f&&c;c||(B=!1),y&&(B=!0),s.textContent=y?n.newPage:n.open,S.hidden=y,v.hidden=B,C.hidden=!B,E.hidden=B||!c,E.disabled=a,L.hidden=!B,N.disabled=a,g.placeholder=B?n.newPagePlaceholder:n.placeholder,b.textContent=e.selectedUid?n.scopeBlock:n.scopeEntry,m.hidden=!e.selectedUid,u.hidden=!!e.selectedUid,h.hidden=!e.selectedUid,k.hidden=B||!e.canUndo,k.disabled=a,P.disabled=a||""===g.value.trim()||B&&""===N.value.trim(),g.disabled=a,O.hidden=!e.features.includes("save")||f,O.disabled=a||!e.unsaved,O.textContent=e.saving?n.saving:n.save,I.hidden=!e.features.includes("publish")||f,I.disabled=a,I.textContent=e.publishing?n.publishing:n.publish,A.hidden=!i,M.hidden=!i,$.hidden=!e.unsaved;let x=e.prompting?B?n.creating:n.working:D?.text??(!i||B||e.selectedUid?"":n.selectHint);p.textContent=x,p.hidden=!x,p.className=`status${e.prompting?" working":""}${!a&&D?.error?" error":""}`},q=()=>T(t.getState()),V=()=>{g.style.height="auto",g.style.height=`${g.scrollHeight}px`},H=async()=>{let e=g.value.trim();if(e&&!t.getState().prompting){D=null;try{if(B){let r=N.value.trim();if(!r)return N.focus();await t.createPage(r,e),D={text:n.created,error:!1}}else{let{summary:r}=await t.prompt(e);D={text:r,error:!1}}g.value="",V()}catch(e){D={text:e instanceof Error?e.message:String(e),error:!0}}q(),g.focus()}},F=e=>{R=e,q(),i&&t.setSelecting(R&&!B)},Y=e=>{B=e,D=null,t.setSelecting(R&&!B),B&&t.select(null),q(),(B?N:g).focus()},_=async e=>{D=null;try{await t[e](),D={text:"save"===e?n.saved:n.published,error:!1}}catch(e){D={text:e instanceof Error?e.message:String(e),error:!0}}q()};l.addEventListener("click",()=>{F(!0),(B?N:g).focus()}),U.addEventListener("click",()=>F(!1)),O.addEventListener("click",()=>void _("save")),I.addEventListener("click",()=>void _("publish")),A.addEventListener("click",()=>t.signOut()),c.addEventListener("click",()=>t.signOut()),m.addEventListener("click",()=>t.select(null)),E.addEventListener("click",()=>Y(!0)),S.addEventListener("click",()=>Y(!1)),N.addEventListener("input",q),N.addEventListener("keydown",e=>{"Enter"!==e.key||e.isComposing?"Escape"===e.key&&F(!1):(e.preventDefault(),g.focus())}),k.addEventListener("click",()=>{D=null,t.undo()}),j.addEventListener("submit",e=>{e.preventDefault(),H()}),g.addEventListener("input",()=>{V(),q()}),g.addEventListener("keydown",e=>{"Enter"!==e.key||e.shiftKey||e.isComposing?"Escape"===e.key&&F(!1):(e.preventDefault(),H())});let W=t.on("change",T);return q(),e.body.appendChild(o),()=>{W(),o.remove()}},E="https://content.obelism.studio",S=(e,t)=>JSON.stringify(e)===JSON.stringify(t),C=Symbol.for("@obelism/content/bridge"),N=m({isEditing:!1,entryId:null,on:()=>{}},new URLSearchParams),L=({url:e,prompt:l=!0,pagePath:g}={})=>{let f;if("u"<typeof window)return N;let v=window;if(v[C])return v[C];if(v.ObelismBridge)f=m(v.ObelismBridge,new URLSearchParams(v.location.search));else{let m=(f=((e,r)=>{let l=new URLSearchParams(e.location.search),g=e.parent!==e&&l.has("cms_preview"),m=p(),f=[],v={...c,selecting:!0},y=e=>{v={...v,...e},m.emit("change",v)},x={isEditing:g,mode:g?"editor":null,entryId:l.get("cms_entry"),environment:l.get(n),on:m.on,getState:()=>v,...u,destroy:()=>{f.splice(0).forEach(e=>e())}};if(!g)return x;let w=e.document,k=(t,n={})=>{e.parent.postMessage({source:"obelism-content-bridge",type:t,...n},r)},E=w.createElement("style");E.textContent=d(),w.head.appendChild(E),f.push(()=>E.remove());let S=(e,t)=>{if(w.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),v.selectedUid!==(e??null)&&y({selectedUid:e??null}),!e)return;let r=w.querySelector(b(e));r&&(r.setAttribute("data-cms-selected",""),t&&r.scrollIntoView({behavior:"smooth",block:"center"}))},C=e=>{let r=e.target,n=r?.closest?.(`[${t}]`);if(!n)return;e.preventDefault(),e.stopPropagation();let o=n.getAttribute(t);S(o,!1),k("select",{uid:o})};w.addEventListener("click",C,!0),f.push(()=>w.removeEventListener("click",C,!0));let N=()=>k("ready",{url:e.location.href}),L=new Map,U=0,P=e=>{let t=L.get(e);return t?(clearTimeout(t.timer),L.delete(e),0===L.size&&y({prompting:!1}),t):null};x.prompt=(e,t={})=>{let r=e.trim();if(!r)return Promise.reject(Error("Say what to change"));if(!v.features.includes("prompt"))return Promise.reject(Error("The editor does not offer prompts here"));let n=`prompt_${++U}`,o=void 0===t.uid?v.selectedUid:t.uid;return new Promise((e,t)=>{let a=setTimeout(()=>{P(n)?.reject(Error("The editor did not answer in time"))},12e4);L.set(n,{resolve:e,reject:t,timer:a}),y({prompting:!0}),k("prompt",{id:n,prompt:r,uid:o})})},x.undo=()=>k("undo"),x.select=e=>{S(e,!1),k("select",{uid:e})};let $=null;f.push(()=>$?.destroy()),f.push(()=>{for(let e of L.keys())P(e)?.reject(Error("The bridge was destroyed"))});let A=t=>{if(t.origin!==r||t.source!==e.parent)return;let n=t.data;if(n&&"obelism-content-editor"===n.source){if("connect"===n.type)N();else if("update"===n.type&&n.entry)m.emit("update",{entry:n.entry,selectedUid:n.selectedUid??null}),e.requestAnimationFrame(()=>S(n.selectedUid,!1)),$?.refresh();else if("select"===n.type)S(n.uid,!0);else if("saved"===n.type||"published"===n.type)m.emit(n.type,{}),0===m.listeners.update.size&&e.location.reload();else if("features"===n.type&&Array.isArray(n.features))y({features:n.features.filter(e=>"string"==typeof e)});else if("stats"===n.type)y({stats:(e=>{if(!e||"object"!=typeof e||Array.isArray(e))return{};let t={};for(let[r,n]of Object.entries(e)){let{clicks:e,clickers:o,viewers:a,rate:i}=n??{};h(e)&&h(o)&&h(a)&&h(i)&&(t[r]={clicks:e,clickers:o,viewers:a,rate:i})}return t})(n.stats),statsVisible:!1!==n.visible}),($||Object.keys(v.stats).length)&&($??($=((e,{selector:t,onSelect:r,labels:n})=>{let l=e.document,d={...o,...n},c=l.createElement("div");c.setAttribute("data-obelism-stats","");let p=c.attachShadow({mode:"open"}),g=l.createElement("style");g.textContent=i;let u=l.createElement("div");u.className="layer",p.append(g,u),l.body.appendChild(c);let h={},b=!1,m=null,f=()=>{if(m=null,u.replaceChildren(),u.hidden=!b,!b)return;let n=e.innerHeight;for(let[o,i]of Object.entries(h)){let c=l.querySelector(t(o));if(!c)continue;let p=c.getBoundingClientRect();if(!p.width&&!p.height||p.bottom<0||p.top>n)continue;let g=l.createElement("button");g.type="button",g.className="badge",g.dataset.uid=o,g.textContent=a(d.clicked,{rate:Math.round(100*i.rate)}),g.title=a(d.title,{clickers:i.clickers,viewers:i.viewers,clicks:i.clicks});let{place:h,top:b,left:m}=s(p,e.innerWidth);g.dataset.place=h,g.style.top=`${b}px`,g.style.left=`${m}px`,g.addEventListener("click",()=>r(o)),u.appendChild(g)}},v=()=>{null===m&&b&&(m=e.requestAnimationFrame(f))};e.addEventListener("scroll",v,{capture:!0,passive:!0}),e.addEventListener("resize",v);let y=e.MutationObserver,x="function"==typeof y?new y(v):null;return x?.observe(l.body,{childList:!0,subtree:!0}),{update(t,r){h=t,b=r,null!==m&&(e.cancelAnimationFrame?.(m),m=null),f()},refresh:v,destroy(){null!==m&&e.cancelAnimationFrame?.(m),e.removeEventListener("scroll",v,{capture:!0}),e.removeEventListener("resize",v),x?.disconnect(),c.remove()}}})(e,{selector:b,onSelect:e=>x.select(e)})),$.update(v.stats,v.statsVisible));else if("promptState"===n.type)y({canUndo:!0===n.canUndo});else if("promptResult"===n.type&&"string"==typeof n.id){let e=P(n.id);if(!e)return;!0===n.ok?e.resolve({summary:"string"==typeof n.summary?n.summary:""}):e.reject(Error("string"==typeof n.error?n.error:"The prompt failed"))}}};return e.addEventListener("message",A),f.push(()=>e.removeEventListener("message",A)),"loading"===w.readyState?(w.addEventListener("DOMContentLoaded",N,{once:!0}),f.push(()=>w.removeEventListener("DOMContentLoaded",N))):N(),x})(v,new URL(e||E).origin)).isEditing||v.parent!==v?null:(e=>{for(let t of e.cookie.split(";")){let[e,...r]=t.trim().split("=");if("obelism_content_editing"===e)try{let e=decodeURIComponent(r.join("="));return e.startsWith("/")&&!e.startsWith("//")?e:null}catch{break}}return null})(v.document);if(m&&(f=((e,n,{pagePath:o=e=>`/${e}`}={})=>{let a=e.document,i=p(),s=[],l=new Map,g=[],u=new Set,h=null,m=c,f=e=>{m={...m,...e},i.emit("change",m)},v=()=>f({unsaved:[...l.values()].some(({content:e,saved:t})=>!S(e,t)),canUndo:g.length>0}),y=async(t,r)=>{let o=await e.fetch(`${n}/api/${t}`,void 0===r?{credentials:"same-origin"}:{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","x-obelism-content":"1"},body:JSON.stringify(r)}),a=await o.json().catch(()=>({}));if(401===o.status)throw f({features:[]}),Error("You were signed out. Sign in again to edit.");if(!o.ok)throw Error("string"==typeof a.error?a.error:`Obelism Content answered ${o.status}`);return a},x=()=>a.querySelector(`[${r}]`)?.getAttribute(r)??null,w=()=>{if(!h)return;let e=h.space.locales.map(({code:e})=>e),t=a.documentElement.lang;if(e.includes(t))return t;let r=t.split("-")[0]??"";return e.includes(r)?r:h.space.defaultLocale},k=async e=>{let t=l.get(e);if(t)return t;let r=await y(`entries/${encodeURIComponent(e)}`),n={content:r.content,saved:r.content,revision:r.draftRevision};return l.set(e,n),n},E=e=>{a.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),m.selectedUid!==e&&f({selectedUid:e}),e&&a.querySelector(b(e))?.setAttribute("data-cms-selected","")},C=async t=>{if(0===i.listeners.update.size)return;let r=l.get(t)?.content,{entry:n}=await y(`entries/${encodeURIComponent(t)}/preview`,{content:r,locale:w()});i.emit("update",{entry:n,selectedUid:m.selectedUid}),e.requestAnimationFrame(()=>E(m.selectedUid))},N=t=>{i.emit(t,{}),0===i.listeners.update.size&&e.location.reload()},L=async()=>{for(let[e,t]of l){if(S(t.content,t.saved))continue;let{draftRevision:r}=await y(`entries/${encodeURIComponent(e)}/save`,{content:t.content,baseRevision:t.revision});t.saved=t.content,t.revision=r,u.add(e)}g.length=0,v()},U={isEditing:!0,mode:"site",get entryId(){return x()},environment:null,on:i.on,getState:()=>m,async prompt(e,t={}){let n,o=e.trim();if(!o)throw Error("Say what to change");if(!m.features.includes("prompt"))throw Error("You may not edit content here");let i=void 0===t.uid?m.selectedUid:t.uid,s=(n=i?a.querySelector(b(i)):null,n?.closest(`[${r}]`)?.getAttribute(r)??x());if(!s)throw Error("Nothing on this page is marked as an entry: add editable(entry)");f({prompting:!0});try{let e=await k(s),t=await y(`entries/${encodeURIComponent(s)}/prompt`,{prompt:o,content:e.content,locale:w(),uid:i});return g.push({entryId:s,content:e.content}),e.content=t.content,v(),await C(s),{summary:t.summary}}finally{f({prompting:!1})}},async createPage(t,r){let n=r.trim();if(!n)throw Error("Say what the page is for");if(!t.trim())throw Error("Say where the page goes, like /pricing");if(!m.features.includes("create"))throw Error("You may not add pages here");if(m.unsaved)throw Error("Save or undo your changes before adding a page");f({prompting:!0});try{let r=await y("entries",{path:t,prompt:n,locale:w(),from:x()}),a=o(r.path);return e.location.assign(a),{summary:r.summary,entryId:r.entryId,path:r.path,url:a}}finally{f({prompting:!1})}},undo(){let e=g.pop(),t=e&&l.get(e.entryId);e&&t&&(t.content=e.content,v(),C(e.entryId).catch(e=>console.error("[@obelism/content bridge]",e)))},async save(){if(!m.features.includes("save"))throw Error("You may not edit content here");f({saving:!0});try{await L()}finally{f({saving:!1})}N("saved")},async publish(){if(!m.features.includes("publish"))throw Error("You may not publish here");f({publishing:!0});try{await L();let e=x(),t=u.size>0?[...u]:e?[e]:[];if(0===t.length)throw Error("Nothing on this page is marked as an entry: add editable(entry)");for(let e of t)await y(`entries/${encodeURIComponent(e)}/publish`,{}),u.delete(e)}finally{f({publishing:!1})}N("published")},signOut(){if(m.unsaved&&!e.confirm("Sign out and lose the changes you have not saved?"))return;l.clear(),v();let t=e.location.pathname+e.location.search;e.location.href=`${n}/logout?return_to=${encodeURIComponent(t)}`},select(e){E(e)},setSelecting(e){e?a.documentElement.setAttribute("data-cms-selecting",""):a.documentElement.removeAttribute("data-cms-selecting"),f({selecting:e})},destroy(){s.splice(0).forEach(e=>e())}},P=a.createElement("style");P.textContent=d("html[data-cms-selecting] "),a.head.appendChild(P),s.push(()=>P.remove()),s.push(()=>a.documentElement.removeAttribute("data-cms-selecting"));let $=e=>{if(!m.selecting)return;let r=e.target,n=r?.closest?.(`[${t}]`);n&&(e.preventDefault(),e.stopPropagation(),E(n.getAttribute(t)))};a.addEventListener("click",$,!0),s.push(()=>a.removeEventListener("click",$,!0));let A=e=>{m.unsaved&&(e.preventDefault(),e.returnValue="")};return e.addEventListener("beforeunload",A),s.push(()=>e.removeEventListener("beforeunload",A)),y("session").then(e=>{h=e,f({features:e.features})}).catch(e=>console.error("[@obelism/content bridge]",e)),U})(v,m,{pagePath:g})),f.isEditing&&l){let e=k(v.document,f,!0===l?{}:l),t=f.destroy;f.destroy=()=>{e(),t()}}}return v[C]=f,f};(e=window).ObelismBridge=L((e=>{let t=e?.dataset.url,r=t||E;if(!t&&e?.src){let t=new URL(e.src);"/bridge.js"===t.pathname&&(r=t.origin)}return{url:r,prompt:e?.dataset.prompt!=="false"}})(document.currentScript)),e.ObelismContent={getContentBridge:L,mountPromptBox:k}}();
182
+ `,x=(e,t,r={},...n)=>{let o=e.createElement(t);for(let t of(Object.assign(o,r),n))o.appendChild("string"==typeof t?e.createTextNode(t):t);return o},w=(e,t)=>{let r=x(e,"span",{className:"icon"});return r.setAttribute("aria-hidden","true"),r.innerHTML=`<svg viewBox="0 0 24 24">${v[t]}</svg>`,r},k=(e,t,r={})=>{let n={...f,...r.labels},o=x(e,"div");o.setAttribute("data-obelism-content-prompt","");let a=o.attachShadow({mode:"open"}),i="site"===t.mode,s=x(e,"span",{textContent:n.open}),l=x(e,"button",{className:"pill",type:"button"},s,x(e,"span",{className:"mark",textContent:"⁜"})),d=x(e,"span",{className:"pill compact"},`${n.viewingDrafts} \xb7 `),c=x(e,"button",{className:"link",type:"button",textContent:n.signOut});d.appendChild(c);let p=x(e,"p",{className:"status"});p.setAttribute("role","status");let g=x(e,"textarea",{placeholder:n.placeholder,rows:1});g.setAttribute("aria-label",n.open);let u=w(e,"page"),h=w(e,"block"),b=x(e,"span"),m=x(e,"button",{type:"button",title:n.clearBlock});m.setAttribute("aria-label",n.clearBlock),m.appendChild(w(e,"close"));let v=x(e,"span",{className:"chip scope"},u,h,b,m),k=x(e,"button",{className:"action chip",type:"button"},w(e,"undo"),n.undo),E=x(e,"button",{className:"action chip",type:"button"},w(e,"newPage"),n.newPage),S=x(e,"button",{type:"button",title:n.newPageCancel});S.setAttribute("aria-label",n.newPageCancel),S.appendChild(w(e,"close"));let C=x(e,"span",{className:"chip scope"},w(e,"newPage"),x(e,"span",{textContent:n.newPage}),S),N=x(e,"input",{type:"text",placeholder:n.pathPlaceholder,autocomplete:"off",spellcheck:!1}),L=x(e,"label",{className:"path"},x(e,"span",{textContent:n.path}),N),U=x(e,"button",{className:"close icon-button",type:"button",title:n.close},w(e,"close"));U.setAttribute("aria-label",n.close);let P=x(e,"button",{className:"action primary icon-button",type:"submit"},x(e,"span",{className:"visually-hidden",textContent:n.send}),w(e,"send"));P.title=n.send;let $=x(e,"span",{className:"unsaved"});$.appendChild(x(e,"span",{className:"dot"})),$.appendChild(e.createTextNode(n.unsaved));let A=x(e,"button",{className:"link",type:"button",textContent:n.signOut}),O=x(e,"button",{className:"action chip",type:"button",textContent:n.save}),I=x(e,"button",{className:"action chip",type:"button",textContent:n.publish}),M=x(e,"div",{className:"footer"},$,x(e,"span",{className:"spacer"}),A,O,I),j=x(e,"form",{className:"panel"},p,L,g,x(e,"div",{className:"toolbar"},v,C,k,E,x(e,"span",{className:"spacer"}),U,x(e,"span",{className:"divider"}),P),M),z=x(e,"div",{className:"root"},x(e,"div",{className:"bar"},l,d),j);a.append(x(e,"style",{textContent:y}),z);let R=!1,B=!1,D=null,T=e=>{let r=e.features.includes("prompt"),o=e.saving||e.publishing,a=e.prompting||o;z.hidden=!r&&!i,l.hidden=R||!r,d.hidden=r||!i,j.hidden=!R||!r;let c=i&&e.features.includes("create"),f=i&&null===t.entryId,y=f&&c;c||(B=!1),y&&(B=!0),s.textContent=y?n.newPage:n.open,S.hidden=y,v.hidden=B,C.hidden=!B,E.hidden=B||!c,E.disabled=a,L.hidden=!B,N.disabled=a,g.placeholder=B?n.newPagePlaceholder:n.placeholder,b.textContent=e.selectedUid?n.scopeBlock:n.scopeEntry,m.hidden=!e.selectedUid,u.hidden=!!e.selectedUid,h.hidden=!e.selectedUid,k.hidden=B||!e.canUndo,k.disabled=a,P.disabled=a||""===g.value.trim()||B&&""===N.value.trim(),g.disabled=a,O.hidden=!e.features.includes("save")||f,O.disabled=a||!e.unsaved,O.textContent=e.saving?n.saving:n.save,I.hidden=!e.features.includes("publish")||f,I.disabled=a,I.textContent=e.publishing?n.publishing:n.publish,A.hidden=!i,M.hidden=!i,$.hidden=!e.unsaved;let x=e.prompting?B?n.creating:n.working:D?.text??(!i||B||e.selectedUid?"":n.selectHint);p.textContent=x,p.hidden=!x,p.className=`status${e.prompting?" working":""}${!a&&D?.error?" error":""}`},q=()=>T(t.getState()),V=()=>{g.style.height="auto",g.style.height=`${g.scrollHeight}px`},H=async()=>{let e=g.value.trim();if(e&&!t.getState().prompting){D=null;try{if(B){let r=N.value.trim();if(!r)return N.focus();await t.createPage(r,e),D={text:n.created,error:!1}}else{let{summary:r}=await t.prompt(e);D={text:r,error:!1}}g.value="",V()}catch(e){D={text:e instanceof Error?e.message:String(e),error:!0}}q(),g.focus()}},F=e=>{R=e,q(),i&&t.setSelecting(R&&!B)},Y=e=>{B=e,D=null,t.setSelecting(R&&!B),B&&t.select(null),q(),(B?N:g).focus()},_=async e=>{D=null;try{await t[e](),D={text:"save"===e?n.saved:n.published,error:!1}}catch(e){D={text:e instanceof Error?e.message:String(e),error:!0}}q()};l.addEventListener("click",()=>{F(!0),(B?N:g).focus()}),U.addEventListener("click",()=>F(!1)),O.addEventListener("click",()=>void _("save")),I.addEventListener("click",()=>void _("publish")),A.addEventListener("click",()=>t.signOut()),c.addEventListener("click",()=>t.signOut()),m.addEventListener("click",()=>t.select(null)),E.addEventListener("click",()=>Y(!0)),S.addEventListener("click",()=>Y(!1)),N.addEventListener("input",q),N.addEventListener("keydown",e=>{"Enter"!==e.key||e.isComposing?"Escape"===e.key&&F(!1):(e.preventDefault(),g.focus())}),k.addEventListener("click",()=>{D=null,t.undo()}),j.addEventListener("submit",e=>{e.preventDefault(),H()}),g.addEventListener("input",()=>{V(),q()}),g.addEventListener("keydown",e=>{"Enter"!==e.key||e.shiftKey||e.isComposing?"Escape"===e.key&&F(!1):(e.preventDefault(),H())});let W=t.on("change",T);return q(),e.body.appendChild(o),()=>{W(),o.remove()}},E="https://content.obelism.studio",S=(e,t)=>JSON.stringify(e)===JSON.stringify(t),C=Symbol.for("@obelism/content/bridge"),N=m({isEditing:!1,entryId:null,on:()=>{}},new URLSearchParams),L=({url:e,prompt:l=!0,pagePath:g}={})=>{let f;if("u"<typeof window)return N;let v=window;if(v[C])return v[C];if(v.ObelismBridge)f=m(v.ObelismBridge,new URLSearchParams(v.location.search));else{let m=(f=((e,r)=>{let l=new URLSearchParams(e.location.search),g=e.parent!==e&&l.has("cms_preview"),m=p(),f=[],v={...c,selecting:!0},y=e=>{v={...v,...e},m.emit("change",v)},x={isEditing:g,mode:g?"editor":null,entryId:l.get("cms_entry"),environment:l.get(n),on:m.on,getState:()=>v,...u,destroy:()=>{f.splice(0).forEach(e=>e())}};if(!g)return x;let w=e.document,k=(t,n={})=>{e.parent.postMessage({source:"obelism-content-bridge",type:t,...n},r)},E=w.createElement("style");E.textContent=d(),w.head.appendChild(E),f.push(()=>E.remove());let S=(e,t)=>{if(w.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),v.selectedUid!==(e??null)&&y({selectedUid:e??null}),!e)return;let r=w.querySelector(b(e));r&&(r.setAttribute("data-cms-selected",""),t&&r.scrollIntoView({behavior:"smooth",block:"center"}))},C=e=>{let r=e.target,n=r?.closest?.(`[${t}]`);if(!n)return;e.preventDefault(),e.stopPropagation();let o=n.getAttribute(t);S(o,!1),k("select",{uid:o})};w.addEventListener("click",C,!0),f.push(()=>w.removeEventListener("click",C,!0));let N=()=>k("ready",{url:e.location.href}),L=new Map,U=0,P=e=>{let t=L.get(e);return t?(clearTimeout(t.timer),L.delete(e),0===L.size&&y({prompting:!1}),t):null};x.prompt=(e,t={})=>{let r=e.trim();if(!r)return Promise.reject(Error("Say what to change"));if(!v.features.includes("prompt"))return Promise.reject(Error("The editor does not offer prompts here"));let n=`prompt_${++U}`,o=void 0===t.uid?v.selectedUid:t.uid;return new Promise((e,t)=>{let a=setTimeout(()=>{P(n)?.reject(Error("The editor did not answer in time"))},12e4);L.set(n,{resolve:e,reject:t,timer:a}),y({prompting:!0}),k("prompt",{id:n,prompt:r,uid:o})})},x.undo=()=>k("undo"),x.select=e=>{S(e,!1),k("select",{uid:e})};let $=null;f.push(()=>$?.destroy()),f.push(()=>{for(let e of L.keys())P(e)?.reject(Error("The bridge was destroyed"))});let A=t=>{if(t.origin!==r||t.source!==e.parent)return;let n=t.data;if(n&&"obelism-content-editor"===n.source){if("connect"===n.type)N();else if("update"===n.type&&n.entry)m.emit("update",{entry:n.entry,selectedUid:n.selectedUid??null}),e.requestAnimationFrame(()=>S(n.selectedUid,!1)),$?.refresh();else if("select"===n.type)S(n.uid,!0);else if("saved"===n.type||"published"===n.type)m.emit(n.type,{}),0===m.listeners.update.size&&e.location.reload();else if("features"===n.type&&Array.isArray(n.features))y({features:n.features.filter(e=>"string"==typeof e)});else if("stats"===n.type)y({stats:(e=>{if(!e||"object"!=typeof e||Array.isArray(e))return{};let t={};for(let[r,n]of Object.entries(e)){let{clicks:e,clickers:o,viewers:a,rate:i}=n??{};h(e)&&h(o)&&h(a)&&h(i)&&(t[r]={clicks:e,clickers:o,viewers:a,rate:i})}return t})(n.stats),statsVisible:!1!==n.visible}),($||Object.keys(v.stats).length)&&($??($=((e,{selector:t,onSelect:r,labels:n})=>{let l=e.document,d={...o,...n},c=l.createElement("div");c.setAttribute("data-obelism-stats","");let p=c.attachShadow({mode:"open"}),g=l.createElement("style");g.textContent=i;let u=l.createElement("div");u.className="layer",p.append(g,u),l.body.appendChild(c);let h={},b=!1,m=null,f=()=>{if(m=null,u.replaceChildren(),u.hidden=!b,!b)return;let n=e.innerHeight;for(let[o,i]of Object.entries(h)){let c=l.querySelector(t(o));if(!c)continue;let p=c.getBoundingClientRect();if(!p.width&&!p.height||p.bottom<0||p.top>n)continue;let g=l.createElement("button");g.type="button",g.className="badge",g.dataset.uid=o,g.textContent=a(d.clicked,{rate:Math.round(100*i.rate)}),g.title=a(d.title,{clickers:i.clickers,viewers:i.viewers,clicks:i.clicks});let{place:h,top:b,left:m}=s(p,e.innerWidth);g.dataset.place=h,g.style.top=`${b}px`,g.style.left=`${m}px`,g.addEventListener("click",()=>r(o)),u.appendChild(g)}},v=()=>{null===m&&b&&(m=e.requestAnimationFrame(f))};e.addEventListener("scroll",v,{capture:!0,passive:!0}),e.addEventListener("resize",v);let y=e.MutationObserver,x="function"==typeof y?new y(v):null;return x?.observe(l.body,{childList:!0,subtree:!0}),{update(t,r){h=t,b=r,null!==m&&(e.cancelAnimationFrame?.(m),m=null),f()},refresh:v,destroy(){null!==m&&e.cancelAnimationFrame?.(m),e.removeEventListener("scroll",v,{capture:!0}),e.removeEventListener("resize",v),x?.disconnect(),c.remove()}}})(e,{selector:b,onSelect:e=>x.select(e)})),$.update(v.stats,v.statsVisible));else if("promptState"===n.type)y({canUndo:!0===n.canUndo});else if("promptResult"===n.type&&"string"==typeof n.id){let e=P(n.id);if(!e)return;!0===n.ok?e.resolve({summary:"string"==typeof n.summary?n.summary:""}):e.reject(Error("string"==typeof n.error?n.error:"The prompt failed"))}}};return e.addEventListener("message",A),f.push(()=>e.removeEventListener("message",A)),"loading"===w.readyState?(w.addEventListener("DOMContentLoaded",N,{once:!0}),f.push(()=>w.removeEventListener("DOMContentLoaded",N))):N(),x})(v,new URL(e||E).origin)).isEditing||v.parent!==v?null:(e=>{for(let t of e.cookie.split(";")){let[e,...r]=t.trim().split("=");if("obelism_content_editing"===e)try{let e=decodeURIComponent(r.join("="));return e.startsWith("/")&&!e.startsWith("//")?e:null}catch{break}}return null})(v.document);if(m&&(f=((e,n,{pagePath:o=e=>`/${e}`}={})=>{let a=e.document,i=p(),s=[],l=new Map,g=[],u=new Set,h=null,m=c,f=e=>{m={...m,...e},i.emit("change",m)},v=()=>f({unsaved:[...l.values()].some(({content:e,saved:t})=>!S(e,t)),canUndo:g.length>0}),y=async(t,r)=>{let o=await e.fetch(`${n}/api/${t}`,void 0===r?{credentials:"same-origin"}:{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","x-obelism-content":"1"},body:JSON.stringify(r)}),a=await o.json().catch(()=>({}));if(401===o.status)throw f({features:[]}),Error("You were signed out. Sign in again to edit.");if(!o.ok)throw Error("string"==typeof a.error?a.error:`Obelism Content answered ${o.status}`);return a},x=()=>a.querySelector(`[${r}]`)?.getAttribute(r)??null,w=()=>{if(!h)return;let e=h.space.locales.map(({code:e})=>e),t=a.documentElement.lang;if(e.includes(t))return t;let r=t.split("-")[0]??"";return e.includes(r)?r:h.space.defaultLocale},k=async e=>{let t=l.get(e);if(t)return t;let r=await y(`entries/${encodeURIComponent(e)}`),n={content:r.content,saved:r.content,revision:r.draftRevision};return l.set(e,n),n},E=e=>{a.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),m.selectedUid!==e&&f({selectedUid:e}),e&&a.querySelector(b(e))?.setAttribute("data-cms-selected","")},C=async t=>{if(0===i.listeners.update.size)return;let r=l.get(t)?.content,{entry:n}=await y(`entries/${encodeURIComponent(t)}/preview`,{content:r,locale:w()});i.emit("update",{entry:n,selectedUid:m.selectedUid}),e.requestAnimationFrame(()=>E(m.selectedUid))},N=t=>{i.emit(t,{}),0===i.listeners.update.size&&e.location.reload()},L=async()=>{for(let[e,t]of l){if(S(t.content,t.saved))continue;let{draftRevision:r}=await y(`entries/${encodeURIComponent(e)}/save`,{content:t.content,baseRevision:t.revision});t.saved=t.content,t.revision=r,u.add(e)}g.length=0,v()},U={isEditing:!0,mode:"site",get entryId(){return x()},environment:null,on:i.on,getState:()=>m,async prompt(e,t={}){let n,o=e.trim();if(!o)throw Error("Say what to change");if(!m.features.includes("prompt"))throw Error("You may not edit content here");let i=void 0===t.uid?m.selectedUid:t.uid,s=(n=i?a.querySelector(b(i)):null,n?.closest(`[${r}]`)?.getAttribute(r)??x());if(!s)throw Error("Nothing on this page is marked as an entry: add editable(entry)");f({prompting:!0});try{let e=await k(s),t=await y(`entries/${encodeURIComponent(s)}/prompt`,{prompt:o,content:e.content,locale:w(),uid:i});return g.push({entryId:s,content:e.content}),e.content=t.content,v(),await C(s),{summary:t.summary}}finally{f({prompting:!1})}},async createPage(t,r){let n=r.trim();if(!n)throw Error("Say what the page is for");if(!t.trim())throw Error("Say where the page goes, like /pricing");if(!m.features.includes("create"))throw Error("You may not add pages here");if(m.unsaved)throw Error("Save or undo your changes before adding a page");f({prompting:!0});try{let r=await y("entries",{path:t,prompt:n,locale:w(),from:x()}),a=o(r.path,w()??"");return e.location.assign(a),{summary:r.summary,entryId:r.entryId,path:r.path,url:a}}finally{f({prompting:!1})}},undo(){let e=g.pop(),t=e&&l.get(e.entryId);e&&t&&(t.content=e.content,v(),C(e.entryId).catch(e=>console.error("[@obelism/content bridge]",e)))},async save(){if(!m.features.includes("save"))throw Error("You may not edit content here");f({saving:!0});try{await L()}finally{f({saving:!1})}N("saved")},async publish(){if(!m.features.includes("publish"))throw Error("You may not publish here");f({publishing:!0});try{await L();let e=x(),t=u.size>0?[...u]:e?[e]:[];if(0===t.length)throw Error("Nothing on this page is marked as an entry: add editable(entry)");for(let e of t)await y(`entries/${encodeURIComponent(e)}/publish`,{}),u.delete(e)}finally{f({publishing:!1})}N("published")},signOut(){if(m.unsaved&&!e.confirm("Sign out and lose the changes you have not saved?"))return;l.clear(),v();let t=e.location.pathname+e.location.search;e.location.href=`${n}/logout?return_to=${encodeURIComponent(t)}`},select(e){E(e)},setSelecting(e){e?a.documentElement.setAttribute("data-cms-selecting",""):a.documentElement.removeAttribute("data-cms-selecting"),f({selecting:e})},destroy(){s.splice(0).forEach(e=>e())}},P=a.createElement("style");P.textContent=d("html[data-cms-selecting] "),a.head.appendChild(P),s.push(()=>P.remove()),s.push(()=>a.documentElement.removeAttribute("data-cms-selecting"));let $=e=>{if(!m.selecting)return;let r=e.target,n=r?.closest?.(`[${t}]`);n&&(e.preventDefault(),e.stopPropagation(),E(n.getAttribute(t)))};a.addEventListener("click",$,!0),s.push(()=>a.removeEventListener("click",$,!0));let A=e=>{m.unsaved&&(e.preventDefault(),e.returnValue="")};return e.addEventListener("beforeunload",A),s.push(()=>e.removeEventListener("beforeunload",A)),y("session").then(e=>{h=e,f({features:e.features})}).catch(e=>console.error("[@obelism/content bridge]",e)),U})(v,m,{pagePath:g})),f.isEditing&&l){let e=k(v.document,f,!0===l?{}:l),t=f.destroy;f.destroy=()=>{e(),t()}}}return v[C]=f,f};(e=window).ObelismBridge=L((e=>{let t=e?.dataset.url,r=t||E;if(!t&&e?.src){let t=new URL(e.src);"/bridge.js"===t.pathname&&(r=t.origin)}return{url:r,prompt:e?.dataset.prompt!=="false"}})(document.currentScript)),e.ObelismContent={getContentBridge:L,mountPromptBox:k}}();
package/dist/bridge.js CHANGED
@@ -1219,7 +1219,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
1219
1219
  locale: locale(),
1220
1220
  from: pageEntryId()
1221
1221
  });
1222
- const url = pagePath(created.path);
1222
+ const url = pagePath(created.path, locale() ?? '');
1223
1223
  win.location.assign(url);
1224
1224
  return {
1225
1225
  summary: created.summary,
package/dist/bridge.mjs CHANGED
@@ -1217,7 +1217,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
1217
1217
  locale: locale(),
1218
1218
  from: pageEntryId()
1219
1219
  });
1220
- const url = pagePath(created.path);
1220
+ const url = pagePath(created.path, locale() ?? '');
1221
1221
  win.location.assign(url);
1222
1222
  return {
1223
1223
  summary: created.summary,
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RichTextFormat, E as Entry, a as EntryList, M as ModelFile, A as Asset, B as Block, I as ImproveBlock, b as EntryStub, c as RichTextNode } from './types-nyvor49r.js';
2
- export { C as ContentTypeDefinition, d as ContentTypeKind, e as Environment, F as FieldDefinition, f as FieldType, g as RichTextDocument, h as RichTextMark } from './types-nyvor49r.js';
1
+ import { R as RichTextFormat, E as Entry, a as RedirectList, b as Redirect, c as EntryList, M as ModelFile, A as Asset, B as Block, I as ImproveBlock, d as EntryStub, e as RichTextNode } from './types-2evub96q.js';
2
+ export { C as ContentTypeDefinition, f as ContentTypeKind, g as Environment, F as FieldDefinition, h as FieldType, i as RichTextDocument, j as RichTextMark } from './types-2evub96q.js';
3
3
 
4
4
  type ContentClientOptions = {
5
5
  token: string;
@@ -26,11 +26,29 @@ type EntriesQuery = RequestOptions & {
26
26
  limit?: number | undefined;
27
27
  skip?: number | undefined;
28
28
  };
29
+ type RedirectsQuery = {
30
+ locale?: string | undefined;
31
+ limit?: number | undefined;
32
+ skip?: number | undefined;
33
+ init?: RequestInit | undefined;
34
+ };
35
+ type PageLookup<TEntry extends Entry = Entry> = {
36
+ entry: TEntry;
37
+ redirect?: undefined;
38
+ } | {
39
+ entry?: undefined;
40
+ redirect: {
41
+ path: string;
42
+ };
43
+ };
29
44
  declare class ContentClient {
30
45
  #private;
31
46
  readonly url: string;
32
47
  constructor({ token, url, locale, depth, richText, timeout, init, fetch: fetchImpl, }: ContentClientOptions);
33
48
  getPage: <TEntry extends Entry = Entry>(path: string, options?: RequestOptions) => Promise<TEntry | null>;
49
+ findPage: <TEntry extends Entry = Entry>(path: string, options?: RequestOptions) => Promise<PageLookup<TEntry> | null>;
50
+ getRedirects: ({ locale, limit, skip, init, }?: RedirectsQuery) => Promise<RedirectList>;
51
+ getAllRedirects(query?: Omit<RedirectsQuery, 'skip'>): AsyncGenerator<Redirect, void, undefined>;
34
52
  getEntry: <TEntry extends Entry = Entry>(id: string, options?: RequestOptions) => Promise<TEntry | null>;
35
53
  getEntries: <TEntry extends Entry = Entry>({ contentType, parentPath, fields, order, limit, skip, ...options }?: EntriesQuery) => Promise<EntryList<TEntry>>;
36
54
  getFirstEntry: <TEntry extends Entry = Entry>(query?: Omit<EntriesQuery, "limit" | "skip">) => Promise<TEntry | null>;
@@ -98,6 +116,6 @@ declare const richTextToHtml: (doc: RichTextNode | null | undefined, { renderEnt
98
116
  declare const headingLevel: (node: RichTextNode) => 1 | 2 | 3 | 4 | 5 | 6;
99
117
  declare const richTextToPlainText: (doc: RichTextNode | null | undefined) => string;
100
118
 
101
- export { Asset, Block, ContentApiError, ContentClient, DEFAULT_URL, ENTRY_ATTRIBUTE, Entry, EntryList, EntryStub, IMPROVE_BLOCK, ImproveBlock, ModelFile, PREVIEW_PARAMS, RichTextFormat, RichTextNode, SAFE_HREF, UID_ATTRIBUTE, editable, escapeHtml, headingLevel, imageSrcSet, imageUrl, isImproveBlock, pickImproveVariant, readPreviewParams, richTextToHtml, richTextToPlainText };
102
- export type { ContentClientOptions, EntriesQuery, ImageTransform, PreviewParams, RequestOptions, RichTextHtmlOptions };
119
+ export { Asset, Block, ContentApiError, ContentClient, DEFAULT_URL, ENTRY_ATTRIBUTE, Entry, EntryList, EntryStub, IMPROVE_BLOCK, ImproveBlock, ModelFile, PREVIEW_PARAMS, Redirect, RedirectList, RichTextFormat, RichTextNode, SAFE_HREF, UID_ATTRIBUTE, editable, escapeHtml, headingLevel, imageSrcSet, imageUrl, isImproveBlock, pickImproveVariant, readPreviewParams, richTextToHtml, richTextToPlainText };
120
+ export type { ContentClientOptions, EntriesQuery, ImageTransform, PageLookup, PreviewParams, RedirectsQuery, RequestOptions, RichTextHtmlOptions };
103
121
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sources":["../src/client.ts","../src/config.ts","../src/editable.ts","../src/errors.ts","../src/images.ts","../src/improve.ts","../src/preview.ts","../src/richText.ts"],"mappings":";;;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA;AACA,eAAe,cAAc;AAC7B;AACA,WAAW,WAAW;AACtB,mBAAmB,KAAK;AACxB;AACA,KAAK,cAAc;AACnB;AACA;AACA,eAAe,cAAc;AAC7B,WAAW,WAAW;AACtB;AACA,KAAK,gBAAgB;AACrB,KAAK,YAAY,GAAG,cAAc;AAClC;AACA;AACA,aAAa,MAAM,SAAS,gBAAgB,YAAY,gBAAgB;AACxE;AACA;AACA;AACA;AACA,cAAc,aAAa;AAC3B;AACA;AACA,2FAA2F,oBAAoB;AAC/G,6BAA6B,KAAK,GAAG,KAAK,0BAA0B,cAAc,KAAK,OAAO;AAC9F,8BAA8B,KAAK,GAAG,KAAK,wBAAwB,cAAc,KAAK,OAAO;AAC7F,gCAAgC,KAAK,GAAG,KAAK,wEAAwE,YAAY,KAAK,OAAO,CAAC,SAAS;AACvJ,mCAAmC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,wBAAwB,OAAO;AAC1G,iCAAiC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,YAAY,cAAc;AACnG,gCAAgC,IAAI,CAAC,cAAc,cAAc,OAAO,CAAC,SAAS;AAClF;;ACpCA,cAAc,WAAW;;ACAzB,cAAc,aAAa;AAC3B,cAAc,eAAe;AAC7B,KAAK,QAAQ;AACb;AACA;AACA;AACA,cAAc,QAAQ;AACtB;AACA;AACA;AACA;AACA,yBAAyB,QAAQ;;ACXjC,cAAc,eAAe,SAAS,KAAK;AAC3C;AACA;AACA;AACA;AACA;;ACJA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,QAAQ,UAAU,IAAI,CAAC,KAAK,+BAA+B,cAAc;AACvF,cAAc,WAAW,UAAU,IAAI,CAAC,KAAK,iDAAiD,IAAI,CAAC,cAAc;;ACRjH,cAAc,aAAa;AAC3B,cAAc,cAAc,UAAU,KAAK,cAAc,YAAY;AACrE,cAAc,kBAAkB,kBAAkB,KAAK,SAAS,YAAY;;ACH5E,cAAc,cAAc;AAC5B;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,KAAK,iBAAiB,GAAG,GAAG,GAAG,eAAe,YAAY,MAAM;AAChE,cAAc,iBAAiB,UAAU,iBAAiB,KAAK,aAAa;;ACX5E,cAAc,SAAS,EAAE,MAAM;AAC/B,cAAc,UAAU;AACxB,KAAK,mBAAmB;AACxB,0BAA0B,KAAK,GAAG,SAAS,QAAQ,YAAY;AAC/D,0BAA0B,KAAK,QAAQ,YAAY;AACnD;AACA,cAAc,cAAc,QAAQ,YAAY,oDAAoD,mBAAmB;AACvH,cAAc,YAAY,SAAS,YAAY;AAC/C,cAAc,mBAAmB,QAAQ,YAAY;;","names":[]}
1
+ {"version":3,"file":"index.d.mts","sources":["../src/client.ts","../src/config.ts","../src/editable.ts","../src/errors.ts","../src/images.ts","../src/improve.ts","../src/preview.ts","../src/richText.ts"],"mappings":";;;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA;AACA,eAAe,cAAc;AAC7B;AACA,WAAW,WAAW;AACtB,mBAAmB,KAAK;AACxB;AACA,KAAK,cAAc;AACnB;AACA;AACA,eAAe,cAAc;AAC7B,WAAW,WAAW;AACtB;AACA,KAAK,gBAAgB;AACrB,KAAK,YAAY,GAAG,cAAc;AAClC;AACA;AACA,aAAa,MAAM,SAAS,gBAAgB,YAAY,gBAAgB;AACxE;AACA;AACA;AACA;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA,WAAW,WAAW;AACtB;AACA,KAAK,UAAU,gBAAgB,KAAK,GAAG,KAAK;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,aAAa;AAC3B;AACA;AACA,2FAA2F,oBAAoB;AAC/G,6BAA6B,KAAK,GAAG,KAAK,0BAA0B,cAAc,KAAK,OAAO;AAC9F,8BAA8B,KAAK,GAAG,KAAK,0BAA0B,cAAc,KAAK,OAAO,CAAC,UAAU;AAC1G,oDAAoD,cAAc,KAAK,OAAO,CAAC,YAAY;AAC3F,4BAA4B,IAAI,CAAC,cAAc,YAAY,cAAc,CAAC,QAAQ;AAClF,8BAA8B,KAAK,GAAG,KAAK,wBAAwB,cAAc,KAAK,OAAO;AAC7F,gCAAgC,KAAK,GAAG,KAAK,wEAAwE,YAAY,KAAK,OAAO,CAAC,SAAS;AACvJ,mCAAmC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,wBAAwB,OAAO;AAC1G,iCAAiC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,YAAY,cAAc;AACnG,gCAAgC,IAAI,CAAC,cAAc,cAAc,OAAO,CAAC,SAAS;AAClF;;ACtDA,cAAc,WAAW;;ACAzB,cAAc,aAAa;AAC3B,cAAc,eAAe;AAC7B,KAAK,QAAQ;AACb;AACA;AACA;AACA,cAAc,QAAQ;AACtB;AACA;AACA;AACA;AACA,yBAAyB,QAAQ;;ACXjC,cAAc,eAAe,SAAS,KAAK;AAC3C;AACA;AACA;AACA;AACA;;ACJA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,QAAQ,UAAU,IAAI,CAAC,KAAK,+BAA+B,cAAc;AACvF,cAAc,WAAW,UAAU,IAAI,CAAC,KAAK,iDAAiD,IAAI,CAAC,cAAc;;ACRjH,cAAc,aAAa;AAC3B,cAAc,cAAc,UAAU,KAAK,cAAc,YAAY;AACrE,cAAc,kBAAkB,kBAAkB,KAAK,SAAS,YAAY;;ACH5E,cAAc,cAAc;AAC5B;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,KAAK,iBAAiB,GAAG,GAAG,GAAG,eAAe,YAAY,MAAM;AAChE,cAAc,iBAAiB,UAAU,iBAAiB,KAAK,aAAa;;ACX5E,cAAc,SAAS,EAAE,MAAM;AAC/B,cAAc,UAAU;AACxB,KAAK,mBAAmB;AACxB,0BAA0B,KAAK,GAAG,SAAS,QAAQ,YAAY;AAC/D,0BAA0B,KAAK,QAAQ,YAAY;AACnD;AACA,cAAc,cAAc,QAAQ,YAAY,oDAAoD,mBAAmB;AACvH,cAAc,YAAY,SAAS,YAAY;AAC/C,cAAc,mBAAmB,QAAQ,YAAY;;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RichTextFormat, E as Entry, a as EntryList, M as ModelFile, A as Asset, B as Block, I as ImproveBlock, b as EntryStub, c as RichTextNode } from './types-nyvor49r.js';
2
- export { C as ContentTypeDefinition, d as ContentTypeKind, e as Environment, F as FieldDefinition, f as FieldType, g as RichTextDocument, h as RichTextMark } from './types-nyvor49r.js';
1
+ import { R as RichTextFormat, E as Entry, a as RedirectList, b as Redirect, c as EntryList, M as ModelFile, A as Asset, B as Block, I as ImproveBlock, d as EntryStub, e as RichTextNode } from './types-2evub96q.js';
2
+ export { C as ContentTypeDefinition, f as ContentTypeKind, g as Environment, F as FieldDefinition, h as FieldType, i as RichTextDocument, j as RichTextMark } from './types-2evub96q.js';
3
3
 
4
4
  type ContentClientOptions = {
5
5
  token: string;
@@ -26,11 +26,29 @@ type EntriesQuery = RequestOptions & {
26
26
  limit?: number | undefined;
27
27
  skip?: number | undefined;
28
28
  };
29
+ type RedirectsQuery = {
30
+ locale?: string | undefined;
31
+ limit?: number | undefined;
32
+ skip?: number | undefined;
33
+ init?: RequestInit | undefined;
34
+ };
35
+ type PageLookup<TEntry extends Entry = Entry> = {
36
+ entry: TEntry;
37
+ redirect?: undefined;
38
+ } | {
39
+ entry?: undefined;
40
+ redirect: {
41
+ path: string;
42
+ };
43
+ };
29
44
  declare class ContentClient {
30
45
  #private;
31
46
  readonly url: string;
32
47
  constructor({ token, url, locale, depth, richText, timeout, init, fetch: fetchImpl, }: ContentClientOptions);
33
48
  getPage: <TEntry extends Entry = Entry>(path: string, options?: RequestOptions) => Promise<TEntry | null>;
49
+ findPage: <TEntry extends Entry = Entry>(path: string, options?: RequestOptions) => Promise<PageLookup<TEntry> | null>;
50
+ getRedirects: ({ locale, limit, skip, init, }?: RedirectsQuery) => Promise<RedirectList>;
51
+ getAllRedirects(query?: Omit<RedirectsQuery, 'skip'>): AsyncGenerator<Redirect, void, undefined>;
34
52
  getEntry: <TEntry extends Entry = Entry>(id: string, options?: RequestOptions) => Promise<TEntry | null>;
35
53
  getEntries: <TEntry extends Entry = Entry>({ contentType, parentPath, fields, order, limit, skip, ...options }?: EntriesQuery) => Promise<EntryList<TEntry>>;
36
54
  getFirstEntry: <TEntry extends Entry = Entry>(query?: Omit<EntriesQuery, "limit" | "skip">) => Promise<TEntry | null>;
@@ -98,6 +116,6 @@ declare const richTextToHtml: (doc: RichTextNode | null | undefined, { renderEnt
98
116
  declare const headingLevel: (node: RichTextNode) => 1 | 2 | 3 | 4 | 5 | 6;
99
117
  declare const richTextToPlainText: (doc: RichTextNode | null | undefined) => string;
100
118
 
101
- export { Asset, Block, ContentApiError, ContentClient, DEFAULT_URL, ENTRY_ATTRIBUTE, Entry, EntryList, EntryStub, IMPROVE_BLOCK, ImproveBlock, ModelFile, PREVIEW_PARAMS, RichTextFormat, RichTextNode, SAFE_HREF, UID_ATTRIBUTE, editable, escapeHtml, headingLevel, imageSrcSet, imageUrl, isImproveBlock, pickImproveVariant, readPreviewParams, richTextToHtml, richTextToPlainText };
102
- export type { ContentClientOptions, EntriesQuery, ImageTransform, PreviewParams, RequestOptions, RichTextHtmlOptions };
119
+ export { Asset, Block, ContentApiError, ContentClient, DEFAULT_URL, ENTRY_ATTRIBUTE, Entry, EntryList, EntryStub, IMPROVE_BLOCK, ImproveBlock, ModelFile, PREVIEW_PARAMS, Redirect, RedirectList, RichTextFormat, RichTextNode, SAFE_HREF, UID_ATTRIBUTE, editable, escapeHtml, headingLevel, imageSrcSet, imageUrl, isImproveBlock, pickImproveVariant, readPreviewParams, richTextToHtml, richTextToPlainText };
120
+ export type { ContentClientOptions, EntriesQuery, ImageTransform, PageLookup, PreviewParams, RedirectsQuery, RequestOptions, RichTextHtmlOptions };
103
121
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sources":["../src/client.ts","../src/config.ts","../src/editable.ts","../src/errors.ts","../src/images.ts","../src/improve.ts","../src/preview.ts","../src/richText.ts"],"mappings":";;;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA;AACA,eAAe,cAAc;AAC7B;AACA,WAAW,WAAW;AACtB,mBAAmB,KAAK;AACxB;AACA,KAAK,cAAc;AACnB;AACA;AACA,eAAe,cAAc;AAC7B,WAAW,WAAW;AACtB;AACA,KAAK,gBAAgB;AACrB,KAAK,YAAY,GAAG,cAAc;AAClC;AACA;AACA,aAAa,MAAM,SAAS,gBAAgB,YAAY,gBAAgB;AACxE;AACA;AACA;AACA;AACA,cAAc,aAAa;AAC3B;AACA;AACA,2FAA2F,oBAAoB;AAC/G,6BAA6B,KAAK,GAAG,KAAK,0BAA0B,cAAc,KAAK,OAAO;AAC9F,8BAA8B,KAAK,GAAG,KAAK,wBAAwB,cAAc,KAAK,OAAO;AAC7F,gCAAgC,KAAK,GAAG,KAAK,wEAAwE,YAAY,KAAK,OAAO,CAAC,SAAS;AACvJ,mCAAmC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,wBAAwB,OAAO;AAC1G,iCAAiC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,YAAY,cAAc;AACnG,gCAAgC,IAAI,CAAC,cAAc,cAAc,OAAO,CAAC,SAAS;AAClF;;ACpCA,cAAc,WAAW;;ACAzB,cAAc,aAAa;AAC3B,cAAc,eAAe;AAC7B,KAAK,QAAQ;AACb;AACA;AACA;AACA,cAAc,QAAQ;AACtB;AACA;AACA;AACA;AACA,yBAAyB,QAAQ;;ACXjC,cAAc,eAAe,SAAS,KAAK;AAC3C;AACA;AACA;AACA;AACA;;ACJA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,QAAQ,UAAU,IAAI,CAAC,KAAK,+BAA+B,cAAc;AACvF,cAAc,WAAW,UAAU,IAAI,CAAC,KAAK,iDAAiD,IAAI,CAAC,cAAc;;ACRjH,cAAc,aAAa;AAC3B,cAAc,cAAc,UAAU,KAAK,cAAc,YAAY;AACrE,cAAc,kBAAkB,kBAAkB,KAAK,SAAS,YAAY;;ACH5E,cAAc,cAAc;AAC5B;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,KAAK,iBAAiB,GAAG,GAAG,GAAG,eAAe,YAAY,MAAM;AAChE,cAAc,iBAAiB,UAAU,iBAAiB,KAAK,aAAa;;ACX5E,cAAc,SAAS,EAAE,MAAM;AAC/B,cAAc,UAAU;AACxB,KAAK,mBAAmB;AACxB,0BAA0B,KAAK,GAAG,SAAS,QAAQ,YAAY;AAC/D,0BAA0B,KAAK,QAAQ,YAAY;AACnD;AACA,cAAc,cAAc,QAAQ,YAAY,oDAAoD,mBAAmB;AACvH,cAAc,YAAY,SAAS,YAAY;AAC/C,cAAc,mBAAmB,QAAQ,YAAY;;","names":[]}
1
+ {"version":3,"file":"index.d.ts","sources":["../src/client.ts","../src/config.ts","../src/editable.ts","../src/errors.ts","../src/images.ts","../src/improve.ts","../src/preview.ts","../src/richText.ts"],"mappings":";;;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA;AACA,eAAe,cAAc;AAC7B;AACA,WAAW,WAAW;AACtB,mBAAmB,KAAK;AACxB;AACA,KAAK,cAAc;AACnB;AACA;AACA,eAAe,cAAc;AAC7B,WAAW,WAAW;AACtB;AACA,KAAK,gBAAgB;AACrB,KAAK,YAAY,GAAG,cAAc;AAClC;AACA;AACA,aAAa,MAAM,SAAS,gBAAgB,YAAY,gBAAgB;AACxE;AACA;AACA;AACA;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA,WAAW,WAAW;AACtB;AACA,KAAK,UAAU,gBAAgB,KAAK,GAAG,KAAK;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,aAAa;AAC3B;AACA;AACA,2FAA2F,oBAAoB;AAC/G,6BAA6B,KAAK,GAAG,KAAK,0BAA0B,cAAc,KAAK,OAAO;AAC9F,8BAA8B,KAAK,GAAG,KAAK,0BAA0B,cAAc,KAAK,OAAO,CAAC,UAAU;AAC1G,oDAAoD,cAAc,KAAK,OAAO,CAAC,YAAY;AAC3F,4BAA4B,IAAI,CAAC,cAAc,YAAY,cAAc,CAAC,QAAQ;AAClF,8BAA8B,KAAK,GAAG,KAAK,wBAAwB,cAAc,KAAK,OAAO;AAC7F,gCAAgC,KAAK,GAAG,KAAK,wEAAwE,YAAY,KAAK,OAAO,CAAC,SAAS;AACvJ,mCAAmC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,wBAAwB,OAAO;AAC1G,iCAAiC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,YAAY,cAAc;AACnG,gCAAgC,IAAI,CAAC,cAAc,cAAc,OAAO,CAAC,SAAS;AAClF;;ACtDA,cAAc,WAAW;;ACAzB,cAAc,aAAa;AAC3B,cAAc,eAAe;AAC7B,KAAK,QAAQ;AACb;AACA;AACA;AACA,cAAc,QAAQ;AACtB;AACA;AACA;AACA;AACA,yBAAyB,QAAQ;;ACXjC,cAAc,eAAe,SAAS,KAAK;AAC3C;AACA;AACA;AACA;AACA;;ACJA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,QAAQ,UAAU,IAAI,CAAC,KAAK,+BAA+B,cAAc;AACvF,cAAc,WAAW,UAAU,IAAI,CAAC,KAAK,iDAAiD,IAAI,CAAC,cAAc;;ACRjH,cAAc,aAAa;AAC3B,cAAc,cAAc,UAAU,KAAK,cAAc,YAAY;AACrE,cAAc,kBAAkB,kBAAkB,KAAK,SAAS,YAAY;;ACH5E,cAAc,cAAc;AAC5B;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,KAAK,iBAAiB,GAAG,GAAG,GAAG,eAAe,YAAY,MAAM;AAChE,cAAc,iBAAiB,UAAU,iBAAiB,KAAK,aAAa;;ACX5E,cAAc,SAAS,EAAE,MAAM;AAC/B,cAAc,UAAU;AACxB,KAAK,mBAAmB;AACxB,0BAA0B,KAAK,GAAG,SAAS,QAAQ,YAAY;AAC/D,0BAA0B,KAAK,QAAQ,YAAY;AACnD;AACA,cAAc,cAAc,QAAQ,YAAY,oDAAoD,mBAAmB;AACvH,cAAc,YAAY,SAAS,YAAY;AAC/C,cAAc,mBAAmB,QAAQ,YAAY;;","names":[]}
package/dist/index.js CHANGED
@@ -42,17 +42,45 @@ class ContentClient {
42
42
  #fetch;
43
43
  constructor({ token, url, locale, depth, richText, timeout, init, fetch: fetchImpl }){
44
44
  // The page at a path (`blog/hello`), or null when there is none. A site's
45
- // catch-all route asks for this.
46
- this.getPage = async (path, options = {})=>{
45
+ // catch-all route asks for this. The path is looked up in `locale`'s
46
+ // paths (the default locale's when left out or `*`): pages can have a
47
+ // slug per locale.
48
+ this.getPage = async (path, options = {})=>(await this.findPage(path, options))?.entry ?? null;
49
+ // Like getPage, but a path a page moved away from answers where it is
50
+ // now, so the site can send a 301 there instead of a 404.
51
+ //
52
+ // const found = await content.findPage(path, { locale })
53
+ // if (found?.redirect) return redirect(`/${locale}/${found.redirect.path}`, 301)
54
+ // if (!found) return notFound()
55
+ this.findPage = async (path, options = {})=>{
47
56
  const trimmed = trimSlashes(path);
48
57
  if (!trimmed) throw new Error('getPage needs a path');
49
58
  const result = await this.#request('/pages', {
50
59
  path: trimmed
51
60
  }, options, {
52
- notFound: true
61
+ notFound: 'body'
53
62
  });
54
- return result?.entry ?? null;
63
+ if (result && 'entry' in result) return {
64
+ entry: result.entry
65
+ };
66
+ const to = result?.redirect?.path;
67
+ return typeof to === 'string' ? {
68
+ redirect: {
69
+ path: to
70
+ }
71
+ } : null;
55
72
  };
73
+ // One page of redirects: every path a page moved away from, per locale,
74
+ // with where it is now. For redirect rules built ahead of time.
75
+ this.getRedirects = ({ locale, limit, skip, init } = {})=>this.#request('/redirects', {
76
+ limit,
77
+ skip
78
+ }, {
79
+ locale,
80
+ init
81
+ }, {
82
+ defaults: false
83
+ });
56
84
  // An entry by id, or null when there is none (or it isn't published, for a
57
85
  // delivery token).
58
86
  this.getEntry = async (id, options = {})=>{
@@ -93,6 +121,21 @@ class ContentClient {
93
121
  this.#init = init;
94
122
  this.#fetch = fetchImpl ?? ((...args)=>fetch(...args));
95
123
  }
124
+ // Every redirect, fetching pages of `limit` (default 1000) as it goes.
125
+ async *getAllRedirects(query = {}) {
126
+ const limit = query.limit ?? 1000;
127
+ let skip = 0;
128
+ while(true){
129
+ const page = await this.getRedirects({
130
+ ...query,
131
+ limit,
132
+ skip
133
+ });
134
+ yield* page.items;
135
+ skip += page.items.length;
136
+ if (page.items.length === 0 || skip >= page.total) return;
137
+ }
138
+ }
96
139
  // Every entry matching the query, fetching pages of `limit` (default 100)
97
140
  // as it goes.
98
141
  //
@@ -111,7 +154,8 @@ class ContentClient {
111
154
  if (page.items.length === 0 || skip >= page.total) return;
112
155
  }
113
156
  }
114
- // null for a 404 when `notFound` is set; throws ContentApiError otherwise.
157
+ // For a 404, null when `notFound` is set, or the body as is for 'body';
158
+ // throws ContentApiError otherwise.
115
159
  async #request(path, query, { init, ...options }, { notFound = false, defaults = true } = {}) {
116
160
  // Checked here rather than in the constructor, so a module creating a
117
161
  // client still loads (and a build still runs) without the variable.
@@ -149,7 +193,7 @@ class ContentClient {
149
193
  timeout
150
194
  ]) : timeout
151
195
  });
152
- if (notFound && response.status === 404) return null;
196
+ if (notFound === true && response.status === 404) return null;
153
197
  const text = await response.text();
154
198
  let body = text;
155
199
  try {
@@ -157,6 +201,9 @@ class ContentClient {
157
201
  } catch {
158
202
  // Not JSON: keep the text for the error.
159
203
  }
204
+ if (notFound === 'body' && response.status === 404) {
205
+ return typeof body === 'object' && body !== null ? body : null;
206
+ }
160
207
  if (!response.ok) throw new ContentApiError(response.status, url, body);
161
208
  return body;
162
209
  }
package/dist/index.mjs CHANGED
@@ -40,17 +40,45 @@ class ContentClient {
40
40
  #fetch;
41
41
  constructor({ token, url, locale, depth, richText, timeout, init, fetch: fetchImpl }){
42
42
  // The page at a path (`blog/hello`), or null when there is none. A site's
43
- // catch-all route asks for this.
44
- this.getPage = async (path, options = {})=>{
43
+ // catch-all route asks for this. The path is looked up in `locale`'s
44
+ // paths (the default locale's when left out or `*`): pages can have a
45
+ // slug per locale.
46
+ this.getPage = async (path, options = {})=>(await this.findPage(path, options))?.entry ?? null;
47
+ // Like getPage, but a path a page moved away from answers where it is
48
+ // now, so the site can send a 301 there instead of a 404.
49
+ //
50
+ // const found = await content.findPage(path, { locale })
51
+ // if (found?.redirect) return redirect(`/${locale}/${found.redirect.path}`, 301)
52
+ // if (!found) return notFound()
53
+ this.findPage = async (path, options = {})=>{
45
54
  const trimmed = trimSlashes(path);
46
55
  if (!trimmed) throw new Error('getPage needs a path');
47
56
  const result = await this.#request('/pages', {
48
57
  path: trimmed
49
58
  }, options, {
50
- notFound: true
59
+ notFound: 'body'
51
60
  });
52
- return result?.entry ?? null;
61
+ if (result && 'entry' in result) return {
62
+ entry: result.entry
63
+ };
64
+ const to = result?.redirect?.path;
65
+ return typeof to === 'string' ? {
66
+ redirect: {
67
+ path: to
68
+ }
69
+ } : null;
53
70
  };
71
+ // One page of redirects: every path a page moved away from, per locale,
72
+ // with where it is now. For redirect rules built ahead of time.
73
+ this.getRedirects = ({ locale, limit, skip, init } = {})=>this.#request('/redirects', {
74
+ limit,
75
+ skip
76
+ }, {
77
+ locale,
78
+ init
79
+ }, {
80
+ defaults: false
81
+ });
54
82
  // An entry by id, or null when there is none (or it isn't published, for a
55
83
  // delivery token).
56
84
  this.getEntry = async (id, options = {})=>{
@@ -91,6 +119,21 @@ class ContentClient {
91
119
  this.#init = init;
92
120
  this.#fetch = fetchImpl ?? ((...args)=>fetch(...args));
93
121
  }
122
+ // Every redirect, fetching pages of `limit` (default 1000) as it goes.
123
+ async *getAllRedirects(query = {}) {
124
+ const limit = query.limit ?? 1000;
125
+ let skip = 0;
126
+ while(true){
127
+ const page = await this.getRedirects({
128
+ ...query,
129
+ limit,
130
+ skip
131
+ });
132
+ yield* page.items;
133
+ skip += page.items.length;
134
+ if (page.items.length === 0 || skip >= page.total) return;
135
+ }
136
+ }
94
137
  // Every entry matching the query, fetching pages of `limit` (default 100)
95
138
  // as it goes.
96
139
  //
@@ -109,7 +152,8 @@ class ContentClient {
109
152
  if (page.items.length === 0 || skip >= page.total) return;
110
153
  }
111
154
  }
112
- // null for a 404 when `notFound` is set; throws ContentApiError otherwise.
155
+ // For a 404, null when `notFound` is set, or the body as is for 'body';
156
+ // throws ContentApiError otherwise.
113
157
  async #request(path, query, { init, ...options }, { notFound = false, defaults = true } = {}) {
114
158
  // Checked here rather than in the constructor, so a module creating a
115
159
  // client still loads (and a build still runs) without the variable.
@@ -147,7 +191,7 @@ class ContentClient {
147
191
  timeout
148
192
  ]) : timeout
149
193
  });
150
- if (notFound && response.status === 404) return null;
194
+ if (notFound === true && response.status === 404) return null;
151
195
  const text = await response.text();
152
196
  let body = text;
153
197
  try {
@@ -155,6 +199,9 @@ class ContentClient {
155
199
  } catch {
156
200
  // Not JSON: keep the text for the error.
157
201
  }
202
+ if (notFound === 'body' && response.status === 404) {
203
+ return typeof body === 'object' && body !== null ? body : null;
204
+ }
158
205
  if (!response.ok) throw new ContentApiError(response.status, url, body);
159
206
  return body;
160
207
  }
@@ -17,6 +17,7 @@ type Entry<TFields extends Record<string, unknown> = Record<string, unknown>, TC
17
17
  name: string;
18
18
  slug: string | null;
19
19
  path: string | null;
20
+ paths: Record<string, string>;
20
21
  publishedAt: string | null;
21
22
  firstPublishedAt: string | null;
22
23
  updatedAt: string;
@@ -55,6 +56,17 @@ type EntryList<TEntry extends Entry = Entry> = {
55
56
  limit: number;
56
57
  skip: number;
57
58
  };
59
+ type Redirect = {
60
+ locale: string;
61
+ from: string;
62
+ to: string;
63
+ };
64
+ type RedirectList = {
65
+ items: Redirect[];
66
+ total: number;
67
+ limit: number;
68
+ skip: number;
69
+ };
58
70
  type ContentTypeKind = 'page' | 'data' | 'block';
59
71
  type BaseField = {
60
72
  apiId: string;
@@ -121,5 +133,5 @@ type ModelFile = {
121
133
  contentTypes: ContentTypeDefinition[];
122
134
  };
123
135
 
124
- export type { Asset as A, Block as B, ContentTypeDefinition as C, Entry as E, FieldDefinition as F, ImproveBlock as I, ModelFile as M, RichTextFormat as R, EntryList as a, EntryStub as b, RichTextNode as c, ContentTypeKind as d, Environment as e, FieldType as f, RichTextDocument as g, RichTextMark as h };
125
- //# sourceMappingURL=types-nyvor49r.d.ts.map
136
+ export type { Asset as A, Block as B, ContentTypeDefinition as C, Entry as E, FieldDefinition as F, ImproveBlock as I, ModelFile as M, RichTextFormat as R, RedirectList as a, Redirect as b, EntryList as c, EntryStub as d, RichTextNode as e, ContentTypeKind as f, Environment as g, FieldType as h, RichTextDocument as i, RichTextMark as j };
137
+ //# sourceMappingURL=types-2evub96q.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-2evub96q.d.ts","sources":["../src/types.ts"],"mappings":"AAAA,KAAK,WAAW;AAChB,KAAK,cAAc;AACnB,KAAK,KAAK;AACV;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oBAAoB,MAAM;AAC1B,kBAAkB,MAAM;AACxB;AACA,KAAK,KAAK,iBAAiB,MAAM,oBAAoB,MAAM;AAC3D;AACA;AACA;AACA;AACA;AACA,WAAW,MAAM;AACjB;AACA;AACA;AACA;AACA;AACA,KAAK,SAAS;AACd;AACA;AACA;AACA,KAAK,KAAK,iBAAiB,MAAM,oBAAoB,MAAM;AAC3D;AACA;AACA;AACA,KAAK,YAAY,gBAAgB,KAAK,GAAG,KAAK,IAAI,KAAK;AACvD;AACA;AACA,cAAc,MAAM;AACpB;AACA,KAAK,YAAY;AACjB;AACA,YAAY,MAAM;AAClB;AACA,KAAK,YAAY;AACjB;AACA,YAAY,MAAM;AAClB,cAAc,YAAY;AAC1B;AACA,YAAY,YAAY;AACxB;AACA,KAAK,gBAAgB,GAAG,YAAY;AACpC;AACA;AACA,KAAK,SAAS,gBAAgB,KAAK,GAAG,KAAK;AAC3C;AACA;AACA;AACA;AACA;AACA,KAAK,QAAQ;AACb;AACA;AACA;AACA;AACA,KAAK,YAAY;AACjB,WAAW,QAAQ;AACnB;AACA;AACA;AACA;AACA,KAAK,eAAe;AACpB,KAAK,SAAS;AACd;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,eAAe,GAAG,SAAS;AAChC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,SAAS,GAAG,eAAe;AAChC,KAAK,qBAAqB;AAC1B;AACA;AACA,UAAU,eAAe;AACzB;AACA,YAAY,eAAe;AAC3B;AACA,KAAK,SAAS;AACd;AACA;AACA,kBAAkB,qBAAqB;AACvC;;","names":[]}
@@ -5,6 +5,7 @@ type WebhookEntry = {
5
5
  contentType: string;
6
6
  name: string;
7
7
  path: string | null;
8
+ paths: Record<string, string>;
8
9
  };
9
10
  type WebhookEventOf<TType extends string, TData> = {
10
11
  type: TType;
@@ -25,6 +26,7 @@ type EntryDeletedEvent = WebhookEventOf<'entry.deleted', {
25
26
  type EntryMovedEvent = WebhookEventOf<'entry.moved', {
26
27
  entry: WebhookEntry;
27
28
  previousPath: string | null;
29
+ previousPaths: Record<string, string>;
28
30
  }>;
29
31
  type AssetUpdatedEvent = WebhookEventOf<'asset.updated', {
30
32
  asset: {
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.d.mts","sources":["../src/webhooks.ts"],"mappings":";;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA,iBAAiB,WAAW;AAC5B;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc;AACzC,WAAW,YAAY;AACvB;AACA,KAAK,qBAAqB,GAAG,cAAc;AAC3C,WAAW,YAAY;AACvB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC,WAAW,YAAY;AACvB;AACA,KAAK,eAAe,GAAG,cAAc;AACrC,WAAW,YAAY;AACvB;AACA;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC;AACA;AACA;AACA;AACA,aAAa,YAAY;AACzB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC;AACA;AACA;AACA;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc;AACzC;AACA;AACA;AACA,KAAK,gBAAgB,GAAG,cAAc,iBAAiB,MAAM;AAC7D,KAAK,YAAY,GAAG,mBAAmB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,eAAe,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,gBAAgB;AACtL,KAAK,gBAAgB,GAAG,YAAY;AACpC,cAAc,wBAAwB,SAAS,KAAK;AACpD;AACA;AACA,KAAK,cAAc;AACnB;AACA,aAAa,OAAO,GAAG,MAAM,0CAA0C,QAAQ;AAC/E;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA,cAAc,aAAa,YAAY,OAAO,GAAG,cAAc,wCAAwC,oBAAoB,KAAK,OAAO,CAAC,YAAY;AACpJ,KAAK,qBAAqB;AAC1B;AACA,qBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA,cAAc,oBAAoB,mCAAmC,qBAAqB,eAAe,OAAO,KAAK,OAAO,CAAC,QAAQ","names":[]}
1
+ {"version":3,"file":"webhooks.d.mts","sources":["../src/webhooks.ts"],"mappings":";;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA,WAAW,MAAM;AACjB;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA,iBAAiB,WAAW;AAC5B;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc;AACzC,WAAW,YAAY;AACvB;AACA,KAAK,qBAAqB,GAAG,cAAc;AAC3C,WAAW,YAAY;AACvB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC,WAAW,YAAY;AACvB;AACA,KAAK,eAAe,GAAG,cAAc;AACrC,WAAW,YAAY;AACvB;AACA,mBAAmB,MAAM;AACzB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC;AACA;AACA;AACA;AACA,aAAa,YAAY;AACzB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC;AACA;AACA;AACA;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc;AACzC;AACA;AACA;AACA,KAAK,gBAAgB,GAAG,cAAc,iBAAiB,MAAM;AAC7D,KAAK,YAAY,GAAG,mBAAmB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,eAAe,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,gBAAgB;AACtL,KAAK,gBAAgB,GAAG,YAAY;AACpC,cAAc,wBAAwB,SAAS,KAAK;AACpD;AACA;AACA,KAAK,cAAc;AACnB;AACA,aAAa,OAAO,GAAG,MAAM,0CAA0C,QAAQ;AAC/E;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA,cAAc,aAAa,YAAY,OAAO,GAAG,cAAc,wCAAwC,oBAAoB,KAAK,OAAO,CAAC,YAAY;AACpJ,KAAK,qBAAqB;AAC1B;AACA,qBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA,cAAc,oBAAoB,mCAAmC,qBAAqB,eAAe,OAAO,KAAK,OAAO,CAAC,QAAQ","names":[]}
@@ -5,6 +5,7 @@ type WebhookEntry = {
5
5
  contentType: string;
6
6
  name: string;
7
7
  path: string | null;
8
+ paths: Record<string, string>;
8
9
  };
9
10
  type WebhookEventOf<TType extends string, TData> = {
10
11
  type: TType;
@@ -25,6 +26,7 @@ type EntryDeletedEvent = WebhookEventOf<'entry.deleted', {
25
26
  type EntryMovedEvent = WebhookEventOf<'entry.moved', {
26
27
  entry: WebhookEntry;
27
28
  previousPath: string | null;
29
+ previousPaths: Record<string, string>;
28
30
  }>;
29
31
  type AssetUpdatedEvent = WebhookEventOf<'asset.updated', {
30
32
  asset: {
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.d.ts","sources":["../src/webhooks.ts"],"mappings":";;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA,iBAAiB,WAAW;AAC5B;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc;AACzC,WAAW,YAAY;AACvB;AACA,KAAK,qBAAqB,GAAG,cAAc;AAC3C,WAAW,YAAY;AACvB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC,WAAW,YAAY;AACvB;AACA,KAAK,eAAe,GAAG,cAAc;AACrC,WAAW,YAAY;AACvB;AACA;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC;AACA;AACA;AACA;AACA,aAAa,YAAY;AACzB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC;AACA;AACA;AACA;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc;AACzC;AACA;AACA;AACA,KAAK,gBAAgB,GAAG,cAAc,iBAAiB,MAAM;AAC7D,KAAK,YAAY,GAAG,mBAAmB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,eAAe,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,gBAAgB;AACtL,KAAK,gBAAgB,GAAG,YAAY;AACpC,cAAc,wBAAwB,SAAS,KAAK;AACpD;AACA;AACA,KAAK,cAAc;AACnB;AACA,aAAa,OAAO,GAAG,MAAM,0CAA0C,QAAQ;AAC/E;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA,cAAc,aAAa,YAAY,OAAO,GAAG,cAAc,wCAAwC,oBAAoB,KAAK,OAAO,CAAC,YAAY;AACpJ,KAAK,qBAAqB;AAC1B;AACA,qBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA,cAAc,oBAAoB,mCAAmC,qBAAqB,eAAe,OAAO,KAAK,OAAO,CAAC,QAAQ","names":[]}
1
+ {"version":3,"file":"webhooks.d.ts","sources":["../src/webhooks.ts"],"mappings":";;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA,WAAW,MAAM;AACjB;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA,iBAAiB,WAAW;AAC5B;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc;AACzC,WAAW,YAAY;AACvB;AACA,KAAK,qBAAqB,GAAG,cAAc;AAC3C,WAAW,YAAY;AACvB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC,WAAW,YAAY;AACvB;AACA,KAAK,eAAe,GAAG,cAAc;AACrC,WAAW,YAAY;AACvB;AACA,mBAAmB,MAAM;AACzB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC;AACA;AACA;AACA;AACA,aAAa,YAAY;AACzB;AACA,KAAK,iBAAiB,GAAG,cAAc;AACvC;AACA;AACA;AACA;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc;AACzC;AACA;AACA;AACA,KAAK,gBAAgB,GAAG,cAAc,iBAAiB,MAAM;AAC7D,KAAK,YAAY,GAAG,mBAAmB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,eAAe,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,gBAAgB;AACtL,KAAK,gBAAgB,GAAG,YAAY;AACpC,cAAc,wBAAwB,SAAS,KAAK;AACpD;AACA;AACA,KAAK,cAAc;AACnB;AACA,aAAa,OAAO,GAAG,MAAM,0CAA0C,QAAQ;AAC/E;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA,cAAc,aAAa,YAAY,OAAO,GAAG,cAAc,wCAAwC,oBAAoB,KAAK,OAAO,CAAC,YAAY;AACpJ,KAAK,qBAAqB;AAC1B;AACA,qBAAqB,YAAY;AACjC;AACA;AACA;AACA;AACA,cAAc,oBAAoB,mCAAmC,qBAAqB,eAAe,OAAO,KAAK,OAAO,CAAC,QAAQ","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@obelism/content",
3
3
  "description": "Obelism Content SDK: the delivery API client, the visual editor bridge with prompt editing, editing on your own site, webhook verification, and the content model CLI",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "keywords": [
6
6
  "cms",
7
7
  "headless-cms",
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-nyvor49r.d.ts","sources":["../src/types.ts"],"mappings":"AAAA,KAAK,WAAW;AAChB,KAAK,cAAc;AACnB,KAAK,KAAK;AACV;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oBAAoB,MAAM;AAC1B,kBAAkB,MAAM;AACxB;AACA,KAAK,KAAK,iBAAiB,MAAM,oBAAoB,MAAM;AAC3D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,SAAS;AACd;AACA;AACA;AACA,KAAK,KAAK,iBAAiB,MAAM,oBAAoB,MAAM;AAC3D;AACA;AACA;AACA,KAAK,YAAY,gBAAgB,KAAK,GAAG,KAAK,IAAI,KAAK;AACvD;AACA;AACA,cAAc,MAAM;AACpB;AACA,KAAK,YAAY;AACjB;AACA,YAAY,MAAM;AAClB;AACA,KAAK,YAAY;AACjB;AACA,YAAY,MAAM;AAClB,cAAc,YAAY;AAC1B;AACA,YAAY,YAAY;AACxB;AACA,KAAK,gBAAgB,GAAG,YAAY;AACpC;AACA;AACA,KAAK,SAAS,gBAAgB,KAAK,GAAG,KAAK;AAC3C;AACA;AACA;AACA;AACA;AACA,KAAK,eAAe;AACpB,KAAK,SAAS;AACd;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,eAAe,GAAG,SAAS;AAChC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,SAAS,GAAG,eAAe;AAChC,KAAK,qBAAqB;AAC1B;AACA;AACA,UAAU,eAAe;AACzB;AACA,YAAY,eAAe;AAC3B;AACA,KAAK,SAAS;AACd;AACA;AACA,kBAAkB,qBAAqB;AACvC;;","names":[]}