@obelism/content 0.28.4 → 0.30.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
@@ -371,8 +371,14 @@ path, paths }`, plus `previousPath` and `previousPaths` when moved), `asset.upda
371
371
  `locales.changed` (`data.defaultLocale`, `data.locales`),
372
372
  `entry.review_requested` (`data.entry`, `data.revision`, `data.note`) and
373
373
  `entry.review_closed` (`data.outcome`: `approved` or `sent_back` with
374
- `data.reason`) for environments whose publishing is reviewed, and
375
- `webhook.test`.
374
+ `data.reason`) for environments whose publishing is reviewed,
375
+ `block.published` (`data.entry`: the shared block, `data.entries`: the
376
+ published pages it updated), `release.published` and
377
+ `release.review_requested` (`data.release`: `{ id, name }`, `data.entries`
378
+ with each entry's `action`, `publish` or `unpublish`, and `data.note` on a
379
+ review request), `product.updated`, `product.deleted`, `collection.updated`
380
+ and `collection.deleted` (`data.entries` and `data.tags` a store's change
381
+ touches), and `webhook.test`.
376
382
 
377
383
  ## Click rates
378
384
 
@@ -477,8 +483,26 @@ const blocks = layout.flatMap((block) => {
477
483
  ```
478
484
 
479
485
  `pickImproveVariant(block, option, fallback?)` falls back to `fallback`'s
480
- variant, else the first one, when the option is `null` (the test isn't
481
- active or the visitor isn't in it) or has no variant.
486
+ variant, else the block's own `fallback`, else the first one, when the option
487
+ is `null` (the test isn't active or the visitor isn't in it) or has no
488
+ variant.
489
+
490
+ That is for a page rendered per request. A prerendered page carries every
491
+ variant, tagged, and a script in its head shows the visitor's before the
492
+ first paint, with `seedTestCookies` from `@obelism/improve-sdk/server` at the
493
+ edge; the block's `mode` (`instant` or `after-load`, from the page type's
494
+ fold) says whether the other variants are markup or JSON. `content init`
495
+ writes the whole pattern to `content/examples/_improve.md`, and Content's
496
+ [A/B tests guide](https://content.obelism.studio/docs/guides/ab-tests) has
497
+ the same.
498
+
499
+ ## Shared blocks
500
+
501
+ A block shared between pages arrives as the block it is, with its fields,
502
+ plus `_sharedFrom: { id, name }`: the block entry it comes from. Render it
503
+ like any other block. A page left out of the block's last publish keeps the
504
+ copy that was live then, marked with `heldAt` (ISO 8601). Publishing a shared
505
+ block sends `block.published` (see Webhooks) instead of `entry.published`.
482
506
 
483
507
  ## Shopify
484
508
 
@@ -545,7 +569,8 @@ npx @obelism/content init
545
569
 
546
570
  It writes the model file (`content/content-model.json`), an example of what
547
571
  the delivery API answers for every content type (`content/examples/`, with
548
- the A/B test or flag block while your organization has improve on),
572
+ the A/B test or flag block and how to render it while your organization has
573
+ improve on),
549
574
  installs `@obelism/content` with your package manager, and adds the token variables to
550
575
  `.env.example`. Options: `--dir` (default `content`), `--url`,
551
576
  `--skip-install`. Agents get the same steps from the CMS's
@@ -87,6 +87,8 @@ const buildExamples = (model, improve)=>{
87
87
  _type: '_improve',
88
88
  kind: source.kind,
89
89
  slug: source.slug,
90
+ fallback: source.defaultValue ?? source.options[0]?.slug ?? null,
91
+ mode: 'instant',
90
92
  variants: Object.fromEntries(source.options.map((option)=>[
91
93
  option.slug,
92
94
  first && depth < MAX_DEPTH ? [
@@ -212,6 +214,341 @@ const buildExamples = (model, improve)=>{
212
214
  return examples;
213
215
  };
214
216
 
217
+ // The CMS's docs/adr/0035: one copy of the site's half, beside the example.
218
+ const IMPROVE_GUIDE = `# Rendering the improve block
219
+
220
+ \`_improve.json\` is what the delivery API answers for the improve block: one
221
+ list of blocks per option of an Obelism Improve test or flag. Editors add,
222
+ run and end these tests without you, so set the site up once and a page can
223
+ carry any number of tests without a build, a route or a deploy per test.
224
+
225
+ Written by \`content init\`; the guide is
226
+ https://content.obelism.studio/docs/guides/ab-tests.
227
+
228
+ ## Rendered per request
229
+
230
+ If the page is rendered per request, pick the variant while you render and
231
+ skip the rest of this file:
232
+
233
+ \`\`\`ts
234
+ import { isImproveBlock, pickImproveVariant } from '@obelism/content'
235
+
236
+ // \`improve\` is Improve's server SDK (@obelism/improve-sdk/server) with its
237
+ // datafile loaded; \`visitorId\` its cookie on the request.
238
+ const blocks = fields.body.flatMap((block) => {
239
+ if (!isImproveBlock(block)) return [block]
240
+ const option =
241
+ block.kind === 'flag'
242
+ ? improve.getFlagValue(block.slug, visitorId, userAgent, { country })
243
+ : improve.getTestValue(block.slug, visitorId, userAgent, { country })
244
+ return pickImproveVariant(block, option)
245
+ })
246
+ \`\`\`
247
+
248
+ ## Prerendered: every variant in the page
249
+
250
+ A page built once (static, ISR) is the same bytes for every visitor. It
251
+ carries every variant, tagged, and a script in the head shows one before the
252
+ first paint. Five pieces, set up once.
253
+
254
+ ### 1. The block renderer
255
+
256
+ The branch of your block renderer that meets \`_improve\`. \`mode\` is stamped
257
+ when the entry is published: \`instant\` puts every variant in the markup,
258
+ \`after-load\` only the \`fallback\` one, with the others as JSON beside it.
259
+
260
+ \`\`\`tsx
261
+ import { isImproveBlock, type Block, type ImproveBlock } from '@obelism/content'
262
+
263
+ import { AfterLoadVariants } from './AfterLoadVariants'
264
+
265
+ export const BlockRenderer = ({ block }: { block: Block }) => {
266
+ if (isImproveBlock(block)) {
267
+ return block.mode === 'after-load' ? (
268
+ <AfterLoadVariants block={block} />
269
+ ) : (
270
+ <ImproveVariants block={block} />
271
+ )
272
+ }
273
+ const Component = components[block._type]
274
+ return Component ? <Component block={block} /> : null
275
+ }
276
+
277
+ const ImproveVariants = ({ block }: { block: ImproveBlock }) => (
278
+ <>
279
+ {Object.entries(block.variants ?? {}).map(([option, blocks]) => (
280
+ <div key={option} data-ob-imp={block.slug} data-ob-opt={option}>
281
+ {blocks.map((child) => (
282
+ <BlockRenderer block={child} key={child._uid} />
283
+ ))}
284
+ </div>
285
+ ))}
286
+ </>
287
+ )
288
+ \`\`\`
289
+
290
+ The element around a variant gets \`display: contents\` from the style below,
291
+ so a block in a grid or a flex row keeps its place.
292
+
293
+ ### 2. The head
294
+
295
+ The instant tests of the page, their option lists, a classic script and one
296
+ style rule per test, all in \`<head>\` before the body. Never \`type="module"\`,
297
+ \`defer\` or \`async\`: those run after the variants are painted. Allow the
298
+ script in a CSP by its hash; Cloudflare's Rocket Loader needs
299
+ \`data-cfasync="false"\` on it.
300
+
301
+ \`\`\`ts
302
+ import type { ImproveBlock } from '@obelism/content'
303
+
304
+ const SAFE = /^[a-z0-9-]+$/
305
+
306
+ // Every improve block in an entry, however deep.
307
+ export const improveBlocks = (value: unknown, found: ImproveBlock[] = []) => {
308
+ if (Array.isArray(value)) value.forEach((item) => improveBlocks(item, found))
309
+ else if (value && typeof value === 'object') {
310
+ const record = value as Record<string, unknown>
311
+ if (record._type === '_improve') found.push(value as ImproveBlock)
312
+ for (const child of Object.values(record)) improveBlocks(child, found)
313
+ }
314
+ return found
315
+ }
316
+
317
+ // The meta's content and the style, from the page's instant placements.
318
+ // Nothing (and no script) when it has none.
319
+ export const improveHead = (entry: unknown) => {
320
+ const tests = new Map<string, { options: string[]; fallback: string }>()
321
+ for (const block of improveBlocks(entry)) {
322
+ if (block.mode === 'after-load' || !SAFE.test(block.slug)) continue
323
+ const options = Object.keys(block.variants ?? {}).filter((option) =>
324
+ SAFE.test(option),
325
+ )
326
+ const fallback =
327
+ block.fallback && options.includes(block.fallback)
328
+ ? block.fallback
329
+ : options[0]
330
+ if (fallback) tests.set(block.slug, { options, fallback })
331
+ }
332
+ if (tests.size === 0) return null
333
+
334
+ const meta = JSON.stringify(
335
+ Object.fromEntries([...tests].map(([slug, { options }]) => [slug, options])),
336
+ )
337
+ const hidden = [...tests].flatMap(([slug, { options, fallback }]) => [
338
+ \`html:not([data-imp-\${slug}]) [data-ob-imp="\${slug}"]:not([data-ob-opt="\${fallback}"])\`,
339
+ ...options.map(
340
+ (option) =>
341
+ \`html[data-imp-\${slug}="\${option}"] [data-ob-imp="\${slug}"]:not([data-ob-opt="\${option}"])\`,
342
+ ),
343
+ ])
344
+ const style = \`[data-ob-imp]{display:contents}\\n\${hidden.join(',\\n')}{display:none}\`
345
+ return { meta, style }
346
+ }
347
+ \`\`\`
348
+
349
+ \`\`\`html
350
+ <meta name="obelism-improve" content='{"homepage-hero":["control","variation"]}' />
351
+ <script data-cfasync="false">
352
+ ;(function () {
353
+ var meta = document.querySelector('meta[name="obelism-improve"]')
354
+ if (!meta) return
355
+ var tests = JSON.parse(meta.content)
356
+ var cookies = document.cookie.split('; ')
357
+ for (var slug in tests) {
358
+ for (var i = 0; i < cookies.length; i++) {
359
+ if (cookies[i].indexOf(slug + '=') !== 0) continue
360
+ var value = cookies[i].slice(slug.length + 1)
361
+ if (tests[slug].indexOf(value) > -1)
362
+ document.documentElement.setAttribute('data-imp-' + slug, value)
363
+ }
364
+ }
365
+ })()
366
+ </script>
367
+ <style>
368
+ /* improveHead(entry).style */
369
+ </style>
370
+ \`\`\`
371
+
372
+ In React, \`improveHead(entry)\` gives the meta's \`content\` and the style's
373
+ text: \`<meta name="obelism-improve" content={head.meta} />\`, then the script
374
+ as a string in \`<script dangerouslySetInnerHTML={{ __html: RUNTIME }} />\`,
375
+ then \`<style dangerouslySetInnerHTML={{ __html: head.style }} />\`. The slugs
376
+ and options in both passed \`SAFE\`, so neither can close its tag.
377
+
378
+ The option list matters: a cookie can name an option the block no longer
379
+ has, and without the check every variant of that block would hide. Only the
380
+ variants that lost are ever hidden; the one that shows keeps your styles.
381
+
382
+ ### 3. The edge
383
+
384
+ An edge function in front of the page decides every active test and flag
385
+ before the page is served, and keeps the cookies a visitor already has.
386
+ Without one, every visitor gets the fallback and the test measures nothing.
387
+
388
+ \`\`\`ts
389
+ import { ImproveServerSDK } from '@obelism/improve-sdk/server'
390
+
391
+ // Per request: the instance keeps its datafile for as long as it lives.
392
+ const improve = new ImproveServerSDK({
393
+ organizationId: 'org_…',
394
+ spaceId: 'space_…',
395
+ environment: 'production',
396
+ token: env.IMPROVE_TOKEN,
397
+ })
398
+ await improve.fetchConfig()
399
+
400
+ const { cookies } = improve.seedTestCookies({
401
+ cookieHeader: request.headers.get('Cookie'),
402
+ userAgent: request.headers.get('User-Agent') ?? '',
403
+ geo: { country: request.headers.get('CF-IPCountry') ?? undefined },
404
+ })
405
+ for (const { header } of cookies) response.headers.append('Set-Cookie', header)
406
+ \`\`\`
407
+
408
+ Leave bots out (they get the fallback and no cookie), never send
409
+ \`Vary: Cookie\`, and keep caching the page itself: only the cookies are per
410
+ visitor.
411
+
412
+ ### 4. The exposure
413
+
414
+ Results count exposures, and nothing has recorded one: the page decided
415
+ nothing. After paint, ask Improve's client SDK once per test the edge
416
+ decided; it reads the cookie, records the exposure and does not bucket again.
417
+
418
+ \`\`\`ts
419
+ // improveExposure.ts
420
+ import type { ImproveBlock } from '@obelism/content'
421
+ import { ImproveClientSDK } from '@obelism/improve-sdk/client'
422
+
423
+ let ready: Promise<ImproveClientSDK> | undefined
424
+ const recorded = new Set<string>()
425
+
426
+ // Once per test, however many placements the page has.
427
+ export const recordExposure = async ({ slug, kind }: ImproveBlock) => {
428
+ if (recorded.has(slug)) return
429
+ recorded.add(slug)
430
+ ready ??= (async () => {
431
+ const improve = new ImproveClientSDK({
432
+ organizationId: 'org_…',
433
+ spaceId: 'space_…',
434
+ environment: 'production',
435
+ })
436
+ await improve.fetchConfig()
437
+ return improve
438
+ })()
439
+ const improve = await ready
440
+ if (kind === 'flag') improve.getFlagValue(slug)
441
+ else improve.getTestValue(slug)
442
+ }
443
+ \`\`\`
444
+
445
+ \`\`\`tsx
446
+ 'use client'
447
+ import { useEffect } from 'react'
448
+ import type { ImproveBlock } from '@obelism/content'
449
+
450
+ import { recordExposure } from './improveExposure'
451
+
452
+ // Once per page, with improveBlocks(entry).
453
+ export const ImproveExposures = ({ blocks }: { blocks: ImproveBlock[] }) => {
454
+ useEffect(() => {
455
+ for (const block of blocks) {
456
+ if (block.mode === 'after-load') continue
457
+ if (document.documentElement.hasAttribute(\`data-imp-\${block.slug}\`))
458
+ recordExposure(block)
459
+ }
460
+ }, [blocks])
461
+ return null
462
+ }
463
+ \`\`\`
464
+
465
+ ### 5. After-load placements
466
+
467
+ Below the page type's fold, publishing stamps \`mode: "after-load"\`: the
468
+ fallback variant is markup, the others JSON in the page, swapped in on mount.
469
+ No second variant's markup or images, and no head script for a page that has
470
+ only these. It needs a block renderer that runs in the browser.
471
+
472
+ \`\`\`tsx
473
+ 'use client'
474
+ import { useEffect, useState } from 'react'
475
+ import type { Block, ImproveBlock } from '@obelism/content'
476
+
477
+ import { BlockRenderer } from './BlockRenderer'
478
+ import { recordExposure } from './improveExposure'
479
+
480
+ // \`<\` escaped, so a block's text can't close the script.
481
+ const json = (value: unknown) =>
482
+ JSON.stringify(value).replace(/</g, '\\\\u003c')
483
+
484
+ const readCookie = (name: string) =>
485
+ document.cookie
486
+ .split('; ')
487
+ .find((row) => row.startsWith(\`\${name}=\`))
488
+ ?.slice(name.length + 1)
489
+
490
+ export const AfterLoadVariants = ({ block }: { block: ImproveBlock }) => {
491
+ const variants = block.variants ?? {}
492
+ const fallback =
493
+ block.fallback && variants[block.fallback]
494
+ ? block.fallback
495
+ : Object.keys(variants)[0]
496
+ const others = Object.fromEntries(
497
+ Object.entries(variants).filter(([option]) => option !== fallback),
498
+ )
499
+ const [shown, setShown] = useState<{ option: string | undefined; blocks: Block[] }>({
500
+ option: fallback,
501
+ blocks: (fallback && variants[fallback]) || [],
502
+ })
503
+
504
+ useEffect(() => {
505
+ const option = readCookie(block.slug)
506
+ if (!option) return
507
+ const script = document.querySelector(
508
+ \`script[data-ob-variants="\${CSS.escape(block.slug)}"]\`,
509
+ )
510
+ const arms: Record<string, Block[]> =
511
+ option === fallback ? variants : JSON.parse(script?.textContent || '{}')
512
+ // The cookie is the visitor's to edit: only an option the block has.
513
+ const blocks = Object.hasOwn(arms, option) ? arms[option] : undefined
514
+ if (!Array.isArray(blocks)) return
515
+ setShown({ option, blocks })
516
+ document.documentElement.setAttribute(\`data-imp-\${block.slug}\`, option)
517
+ recordExposure(block)
518
+ }, [block.slug])
519
+
520
+ return (
521
+ <>
522
+ <div
523
+ data-ob-imp={block.slug}
524
+ data-ob-opt={shown.option}
525
+ style={{ display: 'contents' }}
526
+ >
527
+ {shown.blocks.map((child) => (
528
+ <BlockRenderer block={child} key={child._uid} />
529
+ ))}
530
+ </div>
531
+ <script
532
+ type="application/json"
533
+ data-ob-variants={block.slug}
534
+ dangerouslySetInnerHTML={{ __html: json(others) }}
535
+ />
536
+ </>
537
+ )
538
+ }
539
+ \`\`\`
540
+
541
+ Its own \`display: contents\` is inline: a page with only after-load
542
+ placements has no head style. The exposure is recorded when the swap lands, not before: a visitor who never
543
+ gets the variant is not counted as seeing it.
544
+
545
+ ## Ending a test
546
+
547
+ Pausing it in Improve only stops new assignments. In Content, **Keep a
548
+ variant** on the block replaces it with that option's blocks; publishing the
549
+ page ends the test for everyone.
550
+ `;
551
+
215
552
  // Turns a model plan from the management API into text for a terminal, or
216
553
  // Markdown for a GitHub step summary or pull request comment.
217
554
  const ACTION_SIGN = {
@@ -535,8 +872,11 @@ const runInitCommand = async ({ url, token, dir, install: shouldInstall, cwd = p
535
872
  for (const [apiId, example] of Object.entries(examples)){
536
873
  await writeJson(node_path.join(examplesDir, `${apiId}.json`), example);
537
874
  }
875
+ if (improve.enabled) {
876
+ await promises.writeFile(node_path.join(examplesDir, '_improve.md'), IMPROVE_GUIDE);
877
+ }
538
878
  const count = Object.keys(examples).length;
539
- log(`Wrote ${count} ${count === 1 ? 'example' : 'examples'} to ${node_path.join(dir, 'examples')}${improve.enabled ? ', with the A/B test or flag block (_improve.json): improve is on' : ''}.`);
879
+ log(`Wrote ${count} ${count === 1 ? 'example' : 'examples'} to ${node_path.join(dir, 'examples')}${improve.enabled ? ', with the A/B test or flag block (_improve.json) and how to render it (_improve.md): improve is on' : ''}.`);
540
880
  if (await writeEnvExample(cwd, url)) {
541
881
  log('Added the token variables to .env.example.');
542
882
  }
@@ -548,7 +888,7 @@ const runInitCommand = async ({ url, token, dir, install: shouldInstall, cwd = p
548
888
  '- Set CONTENT_SPACE_ID and CONTENT_ENVIRONMENT, and pass them as space and environment to the ContentClient you read cms_token with, so a cms_token for another space reads nothing.',
549
889
  '- Fetch content with ContentClient from @obelism/content; the examples show what each type looks like.',
550
890
  ...improve.enabled ? [
551
- "- Render _improve blocks: ask improve's SDK for the visitor's option (getTestValue / getFlagValue with the block's slug) and render pickImproveVariant(block, option)."
891
+ `- Render _improve blocks as ${node_path.join(dir, 'examples', '_improve.md')} says: every variant tagged in the page, a script in the head, seedTestCookies at the edge and an exposure after paint (or pickImproveVariant on a page rendered per request).`
552
892
  ] : [],
553
893
  `- Keep the model in step with \`content model pull|plan|apply\` (${node_path.join(dir, 'content-model.json')}).`
554
894
  ].join('\n'));
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { TokenSource } from './oidc.mjs';
2
- 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, S as ShopifyItem, L as LinkTarget, d as Link, e as EntryStub, f as RichTextNode } from './types-b6jbj3g6.js';
3
- export { g as AssetProduct, C as ContentTypeAccent, h as ContentTypeDefinition, i as ContentTypeKind, j as Environment, F as FieldCondition, k as FieldDefinition, l as FieldGroup, m as FieldType, n as RichTextDocument, o as RichTextMark } from './types-b6jbj3g6.js';
2
+ 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, S as ShopifyItem, L as LinkTarget, d as Link, e as EntryStub, f as RichTextNode } from './types-e32p5mys.js';
3
+ export { g as AssetProduct, C as ContentTypeAccent, h as ContentTypeDefinition, i as ContentTypeKind, j as Environment, F as FieldCondition, k as FieldDefinition, l as FieldGroup, m as FieldType, n as RichTextDocument, o as RichTextMark, p as SharedFrom } from './types-e32p5mys.js';
4
4
 
5
5
  type ContentClientOptions = {
6
6
  token: TokenSource;
@@ -117,7 +117,7 @@ declare const imageSrcSet: (asset: (Pick<Asset, "url"> & Partial<Pick<Asset, "fo
117
117
 
118
118
  declare const IMPROVE_BLOCK = "_improve";
119
119
  declare const isImproveBlock: (block: Block) => block is ImproveBlock;
120
- declare const pickImproveVariant: <TBlock extends Block>(block: ImproveBlock<TBlock>, option: string | null | undefined, fallback?: string) => TBlock[];
120
+ declare const pickImproveVariant: <TBlock extends Block>(block: ImproveBlock<TBlock>, option: string | null | undefined, fallback?: string | null) => TBlock[];
121
121
 
122
122
  type LinkHrefOptions = {
123
123
  url?: ((path: string) => string) | undefined;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { TokenSource } from './oidc.js';
2
- 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, S as ShopifyItem, L as LinkTarget, d as Link, e as EntryStub, f as RichTextNode } from './types-b6jbj3g6.js';
3
- export { g as AssetProduct, C as ContentTypeAccent, h as ContentTypeDefinition, i as ContentTypeKind, j as Environment, F as FieldCondition, k as FieldDefinition, l as FieldGroup, m as FieldType, n as RichTextDocument, o as RichTextMark } from './types-b6jbj3g6.js';
2
+ 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, S as ShopifyItem, L as LinkTarget, d as Link, e as EntryStub, f as RichTextNode } from './types-e32p5mys.js';
3
+ export { g as AssetProduct, C as ContentTypeAccent, h as ContentTypeDefinition, i as ContentTypeKind, j as Environment, F as FieldCondition, k as FieldDefinition, l as FieldGroup, m as FieldType, n as RichTextDocument, o as RichTextMark, p as SharedFrom } from './types-e32p5mys.js';
4
4
 
5
5
  type ContentClientOptions = {
6
6
  token: TokenSource;
@@ -117,7 +117,7 @@ declare const imageSrcSet: (asset: (Pick<Asset, "url"> & Partial<Pick<Asset, "fo
117
117
 
118
118
  declare const IMPROVE_BLOCK = "_improve";
119
119
  declare const isImproveBlock: (block: Block) => block is ImproveBlock;
120
- declare const pickImproveVariant: <TBlock extends Block>(block: ImproveBlock<TBlock>, option: string | null | undefined, fallback?: string) => TBlock[];
120
+ declare const pickImproveVariant: <TBlock extends Block>(block: ImproveBlock<TBlock>, option: string | null | undefined, fallback?: string | null) => TBlock[];
121
121
 
122
122
  type LinkHrefOptions = {
123
123
  url?: ((path: string) => string) | undefined;
package/dist/index.js CHANGED
@@ -33,6 +33,9 @@ const fieldParams = (fields)=>Object.fromEntries(Object.entries(fields ?? {}).ma
33
33
  ];
34
34
  }));
35
35
  const trimSlashes = (path)=>path.replace(/^\/+|\/+$/g, '');
36
+ // `/space_…/<environment>`: the part of a scoped delivery path between
37
+ // DELIVERY_PATH and the operation.
38
+ const SCOPE = /^\/space_[^/]+\/[^/]+$/;
36
39
  // Reads one space environment's content through the delivery API. The token
37
40
  // decides the space, the environment and whether drafts are visible (for a
38
41
  // host token, with `drafts`).
@@ -42,11 +45,17 @@ const trimSlashes = (path)=>path.replace(/^\/+|\/+$/g, '');
42
45
  class ContentClient {
43
46
  #token;
44
47
  #place;
48
+ // Where the API answers this client's token: it sends a token that reads
49
+ // published content to `/api/delivery/v1/{space}/{environment}/…` with a
50
+ // 308, so the client asks there directly after the first one. Never used
51
+ // for a request that reads drafts, which the scoped path refuses.
52
+ #scope;
45
53
  #defaults;
46
54
  #timeout;
47
55
  #init;
48
56
  #fetch;
49
57
  constructor({ token, drafts, space, environment, url, locale, depth, richText, timeout, init, fetch: fetchImpl }){
58
+ this.#scope = null;
50
59
  // The page at a path (`blog/hello`), or null when there is none. A site's
51
60
  // catch-all route asks for this. The path is looked up in `locale`'s
52
61
  // paths (the default locale's when left out or `*`): pages can have a
@@ -171,7 +180,7 @@ class ContentClient {
171
180
  }
172
181
  // For a 404, null when `notFound` is set, or the body as is for 'body';
173
182
  // throws ContentApiError otherwise.
174
- async #request(path, query, { init, ...options }, { notFound = false, defaults = true } = {}) {
183
+ async #request(path, query, { init, ...options }, { notFound = false, defaults = true, scoped = true } = {}) {
175
184
  // Checked here rather than in the constructor, so a module creating a
176
185
  // client still loads (and a build still runs) without the variable.
177
186
  if (!this.#token) throw new Error('ContentClient needs a token');
@@ -190,7 +199,8 @@ class ContentClient {
190
199
  if (value !== undefined) params.set(key, String(value));
191
200
  }
192
201
  const search = params.size > 0 ? `?${params}` : '';
193
- const url = `${this.url}${config.DELIVERY_PATH}${path}${search}`;
202
+ const scope = drafts || !scoped ? null : this.#scope;
203
+ const url = `${this.url}${config.DELIVERY_PATH}${scope ?? ''}${path}${search}`;
194
204
  const merged = {
195
205
  ...this.#init,
196
206
  ...init
@@ -215,6 +225,21 @@ class ContentClient {
215
225
  timeout
216
226
  ]) : timeout
217
227
  });
228
+ // A token that changed, or now reads drafts, is refused on the path
229
+ // learned for the one before it: ask unscoped once, and learn again.
230
+ if (scope && (response.status === 401 || response.status === 403)) {
231
+ if (this.#scope === scope) this.#scope = null;
232
+ await response.body?.cancel();
233
+ return this.#request(path, query, {
234
+ init,
235
+ ...options
236
+ }, {
237
+ notFound,
238
+ defaults,
239
+ scoped: false
240
+ });
241
+ }
242
+ if (!scope && !drafts && response.redirected) this.#learnScope(response, path);
218
243
  if (notFound === true && response.status === 404) return null;
219
244
  const text = await response.text();
220
245
  let body = text;
@@ -229,6 +254,18 @@ class ContentClient {
229
254
  if (!response.ok) throw new ContentApiError(response.status, url, body);
230
255
  return body;
231
256
  }
257
+ #learnScope(response, path) {
258
+ let redirected;
259
+ try {
260
+ redirected = new URL(response.url);
261
+ } catch {
262
+ return;
263
+ }
264
+ const { origin, pathname } = redirected;
265
+ if (origin !== new URL(this.url).origin || !pathname.startsWith(`${config.DELIVERY_PATH}/`) || !pathname.endsWith(path)) return;
266
+ const scope = pathname.slice(config.DELIVERY_PATH.length, pathname.length - path.length);
267
+ if (SCOPE.test(scope)) this.#scope = scope;
268
+ }
232
269
  }
233
270
  const withoutUndefined = (value)=>Object.fromEntries(Object.entries(value).filter(([, entry])=>entry !== undefined));
234
271
  const headersToObject = (headers)=>headers ? Object.fromEntries(new Headers(headers)) : {};
@@ -277,11 +314,14 @@ const isImproveBlock = (block)=>block._type === IMPROVE_BLOCK;
277
314
  // improve's SDK returned for the visitor (getTestValue / getFlagValue). It
278
315
  // is null when the test or flag isn't active or the visitor isn't in it;
279
316
  // then, or for an option the block has no variant for, the `fallback`
280
- // option's variant, else the first one (usually the control).
317
+ // option's variant, else the first one.
281
318
  const pickImproveVariant = (block, option, fallback)=>{
282
319
  const variants = block.variants ?? {};
283
320
  if (option && variants[option]) return variants[option];
284
- if (fallback && variants[fallback]) return variants[fallback];
321
+ // The arm Improve gives a visitor outside the test, when the caller names
322
+ // none: it is not always the first variant.
323
+ const behind = fallback ?? block.fallback;
324
+ if (behind && variants[behind]) return variants[behind];
285
325
  return Object.values(variants)[0] ?? [];
286
326
  };
287
327
 
package/dist/index.mjs CHANGED
@@ -31,6 +31,9 @@ const fieldParams = (fields)=>Object.fromEntries(Object.entries(fields ?? {}).ma
31
31
  ];
32
32
  }));
33
33
  const trimSlashes = (path)=>path.replace(/^\/+|\/+$/g, '');
34
+ // `/space_…/<environment>`: the part of a scoped delivery path between
35
+ // DELIVERY_PATH and the operation.
36
+ const SCOPE = /^\/space_[^/]+\/[^/]+$/;
34
37
  // Reads one space environment's content through the delivery API. The token
35
38
  // decides the space, the environment and whether drafts are visible (for a
36
39
  // host token, with `drafts`).
@@ -40,11 +43,17 @@ const trimSlashes = (path)=>path.replace(/^\/+|\/+$/g, '');
40
43
  class ContentClient {
41
44
  #token;
42
45
  #place;
46
+ // Where the API answers this client's token: it sends a token that reads
47
+ // published content to `/api/delivery/v1/{space}/{environment}/…` with a
48
+ // 308, so the client asks there directly after the first one. Never used
49
+ // for a request that reads drafts, which the scoped path refuses.
50
+ #scope;
43
51
  #defaults;
44
52
  #timeout;
45
53
  #init;
46
54
  #fetch;
47
55
  constructor({ token, drafts, space, environment, url, locale, depth, richText, timeout, init, fetch: fetchImpl }){
56
+ this.#scope = null;
48
57
  // The page at a path (`blog/hello`), or null when there is none. A site's
49
58
  // catch-all route asks for this. The path is looked up in `locale`'s
50
59
  // paths (the default locale's when left out or `*`): pages can have a
@@ -169,7 +178,7 @@ class ContentClient {
169
178
  }
170
179
  // For a 404, null when `notFound` is set, or the body as is for 'body';
171
180
  // throws ContentApiError otherwise.
172
- async #request(path, query, { init, ...options }, { notFound = false, defaults = true } = {}) {
181
+ async #request(path, query, { init, ...options }, { notFound = false, defaults = true, scoped = true } = {}) {
173
182
  // Checked here rather than in the constructor, so a module creating a
174
183
  // client still loads (and a build still runs) without the variable.
175
184
  if (!this.#token) throw new Error('ContentClient needs a token');
@@ -188,7 +197,8 @@ class ContentClient {
188
197
  if (value !== undefined) params.set(key, String(value));
189
198
  }
190
199
  const search = params.size > 0 ? `?${params}` : '';
191
- const url = `${this.url}${DELIVERY_PATH}${path}${search}`;
200
+ const scope = drafts || !scoped ? null : this.#scope;
201
+ const url = `${this.url}${DELIVERY_PATH}${scope ?? ''}${path}${search}`;
192
202
  const merged = {
193
203
  ...this.#init,
194
204
  ...init
@@ -213,6 +223,21 @@ class ContentClient {
213
223
  timeout
214
224
  ]) : timeout
215
225
  });
226
+ // A token that changed, or now reads drafts, is refused on the path
227
+ // learned for the one before it: ask unscoped once, and learn again.
228
+ if (scope && (response.status === 401 || response.status === 403)) {
229
+ if (this.#scope === scope) this.#scope = null;
230
+ await response.body?.cancel();
231
+ return this.#request(path, query, {
232
+ init,
233
+ ...options
234
+ }, {
235
+ notFound,
236
+ defaults,
237
+ scoped: false
238
+ });
239
+ }
240
+ if (!scope && !drafts && response.redirected) this.#learnScope(response, path);
216
241
  if (notFound === true && response.status === 404) return null;
217
242
  const text = await response.text();
218
243
  let body = text;
@@ -227,6 +252,18 @@ class ContentClient {
227
252
  if (!response.ok) throw new ContentApiError(response.status, url, body);
228
253
  return body;
229
254
  }
255
+ #learnScope(response, path) {
256
+ let redirected;
257
+ try {
258
+ redirected = new URL(response.url);
259
+ } catch {
260
+ return;
261
+ }
262
+ const { origin, pathname } = redirected;
263
+ if (origin !== new URL(this.url).origin || !pathname.startsWith(`${DELIVERY_PATH}/`) || !pathname.endsWith(path)) return;
264
+ const scope = pathname.slice(DELIVERY_PATH.length, pathname.length - path.length);
265
+ if (SCOPE.test(scope)) this.#scope = scope;
266
+ }
230
267
  }
