@obelism/content 0.16.0 → 0.17.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
@@ -295,6 +295,27 @@ const xml = sitemapXml(pages, {
295
295
  })
296
296
  ```
297
297
 
298
+ ## Links
299
+
300
+ A `link` field goes to a page of the space or to any address, each with an
301
+ optional query and anchor. It is delivered as `Link`: the page's `id`,
302
+ `title`, `slug` and `path` (never its fields), or the `url`, plus `query` and
303
+ `hash` without their `?` and `#`. A link to a page that isn't published is
304
+ `null`. Where a page's path lives on your site is your routing, so
305
+ `linkHref` takes it as `url` (default `/<path>`) and adds the query and hash:
306
+
307
+ ```tsx
308
+ import { linkHref } from '@obelism/content'
309
+
310
+ ;<a
311
+ href={
312
+ linkHref(button.link, { url: (path) => `/${locale}/${path}` }) ?? undefined
313
+ }
314
+ >
315
+ {button.label}
316
+ </a>
317
+ ```
318
+
298
319
  ## A/B tests and feature flags
299
320
 
300
321
  Any blocks field can hold the built-in improve block (`ImproveBlock`,
@@ -319,6 +340,38 @@ const blocks = layout.flatMap((block) => {
319
340
  variant, else the first one, when the option is `null` (the test isn't
320
341
  active or the visitor isn't in it) or has no variant.
321
342
 
343
+ ## Shopify
344
+
345
+ A `shopify` field holds a collection or product of the Shopify store
346
+ connected to the space (Settings in Obelism Content), as `ShopifyItem`:
347
+ `{ id, handle }`. Content stores only that, so fetch prices, stock and images
348
+ live from Shopify's Storefront API with the store's domain and Storefront
349
+ token (Settings → Copy as .env):
350
+
351
+ ```ts
352
+ import { fetchShopifyCollection } from '@obelism/content'
353
+
354
+ // A productGrid block: { heading, collection, columns }.
355
+ const collection = await fetchShopifyCollection(block.collection, {
356
+ shop: process.env.SHOPIFY_STORE_DOMAIN!,
357
+ storefrontToken: process.env.SHOPIFY_STOREFRONT_TOKEN!,
358
+ first: 12,
359
+ country: 'NL', // optional: the market's prices
360
+ })
361
+
362
+ collection?.products.map((product) => ({
363
+ href: `/products/${product.handle}`,
364
+ title: product.title,
365
+ price: product.priceRange.minVariantPrice,
366
+ image: product.featuredImage,
367
+ }))
368
+ ```
369
+
370
+ `fetchShopifyProducts(items, options)` does the same for a product field
371
+ (one item or a list), in the field's order. Both leave out what the store no
372
+ longer has. A Hydrogen or Liquid site can use the `id` with its own
373
+ Storefront queries instead.
374
+
322
375
  ## CLI
323
376
 
324
377
  Start a site's repository on a space, in its root:
@@ -372,6 +425,7 @@ locale, rich text as a document), and `| null` when empty:
372
425
  | select | the options' values, `'a' \| 'b'` (a list when multiple) |
373
426
  | asset | `Asset` (a list when multiple) |
374
427
  | reference | the allowed entry types or their `EntryStub`s (a list when multiple) |
428
+ | link | `Link` |
375
429
  | blocks | a list of the allowed block types and `ImproveBlock` |
376
430
  | richtext | `RichTextDocument` |
377
431
  | json | `unknown` |
@@ -146,6 +146,31 @@ const buildExamples = (model, improve)=>{
146
146
  improveBlock(field.allowedBlocks, depth + 1)
147
147
  ] : blocks;
148
148
  }
149
+ case 'link':
150
+ return {
151
+ type: 'entry',
152
+ entry: {
153
+ id: 'entry_example',
154
+ title: 'Example page',
155
+ slug: 'example',
156
+ path: 'example'
157
+ },
158
+ query: null,
159
+ hash: null
160
+ };
161
+ case 'shopify':
162
+ {
163
+ const item = field.resource === 'collection' ? {
164
+ id: 'gid://shopify/Collection/1',
165
+ handle: 'example-collection'
166
+ } : {
167
+ id: 'gid://shopify/Product/1',
168
+ handle: 'example-product'
169
+ };
170
+ return field.multiple ? [
171
+ item
172
+ ] : item;
173
+ }
149
174
  case 'json':
150
175
  return {};
151
176
  }
@@ -535,7 +560,9 @@ const SDK_TYPES = [
535
560
  'Entry',
536
561
  'EntryStub',
537
562
  'ImproveBlock',
538
- 'RichTextDocument'
563
+ 'Link',
564
+ 'RichTextDocument',
565
+ 'ShopifyItem'
539
566
  ];
540
567
  // The unions of every entry and block type the file ends with.
541
568
  const UNION_NAMES = [
@@ -607,6 +634,10 @@ const generateTypes = (model)=>{
607
634
  improve
608
635
  ]), true);
