@obelism/content 0.28.0 → 0.28.2

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/dist/bridge.mjs CHANGED
@@ -1,19 +1,22 @@
1
- import { P as PREVIEW_PARAMS, U as UID_ATTRIBUTE, E as ENTRY_ATTRIBUTE, C as COLLECTION_ATTRIBUTE, a as COLLECTION_HANDLE_ATTRIBUTE, b as PRODUCT_ATTRIBUTE, c as PRODUCT_HANDLE_ATTRIBUTE } from './preview-el7sfk3h.mjs';
1
+ import { U as UID_ATTRIBUTE, P as PREVIEW_PARAMS, E as ENTRY_ATTRIBUTE, C as COLLECTION_ATTRIBUTE, a as COLLECTION_HANDLE_ATTRIBUTE, b as PRODUCT_ATTRIBUTE, c as PRODUCT_HANDLE_ATTRIBUTE } from './preview-el7sfk3h.mjs';
2
2
  import { SITE_EDITING_COOKIE, SITE_REQUEST_HEADER } from './site.mjs';
3
3
  import { n as normalizeUrl, D as DEFAULT_URL } from './config-c6qktlz6.mjs';
4
4
 
5
- // Click rates on the page: in the visual editor, a small label on each block
6
- // that numbers has clicks for ("24% clicked"), from the editor's `stats`
7
- // message. The editor's "Show click rates" switch turns it on and off. Plain
8
- // DOM in a shadow root, like the prompt box, in a fixed layer that follows
9
- // the blocks as the page scrolls, resizes and re-renders.
10
- //
11
- // Sites tag a block's clickable element with `data-numbers-id={block._uid}`
12
- // and load Obelism numbers' script; the CMS reads the rates from numbers
13
- // (the CMS's content/docs/guides/canvas.mdx, "Click rates").
14
5
  const DEFAULT_STATS_LABELS = {
15
6
  clicked: '{rate}% clicked',
16
- title: '{clickers} of {viewers} visitors of its page clicked it in the last 30 days ({clicks} clicks)'
7
+ clickedShort: '{rate}%',
8
+ title: '{clickers} of {viewers} visitors of its page clicked it in the last 30 days ({clicks} clicks)',
9
+ noClicks: 'No clicks yet in the last 30 days',
10
+ counting: '{clicks} clicks so far: a click rate once {needed} visitors saw its page ({viewers} yet)'
11
+ };
12
+ // Fewer visitors than this make a rate noise.
13
+ const MIN_VIEWERS = 20;
14
+ const TAG_ATTRIBUTE = 'data-numbers-id';
15
+ const statsStatus = (block)=>!block || block.clicks === 0 ? 'none' : block.viewers < MIN_VIEWERS ? 'counting' : 'rate';
16
+ // Lucide's x and hourglass.
17
+ const ICONS$1 = {
18
+ none: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
19
+ counting: '<path d="M5 22h14"/><path d="M5 2h14"/><path d="M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"/><path d="M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"/>'
17
20
  };
18
21
  const fill$1 = (template, values)=>template.replace(/\{(\w+)\}/g, (match, key)=>key in values ? String(values[key]) : match);
19
22
  // The same tokens as the prompt box: an ink pill, paper text, the orange
@@ -39,37 +42,42 @@ const CSS$2 = `
39
42
  content: ''; width: 8px; height: 8px; border-radius: 50%;
40
43
  background: rgb(212 130 30);
41
44
  }
42
- .badge[data-place='beside'] { transform: translateY(-50%); }
43
- .badge[data-place='above'] { transform: translate(-100%, -100%); }
44
- .badge:focus-visible { outline: 2px solid rgb(212 130 30); outline-offset: 2px; }
45
+ .badge:focus-visible, .tag:focus-visible { outline: 2px solid rgb(212 130 30); outline-offset: 2px; }
46
+ .tag {
47
+ position: absolute; transform: translate(-50%, -50%);
48
+ display: grid; place-items: center; width: 20px; height: 20px; padding: 0;
49
+ cursor: pointer; pointer-events: auto; border: 0; border-radius: 50%;
50
+ color: rgb(242 240 236); background: rgb(23 22 20 / .88);
51
+ box-shadow: 0 4px 10px -6px rgb(23 22 20 / .5);
52
+ }
53
+ .tag::after { content: ''; position: absolute; inset: -6px; }
54
+ .tag[data-status='counting'] { color: rgb(212 130 30); }
55
+ .tag[data-status='rate'] {
56
+ width: auto; height: 20px; padding: 0 7px; border-radius: 5em;
57
+ font-size: 11px; transform: translate(-70%, -50%);
58
+ }
59
+ .tag svg {
60
+ width: 12px; height: 12px; fill: none; stroke: currentColor;
61
+ stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round;
62
+ }
45
63
  `;
