@wocart/storefront 2.2.0 → 3.1.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/USAGE.md +189 -95
  3. package/dist/answer-readers.js +16 -43
  4. package/dist/astro.d.ts +6 -2
  5. package/dist/astro.js +3 -1
  6. package/dist/client/index.d.ts +1 -1
  7. package/dist/client/index.js +1 -1
  8. package/dist/client/products.d.ts +2 -3
  9. package/dist/client/products.js +2 -3
  10. package/dist/endpoints/revalidate-endpoint.d.ts +37 -0
  11. package/dist/endpoints/revalidate-endpoint.js +32 -0
  12. package/dist/endpoints/storefront-routes.js +1 -1
  13. package/dist/index.d.ts +8 -4
  14. package/dist/index.js +4 -2
  15. package/dist/locale-routing.d.ts +11 -20
  16. package/dist/locale-routing.js +28 -8
  17. package/dist/middleware.d.ts +6 -26
  18. package/dist/middleware.js +0 -9
  19. package/dist/product-view.d.ts +0 -8
  20. package/dist/product-view.js +0 -17
  21. package/dist/revalidate.d.ts +6 -0
  22. package/dist/revalidate.js +6 -0
  23. package/dist/routes.d.ts +44 -27
  24. package/dist/routes.js +54 -13
  25. package/dist/schemas.d.ts +144 -222
  26. package/dist/schemas.js +77 -137
  27. package/dist/seo.js +2 -2
  28. package/dist/server.d.ts +1 -1
  29. package/dist/server.js +1 -1
  30. package/dist/store-request.d.ts +5 -4
  31. package/dist/store-request.js +1 -1
  32. package/dist/storefront-middleware.d.ts +88 -0
  33. package/dist/storefront-middleware.js +37 -0
  34. package/dist/thank-you.d.ts +32 -0
  35. package/dist/thank-you.js +33 -0
  36. package/dist/types.d.ts +76 -111
  37. package/dist/unavailable-store.d.ts +23 -0
  38. package/dist/unavailable-store.js +58 -0
  39. package/dist/variant-picker.d.ts +6 -2
  40. package/dist/variant-picker.js +6 -9
  41. package/dist/whatsapp.d.ts +59 -6
  42. package/dist/whatsapp.js +28 -10
  43. package/package.json +1 -1
@@ -2,59 +2,33 @@ const isObject = (value) => typeof value === "object" && value !== null && !Arra
2
2
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
3
3
  const isInteger = (value) => typeof value === "number" && Number.isSafeInteger(value);
4
4
  const isNullableString = (value) => value === null || typeof value === "string";
5
- /** An optional field the schema reads as `.nullish()`: absent stays absent, null and the type are kept, anything else refuses. */
6
- function nullish(source, key, accepts) {
7
- if (!(key in source) || source[key] === undefined)
8
- return { ok: true, entry: {} };
9
- const value = source[key];
10
- return value === null || accepts(value) ? { ok: true, entry: { [key]: value } } : { ok: false, entry: {} };
11
- }
12
- const isString = (value) => typeof value === "string";
13
5
  const BLOCKERS = ["missing", "unlisted", "out_of_stock"];
6
+ const FREE_BY = ["offer", "coupon", "threshold"];
14
7
  function readQuoteLine(value) {
15
8
  if (!isObject(value))
16
9
  return null;
17
- const { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, maxQuantity, blocker } = value;
10
+ const { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, offerId, offerName, discount, maxQuantity, blocker } = value;
18
11
  if (typeof productId !== "string" || !isNullableString(variantId) || !isNullableString(variantLabel) || typeof productName !== "string")
19
12
  return null;
20
13
  if (!isInteger(quantity) || !isFiniteNumber(unitPrice) || !isFiniteNumber(lineTotal))
21
14
  return null;
15
+ if (!isNullableString(offerId) || !isNullableString(offerName) || !isFiniteNumber(discount))
16
+ return null;
22
17
  if (maxQuantity !== null && !isFiniteNumber(maxQuantity))
23
18
  return null;
24
19
  if (blocker !== null && !(typeof blocker === "string" && BLOCKERS.includes(blocker)))
25
20
  return null;
26
- const offerId = nullish(value, "offerId", isString);
27
- const offerName = nullish(value, "offerName", isString);
28
- const discount = nullish(value, "discount", isFiniteNumber);
29
- if (!offerId.ok || !offerName.ok || !discount.ok)
30
- return null;
31
- return {
32
- productId,
33
- variantId,
34
- variantLabel,
35
- productName,
36
- quantity,
37
- unitPrice,
38
- lineTotal,
39
- ...offerId.entry,
40
- ...offerName.entry,
41
- ...discount.entry,
42
- maxQuantity,
43
- blocker: blocker,
44
- };
21
+ return { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, offerId, offerName, discount, maxQuantity, blocker: blocker };
45
22
  }
