@shop-on/theme-server 0.6.0 → 0.7.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.
@@ -9309,6 +9309,18 @@ export type introspection_types = {
9309
9309
  ofType: null;
9310
9310
  };
9311
9311
  };
9312
+ 'returnPolicy': {
9313
+ name: 'returnPolicy';
9314
+ type: {
9315
+ kind: 'NON_NULL';
9316
+ name: never;
9317
+ ofType: {
9318
+ kind: 'OBJECT';
9319
+ name: 'ShopReturnPolicy';
9320
+ ofType: null;
9321
+ };
9322
+ };
9323
+ };
9312
9324
  'seo': {
9313
9325
  name: 'seo';
9314
9326
  type: {
@@ -16538,6 +16550,18 @@ export type introspection_types = {
16538
16550
  ofType: null;
16539
16551
  };
16540
16552
  };
16553
+ 'finalSale': {
16554
+ name: 'finalSale';
16555
+ type: {
16556
+ kind: 'NON_NULL';
16557
+ name: never;
16558
+ ofType: {
16559
+ kind: 'SCALAR';
16560
+ name: 'Boolean';
16561
+ ofType: null;
16562
+ };
16563
+ };
16564
+ };
16541
16565
  'giftCard': {
16542
16566
  name: 'giftCard';
16543
16567
  type: {
@@ -17980,6 +18004,14 @@ export type introspection_types = {
17980
18004
  ofType: null;
17981
18005
  };
17982
18006
  };
18007
+ 'gtin': {
18008
+ name: 'gtin';
18009
+ type: {
18010
+ kind: 'SCALAR';
18011
+ name: 'String';
18012
+ ofType: null;
18013
+ };
18014
+ };
17983
18015
  'id': {
17984
18016
  name: 'id';
17985
18017
  type: {
@@ -18276,6 +18308,64 @@ export type introspection_types = {
18276
18308
  };
18277
18309
  };
18278
18310
  };
18311
+ 'ShopReturnPolicy': {
18312
+ kind: 'OBJECT';
18313
+ name: 'ShopReturnPolicy';
18314
+ fields: {
18315
+ 'configured': {
18316
+ name: 'configured';
18317
+ type: {
18318
+ kind: 'NON_NULL';
18319
+ name: never;
18320
+ ofType: {
18321
+ kind: 'SCALAR';
18322
+ name: 'Boolean';
18323
+ ofType: null;
18324
+ };
18325
+ };
18326
+ };
18327
+ 'flatRateAmount': {
18328
+ name: 'flatRateAmount';
18329
+ type: {
18330
+ kind: 'OBJECT';
18331
+ name: 'Money';
18332
+ ofType: null;
18333
+ };
18334
+ };
18335
+ 'restockingFeePercentage': {
18336
+ name: 'restockingFeePercentage';
18337
+ type: {
18338
+ kind: 'SCALAR';
18339
+ name: 'Float';
18340
+ ofType: null;
18341
+ };
18342
+ };
18343
+ 'returnShippingCost': {
18344
+ name: 'returnShippingCost';
18345
+ type: {
18346
+ kind: 'NON_NULL';
18347
+ name: never;
18348
+ ofType: {
18349
+ kind: 'ENUM';
18350
+ name: 'ShopReturnShippingCostEnum';
18351
+ ofType: null;
18352
+ };
18353
+ };
18354
+ };
18355
+ 'returnWindowDays': {
18356
+ name: 'returnWindowDays';
18357
+ type: {
18358
+ kind: 'SCALAR';
18359
+ name: 'Int';
18360
+ ofType: null;
18361
+ };
18362
+ };
18363
+ };
18364
+ };
18365
+ 'ShopReturnShippingCostEnum': {
18366
+ name: 'ShopReturnShippingCostEnum';
18367
+ enumValues: 'CUSTOMER_PROVIDED' | 'FLAT_RATE' | 'FREE';
18368
+ };
18279
18369
  'ShopSeo': {
18280
18370
  kind: 'OBJECT';
18281
18371
  name: 'ShopSeo';
package/dist/product.d.ts CHANGED
@@ -40,6 +40,8 @@ export type ProductData = {
40
40
  type: string;
41
41
  value: string;
42
42
  }>;
43
+ finalSale?: boolean;
44
+ gtin?: string | null;
43
45
  };
44
46
  export declare const BadgeTagsFragment: import("gql.tada").TadaDocumentNode<{
45
47
  tags: string[];
package/dist/product.js CHANGED
@@ -196,6 +196,7 @@ const FetchProductFullQuery = graphql(`
196
196
  themeTemplate
197
197
  ...BadgeTags
198
198
  giftCard
199
+ finalSale
199
200
  descriptionHtml
200
201
  # Server-side plain-text projection of descriptionHtml — Rails
201
202
  # strips tags AND decodes HTML entities, which a client-side regex
@@ -207,6 +208,7 @@ const FetchProductFullQuery = graphql(`
207
208
  }
208
209
  defaultVariant {
209
210
  ...ProductVariantNode
211
+ gtin
210
212
  }
211
213
  options {
212
214
  ...ProductOptionNode
@@ -477,7 +479,9 @@ async function loadProductFull(handle, locale) {
477
479
  title: p.seo.title,
478
480
  description: p.seo.description
479
481
  },
480
- metafields: toMetafieldMap(p.metafields.nodes)
482
+ metafields: toMetafieldMap(p.metafields.nodes),
483
+ finalSale: p.finalSale,
484
+ gtin: p.defaultVariant.gtin ?? null
481
485
  };
482
486
  } catch (err) {
483
487
  console.warn("[fetchProductFull] failed:", err);
@@ -4753,6 +4753,11 @@ type Shop implements Node {
4753
4753
  """
4754
4754
  refundPolicy: ShopPolicy
4755
4755
 
4756
+ """
4757
+ Per-shop return policy defaults. Always present — an unconfigured shop reads back the defaults.
4758
+ """
4759
+ returnPolicy: ShopReturnPolicy!
4760
+
4756
4761
  """
4757
4762
  Shop-level SEO settings (homepage meta, social image, verification tokens). Null until the merchant configures it.
4758
4763
  """
@@ -7951,6 +7956,11 @@ type ShopProduct implements Node {
7951
7956
  """
7952
7957
  featuredMedia: FileInterface
7953
7958
 
7959
+ """
7960
+ Whether the return policy marks the whole product final sale (no returns). Resolve on one product at a time.
7961
+ """
7962
+ finalSale: Boolean!
7963
+
7954
7964
  """
7955
7965
  True when this is a digital gift-card product (gift-card category + every
7956
7966
  variant unshipped). Powers the storefront recipient form and digital fulfillment.
@@ -8609,6 +8619,11 @@ type ShopProductVariant implements Node {
8609
8619
  First variant media — used as a thumbnail
8610
8620
  """
8611
8621
  featuredMedia: FileInterface
8622
+
8623
+ """
8624
+ The GTIN for structured data: the one the merchant entered, else a barcode that passes the GS1 check; null otherwise.
8625
+ """
8626
+ gtin: String
8612
8627
  id: ID!
8613
8628
 
8614
8629
  """
@@ -8791,6 +8806,56 @@ type ShopProductVariantEdge {
8791
8806
  node: ShopProductVariant!
8792
8807
  }
8793
8808
 
8809
+ """
8810
+ Per-shop return policy defaults. Advisory only — nothing here blocks a merchant-created return.
8811
+ """
8812
+ type ShopReturnPolicy {
8813
+ """
8814
+ Whether the merchant has saved this policy. False means every value is a column default.
8815
+ """
8816
+ configured: Boolean!
8817
+
8818
+ """
8819
+ Flat return-shipping amount in the shop currency. Null unless `returnShippingCost` is FLAT_RATE.
8820
+ """
8821
+ flatRateAmount: Money
8822
+
8823
+ """
8824
+ Default restocking fee, 0..100. Null means no restocking fee is suggested.
8825
+ """
8826
+ restockingFeePercentage: Float
8827
+
8828
+ """
8829
+ Who pays return shipping by default.
8830
+ """
8831
+ returnShippingCost: ShopReturnShippingCostEnum!
8832
+
8833
+ """
8834
+ Days after shipment a buyer may return an item. Null means an unlimited window.
8835
+ """
8836
+ returnWindowDays: Int
8837
+ }
8838
+
8839
+ """
8840
+ Who pays to ship a return back, by default.
8841
+ """
8842
+ enum ShopReturnShippingCostEnum {
8843
+ """
8844
+ The buyer arranges and pays for return shipping.
8845
+ """
8846
+ CUSTOMER_PROVIDED
8847
+
8848
+ """
8849
+ A fixed amount is withheld; see `flatRateAmount`.
8850
+ """
8851
+ FLAT_RATE
8852
+
8853
+ """
8854
+ The merchant covers return shipping.
8855
+ """
8856
+ FREE
8857
+ }
8858
+
8794
8859
  """
8795
8860
  Per-shop SEO settings consumed by the storefront theme.
8796
8861
  """
@@ -0,0 +1,47 @@
1
+ import { ResultOf } from "./graphql.js";
2
+ import { OrganizationSchemaSource, ReturnPolicySchemaSource } from "@shop-on/theme-runtime/template-types";
3
+ //#region src/structured-data.d.ts
4
+ declare const ShopStructuredDataQuery: import("gql.tada").TadaDocumentNode<{
5
+ shop: {
6
+ branding: {
7
+ logo: {
8
+ url: string | null;
9
+ } | null;
10
+ } | null;
11
+ countryCode: string | null;
12
+ refundPolicy: {
13
+ url: string;
14
+ } | null;
15
+ returnPolicy: {
16
+ configured: boolean;
17
+ flatRateAmount: {
18
+ amount: string;
19
+ currencyCode: "CNY" | "EUR" | "GBP" | "JPY" | "KRW" | "MNT" | "RUB" | "USD";
20
+ } | null;
21
+ restockingFeePercentage: number | null;
22
+ returnShippingCost: "CUSTOMER_PROVIDED" | "FLAT_RATE" | "FREE";
23
+ returnWindowDays: number | null;
24
+ };
25
+ } | null;
26
+ }, {}, void>;
27
+ type ShopRecord = NonNullable<ResultOf<typeof ShopStructuredDataQuery>["shop"]>;
28
+ /**
29
+ * The shop's return policy as schema.org reads it, for one product or (with
30
+ * `finalSale` false) the shop as a whole. Undefined until the merchant saves a
31
+ * policy: the defaults an unconfigured shop reads back ("unlimited window,
32
+ * free returns") are no promise the merchant made, and publishing them would
33
+ * tell Google and every agent exactly that. Also undefined without a country,
34
+ * which Google requires.
35
+ */
36
+ export declare function returnPolicySource(shop: ShopRecord, { finalSale, base }: {
37
+ finalSale: boolean;
38
+ base: string;
39
+ }): ReturnPolicySchemaSource | undefined;
40
+ /** A product's return policy: the shop's, or "not permitted" when the
41
+ * product is final sale (`ShopProduct.finalSale`). */
42
+ export declare function fetchProductReturnPolicy(finalSale: boolean, base: string): Promise<ReturnPolicySchemaSource | undefined>;
43
+ /** The homepage's Organization node. Undefined without a name or origin —
44
+ * schema.org needs both and the homepage then emits no node at all. */
45
+ export declare function fetchOrganizationSchema(name: string, base: string): Promise<OrganizationSchemaSource | undefined>;
46
+ //#endregion
47
+ export {};
@@ -0,0 +1,110 @@
1
+ import { memoTTL } from "./cache.js";
2
+ import { graphql, request } from "./graphql.js";
3
+ import { getContentVersion } from "./content-version.js";
4
+ //#region src/structured-data.ts
5
+ const ShopStructuredDataQuery = graphql(`
6
+ query StorefrontShopStructuredData {
7
+ shop {
8
+ countryCode
9
+ branding {
10
+ logo {
11
+ url
12
+ }
13
+ }
14
+ refundPolicy {
15
+ url
16
+ }
17
+ returnPolicy {
18
+ configured
19
+ returnWindowDays
20
+ restockingFeePercentage
21
+ returnShippingCost
22
+ flatRateAmount {
23
+ amount
24
+ currencyCode
25
+ }
26
+ }
27
+ }
28
+ }
29
+ `);
30
+ const fetchShopRecord = memoTTL(async () => (await request(ShopStructuredDataQuery)).shop ?? null, {
31
+ key: () => "",
32
+ version: getContentVersion
33
+ });
34
+ async function safeShopRecord() {
35
+ try {
36
+ return await fetchShopRecord();
37
+ } catch (err) {
38
+ console.warn("[structured-data] shop fetch failed:", err);
39
+ return null;
40
+ }
41
+ }
42
+ const FEES = {
43
+ FREE: "FreeReturn",
44
+ CUSTOMER_PROVIDED: "ReturnFeesCustomerResponsibility",
45
+ FLAT_RATE: "ReturnShippingFees"
46
+ };
47
+ /**
48
+ * The shop's return policy as schema.org reads it, for one product or (with
49
+ * `finalSale` false) the shop as a whole. Undefined until the merchant saves a
50
+ * policy: the defaults an unconfigured shop reads back ("unlimited window,
51
+ * free returns") are no promise the merchant made, and publishing them would
52
+ * tell Google and every agent exactly that. Also undefined without a country,
53
+ * which Google requires.
54
+ */
55
+ function returnPolicySource(shop, { finalSale, base }) {
56
+ const policy = shop.returnPolicy;
57
+ if (!policy.configured || !shop.countryCode) return void 0;
58
+ const url = shop.refundPolicy && base ? `${base}${shop.refundPolicy.url}` : void 0;
59
+ const country = shop.countryCode;
60
+ if (finalSale) return {
61
+ country,
62
+ category: "NotPermitted",
63
+ ...url ? { url } : {}
64
+ };
65
+ const days = policy.returnWindowDays;
66
+ const fee = policy.flatRateAmount;
67
+ const restocking = policy.restockingFeePercentage;
68
+ return {
69
+ country,
70
+ ...days ? {
71
+ category: "FiniteReturnWindow",
72
+ days
73
+ } : { category: "UnlimitedWindow" },
74
+ fees: FEES[policy.returnShippingCost],
75
+ ...policy.returnShippingCost === "FLAT_RATE" && fee ? { shippingFee: {
76
+ amount: Number(fee.amount),
77
+ currency: fee.currencyCode
78
+ } } : {},
79
+ ...restocking ? { restockingFeePercent: restocking } : {},
80
+ ...url ? { url } : {}
81
+ };
82
+ }
83
+ /** A product's return policy: the shop's, or "not permitted" when the
84
+ * product is final sale (`ShopProduct.finalSale`). */
85
+ async function fetchProductReturnPolicy(finalSale, base) {
86
+ const shop = await safeShopRecord();
87
+ return shop ? returnPolicySource(shop, {
88
+ finalSale,
89
+ base
90
+ }) : void 0;
91
+ }
92
+ /** The homepage's Organization node. Undefined without a name or origin —
93
+ * schema.org needs both and the homepage then emits no node at all. */
94
+ async function fetchOrganizationSchema(name, base) {
95
+ if (!name || !base) return void 0;
96
+ const shop = await safeShopRecord();
97
+ const logo = shop?.branding?.logo?.url;
98
+ const returnPolicy = shop ? returnPolicySource(shop, {
99
+ finalSale: false,
100
+ base
101
+ }) : void 0;
102
+ return {
103
+ name,
104
+ url: `${base}/`,
105
+ ...logo ? { logo } : {},
106
+ ...returnPolicy ? { returnPolicy } : {}
107
+ };
108
+ }
109
+ //#endregion
110
+ export { fetchOrganizationSchema, fetchProductReturnPolicy, returnPolicySource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shop-on/theme-server",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "SEE LICENSE IN dist/LICENSE.md",
5
5
  "files": [
6
6
  "dist"
@@ -51,6 +51,7 @@
51
51
  "./shop": "./dist/shop.js",
52
52
  "./shop-seo": "./dist/shop-seo.js",
53
53
  "./store-availability": "./dist/store-availability.js",
54
+ "./structured-data": "./dist/structured-data.js",
54
55
  "./translations": "./dist/translations.js",
55
56
  "./package.json": "./package.json"
56
57
  },
@@ -59,8 +60,8 @@
59
60
  "graphql": "17.0.2",
60
61
  "graphql-request": "7.4.0",
61
62
  "liquidjs": "10.30.0",
62
- "@shop-on/customer-runtime": "0.6.0",
63
- "@shop-on/theme-runtime": "0.6.0"
63
+ "@shop-on/theme-runtime": "0.7.0",
64
+ "@shop-on/customer-runtime": "0.7.0"
64
65
  },
65
66
  "devDependencies": {
66
67
  "@tanstack/react-start": "1.169.3",