231
268
  const withoutUndefined = (value)=>Object.fromEntries(Object.entries(value).filter(([, entry])=>entry !== undefined));
232
269
  const headersToObject = (headers)=>headers ? Object.fromEntries(new Headers(headers)) : {};
@@ -275,11 +312,14 @@ const isImproveBlock = (block)=>block._type === IMPROVE_BLOCK;
275
312
  // improve's SDK returned for the visitor (getTestValue / getFlagValue). It
276
313
  // is null when the test or flag isn't active or the visitor isn't in it;
277
314
  // then, or for an option the block has no variant for, the `fallback`
278
- // option's variant, else the first one (usually the control).
315
+ // option's variant, else the first one.
279
316
  const pickImproveVariant = (block, option, fallback)=>{
280
317
  const variants = block.variants ?? {};
281
318
  if (option && variants[option]) return variants[option];
282
- if (fallback && variants[fallback]) return variants[fallback];
319
+ // The arm Improve gives a visitor outside the test, when the caller names
320
+ // none: it is not always the first variant.
321
+ const behind = fallback ?? block.fallback;
322
+ if (behind && variants[behind]) return variants[behind];
283
323
  return Object.values(variants)[0] ?? [];
284
324
  };
285
325
 
@@ -72,10 +72,18 @@ type Link = {
72
72
  type Block<TFields extends Record<string, unknown> = Record<string, unknown>, TType extends string = string> = {
73
73
  _uid: string;
74
74
  _type: TType;
75
+ _sharedFrom?: SharedFrom;
75
76
  } & TFields;
77
+ type SharedFrom = {
78
+ id: string;
79
+ name: string;
80
+ heldAt?: string;
81
+ };
76
82
  type ImproveBlock<TBlock extends Block = Block> = Block<{
77
83
  kind: 'test' | 'flag';
78
84
  slug: string;
85
+ fallback?: string | null;
86
+ mode?: 'instant' | 'after-load' | null;
79
87
  variants: Record<string, TBlock[]>;
80
88
  }, '_improve'>;
81
89
  type RichTextMark = {
@@ -225,5 +233,5 @@ type ModelFile = {
225
233
  contentTypes: ContentTypeDefinition[];
226
234
  };
227
235
 
228
- export type { Asset as A, Block as B, ContentTypeAccent as C, Entry as E, FieldCondition as F, ImproveBlock as I, LinkTarget as L, ModelFile as M, RichTextFormat as R, ShopifyItem as S, RedirectList as a, Redirect as b, EntryList as c, Link as d, EntryStub as e, RichTextNode as f, AssetProduct as g, ContentTypeDefinition as h, ContentTypeKind as i, Environment as j, FieldDefinition as k, FieldGroup as l, FieldType as m, RichTextDocument as n, RichTextMark as o };
229
- //# sourceMappingURL=types-b6jbj3g6.d.ts.map
236
+ export type { Asset as A, Block as B, ContentTypeAccent as C, Entry as E, FieldCondition as F, ImproveBlock as I, LinkTarget as L, ModelFile as M, RichTextFormat as R, ShopifyItem as S, RedirectList as a, Redirect as b, EntryList as c, Link as d, EntryStub as e, RichTextNode as f, AssetProduct as g, ContentTypeDefinition as h, ContentTypeKind as i, Environment as j, FieldDefinition as k, FieldGroup as l, FieldType as m, RichTextDocument as n, RichTextMark as o, SharedFrom as p };
237
+ //# sourceMappingURL=types-e32p5mys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-e32p5mys.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;AACA;AACA;AACA,eAAe,YAAY;AAC3B;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;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,UAAU;AACf;AACA,oBAAoB,MAAM;AAC1B;AACA;AACA;AACA,KAAK,IAAI;AACT;AACA,WAAW,UAAU;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,WAAW;AACxB,WAAW,UAAU;AACrB;AACA;AACA;AACA;AACA,gBAAgB,WAAW;AAC3B,WAAW,UAAU;AACrB;AACA;AACA;AACA,KAAK,KAAK,iBAAiB,MAAM,oBAAoB,MAAM;AAC3D;AACA;AACA,kBAAkB,UAAU;AAC5B;AACA,KAAK,UAAU;AACf;AACA;AACA;AACA;AACA,KAAK,YAAY,gBAAgB,KAAK,GAAG,KAAK,IAAI,KAAK;AACvD;AACA;AACA;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,WAAW;AAChB;AACA;AACA;AACA,KAAK,eAAe;AACpB,KAAK,SAAS;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB,MAAM;AACvB;AACA,WAAW,cAAc;AACzB;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA,KAAK,UAAU;AACf;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB,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;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;AACA,aAAa,iBAAiB;AAC9B;AACA,aAAa,UAAU;AACvB,YAAY,eAAe;AAC3B;AACA,KAAK,SAAS;AACd;AACA;AACA,kBAAkB,qBAAqB;AACvC;;","names":[]}
@@ -17,6 +17,10 @@ type WebhookEventOf<TType extends string, TData> = {
17
17
  type EntryPublishedEvent = WebhookEventOf<'entry.published', {
18
18
  entry: WebhookEntry;
19
19
  }>;
20
+ type BlockPublishedEvent = WebhookEventOf<'block.published', {
21
+ entry: WebhookEntry;
22
+ entries: WebhookEntry[];
23
+ }>;
20
24
  type EntryUnpublishedEvent = WebhookEventOf<'entry.unpublished', {
21
25
  entry: WebhookEntry;
22
26
  }>;
@@ -56,6 +60,22 @@ type EntryReviewClosedEvent = WebhookEventOf<'entry.review_closed', {
56
60
  revision: number;
57
61
  reason?: string;
58
62
  }>;
63
+ type WebhookRelease = {
64
+ id: string;
65
+ name: string;
66
+ };
67
+ type WebhookReleaseEntry = WebhookEntry & {
68
+ action: 'publish' | 'unpublish';
69
+ };
70
+ type ReleasePublishedEvent = WebhookEventOf<'release.published', {
71
+ release: WebhookRelease;
72
+ entries: WebhookReleaseEntry[];
73
+ }>;
74
+ type ReleaseReviewRequestedEvent = WebhookEventOf<'release.review_requested', {
75
+ release: WebhookRelease;
76
+ note: string | null;
77
+ entries: WebhookReleaseEntry[];
78
+ }>;
59
79
  type WebhookShopifyItem = {
60
80
  id: string;
61
81
  handle: string | null;
@@ -79,7 +99,7 @@ type CollectionDeletedEvent = WebhookEventOf<'collection.deleted', StoreChange &
79
99
  collection: WebhookShopifyItem;
80
100
  }>;
81
101
  type WebhookTestEvent = WebhookEventOf<'webhook.test', Record<string, never>>;
82
- type WebhookEvent = EntryPublishedEvent | EntryUnpublishedEvent | EntryDeletedEvent | EntryMovedEvent | AssetUpdatedEvent | ModelChangedEvent | LocalesChangedEvent | EntryReviewRequestedEvent | EntryReviewClosedEvent | ProductUpdatedEvent | ProductDeletedEvent | CollectionUpdatedEvent | CollectionDeletedEvent | WebhookTestEvent;
102
+ type WebhookEvent = EntryPublishedEvent | BlockPublishedEvent | EntryUnpublishedEvent | EntryDeletedEvent | EntryMovedEvent | AssetUpdatedEvent | ModelChangedEvent | LocalesChangedEvent | EntryReviewRequestedEvent | EntryReviewClosedEvent | ReleasePublishedEvent | ReleaseReviewRequestedEvent | ProductUpdatedEvent | ProductDeletedEvent | CollectionUpdatedEvent | CollectionDeletedEvent | WebhookTestEvent;
83
103
  type WebhookEventType = WebhookEvent['type'];
84
104
  declare class WebhookVerificationError extends Error {
85
105
  constructor(message: string);
@@ -103,5 +123,5 @@ type WebhookHandlerOptions = {
103
123
  declare const createWebhookHandler: ({ secret, onEvent, tolerance }: WebhookHandlerOptions) => (request: Request) => Promise<Response>;
104
124
 
105
125
  export { WebhookVerificationError, createWebhookHandler, verifyWebhook };
106
- export type { AssetUpdatedEvent, CollectionDeletedEvent, CollectionUpdatedEvent, EntryDeletedEvent, EntryMovedEvent, EntryPublishedEvent, EntryReviewClosedEvent, EntryReviewRequestedEvent, EntryUnpublishedEvent, LocalesChangedEvent, ModelChangedEvent, ProductDeletedEvent, ProductUpdatedEvent, VerifyWebhookOptions, WebhookEntry, WebhookEvent, WebhookEventType, WebhookHandlerOptions, WebhookMessage, WebhookShopifyItem, WebhookTestEvent };
126
+ export type { AssetUpdatedEvent, BlockPublishedEvent, CollectionDeletedEvent, CollectionUpdatedEvent, EntryDeletedEvent, EntryMovedEvent, EntryPublishedEvent, EntryReviewClosedEvent, EntryReviewRequestedEvent, EntryUnpublishedEvent, LocalesChangedEvent, ModelChangedEvent, ProductDeletedEvent, ProductUpdatedEvent, ReleasePublishedEvent, ReleaseReviewRequestedEvent, VerifyWebhookOptions, WebhookEntry, WebhookEvent, WebhookEventType, WebhookHandlerOptions, WebhookMessage, WebhookRelease, WebhookReleaseEntry, WebhookShopifyItem, WebhookTestEvent };
107
127
  //# sourceMappingURL=webhooks.d.mts.map
@@ -1 +1 @@
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,yBAAyB,GAAG,cAAc;AAC/C,WAAW,YAAY;AACvB;AACA;AACA;AACA,KAAK,sBAAsB,GAAG,cAAc;AAC5C,WAAW,YAAY;AACvB;AACA;AACA;AACA;AACA,KAAK,kBAAkB;AACvB;AACA;AACA;AACA,KAAK,WAAW;AAChB,aAAa,YAAY;AACzB;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc,oBAAoB,WAAW;AACxE,aAAa,kBAAkB;AAC/B,iBAAiB,kBAAkB;AACnC;AACA,KAAK,mBAAmB,GAAG,cAAc,oBAAoB,WAAW;AACxE,aAAa,kBAAkB;AAC/B,iBAAiB,kBAAkB;AACnC;AACA,KAAK,sBAAsB,GAAG,cAAc,uBAAuB,WAAW;AAC9E,gBAAgB,kBAAkB;AAClC;AACA,KAAK,sBAAsB,GAAG,cAAc,uBAAuB,WAAW;AAC9E,gBAAgB,kBAAkB;AAClC;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,yBAAyB,GAAG,sBAAsB,GAAG,mBAAmB,GAAG,mBAAmB,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,gBAAgB;AACzU,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,mBAAmB,GAAG,cAAc;AACzC,WAAW,YAAY;AACvB,aAAa,YAAY;AACzB;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,yBAAyB,GAAG,cAAc;AAC/C,WAAW,YAAY;AACvB;AACA;AACA;AACA,KAAK,sBAAsB,GAAG,cAAc;AAC5C,WAAW,YAAY;AACvB;AACA;AACA;AACA;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA,KAAK,mBAAmB,GAAG,YAAY;AACvC;AACA;AACA,KAAK,qBAAqB,GAAG,cAAc;AAC3C,aAAa,cAAc;AAC3B,aAAa,mBAAmB;AAChC;AACA,KAAK,2BAA2B,GAAG,cAAc;AACjD,aAAa,cAAc;AAC3B;AACA,aAAa,mBAAmB;AAChC;AACA,KAAK,kBAAkB;AACvB;AACA;AACA;AACA,KAAK,WAAW;AAChB,aAAa,YAAY;AACzB;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc,oBAAoB,WAAW;AACxE,aAAa,kBAAkB;AAC/B,iBAAiB,kBAAkB;AACnC;AACA,KAAK,mBAAmB,GAAG,cAAc,oBAAoB,WAAW;AACxE,aAAa,kBAAkB;AAC/B,iBAAiB,kBAAkB;AACnC;AACA,KAAK,sBAAsB,GAAG,cAAc,uBAAuB,WAAW;AAC9E,gBAAgB,kBAAkB;AAClC;AACA,KAAK,sBAAsB,GAAG,cAAc,uBAAuB,WAAW;AAC9E,gBAAgB,kBAAkB;AAClC;AACA,KAAK,gBAAgB,GAAG,cAAc,iBAAiB,MAAM;AAC7D,KAAK,YAAY,GAAG,mBAAmB,GAAG,mBAAmB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,eAAe,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,yBAAyB,GAAG,sBAAsB,GAAG,qBAAqB,GAAG,2BAA2B,GAAG,mBAAmB,GAAG,mBAAmB,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,gBAAgB;AACrZ,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":[]}
@@ -17,6 +17,10 @@ type WebhookEventOf<TType extends string, TData> = {
17
17
  type EntryPublishedEvent = WebhookEventOf<'entry.published', {
18
18
  entry: WebhookEntry;
19
19
  }>;
20
+ type BlockPublishedEvent = WebhookEventOf<'block.published', {
21
+ entry: WebhookEntry;
22
+ entries: WebhookEntry[];
23
+ }>;
20
24
  type EntryUnpublishedEvent = WebhookEventOf<'entry.unpublished', {
21
25
  entry: WebhookEntry;
22
26
  }>;
@@ -56,6 +60,22 @@ type EntryReviewClosedEvent = WebhookEventOf<'entry.review_closed', {
56
60
  revision: number;
57
61
  reason?: string;
58
62
  }>;
63
+ type WebhookRelease = {
64
+ id: string;
65
+ name: string;
66
+ };
67
+ type WebhookReleaseEntry = WebhookEntry & {
68
+ action: 'publish' | 'unpublish';
69
+ };
70
+ type ReleasePublishedEvent = WebhookEventOf<'release.published', {
71
+ release: WebhookRelease;
72
+ entries: WebhookReleaseEntry[];
73
+ }>;
74
+ type ReleaseReviewRequestedEvent = WebhookEventOf<'release.review_requested', {
75
+ release: WebhookRelease;
76
+ note: string | null;
77
+ entries: WebhookReleaseEntry[];
78
+ }>;
59
79
  type WebhookShopifyItem = {
60
80
  id: string;
61
81
  handle: string | null;
@@ -79,7 +99,7 @@ type CollectionDeletedEvent = WebhookEventOf<'collection.deleted', StoreChange &
79
99
  collection: WebhookShopifyItem;
80
100
  }>;
81
101
  type WebhookTestEvent = WebhookEventOf<'webhook.test', Record<string, never>>;
82
- type WebhookEvent = EntryPublishedEvent | EntryUnpublishedEvent | EntryDeletedEvent | EntryMovedEvent | AssetUpdatedEvent | ModelChangedEvent | LocalesChangedEvent | EntryReviewRequestedEvent | EntryReviewClosedEvent | ProductUpdatedEvent | ProductDeletedEvent | CollectionUpdatedEvent | CollectionDeletedEvent | WebhookTestEvent;
102
+ type WebhookEvent = EntryPublishedEvent | BlockPublishedEvent | EntryUnpublishedEvent | EntryDeletedEvent | EntryMovedEvent | AssetUpdatedEvent | ModelChangedEvent | LocalesChangedEvent | EntryReviewRequestedEvent | EntryReviewClosedEvent | ReleasePublishedEvent | ReleaseReviewRequestedEvent | ProductUpdatedEvent | ProductDeletedEvent | CollectionUpdatedEvent | CollectionDeletedEvent | WebhookTestEvent;
83
103
  type WebhookEventType = WebhookEvent['type'];
84
104
  declare class WebhookVerificationError extends Error {
85
105
  constructor(message: string);
@@ -103,5 +123,5 @@ type WebhookHandlerOptions = {
103
123
  declare const createWebhookHandler: ({ secret, onEvent, tolerance }: WebhookHandlerOptions) => (request: Request) => Promise<Response>;
104
124
 
105
125
  export { WebhookVerificationError, createWebhookHandler, verifyWebhook };
106
- export type { AssetUpdatedEvent, CollectionDeletedEvent, CollectionUpdatedEvent, EntryDeletedEvent, EntryMovedEvent, EntryPublishedEvent, EntryReviewClosedEvent, EntryReviewRequestedEvent, EntryUnpublishedEvent, LocalesChangedEvent, ModelChangedEvent, ProductDeletedEvent, ProductUpdatedEvent, VerifyWebhookOptions, WebhookEntry, WebhookEvent, WebhookEventType, WebhookHandlerOptions, WebhookMessage, WebhookShopifyItem, WebhookTestEvent };
126
+ export type { AssetUpdatedEvent, BlockPublishedEvent, CollectionDeletedEvent, CollectionUpdatedEvent, EntryDeletedEvent, EntryMovedEvent, EntryPublishedEvent, EntryReviewClosedEvent, EntryReviewRequestedEvent, EntryUnpublishedEvent, LocalesChangedEvent, ModelChangedEvent, ProductDeletedEvent, ProductUpdatedEvent, ReleasePublishedEvent, ReleaseReviewRequestedEvent, VerifyWebhookOptions, WebhookEntry, WebhookEvent, WebhookEventType, WebhookHandlerOptions, WebhookMessage, WebhookRelease, WebhookReleaseEntry, WebhookShopifyItem, WebhookTestEvent };
107
127
  //# sourceMappingURL=webhooks.d.ts.map
@@ -1 +1 @@
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,yBAAyB,GAAG,cAAc;AAC/C,WAAW,YAAY;AACvB;AACA;AACA;AACA,KAAK,sBAAsB,GAAG,cAAc;AAC5C,WAAW,YAAY;AACvB;AACA;AACA;AACA;AACA,KAAK,kBAAkB;AACvB;AACA;AACA;AACA,KAAK,WAAW;AAChB,aAAa,YAAY;AACzB;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc,oBAAoB,WAAW;AACxE,aAAa,kBAAkB;AAC/B,iBAAiB,kBAAkB;AACnC;AACA,KAAK,mBAAmB,GAAG,cAAc,oBAAoB,WAAW;AACxE,aAAa,kBAAkB;AAC/B,iBAAiB,kBAAkB;AACnC;AACA,KAAK,sBAAsB,GAAG,cAAc,uBAAuB,WAAW;AAC9E,gBAAgB,kBAAkB;AAClC;AACA,KAAK,sBAAsB,GAAG,cAAc,uBAAuB,WAAW;AAC9E,gBAAgB,kBAAkB;AAClC;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,yBAAyB,GAAG,sBAAsB,GAAG,mBAAmB,GAAG,mBAAmB,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,gBAAgB;AACzU,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,mBAAmB,GAAG,cAAc;AACzC,WAAW,YAAY;AACvB,aAAa,YAAY;AACzB;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,yBAAyB,GAAG,cAAc;AAC/C,WAAW,YAAY;AACvB;AACA;AACA;AACA,KAAK,sBAAsB,GAAG,cAAc;AAC5C,WAAW,YAAY;AACvB;AACA;AACA;AACA;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA,KAAK,mBAAmB,GAAG,YAAY;AACvC;AACA;AACA,KAAK,qBAAqB,GAAG,cAAc;AAC3C,aAAa,cAAc;AAC3B,aAAa,mBAAmB;AAChC;AACA,KAAK,2BAA2B,GAAG,cAAc;AACjD,aAAa,cAAc;AAC3B;AACA,aAAa,mBAAmB;AAChC;AACA,KAAK,kBAAkB;AACvB;AACA;AACA;AACA,KAAK,WAAW;AAChB,aAAa,YAAY;AACzB;AACA;AACA,KAAK,mBAAmB,GAAG,cAAc,oBAAoB,WAAW;AACxE,aAAa,kBAAkB;AAC/B,iBAAiB,kBAAkB;AACnC;AACA,KAAK,mBAAmB,GAAG,cAAc,oBAAoB,WAAW;AACxE,aAAa,kBAAkB;AAC/B,iBAAiB,kBAAkB;AACnC;AACA,KAAK,sBAAsB,GAAG,cAAc,uBAAuB,WAAW;AAC9E,gBAAgB,kBAAkB;AAClC;AACA,KAAK,sBAAsB,GAAG,cAAc,uBAAuB,WAAW;AAC9E,gBAAgB,kBAAkB;AAClC;AACA,KAAK,gBAAgB,GAAG,cAAc,iBAAiB,MAAM;AAC7D,KAAK,YAAY,GAAG,mBAAmB,GAAG,mBAAmB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,eAAe,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,yBAAyB,GAAG,sBAAsB,GAAG,qBAAqB,GAAG,2BAA2B,GAAG,mBAAmB,GAAG,mBAAmB,GAAG,sBAAsB,GAAG,sBAAsB,GAAG,gBAAgB;AACrZ,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.28.4",
4
+ "version": "0.30.0",
5
5
  "keywords": [
6
6
  "cms",
7
7
  "headless-cms",
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-b6jbj3g6.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;AACA;AACA;AACA,eAAe,YAAY;AAC3B;AACA,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;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,UAAU;AACf;AACA,oBAAoB,MAAM;AAC1B;AACA;AACA;AACA,KAAK,IAAI;AACT;AACA,WAAW,UAAU;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,WAAW;AACxB,WAAW,UAAU;AACrB;AACA;AACA;AACA;AACA,gBAAgB,WAAW;AAC3B,WAAW,UAAU;AACrB;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,WAAW;AAChB;AACA;AACA;AACA,KAAK,eAAe;AACpB,KAAK,SAAS;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB,MAAM;AACvB;AACA,WAAW,cAAc;AACzB;AACA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA,KAAK,UAAU;AACf;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB,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;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;AACA,aAAa,iBAAiB;AAC9B;AACA,aAAa,UAAU;AACvB,YAAY,eAAe;AAC3B;AACA,KAAK,SAAS;AACd;AACA;AACA,kBAAkB,qBAAqB;AACvC;;","names":[]}