46
- // Inset from a block's top-right corner, or gap beside a small element.
47
64
  const INSET = 8;
48
- // Below this an element is a button or a link, not a section: its label goes
49
- // beside it (or above, without room on the right) so it doesn't cover it.
65
+ // Below this an element is a button or a link, not a section: a tag on its
66
+ // corner rather than a label over it.
50
67
  const SMALL_HEIGHT = 64;
51
- // Room a label needs beside an element.
52
- const LABEL_WIDTH = 140;
53
- // Where a label goes for an element's box: inside the top-right corner of a
54
- // block, beside a small element, or above one at the right edge.
55
- const placeLabel = (rect, viewportWidth)=>{
56
- if (rect.height >= SMALL_HEIGHT) return {
57
- place: 'inside',
68
+ // A block's label: inside its top-right corner, in view when it starts above
69
+ // the screen.
70
+ const placeLabel = (rect)=>({
58
71
  top: Math.max(rect.top, 0) + INSET,
59
72
  left: rect.right - INSET
60
- };
61
- if (rect.right + INSET + LABEL_WIDTH <= viewportWidth) return {
62
- place: 'beside',
63
- top: rect.top + rect.height / 2,
64
- left: rect.right + INSET
65
- };
66
- return {
67
- place: 'above',
68
- top: rect.top - INSET / 2,
69
- left: rect.right
70
- };
71
- };
72
- const mountStatsOverlay = (win, { selector, onSelect, labels: labelOverrides })=>{
73
+ });
74
+ // A tag's: on the element's top-right corner, like a notification badge,
75
+ // kept on screen.
76
+ const placeTag = (rect, viewport)=>({
77
+ top: Math.min(Math.max(rect.top, 10), viewport.height - 10),
78
+ left: Math.min(rect.right, viewport.width - 10)
79
+ });
80
+ const mountStatsOverlay = (win, { selector, onSelect, checked, labels: labelOverrides })=>{
73
81
  const doc = win.document;
74
82
  const labels = {
75
83
  ...DEFAULT_STATS_LABELS,
@@ -89,37 +97,71 @@ const mountStatsOverlay = (win, { selector, onSelect, labels: labelOverrides })=
89
97
  let stats = {};
90
98
  let visible = false;
91
99
  let frame = null;
100
+ const draw = (id, element, blocks)=>{
101
+ const rect = element.getBoundingClientRect();
102
+ // Not rendered, or off screen.
103
+ if (!rect.width && !rect.height) return;
104
+ if (rect.bottom < 0 || rect.top > win.innerHeight) return;
105
+ const block = stats[id];
106
+ const status = statsStatus(block);
107
+ const label = doc.createElement('button');
108
+ label.type = 'button';
109
+ label.dataset.uid = id;
110
+ label.dataset.status = status;
111
+ const rate = block ? Math.round(block.rate * 100) : 0;
112
+ label.title = status === 'rate' ? fill$1(labels.title, {
113
+ clickers: block.clickers,
114
+ viewers: block.viewers,
115
+ clicks: block.clicks
116
+ }) : status === 'none' ? labels.noClicks : fill$1(labels.counting, {
117
+ clicks: block.clicks,
118
+ viewers: block.viewers,
119
+ needed: MIN_VIEWERS
120
+ });
121
+ const { top, left } = status === 'rate' && rect.height >= SMALL_HEIGHT ? placeLabel(rect) : placeTag(rect, {
122
+ width: win.innerWidth,
123
+ height: win.innerHeight
124
+ });
125
+ if (status === 'rate' && rect.height >= SMALL_HEIGHT) {
126
+ label.className = 'badge';
127
+ label.textContent = fill$1(labels.clicked, {
128
+ rate
129
+ });
130
+ } else {
131
+ label.className = 'tag';
132
+ if (status === 'rate') label.textContent = fill$1(labels.clickedShort, {
133
+ rate
134
+ });
135
+ else label.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${ICONS$1[status]}</svg>`;
136
+ label.setAttribute('aria-label', label.title);
137
+ }
138
+ label.style.top = `${top}px`;
139
+ label.style.left = `${left}px`;
140
+ // Its id names a block by convention; else the block around it.
141
+ const uid = blocks.has(id) ? id : element.closest(`[${UID_ATTRIBUTE}]`)?.getAttribute(UID_ATTRIBUTE);
142
+ if (uid) label.addEventListener('click', ()=>onSelect(uid));
143
+ layer.appendChild(label);
144
+ };
92
145
  const render = ()=>{
93
146
  frame = null;
94
147
  layer.replaceChildren();
95
148
  layer.hidden = !visible;
96
149
  if (!visible) return;
97
- const height = win.innerHeight;
98
- for (const [uid, block] of Object.entries(stats)){
99
- const element = doc.querySelector(selector(uid));
100
- if (!element) continue;
101
- const rect = element.getBoundingClientRect();
102
- // Not rendered, or off screen.
103
- if (!rect.width && !rect.height) continue;
104
- if (rect.bottom < 0 || rect.top > height) continue;
105
- const badge = doc.createElement('button');
106
- badge.type = 'button';
107
- badge.className = 'badge';
108
- badge.dataset.uid = uid;
109
- badge.textContent = fill$1(labels.clicked, {
110
- rate: Math.round(block.rate * 100)
111
- });
112
- badge.title = fill$1(labels.title, {
113
- clickers: block.clickers,
114
- viewers: block.viewers,
115
- clicks: block.clicks
116
- });
117
- const { place, top, left } = placeLabel(rect, win.innerWidth);
118
- badge.dataset.place = place;
119
- badge.style.top = `${top}px`;
120
- badge.style.left = `${left}px`;
121
- badge.addEventListener('click', ()=>onSelect(uid));
122
- layer.appendChild(badge);
150
+ const blocks = new Set();
151
+ for (const element of doc.querySelectorAll(`[${UID_ATTRIBUTE}]`))blocks.add(element.getAttribute(UID_ATTRIBUTE) ?? '');
152
+ const isChecked = checked ?? ((id)=>blocks.has(id));
153
+ const tagged = new Set();
154
+ for (const element of doc.querySelectorAll(`[${TAG_ATTRIBUTE}]`)){
155
+ const id = element.getAttribute(TAG_ATTRIBUTE);
156
+ if (!id || !stats[id] && !isChecked(id)) continue;
157
+ tagged.add(id);
158
+ draw(id, element, blocks);
159
+ }
160
+ // Rates of blocks the site doesn't tag an element of.
161
+ for (const id of Object.keys(stats)){
162
+ if (tagged.has(id) || !blocks.has(id)) continue;
163
+ const element = doc.querySelector(selector(id));
164
+ if (element) draw(id, element, blocks);
123
165
  }
124
166
  };
125
167
  // At most once a frame, however many scroll and mutation events come in.
@@ -784,6 +826,7 @@ const DEFAULT_SITE_EDITOR_LABELS = {
784
826
  envProduction: 'live',
785
827
  scopeNoteBlock: 'Working on {name}. Click another block to switch.',
786
828
  scopeNotePage: 'Working on the whole page. Click a block to change only that block.',
829
+ scopeUp: 'Up to {name}',
787
830
  block: 'Block',
788
831
  previewChip: '{name} · preview',
789
832
  askIntro: 'Describe a change and Obelism shows it on the page. Nothing is stored until you keep it and save.',
@@ -1948,6 +1991,11 @@ const mountFormFrame = (win, container, url, title, handlers)=>{
1948
1991
  // kept in step with the content here, see formFrame.ts), edits JSON (`view`,
1949
1992
  // then `applyJson`: the raw alternative), and lists the space's pages to go
1950
1993
  // to one.
1994
+ // The CMS's limit on ids in one stats request.
1995
+ const MAX_STATS_IDS = 500;
1996
+ const MAX_STATS_ENTRIES = 20;
1997
+ // After the page changes, before asking for the ids it newly tags.
1998
+ const STATS_DELAY_MS = 1000;
1951
1999
  // Plain path segments only: the URL parser drops tabs and newlines and reads
1952
2000
  // \ as /, so anything looser can resolve to another origin.
1953
2001
  const BASE_PATH = /^(?:\/[\w.~-]+)+$/;
@@ -2534,8 +2582,10 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
2534
2582
  if (selecting) doc.documentElement.setAttribute('data-cms-selecting', '');
2535
2583
  else doc.documentElement.removeAttribute('data-cms-selecting');
2536
2584
  setState({
2537
- selecting
2585
+ selecting,
2586
+ statsVisible: selecting
2538
2587
  });
2588
+ showStats();
2539
2589
  },
2540
2590
  destroy () {
2541
2591
  closeForm?.();
@@ -2569,11 +2619,81 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
2569
2619
  };
2570
2620
  win.addEventListener('beforeunload', onBeforeUnload);
2571
2621
  cleanup.push(()=>win.removeEventListener('beforeunload', onBeforeUnload));
2622
+ // The tags on the page's links (statsOverlay.ts) while blocks are
2623
+ // outlined: the rates of the ids the page tags, asked for as they appear.
2624
+ const asked = new Set();
2625
+ const answered = new Set();
2626
+ let statsOff = false;
2627
+ let overlay = null;
2628
+ let statsTimer = null;
2629
+ const showStats = ()=>overlay?.update(state.stats, state.statsVisible);
2630
+ const loadStats = async ()=>{
2631
+ if (statsOff) return;
2632
+ const ids = [
2633
+ ...new Set([
2634
+ ...doc.querySelectorAll(`[${TAG_ATTRIBUTE}]`)
2635
+ ].map((element)=>element.getAttribute(TAG_ATTRIBUTE) ?? ''))
2636
+ ].filter((id)=>id && !asked.has(id));
2637
+ const batch = ids.slice(0, MAX_STATS_IDS);
2638
+ if (!batch.length) return;
2639
+ batch.forEach((id)=>asked.add(id));
2640
+ // So a draft's copy of a live block gets the live one's clicks.
2641
+ const entries = [
2642
+ ...new Set([
2643
+ ...doc.querySelectorAll(`[${ENTRY_ATTRIBUTE}]`)
2644
+ ].map((element)=>element.getAttribute(ENTRY_ATTRIBUTE) ?? ''))
2645
+ ].filter(Boolean).slice(0, MAX_STATS_ENTRIES);
2646
+ try {
2647
+ const { stats } = await api('stats', {
2648
+ uids: batch,
2649
+ ...entries.length > 0 && {
2650
+ entries
2651
+ }
2652
+ });
2653
+ batch.forEach((id)=>answered.add(id));
2654
+ setState({
2655
+ stats: {
2656
+ ...state.stats,
2657
+ ...readStats(stats)
2658
+ }
2659
+ });
2660
+ } catch {
2661
+ // No tags rather than wrong ones, and no asking again.
2662
+ statsOff = true;
2663
+ return;
2664
+ }
2665
+ overlay ??= mountStatsOverlay(win, {
2666
+ selector: blockSelector,
2667
+ onSelect: (uid)=>highlight(uid),
2668
+ checked: (id)=>answered.has(id)
2669
+ });
2670
+ showStats();
2671
+ if (ids.length > batch.length) scheduleStats();
2672
+ };
2673
+ const scheduleStats = ()=>{
2674
+ if (statsTimer !== null || statsOff) return;
2675
+ statsTimer = setTimeout(()=>{
2676
+ statsTimer = null;
2677
+ void loadStats();
2678
+ }, STATS_DELAY_MS);
2679
+ };
2680
+ const Observer = win.MutationObserver;
2681
+ const statsObserver = typeof Observer === 'function' ? new Observer(scheduleStats) : null;
2682
+ cleanup.push(()=>{
2683
+ statsObserver?.disconnect();
2684
+ if (statsTimer !== null) clearTimeout(statsTimer);
2685
+ overlay?.destroy();
2686
+ });
2572
2687
  api('session').then((info)=>{
2573
2688
  session = info;
2574
2689
  setState({
2575
2690
  features: info.features
2576
2691
  });
2692
+ void loadStats();
2693
+ statsObserver?.observe(doc.body, {
2694
+ childList: true,
2695
+ subtree: true
2696
+ });
2577
2697
  }).catch((error)=>console.error('[@obelism/content bridge]', error));
2578
2698
  return bridge;
2579
2699
  };
@@ -2757,6 +2877,7 @@ button:disabled { cursor: not-allowed; }
2757
2877
  flex: none; font-size: 12px; font-weight: 500; text-decoration: underline; text-underline-offset: 2px;
2758
2878
  }
2759
2879
  .note button::after { content: ''; position: absolute; inset: -13px -6px; }
2880
+ .scroll > .note { position: sticky; top: 0; z-index: 1; }
2760
2881
  .intro { color: rgb(var(--muted)); font-size: 13px; }
2761
2882
  .gate {
2762
2883
  display: grid; gap: 8px; margin: 10px 14px 0; padding: 10px 12px; border-radius: 12px;
@@ -3065,11 +3186,11 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
3065
3186
  }, gateKeep, gateUndo));
3066
3187
  gate.setAttribute('role', 'alert');
3067
3188
  const noteText = element(doc, 'span');
3068
- const wholePage = button('', labels.scopeEntry);
3189
+ const up = button('', labels.scopeEntry);
3069
3190
  const cancelNew = button('', labels.newPageCancel);
3070
3191
  const note = element(doc, 'div', {
3071
3192
  className: 'note'
3072
- }, icon('scope'), noteText, wholePage, cancelNew);
3193
+ }, icon('scope'), noteText, up, cancelNew);
3073
3194
  const intro = element(doc, 'p', {
3074
3195
  className: 'intro',
3075
3196
  textContent: labels.askIntro
@@ -3135,10 +3256,10 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
3135
3256
  }, formNote);
3136
3257
  // The same way back as the ask view's note, above the form it changes.
3137
3258
  const fieldsNoteText = element(doc, 'span');
3138
- const wholePageFields = button('', labels.scopeEntry);
3259
+ const upFields = button('', labels.scopeEntry);
3139
3260
  const fieldsNote = element(doc, 'div', {
3140
3261
  className: 'note'
3141
- }, icon('scope'), fieldsNoteText, wholePageFields);
3262
+ }, icon('scope'), fieldsNoteText, upFields);
3142
3263
  const fields = element(doc, 'div', {
3143
3264
  className: 'fields',
3144
3265
  id: 'obelism-fields'
@@ -3218,6 +3339,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
3218
3339
  let hoverUid = null;
3219
3340
  // Block names by uid, from the views; null when it couldn't be read.
3220
3341
  const names = new Map();
3342
+ const parents = new Map();
3221
3343
  let pageNames = 'idle';
3222
3344
  let destroyed = false;
3223
3345
  const formSlot = createFormSlot(bridge, formBox, labels.formTitle, {
@@ -3253,13 +3375,21 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
3253
3375
  }
3254
3376
  return names.get(uid) ?? labels.block;
3255
3377
  };
3256
- const readNames = (value)=>{
3257
- if (Array.isArray(value)) value.forEach(readNames);
3378
+ const readNames = (value, parent = null)=>{
3379
+ if (Array.isArray(value)) value.forEach((item)=>readNames(item, parent));
3258
3380
  else if (isRecord(value)) {
3259
- if (typeof value._uid === 'string' && typeof value._type === 'string') names.set(value._uid, humanize(value._type));
3260
- Object.values(value).forEach(readNames);
3381
+ let uid = parent;
3382
+ if (typeof value._uid === 'string' && typeof value._type === 'string') {
3383
+ uid = value._uid;
3384
+ parents.set(uid, parent);
3385
+ names.set(uid, humanize(value._type));
3386
+ }
3387
+ Object.values(value).forEach((item)=>readNames(item, uid));
3261
3388
  }
3262
3389
  };
3390
+ // The block around `uid`, null at the top of the page: by the page as it
3391
+ // is now, else by its view, for a block whose wrapper the site doesn't mark.
3392
+ const parentOf = (uid)=>doc.querySelector(blockSelector(uid))?.parentElement?.closest(`[${UID_ATTRIBUTE}]`)?.getAttribute(UID_ATTRIBUTE) ?? parents.get(uid) ?? null;
3263
3393
  const readPageNames = ()=>{
3264
3394
  if (pageNames !== 'idle' || bridge.entryId === null) return;
3265
3395
  pageNames = 'loading';
@@ -3553,9 +3683,17 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
3553
3683
  } else {
3554
3684
  noteText.textContent = labels.scopeNotePage;
3555
3685
  }
3686
+ const parent = state.selectedUid ? parentOf(state.selectedUid) : null;
3687
+ const upText = parent ? fill(labels.scopeUp, {
3688
+ name: nameOf(parent)
3689
+ }) : labels.scopeEntry;
3690
+ for (const link of [
3691
+ up,
3692
+ upFields
3693
+ ])if (link.textContent !== upText) link.textContent = upText;
3556
3694
  fieldsNote.hidden = creating || !state.selectedUid;
3557
3695
  note.hidden = offerShop || pageless && !creating && !options.promptWithoutPage;
3558
- wholePage.hidden = creating || !state.selectedUid;
3696
+ up.hidden = creating || !state.selectedUid;
3559
3697
  cancelNew.hidden = !creating || onlyNew;
3560
3698
  intro.hidden = answers.length > 0 || offerShop || creating;
3561
3699
  shop.hidden = !offerShop;
@@ -3665,13 +3803,19 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
3665
3803
  refresh();
3666
3804
  (picking ? picker.search : pageButton).focus();
3667
3805
  };
3668
- // Escape, on the page or in the form: from the pages, from a block to the
3669
- // whole page. True when it did something.
3806
+ // True once back at the page.
3807
+ const stepUp = ()=>{
3808
+ const { selectedUid } = bridge.getState();
3809
+ if (selectedUid) bridge.select(parentOf(selectedUid));
3810
+ return !bridge.getState().selectedUid;
3811
+ };
3812
+ // Escape, on the page or in the form: from the pages, from a block a level
3813
+ // up. True when it did something.
3670
3814
  const stepBack = ()=>{
3671
3815
  const state = bridge.getState();
3672
3816
  if (!panelShown(state)) return false;
3673
3817
  if (picking) setPicking(false);
3674
- else if (state.selectedUid) bridge.select(null);
3818
+ else if (state.selectedUid) stepUp();
3675
3819
  else return false;
3676
3820
  return true;
3677
3821
  };
@@ -3785,13 +3929,11 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
3785
3929
  pageButton.addEventListener('click', ()=>setPicking(!picking));
3786
3930
  newPage.addEventListener('click', ()=>setCreating(true));
3787
3931
  cancelNew.addEventListener('click', ()=>setCreating(false));
3788
- wholePage.addEventListener('click', ()=>{
3789
- bridge.select(null);
3790
- input.focus();
3932
+ up.addEventListener('click', ()=>{
3933
+ if (stepUp()) input.focus();
3791
3934
  });
3792
- wholePageFields.addEventListener('click', ()=>{
3793
- bridge.select(null);
3794
- focusForm();
3935
+ upFields.addEventListener('click', ()=>{
3936
+ if (stepUp()) focusForm();
3795
3937
  });
3796
3938
  shopButton.addEventListener('click', async ()=>{
3797
3939
  shopMessage = null;
package/dist/site.js CHANGED
@@ -115,6 +115,11 @@ const PROXY_ROUTES = [
115
115
  method: 'POST',
116
116
  pattern: /^form$/
117
117
  },
118
+ // Clicks on the links the page tags, from Numbers.
119
+ {
120
+ method: 'POST',
121
+ pattern: /^stats$/
122
+ },
118
123
  {
119
124
  method: 'POST',
120
125
  pattern: /^entries\/[\w-]+\/(preview|prompt|view|apply|save|publish)$/
package/dist/site.mjs CHANGED
@@ -113,6 +113,11 @@ const PROXY_ROUTES = [
113
113
  method: 'POST',
114
114
  pattern: /^form$/
115
115
  },
116
+ // Clicks on the links the page tags, from Numbers.
117
+ {
118
+ method: 'POST',
119
+ pattern: /^stats$/
120
+ },
116
121
  {
117
122
  method: 'POST',
118
123
  pattern: /^entries\/[\w-]+\/(preview|prompt|view|apply|save|publish)$/
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.28.0",
4
+ "version": "0.28.2",
5
5
  "keywords": [
6
6
  "cms",
7
7
  "headless-cms",