609
636
  }
637
+ case 'link':
638
+ return use('Link');
639
+ case 'shopify':
640
+ return list(use('ShopifyItem'), field.multiple);
610
641
  case 'json':
611
642
  return 'unknown';
612
643
  }
package/dist/index.d.mts 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, L as Link, S as ShopifyItem, d as EntryStub, e as RichTextNode } from './types-e5nvgudo.js';
2
+ export { C as ContentTypeDefinition, f as ContentTypeKind, g as Environment, F as FieldDefinition, h as FieldType, i as LinkTarget, j as RichTextDocument, k as RichTextMark } from './types-e5nvgudo.js';
3
3
 
4
4
  type ContentClientOptions = {
5
5
  token: string;
@@ -96,6 +96,11 @@ declare const IMPROVE_BLOCK = "_improve";
96
96
  declare const isImproveBlock: (block: Block) => block is ImproveBlock;
97
97
  declare const pickImproveVariant: <TBlock extends Block>(block: ImproveBlock<TBlock>, option: string | null | undefined, fallback?: string) => TBlock[];
98
98
 
99
+ type LinkHrefOptions = {
100
+ url?: ((path: string) => string) | undefined;
101
+ };
102
+ declare const linkHref: (link: Link | null | undefined, { url }?: LinkHrefOptions) => string | null;
103
+
99
104
  declare const PREVIEW_PARAMS: {
100
105
  readonly preview: "cms_preview";
101
106
  readonly entry: "cms_entry";
@@ -110,6 +115,50 @@ type PreviewParams = {
110
115
  type SearchParamsInput = URL | URLSearchParams | string | Record<string, string | string[] | undefined>;
111
116
  declare const readPreviewParams: (input: SearchParamsInput) => PreviewParams | null;
112
117
 
118
+ declare const SHOPIFY_STOREFRONT_API_VERSION = "2026-10";
119
+ type ShopifyStorefrontOptions = {
120
+ shop: string;
121
+ storefrontToken: string;
122
+ country?: string | undefined;
123
+ language?: string | undefined;
124
+ init?: RequestInit | undefined;
125
+ fetch?: typeof fetch | undefined;
126
+ };
127
+ type ShopifyMoney = {
128
+ amount: string;
129
+ currencyCode: string;
130
+ };
131
+ type ShopifyImage = {
132
+ url: string;
133
+ altText: string | null;
134
+ width: number | null;
135
+ height: number | null;
136
+ };
137
+ type ShopifyProduct = {
138
+ id: string;
139
+ handle: string;
140
+ title: string;
141
+ availableForSale: boolean;
142
+ featuredImage: ShopifyImage | null;
143
+ priceRange: {
144
+ minVariantPrice: ShopifyMoney;
145
+ };
146
+ };
147
+ type ShopifyCollection = {
148
+ id: string;
149
+ handle: string;
150
+ title: string;
151
+ products: ShopifyProduct[];
152
+ };
153
+ declare class ShopifyStorefrontError extends Error {
154
+ readonly status: number;
155
+ constructor(status: number, message: string);
156
+ }
157
+ declare const fetchShopifyCollection: (item: ShopifyItem, options: ShopifyStorefrontOptions & {
158
+ first?: number | undefined;
159
+ }) => Promise<ShopifyCollection | null>;
160
+ declare const fetchShopifyProducts: (items: ShopifyItem | ShopifyItem[] | null | undefined, options: ShopifyStorefrontOptions) => Promise<ShopifyProduct[]>;
161
+
113
162
  declare const SAFE_HREF: RegExp;
114
163
  declare const escapeHtml: (value: string) => string;
115
164
  type RichTextHtmlOptions = {
@@ -135,6 +184,6 @@ type SitemapOptions = LocalizedUrlOptions & {
135
184
  };
136
185
  declare const sitemapXml: (entries: Iterable<Page>, { exclude, ...options }: SitemapOptions) => string;
137
186
 
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 };
187
+ export { Asset, Block, ContentApiError, ContentClient, DEFAULT_URL, ENTRY_ATTRIBUTE, Entry, EntryList, EntryStub, IMPROVE_BLOCK, ImproveBlock, Link, ModelFile, PREVIEW_PARAMS, Redirect, RedirectList, RichTextFormat, RichTextNode, SAFE_HREF, SHOPIFY_STOREFRONT_API_VERSION, ShopifyItem, ShopifyStorefrontError, UID_ATTRIBUTE, alternateLinks, editable, escapeHtml, fetchShopifyCollection, fetchShopifyProducts, headingLevel, imageSrcSet, imageUrl, isImproveBlock, linkHref, pickImproveVariant, readPreviewParams, richTextToHtml, richTextToPlainText, sitemapXml };
188
+ export type { AlternateLink, ContentClientOptions, EntriesQuery, ImageTransform, LinkHrefOptions, LocalizedUrlOptions, PageLookup, PreviewParams, RedirectsQuery, RequestOptions, RichTextHtmlOptions, ShopifyCollection, ShopifyImage, ShopifyMoney, ShopifyProduct, ShopifyStorefrontOptions, SitemapOptions };
140
189
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sources":["../src/client.ts","../src/config.ts","../src/editable.ts","../src/errors.ts","../src/images.ts","../src/improve.ts","../src/preview.ts","../src/richText.ts","../src/sitemap.ts"],"mappings":"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","names":[]}
1
+ {"version":3,"file":"index.d.mts","sources":["../src/client.ts","../src/config.ts","../src/editable.ts","../src/errors.ts","../src/images.ts","../src/improve.ts","../src/links.ts","../src/preview.ts","../src/shopify.ts","../src/richText.ts","../src/sitemap.ts"],"mappings":"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","names":[]}
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, L as Link, S as ShopifyItem, d as EntryStub, e as RichTextNode } from './types-e5nvgudo.js';
2
+ export { C as ContentTypeDefinition, f as ContentTypeKind, g as Environment, F as FieldDefinition, h as FieldType, i as LinkTarget, j as RichTextDocument, k as RichTextMark } from './types-e5nvgudo.js';
3
3
 
4
4
  type ContentClientOptions = {
5
5
  token: string;
@@ -96,6 +96,11 @@ declare const IMPROVE_BLOCK = "_improve";
96
96
  declare const isImproveBlock: (block: Block) => block is ImproveBlock;
97
97
  declare const pickImproveVariant: <TBlock extends Block>(block: ImproveBlock<TBlock>, option: string | null | undefined, fallback?: string) => TBlock[];
98
98
 
99
+ type LinkHrefOptions = {
100
+ url?: ((path: string) => string) | undefined;
101
+ };
102
+ declare const linkHref: (link: Link | null | undefined, { url }?: LinkHrefOptions) => string | null;
103
+
99
104
  declare const PREVIEW_PARAMS: {
100
105
  readonly preview: "cms_preview";
101
106
  readonly entry: "cms_entry";
@@ -110,6 +115,50 @@ type PreviewParams = {
110
115
  type SearchParamsInput = URL | URLSearchParams | string | Record<string, string | string[] | undefined>;
111
116
  declare const readPreviewParams: (input: SearchParamsInput) => PreviewParams | null;
112
117
 
118
+ declare const SHOPIFY_STOREFRONT_API_VERSION = "2026-10";
119
+ type ShopifyStorefrontOptions = {
120
+ shop: string;
121
+ storefrontToken: string;
122
+ country?: string | undefined;
123
+ language?: string | undefined;
124
+ init?: RequestInit | undefined;
125
+ fetch?: typeof fetch | undefined;
126
+ };
127
+ type ShopifyMoney = {
128
+ amount: string;
129
+ currencyCode: string;
130
+ };
131
+ type ShopifyImage = {
132
+ url: string;
133
+ altText: string | null;
134
+ width: number | null;
135
+ height: number | null;
136
+ };
137
+ type ShopifyProduct = {
138
+ id: string;
139
+ handle: string;
140
+ title: string;
141
+ availableForSale: boolean;
142
+ featuredImage: ShopifyImage | null;
143
+ priceRange: {
144
+ minVariantPrice: ShopifyMoney;
145
+ };
146
+ };
147
+ type ShopifyCollection = {
148
+ id: string;
149
+ handle: string;
150
+ title: string;
151
+ products: ShopifyProduct[];
152
+ };
153
+ declare class ShopifyStorefrontError extends Error {
154
+ readonly status: number;
155
+ constructor(status: number, message: string);
156
+ }
157
+ declare const fetchShopifyCollection: (item: ShopifyItem, options: ShopifyStorefrontOptions & {
158
+ first?: number | undefined;
159
+ }) => Promise<ShopifyCollection | null>;
160
+ declare const fetchShopifyProducts: (items: ShopifyItem | ShopifyItem[] | null | undefined, options: ShopifyStorefrontOptions) => Promise<ShopifyProduct[]>;
161
+
113
162
  declare const SAFE_HREF: RegExp;
114
163
  declare const escapeHtml: (value: string) => string;
115
164
  type RichTextHtmlOptions = {
@@ -135,6 +184,6 @@ type SitemapOptions = LocalizedUrlOptions & {
135
184
  };
136
185
  declare const sitemapXml: (entries: Iterable<Page>, { exclude, ...options }: SitemapOptions) => string;
137
186
 
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 };
187
+ export { Asset, Block, ContentApiError, ContentClient, DEFAULT_URL, ENTRY_ATTRIBUTE, Entry, EntryList, EntryStub, IMPROVE_BLOCK, ImproveBlock, Link, ModelFile, PREVIEW_PARAMS, Redirect, RedirectList, RichTextFormat, RichTextNode, SAFE_HREF, SHOPIFY_STOREFRONT_API_VERSION, ShopifyItem, ShopifyStorefrontError, UID_ATTRIBUTE, alternateLinks, editable, escapeHtml, fetchShopifyCollection, fetchShopifyProducts, headingLevel, imageSrcSet, imageUrl, isImproveBlock, linkHref, pickImproveVariant, readPreviewParams, richTextToHtml, richTextToPlainText, sitemapXml };
188
+ export type { AlternateLink, ContentClientOptions, EntriesQuery, ImageTransform, LinkHrefOptions, LocalizedUrlOptions, PageLookup, PreviewParams, RedirectsQuery, RequestOptions, RichTextHtmlOptions, ShopifyCollection, ShopifyImage, ShopifyMoney, ShopifyProduct, ShopifyStorefrontOptions, SitemapOptions };
140
189
  //# 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
@@ -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}` } = {})=>{
270
+ if (!link) return null;
271
+ if (link.type === 'entry' && link.entry.path === null) return null;
272
+ const base = link.type === 'url' ? link.url : 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;
@@ -435,12 +516,17 @@ exports.ContentApiError = ContentApiError;
435
516
  exports.ContentClient = ContentClient;
436
517
  exports.IMPROVE_BLOCK = IMPROVE_BLOCK;
437
518
  exports.SAFE_HREF = SAFE_HREF;
519
+ exports.SHOPIFY_STOREFRONT_API_VERSION = SHOPIFY_STOREFRONT_API_VERSION;
520
+ exports.ShopifyStorefrontError = ShopifyStorefrontError;
438
521
  exports.alternateLinks = alternateLinks;
439
522
  exports.escapeHtml = escapeHtml;
523
+ exports.fetchShopifyCollection = fetchShopifyCollection;
524
+ exports.fetchShopifyProducts = fetchShopifyProducts;
440
525
  exports.headingLevel = headingLevel;
441
526
  exports.imageSrcSet = imageSrcSet;
442
527
  exports.imageUrl = imageUrl;
443
528
  exports.isImproveBlock = isImproveBlock;
529
+ exports.linkHref = linkHref;
444
530
  exports.pickImproveVariant = pickImproveVariant;
445
531
  exports.richTextToHtml = richTextToHtml;
446
532
  exports.richTextToPlainText = richTextToPlainText;
package/dist/index.mjs CHANGED
@@ -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}` } = {})=>{
268
+ if (!link) return null;
269
+ if (link.type === 'entry' && link.entry.path === null) return null;
270
+ const base = link.type === 'url' ? link.url : 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 };
@@ -31,6 +31,23 @@ 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
+ };
34
51
  type Block<TFields extends Record<string, unknown> = Record<string, unknown>, TType extends string = string> = {
35
52
  _uid: string;
36
53
  _type: TType;
@@ -71,6 +88,10 @@ type RedirectList = {
71
88
  limit: number;
72
89
  skip: number;
73
90
  };
91
+ type ShopifyItem = {
92
+ id: string;
93
+ handle: string;
94
+ };
74
95
  type ContentTypeKind = 'page' | 'data' | 'block';
75
96
  type BaseField = {
76
97
  apiId: string;
@@ -128,11 +149,20 @@ type FieldDefinition = BaseField & ({
128
149
  contentTypes?: string[];
129
150
  min?: number;
130
151
  max?: number;
152
+ } | {
153
+ type: 'link';
154
+ contentTypes?: string[];
131
155
  } | {
132
156
  type: 'blocks';
133
157
  allowedBlocks?: string[];
134
158
  min?: number;
135
159
  max?: number;
160
+ } | {
161
+ type: 'shopify';
162
+ resource: 'collection' | 'product';
163
+ multiple?: boolean;
164
+ min?: number;
165
+ max?: number;
136
166
  } | {
137
167
  type: 'json';
138
168
  });
@@ -150,5 +180,5 @@ type ModelFile = {
150
180
  contentTypes: ContentTypeDefinition[];
151
181
  };
152
182
 
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
183
+ export type { Asset as A, Block as B, ContentTypeDefinition as C, Entry as E, FieldDefinition as F, ImproveBlock as I, Link as L, ModelFile as M, RichTextFormat as R, ShopifyItem as S, RedirectList as a, Redirect as b, EntryList as c, EntryStub as d, RichTextNode as e, ContentTypeKind as f, Environment as g, FieldType as h, LinkTarget as i, RichTextDocument as j, RichTextMark as k };
184
+ //# sourceMappingURL=types-e5nvgudo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-e5nvgudo.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,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,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.17.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":[]}