46
23
  function readReward(value) {
47
24
  if (!isObject(value))
48
25
  return null;
49
- const { offerId, productId, productName, quantity } = value;
50
- if (typeof offerId !== "string" || !isNullableString(productId) || !isNullableString(productName) || !isInteger(quantity))
26
+ const { offerId, offerName, productId, productName, variantId, variantLabel, quantity } = value;
27
+ if (typeof offerId !== "string" || !isNullableString(offerName) || !isNullableString(productId) || !isNullableString(productName))
51
28
  return null;
52
- const offerName = nullish(value, "offerName", isString);
53
- const variantId = nullish(value, "variantId", isString);
54
- const variantLabel = nullish(value, "variantLabel", isString);
55
- if (!offerName.ok || !variantId.ok || !variantLabel.ok)
29
+ if (!isNullableString(variantId) || !isNullableString(variantLabel) || !isInteger(quantity))
56
30
  return null;
57
- return { offerId, ...offerName.entry, productId, productName, ...variantId.entry, ...variantLabel.entry, quantity };
31
+ return { offerId, offerName, productId, productName, variantId, variantLabel, quantity };
58
32
  }
59
33
  function readDelivery(value) {
60
34
  if (!isObject(value))
@@ -93,25 +67,24 @@ export function readCartQuote(value) {
93
67
  return null;
94
68
  if (free.threshold !== null && !isFiniteNumber(free.threshold))
95
69
  return null;
70
+ if (free.by !== null && !(typeof free.by === "string" && FREE_BY.includes(free.by)))
71
+ return null;
96
72
  let delivery = null;
97
73
  if ("delivery" in value && value.delivery !== undefined) {
98
74
  delivery = readDelivery(value.delivery);
99
75
  if (delivery === null)
100
76
  return null;
101
77
  }
102
- let coupon;
103
- if ("coupon" in value && value.coupon !== undefined) {
104
- coupon = value.coupon === null ? null : readCouponOutcome(value.coupon);
105
- if (coupon === null && value.coupon !== null)
106
- return null;
107
- }
78
+ const coupon = value.coupon === null ? null : readCouponOutcome(value.coupon);
79
+ if (coupon === null && value.coupon !== null)
80
+ return null;
108
81
  return {
109
82
  lines,
110
83
  subtotal: value.subtotal,
111
84
  rewards,
112
- freeDelivery: { fromOffer: free.fromOffer, threshold: free.threshold, qualified: free.qualified, remaining: free.remaining },
85
+ freeDelivery: { fromOffer: free.fromOffer, threshold: free.threshold, qualified: free.qualified, remaining: free.remaining, by: free.by },
113
86
  ...(delivery === null ? {} : { delivery }),
114
- ...(coupon === undefined ? {} : { coupon }),
87
+ coupon,
115
88
  };
116
89
  }
117
90
  /** The delivery rates of a value (`ShippingRates`), or null when it is not a rate table. */
package/dist/astro.d.ts CHANGED
@@ -4,9 +4,13 @@ export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from
4
4
  export type { StorefrontApiRoute, StorefrontRoutes, StorefrontRoutesOptions, } from "./endpoints/storefront-routes.js";
5
5
  export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
6
6
  export type { PlaceOrderInput, PlaceOrderResult } from "./actions.js";
7
- export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
8
- export type { PrgDecision, ActionMiddlewareDeps, ActionDescriptor } from "./middleware.js";
7
+ export { runFormAction } from "./middleware.js";
8
+ export type { ActionMiddlewareDeps, ActionDescriptor } from "./middleware.js";
9
9
  export { lookupOrder, orderLookupInput } from "./order-lookup.js";
10
10
  export type { OrderLookup, OrderLookupInput } from "./order-lookup.js";
11
11
  export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
12
12
  export type { NotFoundContext, ResolvedStore, StoreRequestOptions, StoreRequestResolution } from "./store-request.js";
13
+ export { createStorefrontMiddleware } from "./storefront-middleware.js";
14
+ export type { PreviewLocals, StorefrontActionContext, StorefrontMiddleware, StorefrontMiddlewareContext, StorefrontMiddlewareOptions, } from "./storefront-middleware.js";
15
+ export { revalidateEndpoint } from "./endpoints/revalidate-endpoint.js";
16
+ export type { RevalidateEndpointContext, RevalidateEndpointOptions } from "./endpoints/revalidate-endpoint.js";
package/dist/astro.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { createProxyEndpoint } from "./endpoints/proxy.js";
2
2
  export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
3
3
  export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
4
- export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
4
+ export { runFormAction } from "./middleware.js";
5
5
  export { lookupOrder, orderLookupInput } from "./order-lookup.js";
6
6
  export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
7
+ export { createStorefrontMiddleware } from "./storefront-middleware.js";
8
+ export { revalidateEndpoint } from "./endpoints/revalidate-endpoint.js";
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * const [config, products] = await Promise.all([
24
24
  * store.config.get(),
25
- * store.products.list({ featured: true, limit: 8 }),
25
+ * store.products.list({ collection: "featured", limit: 8 }),
26
26
  * ]);
27
27
  * ```
28
28
  */
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * const [config, products] = await Promise.all([
24
24
  * store.config.get(),
25
- * store.products.list({ featured: true, limit: 8 }),
25
+ * store.products.list({ collection: "featured", limit: 8 }),
26
26
  * ]);
27
27
  * ```
28
28
  */
@@ -41,9 +41,8 @@ export interface ProductsNamespace {
41
41
  * The products a shopper's words find — every word in the product's name,
42
42
  * in the client's language or the catalog's, or in its tags — as a page,
43
43
  * with the listing filters, sort and paging. Without a `sort`, products
44
- * whose name holds every word come first. Blank words find nothing; a
45
- * server that predates search answers an empty page (reported through
46
- * `onError`), never the whole catalog.
44
+ * whose name holds every word come first. Blank words find nothing, never
45
+ * the whole catalog.
47
46
  */
48
47
  search(text: string, filters?: ProductListFilters): Promise<ProductPage>;
49
48
  /**
@@ -29,7 +29,6 @@ export function createProductsNamespace(core) {
29
29
  categoryId: filters?.categoryId || undefined,
30
30
  collection: filters?.collection || undefined,
31
31
  onSale: filters?.onSale ? "true" : undefined,
32
- featured: filters?.featured ? "true" : undefined,
33
32
  sort: filters?.sort || undefined,
34
33
  limit: filters?.limit === undefined ? undefined : String(filters.limit),
35
34
  offset: filters?.offset ? String(filters.offset) : undefined,
@@ -53,11 +52,11 @@ export function createProductsNamespace(core) {
53
52
  operation,
54
53
  });
55
54
  const rows = envelope?.data ?? [];
56
- return { items: rows.slice(0, size), hasNext: rows.length > size, total: envelope?.total ?? null };
55
+ return { items: rows.slice(0, size), hasNext: rows.length > size, total: envelope?.total ?? 0 };
57
56
  }
58
57
  catch (error) {
59
58
  reportDegradedRead(core.config, error, operation);
60
- return { items: [], hasNext: false, total: null };
59
+ return { items: [], hasNext: false, total: 0 };
61
60
  }
62
61
  };
63
62
  return {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The revalidation route a theme exports, ready-made:
3
+ *
4
+ * ```ts
5
+ * // src/pages/wocart-revalidate.ts
6
+ * import { revalidateEndpoint } from "@wocart/storefront/astro";
7
+ *
8
+ * export const prerender = false;
9
+ * export const { POST, ALL } = revalidateEndpoint({ publicKey: () => WOCART_PREVIEW_KEY, theme: THEME_KEY });
10
+ * ```
11
+ *
12
+ * `POST` verifies the platform's token (`readRevalidateRequest`), drops every
13
+ * page cached under `PAGES_CACHE_TAG` and answers 204; a refused token
14
+ * answers its status and drops nothing. `ALL` answers any other method 405.
15
+ * Neither answer is ever cached.
16
+ */
17
+ import { type RevalidateRequestOptions } from "../revalidate.js";
18
+ /** What the route uses of Astro's `APIContext`. */
19
+ export interface RevalidateEndpointContext {
20
+ request: Request;
21
+ cache: {
22
+ invalidate(options: {
23
+ tags: string[];
24
+ }): Promise<void>;
25
+ };
26
+ }
27
+ export interface RevalidateEndpointOptions {
28
+ /** The deployment's preview public key; a function reads it on each request. Absent: every request answers 404. */
29
+ publicKey: RevalidateRequestOptions["publicKey"] | (() => RevalidateRequestOptions["publicKey"]);
30
+ /** This theme's key. */
31
+ theme: string;
32
+ }
33
+ /** The theme's `/wocart-revalidate` route: `POST` drops the cached pages for a verified token, `ALL` answers 405. */
34
+ export declare function revalidateEndpoint(options: RevalidateEndpointOptions): {
35
+ POST: (context: RevalidateEndpointContext) => Promise<Response>;
36
+ ALL: () => Response;
37
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The revalidation route a theme exports, ready-made:
3
+ *
4
+ * ```ts
5
+ * // src/pages/wocart-revalidate.ts
6
+ * import { revalidateEndpoint } from "@wocart/storefront/astro";
7
+ *
8
+ * export const prerender = false;
9
+ * export const { POST, ALL } = revalidateEndpoint({ publicKey: () => WOCART_PREVIEW_KEY, theme: THEME_KEY });
10
+ * ```
11
+ *
12
+ * `POST` verifies the platform's token (`readRevalidateRequest`), drops every
13
+ * page cached under `PAGES_CACHE_TAG` and answers 204; a refused token
14
+ * answers its status and drops nothing. `ALL` answers any other method 405.
15
+ * Neither answer is ever cached.
16
+ */
17
+ import { PAGES_CACHE_TAG, readRevalidateRequest } from "../revalidate.js";
18
+ const noStore = (status, headers = {}) => new Response(null, { status, headers: { "Cache-Control": "no-store", ...headers } });
19
+ /** The theme's `/wocart-revalidate` route: `POST` drops the cached pages for a verified token, `ALL` answers 405. */
20
+ export function revalidateEndpoint(options) {
21
+ return {
22
+ POST: async (context) => {
23
+ const publicKey = typeof options.publicKey === "function" ? options.publicKey() : options.publicKey;
24
+ const revalidation = await readRevalidateRequest(context.request, { publicKey, theme: options.theme });
25
+ if (!revalidation.ok)
26
+ return noStore(revalidation.status);
27
+ await context.cache.invalidate({ tags: [PAGES_CACHE_TAG] });
28
+ return noStore(204);
29
+ },
30
+ ALL: () => noStore(405, { Allow: "POST" }),
31
+ };
32
+ }
@@ -75,7 +75,7 @@ const reviewsListInput = z.object({
75
75
  */
76
76
  const reviewSubmitInput = z.object({
77
77
  orderNumber: z.string().min(1).max(64),
78
- phone: z.string().min(1).max(32).optional(),
78
+ phone: z.string().min(1).max(32),
79
79
  productId: z.string().min(1).max(200),
80
80
  rating: z.number(),
81
81
  title: z.string().max(500).optional(),
package/dist/index.d.ts CHANGED
@@ -15,13 +15,15 @@ export { isMediaKey, mediaImageSrcset, mediaImageUrl, mediaUrl, pictureOf } from
15
15
  export type { MediaDelivery, MediaDeliveryKind, MediaImage, MediaImageParams, MediaImageSrcsetOptions, PickedImage, Picture } from "./utils/image.js";
16
16
  export { productImageSource } from "./utils/product-image.js";
17
17
  export type { ProductImageSourceInput } from "./utils/product-image.js";
18
- export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, localWhatsAppWidget, } from "./whatsapp.js";
19
- export type { PrefillToken, ResolvePrefillParams } from "./whatsapp.js";
20
- export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
18
+ export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, whatsAppContact } from "./whatsapp.js";
19
+ export type { PrefillToken, WhatsAppContact, WhatsAppPage, WhatsAppWords } from "./whatsapp.js";
20
+ export { SINGLE_LANGUAGE_PAGE_TYPES, STOREFRONT_API_PATHS, STOREFRONT_PAGE_PATHS, isSingleLanguagePage, matchStorefrontPath, showsWidgetOn, storefrontApiPath, storefrontPath } from "./routes.js";
21
21
  export { servedLanguage } from "./locales.js";
22
+ export { unavailableStoreConfig } from "./unavailable-store.js";
23
+ export type { UnavailableStoreOptions } from "./unavailable-store.js";
22
24
  export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
23
25
  export type { LocalePath, LocaleRouting } from "./locale-routing.js";
24
- export type { StorefrontPageMatch, StorefrontPageType, StorefrontPathParams, StorefrontRoute } from "./routes.js";
26
+ export type { StorefrontApiName, StorefrontApiParams, StorefrontPageMatch, StorefrontPageType, StorefrontPathParams } from "./routes.js";
25
27
  export { addedLines, createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
26
28
  export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
27
29
  export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, BasketSelectionLine, } from "./basket.js";
@@ -36,6 +38,8 @@ export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-re
36
38
  export { createQuoter } from "./quote.js";
37
39
  export type { Quoter, QuoterOptions, QuoteResult } from "./quote.js";
38
40
  export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
41
+ export { readThankYou, thankYouHref } from "./thank-you.js";
42
+ export type { ThankYouOrder } from "./thank-you.js";
39
43
  export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
40
44
  export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
41
45
  export type { CanonicalPage, PageMeta, ProductStructuredDataOptions } from "./seo.js";
package/dist/index.js CHANGED
@@ -12,9 +12,10 @@
12
12
  export { formatMoney } from "./utils/money.js";
13
13
  export { isMediaKey, mediaImageSrcset, mediaImageUrl, mediaUrl, pictureOf } from "./utils/image.js";
14
14
  export { productImageSource } from "./utils/product-image.js";
15
- export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, localWhatsAppWidget, } from "./whatsapp.js";
16
- export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
15
+ export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, whatsAppContact } from "./whatsapp.js";
16
+ export { SINGLE_LANGUAGE_PAGE_TYPES, STOREFRONT_API_PATHS, STOREFRONT_PAGE_PATHS, isSingleLanguagePage, matchStorefrontPath, showsWidgetOn, storefrontApiPath, storefrontPath } from "./routes.js";
17
17
  export { servedLanguage } from "./locales.js";
18
+ export { unavailableStoreConfig } from "./unavailable-store.js";
18
19
  export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
19
20
  export { addedLines, createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
20
21
  export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
@@ -26,6 +27,7 @@ export { isWilayaId } from "./wilaya-id.js";
26
27
  export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-readers.js";
27
28
  export { createQuoter } from "./quote.js";
28
29
  export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
30
+ export { readThankYou, thankYouHref } from "./thank-you.js";
29
31
  export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
30
32
  export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
31
33
  export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
@@ -1,16 +1,3 @@
1
- /**
2
- * Language-prefixed storefront URLs — Shopify's
3
- * subfolders: the primary language keeps today's URLs (`/products/x`), every
4
- * other published language lives under its code (`/fr/products/x`), and the
5
- * primary has exactly one address (`/ar/products/x` redirects to
6
- * `/products/x`).
7
- *
8
- * Pure functions over the store's own languages (`StoreConfig.lang` and
9
- * `StoreConfig.locales`): a theme reads the request's language with
10
- * `resolveLocalePath`, builds every internal link with `localizeHref`, and
11
- * lists a page's language versions with `alternateLinks` — the counterpart of
12
- * Shopify's `request.locale.root_url` and `routes`.
13
- */
14
1
  import type { StoreConfig } from "./types.js";
15
2
  /** The store's languages as URLs need them. */
16
3
  export interface LocaleRouting {
@@ -27,7 +14,7 @@ export type LocalePath =
27
14
  locale: string;
28
15
  pathname: string;
29
16
  }
30
- /** The primary under a prefix: send the shopper to its one address. */
17
+ /** The primary under a prefix, or a single-language page under any: send the shopper to its one address. */
31
18
  | {
32
19
  kind: "redirect";
33
20
  location: string;
@@ -35,23 +22,27 @@ export type LocalePath =
35
22
  /**
36
23
  * Which language a request is for, and the path it renders. A first segment
37
24
  * that is a published language other than the primary selects it; the
38
- * primary's own prefix redirects; anything else — an unpublished or unknown
39
- * code included — is an ordinary path in the primary language, so it renders
40
- * or 404s exactly as before.
25
+ * primary's own prefix redirects, and so does any prefix on a page that
26
+ * exists in the primary language only (`isSingleLanguagePage`: `/fr/lp/x` →
27
+ * `/lp/x`); anything else — an unpublished or unknown code included — is an
28
+ * ordinary path in the primary language, so it renders or 404s exactly as
29
+ * before.
41
30
  */
42
31
  export declare function resolveLocalePath(pathname: string, routing: LocaleRouting): LocalePath;
43
32
  /**
44
33
  * An internal link in `locale`: `/products/x` → `/fr/products/x`, `/` → `/fr`.
45
34
  * Left unchanged: the primary language, external and protocol-relative URLs,
46
- * fragments, `mailto:`/`tel:`, endpoints and files, and a link that already
47
- * carries a language prefix. Query strings and fragments are kept.
35
+ * fragments, `mailto:`/`tel:`, endpoints and files, a page that exists in the
36
+ * primary language only (a landing page), and a link that already carries a
37
+ * language prefix. Query strings and fragments are kept.
48
38
  */
49
39
  export declare function localizeHref(href: string, locale: string, routing: LocaleRouting): string;
50
40
  /**
51
41
  * Every published language's URL for one page, primary first, plus
52
42
  * `x-default` (the primary) — the `<link rel="alternate" hreflang>` set.
53
43
  * `pathname` is the page's unprefixed path; `origin` makes the URLs absolute,
54
- * as search engines require.
44
+ * as search engines require. None for a page that exists in the primary
45
+ * language only.
55
46
  */
56
47
  export declare function alternateLinks(pathname: string, routing: LocaleRouting, origin: string): {
57
48
  hreflang: string;
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Language-prefixed storefront URLs — Shopify's
3
+ * subfolders: the primary language keeps today's URLs (`/products/x`), every
4
+ * other published language lives under its code (`/fr/products/x`), and the
5
+ * primary has exactly one address (`/ar/products/x` redirects to
6
+ * `/products/x`).
7
+ *
8
+ * Pure functions over the store's own languages (`StoreConfig.lang` and
9
+ * `StoreConfig.locales`): a theme reads the request's language with
10
+ * `resolveLocalePath`, builds every internal link with `localizeHref`, and
11
+ * lists a page's language versions with `alternateLinks` — the counterpart of
12
+ * Shopify's `request.locale.root_url` and `routes`.
13
+ */
14
+ import { isSingleLanguagePage } from "./routes.js";
1
15
  export function localeRouting(config) {
2
16
  const published = config.locales.map((language) => language.code);
3
17
  return {
@@ -12,16 +26,18 @@ function isPagePath(pathname) {
12
26
  /**
13
27
  * Which language a request is for, and the path it renders. A first segment
14
28
  * that is a published language other than the primary selects it; the
15
- * primary's own prefix redirects; anything else — an unpublished or unknown
16
- * code included — is an ordinary path in the primary language, so it renders
17
- * or 404s exactly as before.
29
+ * primary's own prefix redirects, and so does any prefix on a page that
30
+ * exists in the primary language only (`isSingleLanguagePage`: `/fr/lp/x` →
31
+ * `/lp/x`); anything else — an unpublished or unknown code included — is an
32
+ * ordinary path in the primary language, so it renders or 404s exactly as
33
+ * before.
18
34
  */
19
35
  export function resolveLocalePath(pathname, routing) {
20
36
  const match = /^\/([^/]+)(\/.*)?$/.exec(pathname);
21
37
  const segment = match?.[1];
22
38
  if (segment !== undefined && routing.published.includes(segment)) {
23
39
  const rest = match?.[2] ?? "/";
24
- if (segment === routing.primary)
40
+ if (segment === routing.primary || isSingleLanguagePage(rest))
25
41
  return { kind: "redirect", location: rest };
26
42
  return { kind: "page", locale: segment, pathname: rest };
27
43
  }
@@ -30,8 +46,9 @@ export function resolveLocalePath(pathname, routing) {
30
46
  /**
31
47
  * An internal link in `locale`: `/products/x` → `/fr/products/x`, `/` → `/fr`.
32
48
  * Left unchanged: the primary language, external and protocol-relative URLs,
33
- * fragments, `mailto:`/`tel:`, endpoints and files, and a link that already
34
- * carries a language prefix. Query strings and fragments are kept.
49
+ * fragments, `mailto:`/`tel:`, endpoints and files, a page that exists in the
50
+ * primary language only (a landing page), and a link that already carries a
51
+ * language prefix. Query strings and fragments are kept.
35
52
  */
36
53
  export function localizeHref(href, locale, routing) {
37
54
  if (locale === routing.primary || !href.startsWith("/") || href.startsWith("//"))
@@ -39,7 +56,7 @@ export function localizeHref(href, locale, routing) {
39
56
  const split = /^([^?#]*)(.*)$/.exec(href);
40
57
  const pathname = split[1];
41
58
  const rest = split[2];
42
- if (!isPagePath(pathname))
59
+ if (!isPagePath(pathname) || isSingleLanguagePage(pathname))
43
60
  return href;
44
61
  const first = /^\/([^/]+)/.exec(pathname)?.[1];
45
62
  if (first !== undefined && routing.published.includes(first))
@@ -50,9 +67,12 @@ export function localizeHref(href, locale, routing) {
50
67
  * Every published language's URL for one page, primary first, plus
51
68
  * `x-default` (the primary) — the `<link rel="alternate" hreflang>` set.
52
69
  * `pathname` is the page's unprefixed path; `origin` makes the URLs absolute,
53
- * as search engines require.
70
+ * as search engines require. None for a page that exists in the primary
71
+ * language only.
54
72
  */
55
73
  export function alternateLinks(pathname, routing, origin) {
74
+ if (isSingleLanguagePage(pathname))
75
+ return [];
56
76
  const base = origin.replace(/\/+$/, "");
57
77
  const links = routing.published.map((locale) => ({
58
78
  hreflang: locale,
@@ -53,10 +53,11 @@ export interface ActionDescriptor<TResult extends ActionHandlerResult = ActionHa
53
53
  handler: () => Promise<TResult>;
54
54
  }
55
55
  /**
56
- * `TSerialized` is the framework's result-cookie token, opaque to this SDK:
57
- * Astro 5 used a string, Astro 7 an object (`SerializedActionResult`). The
58
- * generics thread whatever `getActionContext()` hands the theme straight
59
- * through, so no theme ever casts.
56
+ * What `runFormAction` needs from `getActionContext()`. `TSerialized` is the
57
+ * framework's result-cookie token, opaque to this SDK: Astro 5 used a string,
58
+ * Astro 7 an object (`SerializedActionResult`). The generics thread whatever
59
+ * `getActionContext()` hands the theme straight through, so no theme ever
60
+ * casts.
60
61
  */
61
62
  export interface ActionMiddlewareDeps<TResult extends ActionHandlerResult = ActionHandlerResult, TSerialized = string> {
62
63
  action?: ActionDescriptor<TResult>;
@@ -64,31 +65,10 @@ export interface ActionMiddlewareDeps<TResult extends ActionHandlerResult = Acti
64
65
  setActionResult: (name: string, serialized: TSerialized) => void;
65
66
  /** Serializes an ActionResult into the cookie value (Astro's serializeActionResult). */
66
67
  serializeActionResult: (result: TResult) => TSerialized;
67
- /** @deprecated Unused since 1.2.0: nothing redirects. */
68
- referer?: string | null;
69
- /** @deprecated Unused since 1.2.0: nothing redirects. */
70
- pathname?: string;
71
68
  }
72
- export type PrgDecision =
73
- /** Never produced since 1.2.0 — kept so existing `decision.kind` checks compile. */
74
- {
75
- kind: "redirect";
76
- location: string;
77
- status: 303;
78
- }
79
- /** Render the page from this POST. */
80
- | {
81
- kind: "next";
82
- };
83
69
  /**
84
70
  * Run an action called from an HTML form and store its result for the page
85
71
  * this POST renders. Resolves to that result, or `undefined` when the request
86
72
  * is not a form-called action (a JSON call or a plain request is Astro's).
87
73
  */
88
- export declare function runFormAction<TResult extends ActionHandlerResult, TSerialized>(deps: Pick<ActionMiddlewareDeps<TResult, TSerialized>, "action" | "setActionResult" | "serializeActionResult">): Promise<TResult | undefined>;
89
- /**
90
- * @deprecated Use `runFormAction` and call `next()`. Runs the action the same
91
- * way and always answers `{ kind: "next" }`: an errored post renders in place,
92
- * because a redirect drops the result (see the note at the top of this file).
93
- */
94
- export declare function resolveActionRedirectDecision<TResult extends ActionHandlerResult, TSerialized>(deps: ActionMiddlewareDeps<TResult, TSerialized>): Promise<PrgDecision>;
74
+ export declare function runFormAction<TResult extends ActionHandlerResult, TSerialized>(deps: ActionMiddlewareDeps<TResult, TSerialized>): Promise<TResult | undefined>;
@@ -51,12 +51,3 @@ export async function runFormAction(deps) {
51
51
  setActionResult(action.name, serializeActionResult(result));
52
52
  return result;
53
53
  }
54
- /**
55
- * @deprecated Use `runFormAction` and call `next()`. Runs the action the same
56
- * way and always answers `{ kind: "next" }`: an errored post renders in place,
57
- * because a redirect drops the result (see the note at the top of this file).
58
- */
59
- export async function resolveActionRedirectDecision(deps) {
60
- await runFormAction(deps);
61
- return { kind: "next" };
62
- }
@@ -10,11 +10,3 @@ export interface ProductView {
10
10
  }
11
11
  export declare function getProductView(product: ProductDetail): ProductView;
12
12
  export declare function getProductListView(product: ProductList): ProductView;
13
- /**
14
- * A product's tags as a list, whichever read it came from: a product page's
15
- * `tags` is already a list, a listing's is the stored JSON text. Anything
16
- * that is not a list of text reads as no tags.
17
- */
18
- export declare function productTags(product: {
19
- tags: string | null | readonly string[];
20
- }): string[];
@@ -44,20 +44,3 @@ export function getProductListView(product) {
44
44
  ...stockOf(product),
45
45
  };
46
46
  }
47
- /**
48
- * A product's tags as a list, whichever read it came from: a product page's
49
- * `tags` is already a list, a listing's is the stored JSON text. Anything
50
- * that is not a list of text reads as no tags.
51
- */
52
- export function productTags(product) {
53
- let tags = product.tags;
54
- if (typeof tags === "string") {
55
- try {
56
- tags = JSON.parse(tags);
57
- }
58
- catch {
59
- return [];
60
- }
61
- }
62
- return Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [];
63
- }
@@ -1,5 +1,11 @@
1
1
  /** Where a theme that can drop its cached pages answers a revalidation. */
2
2
  export declare const REVALIDATE_PATH = "/wocart-revalidate";
3
+ /**
4
+ * The cache tag every cached page carries, and the one a revalidation drops:
5
+ * a publish (pages, settings or text) can change any page, so a theme tags
6
+ * every cached route with it (`routeRules` in `astro.config.mjs`).
7
+ */
8
+ export declare const PAGES_CACHE_TAG = "pages";
3
9
  /** What a verified revalidation token says. */
4
10
  export interface RevalidateClaims {
5
11
  /** The storefront origin the token was made for. */
@@ -17,6 +17,12 @@
17
17
  import { timingRefusal, verifiedPayload } from "./signed-token.js";
18
18
  /** Where a theme that can drop its cached pages answers a revalidation. */
19
19
  export const REVALIDATE_PATH = "/wocart-revalidate";
20
+ /**
21
+ * The cache tag every cached page carries, and the one a revalidation drops:
22
+ * a publish (pages, settings or text) can change any page, so a theme tags
23
+ * every cached route with it (`routeRules` in `astro.config.mjs`).
24
+ */
25
+ export const PAGES_CACHE_TAG = "pages";
20
26
  /** The longest a revalidation token may live. */
21
27
  const MAX_LIFETIME_SECONDS = 5 * 60;
22
28
  const refuse = (status, reason) => ({ ok: false, status, reason });