@obelism/content 0.16.0 → 0.18.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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-m3j4tltp.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-m3j4tltp.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, S as ShopifyItem, L as LinkTarget, d as Link, e as EntryStub, f as RichTextNode } from './types-mtjco3a5.js';
2
+ export { C as ContentTypeDefinition, g as ContentTypeKind, h as Environment, F as FieldDefinition, i as FieldType, j as RichTextDocument, k as RichTextMark } from './types-mtjco3a5.js';
3
3
 
4
4
  type ContentClientOptions = {
5
5
  token: string;
@@ -60,6 +60,8 @@ declare const DEFAULT_URL = "https://content.obelism.studio";
60
60
 
61
61
  declare const UID_ATTRIBUTE = "data-cms-uid";
62
62
  declare const ENTRY_ATTRIBUTE = "data-cms-entry";
63
+ declare const PRODUCT_ATTRIBUTE = "data-cms-product";
64
+ declare const PRODUCT_HANDLE_ATTRIBUTE = "data-cms-product-handle";
63
65
  type Editable = {
64
66
  'data-cms-uid'?: string;
65
67
  'data-cms-entry'?: string;
@@ -70,6 +72,14 @@ declare const editable: (target: {
70
72
  id: string;
71
73
  fields: unknown;
72
74
  } | null | undefined) => Editable;
75
+ type EditableProduct = {
76
+ 'data-cms-product'?: string;
77
+ 'data-cms-product-handle'?: string;
78
+ };
79
+ declare const editableProduct: (product: {
80
+ id: string;
81
+ handle: string;
82
+ } | null | undefined) => EditableProduct;
73
83
 
74
84
  declare class ContentApiError extends Error {
75
85
  readonly status: number;
@@ -96,6 +106,12 @@ declare const IMPROVE_BLOCK = "_improve";
96
106
  declare const isImproveBlock: (block: Block) => block is ImproveBlock;
97
107
  declare const pickImproveVariant: <TBlock extends Block>(block: ImproveBlock<TBlock>, option: string | null | undefined, fallback?: string) => TBlock[];
98
108
 
109
+ type LinkHrefOptions = {
110
+ url?: ((path: string) => string) | undefined;
111
+ product?: ((product: ShopifyItem, page: LinkTarget | null) => string) | undefined;
112
+ };
113
+ declare const linkHref: (link: Link | null | undefined, { url, product, }?: LinkHrefOptions) => string | null;
114
+
99
115
  declare const PREVIEW_PARAMS: {
100
116
  readonly preview: "cms_preview";
101
117
  readonly entry: "cms_entry";
@@ -110,6 +126,50 @@ type PreviewParams = {
110
126
  type SearchParamsInput = URL | URLSearchParams | string | Record<string, string | string[] | undefined>;
111
127
  declare const readPreviewParams: (input: SearchParamsInput) => PreviewParams | null;
112
128
 
129
+ declare const SHOPIFY_STOREFRONT_API_VERSION = "2026-10";
130
+ type ShopifyStorefrontOptions = {
131
+ shop: string;
132
+ storefrontToken: string;
133
+ country?: string | undefined;
134
+ language?: string | undefined;
135
+ init?: RequestInit | undefined;
136
+ fetch?: typeof fetch | undefined;
137
+ };
138
+ type ShopifyMoney = {
139
+ amount: string;
140
+ currencyCode: string;
141
+ };
142
+ type ShopifyImage = {
143
+ url: string;
144
+ altText: string | null;
145
+ width: number | null;
146
+ height: number | null;
147
+ };
148
+ type ShopifyProduct = {
149
+ id: string;
150
+ handle: string;
151
+ title: string;
152
+ availableForSale: boolean;
153
+ featuredImage: ShopifyImage | null;
154
+ priceRange: {
155
+ minVariantPrice: ShopifyMoney;
156
+ };
157
+ };
158
+ type ShopifyCollection = {
159
+ id: string;
160
+ handle: string;
161
+ title: string;
162
+ products: ShopifyProduct[];
163
+ };
164
+ declare class ShopifyStorefrontError extends Error {
165
+ readonly status: number;
166
+ constructor(status: number, message: string);
167
+ }
168
+ declare const fetchShopifyCollection: (item: ShopifyItem, options: ShopifyStorefrontOptions & {
169
+ first?: number | undefined;
170
+ }) => Promise<ShopifyCollection | null>;
171
+ declare const fetchShopifyProducts: (items: ShopifyItem | ShopifyItem[] | null | undefined, options: ShopifyStorefrontOptions) => Promise<ShopifyProduct[]>;
172
+
113
173
  declare const SAFE_HREF: RegExp;
114
174
  declare const escapeHtml: (value: string) => string;
115
175
  type RichTextHtmlOptions = {
@@ -135,6 +195,6 @@ type SitemapOptions = LocalizedUrlOptions & {
135
195
  };
136
196
  declare const sitemapXml: (entries: Iterable<Page>, { exclude, ...options }: SitemapOptions) => string;
137
197
 
138
- 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, alternateLinks, editable, escapeHtml, headingLevel, imageSrcSet, imageUrl, isImproveBlock, pickImproveVariant, readPreviewParams, richTextToHtml, richTextToPlainText, sitemapXml };
139
- export type { AlternateLink, ContentClientOptions, EntriesQuery, ImageTransform, LocalizedUrlOptions, PageLookup, PreviewParams, RedirectsQuery, RequestOptions, RichTextHtmlOptions, SitemapOptions };
198
+ export { Asset, Block, ContentApiError, ContentClient, DEFAULT_URL, ENTRY_ATTRIBUTE, Entry, EntryList, EntryStub, IMPROVE_BLOCK, ImproveBlock, Link, LinkTarget, ModelFile, PREVIEW_PARAMS, PRODUCT_ATTRIBUTE, PRODUCT_HANDLE_ATTRIBUTE, Redirect, RedirectList, RichTextFormat, RichTextNode, SAFE_HREF, SHOPIFY_STOREFRONT_API_VERSION, ShopifyItem, ShopifyStorefrontError, UID_ATTRIBUTE, alternateLinks, editable, editableProduct, escapeHtml, fetchShopifyCollection, fetchShopifyProducts, headingLevel, imageSrcSet, imageUrl, isImproveBlock, linkHref, pickImproveVariant, readPreviewParams, richTextToHtml, richTextToPlainText, sitemapXml };
199
+ export type { AlternateLink, ContentClientOptions, EntriesQuery, ImageTransform, LinkHrefOptions, LocalizedUrlOptions, PageLookup, PreviewParams, RedirectsQuery, RequestOptions, RichTextHtmlOptions, ShopifyCollection, ShopifyImage, ShopifyMoney, ShopifyProduct, ShopifyStorefrontOptions, SitemapOptions };
140
200
  //# 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","../src/sitemap.ts"],"mappings":"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","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/links.ts","../src/preview.ts","../src/shopify.ts","../src/richText.ts","../src/sitemap.ts"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  var config = require('./config-ci2hxmas.js');
4
- var preview = require('./preview-mpwfmnla.js');
4
+ var preview = require('./preview-ijk3ixwb.js');
5
5
 
6
6
  // A response from the API that wasn't a success. `message` is the API's own
7
7
  // `error` text when it sent one.
@@ -266,6 +266,87 @@ const pickImproveVariant = (block, option, fallback)=>{
266
266
  return Object.values(variants)[0] ?? [];
267
267
  };
268
268
 
269
+ const linkHref = (link, { url = (path)=>`/${path}`, product = ({ handle })=>`/products/${handle}` } = {})=>{
270
+ if (!link) return null;
271
+ if (link.type === 'entry' && link.entry.path === null) return null;
272
+ const base = link.type === 'url' ? link.url : link.type === 'product' ? product(link.product, link.entry) : url(link.entry.path);
273
+ const hashAt = base.indexOf('#');
274
+ const address = hashAt === -1 ? base : base.slice(0, hashAt);
275
+ const ownHash = hashAt === -1 ? null : base.slice(hashAt + 1);
276
+ const withQuery = link.query ? `${address}${address.includes('?') ? '&' : '?'}${link.query}` : address;
277
+ const hash = link.hash ?? ownHash;
278
+ return hash ? `${withQuery}#${hash}` : withQuery;
279
+ };
280
+
281
+ // What a `shopify` field points at, live from the store: the Storefront API
282
+ // with the space's Storefront token (Settings → Copy as .env:
283
+ // SHOPIFY_STORE_DOMAIN and SHOPIFY_STOREFRONT_TOKEN, safe in the browser).
284
+ // Content stores only `{ id, handle }`, so prices and stock are never stale.
285
+ // A Hydrogen or Liquid site can query by the id itself instead.
286
+ // The Storefront API version the queries are written for.
287
+ const SHOPIFY_STOREFRONT_API_VERSION = '2026-10';
288
+ class ShopifyStorefrontError extends Error {
289
+ constructor(status, message){
290
+ super(message), this.status = status;
291
+ this.name = 'ShopifyStorefrontError';
292
+ }
293
+ }
294
+ const PRODUCT_FIELDS = 'id handle title availableForSale featuredImage { url altText width height } priceRange { minVariantPrice { amount currencyCode } }';
295
+ const CONTEXT_VARIABLES = '$country: CountryCode, $language: LanguageCode';
296
+ const IN_CONTEXT = '@inContext(country: $country, language: $language)';
297
+ const storefront = async (options, query, variables)=>{
298
+ const fetchImpl = options.fetch ?? fetch;
299
+ const response = await fetchImpl(`https://${options.shop}/api/${SHOPIFY_STOREFRONT_API_VERSION}/graphql.json`, {
300
+ ...options.init,
301
+ method: 'POST',
302
+ headers: {
303
+ ...options.init?.headers,
304
+ 'content-type': 'application/json',
305
+ 'x-shopify-storefront-access-token': options.storefrontToken
306
+ },
307
+ body: JSON.stringify({
308
+ query,
309
+ variables: {
310
+ ...variables,
311
+ country: options.country?.toUpperCase() ?? null,
312
+ language: options.language?.toUpperCase() ?? null
313
+ }
314
+ })
315
+ });
316
+ if (!response.ok) throw new ShopifyStorefrontError(response.status, `Shopify's Storefront API answered ${response.status}`);
317
+ const body = await response.json();
318
+ if (body.errors?.length || !body.data) throw new ShopifyStorefrontError(response.status, body.errors?.map((error)=>error.message).join('; ') || 'No data from the Storefront API');
319
+ return body.data;
320
+ };
321
+ // A collection field's collection with its first `first` products (default
322
+ // 12, at most 250), in the collection's order; null when the store no
323
+ // longer has it or it isn't published to the token's sales channel.
324
+ const fetchShopifyCollection = async (item, options)=>{
325
+ const first = Math.min(Math.max(Math.trunc(options.first ?? 12), 1), 250);
326
+ const data = await storefront(options, `query Collection($id: ID!, $first: Int!, ${CONTEXT_VARIABLES}) ${IN_CONTEXT} { collection(id: $id) { id handle title products(first: $first) { nodes { ${PRODUCT_FIELDS} } } } }`, {
327
+ id: item.id,
328
+ first
329
+ });
330
+ if (!data.collection) return null;
331
+ const { products, ...collection } = data.collection;
332
+ return {
333
+ ...collection,
334
+ products: products.nodes
335
+ };
336
+ };
337
+ // A product field's products, in the field's order; one the store no
338
+ // longer has (or doesn't sell through this channel) is left out.
339
+ const fetchShopifyProducts = async (items, options)=>{
340
+ const ids = (Array.isArray(items) ? items : items ? [
341
+ items
342
+ ] : []).map(({ id })=>id).slice(0, 250);
343
+ if (ids.length === 0) return [];
344
+ const data = await storefront(options, `query Products($ids: [ID!]!, ${CONTEXT_VARIABLES}) ${IN_CONTEXT} { nodes(ids: $ids) { ... on Product { ${PRODUCT_FIELDS} } } }`, {
345
+ ids
346
+ });
347
+ return data.nodes.filter((node)=>Boolean(node && 'id' in node));
348
+ };
349
+
269
350
  // Links are only rendered for these; anything else (`javascript:` …) keeps its
270
351
  // text without the link. The same rule the CMS validates with.
271
352
  const SAFE_HREF = /^(https?:\/\/|mailto:|tel:|#|\/(?!\/))/i;
@@ -428,19 +509,27 @@ const sitemapXml = (entries, { exclude = hidden, ...options })=>{
428
509
  exports.DEFAULT_URL = config.DEFAULT_URL;
429
510
  exports.ENTRY_ATTRIBUTE = preview.ENTRY_ATTRIBUTE;
430
511
  exports.PREVIEW_PARAMS = preview.PREVIEW_PARAMS;
512
+ exports.PRODUCT_ATTRIBUTE = preview.PRODUCT_ATTRIBUTE;
513
+ exports.PRODUCT_HANDLE_ATTRIBUTE = preview.PRODUCT_HANDLE_ATTRIBUTE;
431
514
  exports.UID_ATTRIBUTE = preview.UID_ATTRIBUTE;
432
515
  exports.editable = preview.editable;
516
+ exports.editableProduct = preview.editableProduct;
433
517
  exports.readPreviewParams = preview.readPreviewParams;
434
518
  exports.ContentApiError = ContentApiError;
435
519
  exports.ContentClient = ContentClient;
436
520
  exports.IMPROVE_BLOCK = IMPROVE_BLOCK;
437
521
  exports.SAFE_HREF = SAFE_HREF;
522
+ exports.SHOPIFY_STOREFRONT_API_VERSION = SHOPIFY_STOREFRONT_API_VERSION;
523
+ exports.ShopifyStorefrontError = ShopifyStorefrontError;
438
524
  exports.alternateLinks = alternateLinks;
439
525
  exports.escapeHtml = escapeHtml;
526
+ exports.fetchShopifyCollection = fetchShopifyCollection;
527
+ exports.fetchShopifyProducts = fetchShopifyProducts;
440
528
  exports.headingLevel = headingLevel;
441
529
  exports.imageSrcSet = imageSrcSet;
442
530
  exports.imageUrl = imageUrl;
443
531
  exports.isImproveBlock = isImproveBlock;
532
+ exports.linkHref = linkHref;
444
533
  exports.pickImproveVariant = pickImproveVariant;
445
534
  exports.richTextToHtml = richTextToHtml;
446
535
  exports.richTextToPlainText = richTextToPlainText;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as normalizeUrl, a as DEFAULT_TIMEOUT, b as DELIVERY_PATH, D as DEFAULT_URL } from './config-c6qktlz6.mjs';
2
- export { E as ENTRY_ATTRIBUTE, P as PREVIEW_PARAMS, U as UID_ATTRIBUTE, e as editable, r as readPreviewParams } from './preview-h7rwp06v.mjs';
2
+ export { E as ENTRY_ATTRIBUTE, P as PREVIEW_PARAMS, a as PRODUCT_ATTRIBUTE, b as PRODUCT_HANDLE_ATTRIBUTE, U as UID_ATTRIBUTE, e as editable, c as editableProduct, r as readPreviewParams } from './preview-jcmxxj92.mjs';
3
3
 
4
4
  // A response from the API that wasn't a success. `message` is the API's own
5
5
  // `error` text when it sent one.
@@ -264,6 +264,87 @@ const pickImproveVariant = (block, option, fallback)=>{
264
264
  return Object.values(variants)[0] ?? [];
265
265
  };
266
266
 
267
+ const linkHref = (link, { url = (path)=>`/${path}`, product = ({ handle })=>`/products/${handle}` } = {})=>{
268
+ if (!link) return null;
269
+ if (link.type === 'entry' && link.entry.path === null) return null;
270
+ const base = link.type === 'url' ? link.url : link.type === 'product' ? product(link.product, link.entry) : url(link.entry.path);
271
+ const hashAt = base.indexOf('#');
272
+ const address = hashAt === -1 ? base : base.slice(0, hashAt);
273
+ const ownHash = hashAt === -1 ? null : base.slice(hashAt + 1);
274
+ const withQuery = link.query ? `${address}${address.includes('?') ? '&' : '?'}${link.query}` : address;
275
+ const hash = link.hash ?? ownHash;
276
+ return hash ? `${withQuery}#${hash}` : withQuery;
277
+ };
278
+
279
+ // What a `shopify` field points at, live from the store: the Storefront API
280
+ // with the space's Storefront token (Settings → Copy as .env:
281
+ // SHOPIFY_STORE_DOMAIN and SHOPIFY_STOREFRONT_TOKEN, safe in the browser).
282
+ // Content stores only `{ id, handle }`, so prices and stock are never stale.
283
+ // A Hydrogen or Liquid site can query by the id itself instead.
284
+ // The Storefront API version the queries are written for.
285
+ const SHOPIFY_STOREFRONT_API_VERSION = '2026-10';
286
+ class ShopifyStorefrontError extends Error {
287
+ constructor(status, message){
288
+ super(message), this.status = status;
289
+ this.name = 'ShopifyStorefrontError';
290
+ }
291
+ }
292
+ const PRODUCT_FIELDS = 'id handle title availableForSale featuredImage { url altText width height } priceRange { minVariantPrice { amount currencyCode } }';
293
+ const CONTEXT_VARIABLES = '$country: CountryCode, $language: LanguageCode';
294
+ const IN_CONTEXT = '@inContext(country: $country, language: $language)';
295
+ const storefront = async (options, query, variables)=>{
296
+ const fetchImpl = options.fetch ?? fetch;
297
+ const response = await fetchImpl(`https://${options.shop}/api/${SHOPIFY_STOREFRONT_API_VERSION}/graphql.json`, {
298
+ ...options.init,
299
+ method: 'POST',
300
+ headers: {
301
+ ...options.init?.headers,
302
+ 'content-type': 'application/json',
303
+ 'x-shopify-storefront-access-token': options.storefrontToken
304
+ },
305
+ body: JSON.stringify({
306
+ query,
307
+ variables: {
308
+ ...variables,
309
+ country: options.country?.toUpperCase() ?? null,
310
+ language: options.language?.toUpperCase() ?? null
311
+ }
312
+ })
313
+ });
314
+ if (!response.ok) throw new ShopifyStorefrontError(response.status, `Shopify's Storefront API answered ${response.status}`);
315
+ const body = await response.json();
316
+ if (body.errors?.length || !body.data) throw new ShopifyStorefrontError(response.status, body.errors?.map((error)=>error.message).join('; ') || 'No data from the Storefront API');
317
+ return body.data;
318
+ };
319
+ // A collection field's collection with its first `first` products (default
320
+ // 12, at most 250), in the collection's order; null when the store no
321
+ // longer has it or it isn't published to the token's sales channel.
322
+ const fetchShopifyCollection = async (item, options)=>{
323
+ const first = Math.min(Math.max(Math.trunc(options.first ?? 12), 1), 250);
324
+ const data = await storefront(options, `query Collection($id: ID!, $first: Int!, ${CONTEXT_VARIABLES}) ${IN_CONTEXT} { collection(id: $id) { id handle title products(first: $first) { nodes { ${PRODUCT_FIELDS} } } } }`, {
325
+ id: item.id,
326
+ first
327
+ });
328
+ if (!data.collection) return null;
329
+ const { products, ...collection } = data.collection;
330
+ return {
331
+ ...collection,
332
+ products: products.nodes
333
+ };
334
+ };
335
+ // A product field's products, in the field's order; one the store no
336
+ // longer has (or doesn't sell through this channel) is left out.
337
+ const fetchShopifyProducts = async (items, options)=>{
338
+ const ids = (Array.isArray(items) ? items : items ? [
339
+ items
340
+ ] : []).map(({ id })=>id).slice(0, 250);
341
+ if (ids.length === 0) return [];
342
+ const data = await storefront(options, `query Products($ids: [ID!]!, ${CONTEXT_VARIABLES}) ${IN_CONTEXT} { nodes(ids: $ids) { ... on Product { ${PRODUCT_FIELDS} } } }`, {
343
+ ids
344
+ });
345
+ return data.nodes.filter((node)=>Boolean(node && 'id' in node));
346
+ };
347
+
267
348
  // Links are only rendered for these; anything else (`javascript:` …) keeps its
268
349
  // text without the link. The same rule the CMS validates with.
269
350
  const SAFE_HREF = /^(https?:\/\/|mailto:|tel:|#|\/(?!\/))/i;
@@ -423,4 +504,4 @@ const sitemapXml = (entries, { exclude = hidden, ...options })=>{
423
504
  ].join('\n');
424
505
  };
425
506
 
426
- export { ContentApiError, ContentClient, DEFAULT_URL, IMPROVE_BLOCK, SAFE_HREF, alternateLinks, escapeHtml, headingLevel, imageSrcSet, imageUrl, isImproveBlock, pickImproveVariant, richTextToHtml, richTextToPlainText, sitemapXml };
507
+ export { ContentApiError, ContentClient, DEFAULT_URL, IMPROVE_BLOCK, SAFE_HREF, SHOPIFY_STOREFRONT_API_VERSION, ShopifyStorefrontError, alternateLinks, escapeHtml, fetchShopifyCollection, fetchShopifyProducts, headingLevel, imageSrcSet, imageUrl, isImproveBlock, linkHref, pickImproveVariant, richTextToHtml, richTextToPlainText, sitemapXml };
@@ -1,5 +1,7 @@
1
1
  const UID_ATTRIBUTE = 'data-cms-uid';
2
2
  const ENTRY_ATTRIBUTE = 'data-cms-entry';
3
+ const PRODUCT_ATTRIBUTE = 'data-cms-product';
4
+ const PRODUCT_HANDLE_ATTRIBUTE = 'data-cms-product-handle';
3
5
  // The attribute that lets editors select a block on the page, in the visual
4
6
  // editor and on the site. Spread it on the block's outermost element:
5
7
  //
@@ -22,6 +24,18 @@ const editable = (target)=>{
22
24
  }
23
25
  return {};
24
26
  };
27
+ // The Shopify product a page shows, on a product route: editing on the site
28
+ // then offers "Add a product page" when the page has no product page yet
29
+ // (no `editable(entry)`), and makes one for this product.
30
+ //
31
+ // <main {...editableProduct(product)} {...editable(page)}>…</main>
32
+ //
33
+ // `product` is the store's (its GID and handle), from the Storefront API or
34
+ // a Shopify field.
35
+ const editableProduct = (product)=>product?.id ? {
36
+ 'data-cms-product': product.id,
37
+ 'data-cms-product-handle': product.handle
38
+ } : {};
25
39
 
26
40
  // The visual editor loads the site in an iframe at the environment's preview
27
41
  // URL plus the entry's path, with these parameters added. `cms_token` is a
@@ -72,6 +86,9 @@ const readPreviewParams = (input)=>{
72
86
 
73
87
  exports.ENTRY_ATTRIBUTE = ENTRY_ATTRIBUTE;
74
88
  exports.PREVIEW_PARAMS = PREVIEW_PARAMS;
89
+ exports.PRODUCT_ATTRIBUTE = PRODUCT_ATTRIBUTE;
90
+ exports.PRODUCT_HANDLE_ATTRIBUTE = PRODUCT_HANDLE_ATTRIBUTE;
75
91
  exports.UID_ATTRIBUTE = UID_ATTRIBUTE;
76
92
  exports.editable = editable;
93
+ exports.editableProduct = editableProduct;
77
94
  exports.readPreviewParams = readPreviewParams;
@@ -1,5 +1,7 @@
1
1
  const UID_ATTRIBUTE = 'data-cms-uid';
2
2
  const ENTRY_ATTRIBUTE = 'data-cms-entry';
3
+ const PRODUCT_ATTRIBUTE = 'data-cms-product';
4
+ const PRODUCT_HANDLE_ATTRIBUTE = 'data-cms-product-handle';
3
5
  // The attribute that lets editors select a block on the page, in the visual
4
6
  // editor and on the site. Spread it on the block's outermost element:
5
7
  //
@@ -22,6 +24,18 @@ const editable = (target)=>{
22
24
  }
23
25
  return {};
24
26
  };
27
+ // The Shopify product a page shows, on a product route: editing on the site
28
+ // then offers "Add a product page" when the page has no product page yet
29
+ // (no `editable(entry)`), and makes one for this product.
30
+ //
31
+ // <main {...editableProduct(product)} {...editable(page)}>…</main>
32
+ //
33
+ // `product` is the store's (its GID and handle), from the Storefront API or
34
+ // a Shopify field.
35
+ const editableProduct = (product)=>product?.id ? {
36
+ 'data-cms-product': product.id,
37
+ 'data-cms-product-handle': product.handle
38
+ } : {};
25
39
 
26
40
  // The visual editor loads the site in an iframe at the environment's preview
27
41
  // URL plus the entry's path, with these parameters added. `cms_token` is a
@@ -70,4 +84,4 @@ const readPreviewParams = (input)=>{
70
84
  };
71
85
  };
72
86
 
73
- export { ENTRY_ATTRIBUTE as E, PREVIEW_PARAMS as P, UID_ATTRIBUTE as U, editable as e, readPreviewParams as r };
87
+ export { ENTRY_ATTRIBUTE as E, PREVIEW_PARAMS as P, UID_ATTRIBUTE as U, PRODUCT_ATTRIBUTE as a, PRODUCT_HANDLE_ATTRIBUTE as b, editableProduct as c, editable as e, readPreviewParams as r };
package/dist/site.js CHANGED
@@ -94,6 +94,11 @@ const PROXY_ROUTES = [
94
94
  method: 'POST',
95
95
  pattern: /^entries$/
96
96
  },
97
+ // "Add a product page" on a product the site marks (editableProduct).
98
+ {
99
+ method: 'POST',
100
+ pattern: /^product-pages$/
101
+ },
97
102
  // The one-time address of the CMS's entry form (the prompt box's Form).
98
103
  {
99
104
  method: 'POST',
package/dist/site.mjs CHANGED
@@ -92,6 +92,11 @@ const PROXY_ROUTES = [
92
92
  method: 'POST',
93
93
  pattern: /^entries$/
94
94
  },
95
+ // "Add a product page" on a product the site marks (editableProduct).
96
+ {
97
+ method: 'POST',
98
+ pattern: /^product-pages$/
99
+ },
95
100
  // The one-time address of the CMS's entry form (the prompt box's Form).
96
101
  {
97
102
  method: 'POST',
@@ -31,6 +31,29 @@ type EntryStub<TContentType extends string = string> = {
31
31
  id: string;
32
32
  contentType: TContentType;
33
33
  };
34
+ type LinkTarget = {
35
+ id: string;
36
+ title: string | Record<string, string>;
37
+ slug: string | null;
38
+ path: string | null;
39
+ };
40
+ type Link = {
41
+ type: 'entry';
42
+ entry: LinkTarget;
43
+ query: string | null;
44
+ hash: string | null;
45
+ } | {
46
+ type: 'url';
47
+ url: string;
48
+ query: string | null;
49
+ hash: string | null;
50
+ } | {
51
+ type: 'product';
52
+ product: ShopifyItem;
53
+ entry: LinkTarget | null;
54
+ query: string | null;
55
+ hash: string | null;
56
+ };
34
57
  type Block<TFields extends Record<string, unknown> = Record<string, unknown>, TType extends string = string> = {
35
58
  _uid: string;
36
59
  _type: TType;
@@ -71,6 +94,10 @@ type RedirectList = {
71
94
  limit: number;
72
95
  skip: number;
73
96
  };
97
+ type ShopifyItem = {
98
+ id: string;
99
+ handle: string;
100
+ };
74
101
  type ContentTypeKind = 'page' | 'data' | 'block';
75
102
  type BaseField = {
76
103
  apiId: string;
@@ -128,11 +155,22 @@ type FieldDefinition = BaseField & ({
128
155
  contentTypes?: string[];
129
156
  min?: number;
130
157
  max?: number;
158
+ } | {
159
+ type: 'link';
160
+ contentTypes?: string[];
161
+ products?: boolean;
131
162
  } | {
132
163
  type: 'blocks';
133
164
  allowedBlocks?: string[];
134
165
  min?: number;
135
166
  max?: number;
167
+ } | {
168
+ type: 'shopify';
169
+ resource: 'collection' | 'product';
170
+ multiple?: boolean;
171
+ unique?: boolean;
172
+ min?: number;
173
+ max?: number;
136
174
  } | {
137
175
  type: 'json';
138
176
  });
@@ -150,5 +188,5 @@ type ModelFile = {
150
188
  contentTypes: ContentTypeDefinition[];
151
189
  };
152
190
 
153
- 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 };
154
- //# sourceMappingURL=types-m3j4tltp.d.ts.map
191
+ export type { Asset as A, Block as B, ContentTypeDefinition as C, Entry as E, FieldDefinition 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, ContentTypeKind as g, Environment as h, FieldType as i, RichTextDocument as j, RichTextMark as k };
192
+ //# sourceMappingURL=types-mtjco3a5.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-mtjco3a5.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;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,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,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,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":[]}
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.16.0",
4
+ "version": "0.18.0",
5
5
  "keywords": [
6
6
  "cms",
7
7
  "headless-cms",
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-m3j4tltp.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;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;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":[]}