@wocart/storefront 3.0.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.
- package/CHANGELOG.md +71 -0
- package/USAGE.md +93 -12
- package/dist/astro.d.ts +4 -0
- package/dist/astro.js +2 -0
- package/dist/endpoints/revalidate-endpoint.d.ts +37 -0
- package/dist/endpoints/revalidate-endpoint.js +32 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/locale-routing.d.ts +11 -20
- package/dist/locale-routing.js +28 -8
- package/dist/revalidate.d.ts +6 -0
- package/dist/revalidate.js +6 -0
- package/dist/routes.d.ts +42 -16
- package/dist/routes.js +48 -4
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/storefront-middleware.d.ts +88 -0
- package/dist/storefront-middleware.js +37 -0
- package/dist/thank-you.d.ts +32 -0
- package/dist/thank-you.js +33 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,77 @@ follows [semantic versioning](https://semver.org/): breaking changes ship only
|
|
|
5
5
|
in a new major version, and an export is marked `@deprecated` for at least one
|
|
6
6
|
minor release before a major removes it.
|
|
7
7
|
|
|
8
|
+
## 3.1.0 — 2026-11-03
|
|
9
|
+
|
|
10
|
+
The theme's request lifecycle, revalidation route, API paths and thank-you
|
|
11
|
+
address come from the SDK, and a landing page is single-language. Additions
|
|
12
|
+
and fixes only; requires platform v7.0.0, like 3.0.0.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `createStorefrontMiddleware(options)` in `@wocart/storefront/astro`: the
|
|
17
|
+
theme's whole `onRequest` in one call. A prerendered file and the
|
|
18
|
+
revalidation route pass through; a request that already has a store keeps
|
|
19
|
+
it; the editor's preview opens only for a verified token and never runs an
|
|
20
|
+
action; any other page gets its store and language, its redirect, its
|
|
21
|
+
form-called action and its unprefixed route. The preview key may be a
|
|
22
|
+
function, read on each request (a secret from `astro:env/server` can be
|
|
23
|
+
set after the module loads). Types `PreviewLocals`,
|
|
24
|
+
`StorefrontMiddleware`, `StorefrontMiddlewareContext`,
|
|
25
|
+
`StorefrontMiddlewareOptions`, `StorefrontActionContext`.
|
|
26
|
+
- `revalidateEndpoint({ publicKey, theme })` in `@wocart/storefront/astro`:
|
|
27
|
+
the theme's `/wocart-revalidate` route, `{ POST, ALL }`. `POST` drops every
|
|
28
|
+
page cached under `PAGES_CACHE_TAG` for a verified platform token (204) and
|
|
29
|
+
nothing otherwise; `ALL` answers 405. Types `RevalidateEndpointContext`,
|
|
30
|
+
`RevalidateEndpointOptions`.
|
|
31
|
+
- `STOREFRONT_API_PATHS` and `storefrontApiPath(name, params?)`: where a
|
|
32
|
+
theme mounts each of `createStorefrontRoutes`' routes and where its browser
|
|
33
|
+
scripts call them (`/api/communes/:wilayaId`, …), keyed like
|
|
34
|
+
`StorefrontRoutes` and checked against it. Defaults, not requirements:
|
|
35
|
+
every browser helper still takes its `endpoint`. Types `StorefrontApiName`,
|
|
36
|
+
`StorefrontApiParams`.
|
|
37
|
+
- `PAGES_CACHE_TAG` (`"pages"`) in `@wocart/storefront/server`: the cache
|
|
38
|
+
tag every cached page carries and a revalidation drops, spelled once.
|
|
39
|
+
- `thankYouHref(order, language?)` and `readThankYou(url)`: the thank-you
|
|
40
|
+
page's address for a placed order (`/thank-you?order=…&total=…&oid=…`, in
|
|
41
|
+
the request's language when given) and its reading back, so no theme spells
|
|
42
|
+
the query itself and an editor can preview the page with a sample order.
|
|
43
|
+
Type `ThankYouOrder`.
|
|
44
|
+
- `SINGLE_LANGUAGE_PAGE_TYPES` (`["landing"]`) and
|
|
45
|
+
`isSingleLanguagePage(pathname)`: the pages that exist in the store's
|
|
46
|
+
primary language only.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- A page's `notFound()` kept the shopper's language only in themes that
|
|
51
|
+
remembered to keep the store on the rewrite to `/404`;
|
|
52
|
+
`createStorefrontMiddleware` always keeps it.
|
|
53
|
+
- A landing page is single-language: `resolveLocalePath` and
|
|
54
|
+
`resolveStoreRequest` send `/fr/lp/x?…` to `/lp/x?…` (301, the query kept,
|
|
55
|
+
the other language never read); `localizeHref` leaves a landing link
|
|
56
|
+
unprefixed; `alternateLinks` lists no alternates for it; an endpoint a
|
|
57
|
+
landing page asks for answers in the primary language.
|
|
58
|
+
|
|
59
|
+
### Theme migration
|
|
60
|
+
|
|
61
|
+
- Replace the body of `src/middleware.ts` with `createStorefrontMiddleware`,
|
|
62
|
+
passing the client factory, the fallback config, the preview route and key
|
|
63
|
+
(`THEME_PREVIEW_PATH`, `() => WOCART_PREVIEW_KEY`, the theme's key), the
|
|
64
|
+
context builder and `getActionContext`. Delete any check of
|
|
65
|
+
`locals.store` on a rewrite and any landing-page language redirect of the
|
|
66
|
+
theme's own.
|
|
67
|
+
- `src/pages/wocart-revalidate.ts` becomes
|
|
68
|
+
`export const { POST, ALL } = revalidateEndpoint({ publicKey: () => WOCART_PREVIEW_KEY, theme: THEME_KEY })`;
|
|
69
|
+
tag cached routes with `PAGES_CACHE_TAG` rather than the literal.
|
|
70
|
+
- A theme's own table of `/api/…` paths becomes `STOREFRONT_API_PATHS`;
|
|
71
|
+
a route with a parameter is called through `storefrontApiPath`.
|
|
72
|
+
- Replace a theme's own thank-you query helpers with `thankYouHref` and
|
|
73
|
+
`readThankYou`; the query keeps its names (`order`, `total`, `oid`), so
|
|
74
|
+
links already sent keep working. A total that is not a whole number reads
|
|
75
|
+
as `null`.
|
|
76
|
+
- A language menu built from `localizeHref` should list no languages on a
|
|
77
|
+
page where `isSingleLanguagePage(pathname)` is true.
|
|
78
|
+
|
|
8
79
|
## 3.0.0 — 2026-11-01
|
|
9
80
|
|
|
10
81
|
The SDK reads exactly what platform v7.0.0 serves: `/store/config` and the
|
package/USAGE.md
CHANGED
|
@@ -632,7 +632,14 @@ alternateLinks(path.pathname, routing, url.origin); // [{ hreflang: "ar",
|
|
|
632
632
|
~~~
|
|
633
633
|
|
|
634
634
|
`localizeHref` leaves external URLs, fragments, endpoints, files and already
|
|
635
|
-
prefixed links alone, so it is safe on any `href`.
|
|
635
|
+
prefixed links alone, so it is safe on any `href`.
|
|
636
|
+
|
|
637
|
+
A landing page (`/lp/:slug`) exists in the primary language only:
|
|
638
|
+
`SINGLE_LANGUAGE_PAGE_TYPES` names its page type and
|
|
639
|
+
`isSingleLanguagePage(pathname)` tells such a page from any other.
|
|
640
|
+
`resolveLocalePath` sends `/fr/lp/x` to `/lp/x`, `localizeHref` leaves a
|
|
641
|
+
landing link unprefixed, and `alternateLinks` lists nothing for it; a
|
|
642
|
+
language menu should list no languages there either. Never redirect a shopper by
|
|
636
643
|
browser language: offer the other languages as links and let them choose.
|
|
637
644
|
`products.list({ limit, offset })` pages through the whole catalog, for a
|
|
638
645
|
sitemap.
|
|
@@ -1678,11 +1685,68 @@ The page reads the result with `Astro.getActionResult()`: on success it
|
|
|
1678
1685
|
redirects to the thank-you page, and on a refusal it renders the form again
|
|
1679
1686
|
with the error in place.
|
|
1680
1687
|
|
|
1688
|
+
The thank-you address is the SDK's: `thankYouHref(order, { locale, routing })`
|
|
1689
|
+
builds `/fr/thank-you?order=…&total=…&oid=…` from the placed order (its
|
|
1690
|
+
number, total and id, never the shopper's details), and the thank-you page
|
|
1691
|
+
reads it back with `readThankYou(Astro.url)`: `{ orderNumber, orderId, total }`,
|
|
1692
|
+
each empty or null when the address lacks it. An editor previews the page
|
|
1693
|
+
with a sample order by building the same address.
|
|
1694
|
+
|
|
1695
|
+
~~~ts
|
|
1696
|
+
import { readThankYou, thankYouHref } from "@wocart/storefront";
|
|
1697
|
+
|
|
1698
|
+
if (result?.data?.orderNumber) return Astro.redirect(thankYouHref(result.data, { locale, routing }));
|
|
1699
|
+
const { orderNumber, total } = readThankYou(Astro.url); // on /thank-you
|
|
1700
|
+
~~~
|
|
1701
|
+
|
|
1681
1702
|
Never redirect from the middleware, on success or on error. Astro 7 keeps an
|
|
1682
1703
|
action result for the response being rendered only, so a redirect arrives at
|
|
1683
1704
|
the next page with no result: the thank-you redirect never fires, and a
|
|
1684
1705
|
refused order shows no message.
|
|
1685
1706
|
|
|
1707
|
+
### The whole middleware in one call
|
|
1708
|
+
|
|
1709
|
+
`createStorefrontMiddleware` is a theme's whole `onRequest`: the store and
|
|
1710
|
+
language of every request, its redirects, the editor's preview and the
|
|
1711
|
+
form-called action, in the order below.
|
|
1712
|
+
|
|
1713
|
+
~~~ts
|
|
1714
|
+
// src/middleware.ts
|
|
1715
|
+
import { getActionContext } from "astro:actions";
|
|
1716
|
+
import { createStorefrontMiddleware } from "@wocart/storefront/astro";
|
|
1717
|
+
import { unavailableStoreConfig } from "@wocart/storefront";
|
|
1718
|
+
import { THEME_PREVIEW_PATH } from "@wocart/theme";
|
|
1719
|
+
|
|
1720
|
+
export const onRequest = createStorefrontMiddleware({
|
|
1721
|
+
client: createStore,
|
|
1722
|
+
fallback: unavailableStoreConfig({ serverUrl, locale: "ar" }),
|
|
1723
|
+
preview: { path: THEME_PREVIEW_PATH, publicKey: () => WOCART_PREVIEW_KEY, theme: THEME_KEY },
|
|
1724
|
+
context: (store) => buildContext(store),
|
|
1725
|
+
getActionContext,
|
|
1726
|
+
});
|
|
1727
|
+
~~~
|
|
1728
|
+
|
|
1729
|
+
1. A prerendered file and the revalidation route (`REVALIDATE_PATH`) pass
|
|
1730
|
+
through with no store.
|
|
1731
|
+
2. A request that already has a store keeps it: `notFound()` rewrites to
|
|
1732
|
+
`/404`, Astro runs the middleware again, and the 404 stays in the
|
|
1733
|
+
shopper's language.
|
|
1734
|
+
3. The preview route reads the draft with `readPreviewRequest`; a refused
|
|
1735
|
+
token answers its status with `Cache-Control: no-store`. A verified one
|
|
1736
|
+
sets `locals.store` from `resolvePreviewStore` and `locals.preview` to
|
|
1737
|
+
`{ claims, draft, token }`, and never runs an action.
|
|
1738
|
+
4. Any other request resolves its store with `resolveStoreRequest`, answers
|
|
1739
|
+
its redirect, sets `locals.store` to the theme's `context(store)`, runs a
|
|
1740
|
+
form-called action with `runFormAction`, then renders the unprefixed
|
|
1741
|
+
route.
|
|
1742
|
+
|
|
1743
|
+
Pass the preview key as a function: a secret from `astro:env/server` can be
|
|
1744
|
+
set after the module loads, and the function reads it on each request.
|
|
1745
|
+
`astro:actions` exists only inside an Astro project, so the theme passes
|
|
1746
|
+
`getActionContext` in. A theme that needs a step of its own composes this
|
|
1747
|
+
handler with Astro's `sequence()`. The sections below are the parts it
|
|
1748
|
+
calls, for a theme that wires them itself.
|
|
1749
|
+
|
|
1686
1750
|
### The request's store and language
|
|
1687
1751
|
|
|
1688
1752
|
`resolveStoreRequest` decides which store a request renders and in which
|
|
@@ -1691,7 +1755,9 @@ from its path (`/fr/products/robe`); the primary's own prefix redirects for
|
|
|
1691
1755
|
good to its one address, and a language the store stopped publishing
|
|
1692
1756
|
redirects for now to the primary page; an endpoint (`/api/…`) or a server
|
|
1693
1757
|
island takes the language of the page that asked, from its same-origin
|
|
1694
|
-
`Referer`, and is never redirected or rewritten
|
|
1758
|
+
`Referer`, and is never redirected or rewritten; a landing page asked for
|
|
1759
|
+
under a language prefix redirects for good to its unprefixed address, the
|
|
1760
|
+
query kept. It answers data — the
|
|
1695
1761
|
request's client, config, language, routing and unprefixed path — and the
|
|
1696
1762
|
theme builds its own context from it:
|
|
1697
1763
|
|
|
@@ -1784,6 +1850,21 @@ export const POST = routes.otpSend;
|
|
|
1784
1850
|
| `reviewSubmit` | POST | `ReviewInput` | 201 `{ success: true, data: { id, visibility } }` |
|
|
1785
1851
|
| `orderStatus` | POST | `{ orderNumber, phone }` | `{ data: ShopperOrderStatus }` |
|
|
1786
1852
|
|
|
1853
|
+
Mount each route at its path in `STOREFRONT_API_PATHS` (`src/pages/api/otp/send.ts`
|
|
1854
|
+
for `otpSend`, `src/pages/api/communes/[wilayaId].ts` for
|
|
1855
|
+
`/api/communes/:wilayaId`), and call it from the browser through the same
|
|
1856
|
+
table, so the files and the scripts never drift:
|
|
1857
|
+
|
|
1858
|
+
~~~ts
|
|
1859
|
+
import { STOREFRONT_API_PATHS, createQuoter, storefrontApiPath } from "@wocart/storefront";
|
|
1860
|
+
|
|
1861
|
+
const quoter = createQuoter({ endpoint: STOREFRONT_API_PATHS.cartValidate });
|
|
1862
|
+
const communes = await fetch(storefrontApiPath("communes", { wilayaId: 16 })); // /api/communes/16
|
|
1863
|
+
~~~
|
|
1864
|
+
|
|
1865
|
+
`orderStatus` has no path there: a theme follows an order with the
|
|
1866
|
+
`orderStatus` action.
|
|
1867
|
+
|
|
1787
1868
|
What every route gets right:
|
|
1788
1869
|
|
|
1789
1870
|
- every POST and PUT refuses a cross-origin request with 403;
|
|
@@ -2034,22 +2115,22 @@ editor — is `@wocart/theme`'s (its README, "Preview").
|
|
|
2034
2115
|
## Showing a publish at once (revalidation)
|
|
2035
2116
|
|
|
2036
2117
|
A theme that caches its pages (route rules with `maxAge` and `tags`) shows a
|
|
2037
|
-
merchant's publish when the cache expires. To show it at once,
|
|
2038
|
-
|
|
2118
|
+
merchant's publish when the cache expires. To show it at once, every cached
|
|
2119
|
+
route carries `PAGES_CACHE_TAG` (`"pages"`, from `@wocart/storefront/server`)
|
|
2120
|
+
and the theme exports the SDK's revalidation route, which drops them:
|
|
2039
2121
|
|
|
2040
2122
|
```ts
|
|
2041
2123
|
// src/pages/wocart-revalidate.ts — REVALIDATE_PATH
|
|
2042
|
-
import
|
|
2043
|
-
import { readRevalidateRequest } from "@wocart/storefront/server";
|
|
2124
|
+
import { revalidateEndpoint } from "@wocart/storefront/astro";
|
|
2044
2125
|
export const prerender = false;
|
|
2045
|
-
export const POST
|
|
2046
|
-
const revalidation = await readRevalidateRequest(context.request, { publicKey: WOCART_PREVIEW_KEY, theme: THEME_KEY });
|
|
2047
|
-
if (!revalidation.ok) return new Response(null, { status: revalidation.status });
|
|
2048
|
-
await context.cache.invalidate({ tags: ["pages"] }); // the tag every cached page carries
|
|
2049
|
-
return new Response(null, { status: 204 });
|
|
2050
|
-
};
|
|
2126
|
+
export const { POST, ALL } = revalidateEndpoint({ publicKey: () => WOCART_PREVIEW_KEY, theme: THEME_KEY });
|
|
2051
2127
|
```
|
|
2052
2128
|
|
|
2129
|
+
`POST` answers 204 after `cache.invalidate({ tags: [PAGES_CACHE_TAG] })` for
|
|
2130
|
+
a verified token, and the token's refusal status otherwise, dropping
|
|
2131
|
+
nothing; `ALL` answers any other method 405. No answer is cached. A theme
|
|
2132
|
+
that wires it itself reads the request with `readRevalidateRequest`.
|
|
2133
|
+
|
|
2053
2134
|
The request is a `POST` from the platform's server with
|
|
2054
2135
|
`Authorization: Bearer <token>`: signed with the same key as previews, but
|
|
2055
2136
|
made only for revalidation (`use: "revalidate"`), for this storefront's
|
package/dist/astro.d.ts
CHANGED
|
@@ -10,3 +10,7 @@ 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
|
@@ -4,3 +4,5 @@ export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./
|
|
|
4
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";
|
|
@@ -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
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,13 +17,13 @@ export { productImageSource } from "./utils/product-image.js";
|
|
|
17
17
|
export type { ProductImageSourceInput } from "./utils/product-image.js";
|
|
18
18
|
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, whatsAppContact } from "./whatsapp.js";
|
|
19
19
|
export type { PrefillToken, WhatsAppContact, WhatsAppPage, WhatsAppWords } from "./whatsapp.js";
|
|
20
|
-
export { STOREFRONT_PAGE_PATHS, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.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
22
|
export { unavailableStoreConfig } from "./unavailable-store.js";
|
|
23
23
|
export type { UnavailableStoreOptions } from "./unavailable-store.js";
|
|
24
24
|
export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
|
|
25
25
|
export type { LocalePath, LocaleRouting } from "./locale-routing.js";
|
|
26
|
-
export type { StorefrontPageMatch, StorefrontPageType, StorefrontPathParams } from "./routes.js";
|
|
26
|
+
export type { StorefrontApiName, StorefrontApiParams, StorefrontPageMatch, StorefrontPageType, StorefrontPathParams } from "./routes.js";
|
|
27
27
|
export { addedLines, createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
28
28
|
export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
|
|
29
29
|
export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, BasketSelectionLine, } from "./basket.js";
|
|
@@ -38,6 +38,8 @@ export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-re
|
|
|
38
38
|
export { createQuoter } from "./quote.js";
|
|
39
39
|
export type { Quoter, QuoterOptions, QuoteResult } from "./quote.js";
|
|
40
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";
|
|
41
43
|
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
42
44
|
export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
43
45
|
export type { CanonicalPage, PageMeta, ProductStructuredDataOptions } from "./seo.js";
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ 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
15
|
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, whatsAppContact } from "./whatsapp.js";
|
|
16
|
-
export { STOREFRONT_PAGE_PATHS, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.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
18
|
export { unavailableStoreConfig } from "./unavailable-store.js";
|
|
19
19
|
export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
|
|
@@ -27,6 +27,7 @@ export { isWilayaId } from "./wilaya-id.js";
|
|
|
27
27
|
export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-readers.js";
|
|
28
28
|
export { createQuoter } from "./quote.js";
|
|
29
29
|
export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
|
|
30
|
+
export { readThankYou, thankYouHref } from "./thank-you.js";
|
|
30
31
|
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
31
32
|
export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
32
33
|
export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
|
package/dist/locale-routing.d.ts
CHANGED
|
@@ -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
|
|
39
|
-
*
|
|
40
|
-
* or
|
|
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,
|
|
47
|
-
*
|
|
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;
|
package/dist/locale-routing.js
CHANGED
|
@@ -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
|
|
16
|
-
*
|
|
17
|
-
* or
|
|
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,
|
|
34
|
-
*
|
|
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,
|
package/dist/revalidate.d.ts
CHANGED
|
@@ -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. */
|
package/dist/revalidate.js
CHANGED
|
@@ -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 });
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The storefront's page types and their public paths.
|
|
3
|
-
*
|
|
4
|
-
* A page type is what a URL *means* — a product, a category, a store page, a
|
|
5
|
-
* landing page — and the path is where it lives. The paths are a platform
|
|
6
|
-
* contract, not a theme decision: the platform builds them without running
|
|
7
|
-
* the theme (the dashboard shows a page's public link, the server names the
|
|
8
|
-
* landing page in a conversion's source URL), so a theme renders each page
|
|
9
|
-
* type it supports at its path and builds every link to one with
|
|
10
|
-
* `storefrontPath` — never a hand-written string. Any other route is the
|
|
11
|
-
* theme's own (`/about`, `/offers/summer` …): Astro's file routing owns those,
|
|
12
|
-
* and nothing here constrains them.
|
|
13
|
-
*
|
|
14
|
-
* Language prefixes are applied on top: `localizeHref(storefrontPath(…),
|
|
15
|
-
* locale, routing)`.
|
|
16
|
-
*/
|
|
17
1
|
import type { WidgetSurfaces } from "./types.js";
|
|
18
2
|
/** Each page type's public path; `:handle` / `:slug` mark its one parameter. */
|
|
19
3
|
export declare const STOREFRONT_PAGE_PATHS: {
|
|
@@ -52,6 +36,39 @@ export interface StorefrontPageMatch {
|
|
|
52
36
|
* not a path to render.
|
|
53
37
|
*/
|
|
54
38
|
export declare function storefrontPath<T extends StorefrontPageType>(type: T, params?: StorefrontPathParams<T>): string;
|
|
39
|
+
/**
|
|
40
|
+
* Where a theme mounts each of `createStorefrontRoutes`' routes
|
|
41
|
+
* (`src/pages/api/…`), and where its browser scripts call them: one table,
|
|
42
|
+
* keyed like `StorefrontRoutes`, so the route files and the scripts never
|
|
43
|
+
* drift. Defaults, not requirements: every browser helper still takes its
|
|
44
|
+
* `endpoint`. `:wilayaId` and `:productId` mark a route's one parameter.
|
|
45
|
+
* `orderStatus` has no path here: a theme follows an order through its
|
|
46
|
+
* `orderStatus` action, not a route.
|
|
47
|
+
*/
|
|
48
|
+
export declare const STOREFRONT_API_PATHS: {
|
|
49
|
+
readonly otpSend: "/api/otp/send";
|
|
50
|
+
readonly otpVerify: "/api/otp/verify";
|
|
51
|
+
readonly cartValidate: "/api/cart/validate";
|
|
52
|
+
readonly abandonedUpsert: "/api/abandoned/upsert";
|
|
53
|
+
readonly abandonedConvert: "/api/abandoned/convert";
|
|
54
|
+
readonly communes: "/api/communes/:wilayaId";
|
|
55
|
+
readonly reviewsList: "/api/reviews/:productId";
|
|
56
|
+
readonly reviewEligibility: "/api/reviews/eligibility";
|
|
57
|
+
readonly reviewUpload: "/api/reviews/upload";
|
|
58
|
+
readonly reviewSubmit: "/api/reviews/submit";
|
|
59
|
+
};
|
|
60
|
+
/** A storefront route of `createStorefrontRoutes`. */
|
|
61
|
+
export type StorefrontApiName = keyof typeof STOREFRONT_API_PATHS;
|
|
62
|
+
/** The parameter a route's path needs: `{ wilayaId }` for the communes, `{ productId }` for a product's reviews, nothing otherwise. */
|
|
63
|
+
export type StorefrontApiParams<N extends StorefrontApiName> = ParamOf<(typeof STOREFRONT_API_PATHS)[N]> extends never ? Record<string, never> : {
|
|
64
|
+
[K in ParamOf<(typeof STOREFRONT_API_PATHS)[N]>]: string | number;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* A storefront route's same-origin path, its parameter encoded:
|
|
68
|
+
* `storefrontApiPath("communes", { wilayaId: 16 })` → `/api/communes/16`.
|
|
69
|
+
* Throws when the parameter is missing.
|
|
70
|
+
*/
|
|
71
|
+
export declare function storefrontApiPath<N extends StorefrontApiName>(name: N, params?: StorefrontApiParams<N>): string;
|
|
55
72
|
/**
|
|
56
73
|
* Which page type a pathname is, and its parameter (decoded) — or null for
|
|
57
74
|
* any other path. Exact matches only: `/products/` is the catalog,
|
|
@@ -60,6 +77,15 @@ export declare function storefrontPath<T extends StorefrontPageType>(type: T, pa
|
|
|
60
77
|
* language prefix (`resolveLocalePath` gives it).
|
|
61
78
|
*/
|
|
62
79
|
export declare function matchStorefrontPath(rawPathname: string): StorefrontPageMatch | null;
|
|
80
|
+
/**
|
|
81
|
+
* The page types that exist in the store's primary language only. A landing
|
|
82
|
+
* page is one campaign — a stack of images and one order form, written once
|
|
83
|
+
* for the ads that point at it — so it has no `/fr/lp/…` address, no language
|
|
84
|
+
* menu and no alternate links.
|
|
85
|
+
*/
|
|
86
|
+
export declare const SINGLE_LANGUAGE_PAGE_TYPES: readonly StorefrontPageType[];
|
|
87
|
+
/** Whether a pathname (without its language prefix) is a page that exists in the primary language only. */
|
|
88
|
+
export declare function isSingleLanguagePage(rawPathname: string): boolean;
|
|
63
89
|
/**
|
|
64
90
|
* Whether the widget belongs on a pathname: the merchant answers "which
|
|
65
91
|
* surfaces" (`surfaces[surface]`); this answers which surface a pathname *is*,
|
package/dist/routes.js
CHANGED
|
@@ -23,13 +23,45 @@ export const STOREFRONT_PAGE_PATHS = {
|
|
|
23
23
|
* not a path to render.
|
|
24
24
|
*/
|
|
25
25
|
export function storefrontPath(type, params) {
|
|
26
|
-
return STOREFRONT_PAGE_PATHS[type]
|
|
26
|
+
return fillPath(STOREFRONT_PAGE_PATHS[type], `storefrontPath("${type}")`, params);
|
|
27
|
+
}
|
|
28
|
+
function fillPath(template, caller, params) {
|
|
29
|
+
return template.replace(/:([a-zA-Z]+)/g, (_, name) => {
|
|
27
30
|
const value = params?.[name];
|
|
28
|
-
if (
|
|
29
|
-
throw new Error(
|
|
30
|
-
return encodeURIComponent(value);
|
|
31
|
+
if (value === undefined || value === "")
|
|
32
|
+
throw new Error(`${caller} needs "${name}"`);
|
|
33
|
+
return encodeURIComponent(String(value));
|
|
31
34
|
});
|
|
32
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Where a theme mounts each of `createStorefrontRoutes`' routes
|
|
38
|
+
* (`src/pages/api/…`), and where its browser scripts call them: one table,
|
|
39
|
+
* keyed like `StorefrontRoutes`, so the route files and the scripts never
|
|
40
|
+
* drift. Defaults, not requirements: every browser helper still takes its
|
|
41
|
+
* `endpoint`. `:wilayaId` and `:productId` mark a route's one parameter.
|
|
42
|
+
* `orderStatus` has no path here: a theme follows an order through its
|
|
43
|
+
* `orderStatus` action, not a route.
|
|
44
|
+
*/
|
|
45
|
+
export const STOREFRONT_API_PATHS = {
|
|
46
|
+
otpSend: "/api/otp/send",
|
|
47
|
+
otpVerify: "/api/otp/verify",
|
|
48
|
+
cartValidate: "/api/cart/validate",
|
|
49
|
+
abandonedUpsert: "/api/abandoned/upsert",
|
|
50
|
+
abandonedConvert: "/api/abandoned/convert",
|
|
51
|
+
communes: "/api/communes/:wilayaId",
|
|
52
|
+
reviewsList: "/api/reviews/:productId",
|
|
53
|
+
reviewEligibility: "/api/reviews/eligibility",
|
|
54
|
+
reviewUpload: "/api/reviews/upload",
|
|
55
|
+
reviewSubmit: "/api/reviews/submit",
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* A storefront route's same-origin path, its parameter encoded:
|
|
59
|
+
* `storefrontApiPath("communes", { wilayaId: 16 })` → `/api/communes/16`.
|
|
60
|
+
* Throws when the parameter is missing.
|
|
61
|
+
*/
|
|
62
|
+
export function storefrontApiPath(name, params) {
|
|
63
|
+
return fillPath(STOREFRONT_API_PATHS[name], `storefrontApiPath("${name}")`, params);
|
|
64
|
+
}
|
|
33
65
|
const MATCHERS = Object.entries(STOREFRONT_PAGE_PATHS).map(([type, path]) => {
|
|
34
66
|
const names = [];
|
|
35
67
|
const source = path.replace(/:([a-z]+)/g, (_, name) => {
|
|
@@ -60,6 +92,18 @@ export function matchStorefrontPath(rawPathname) {
|
|
|
60
92
|
}
|
|
61
93
|
return null;
|
|
62
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* The page types that exist in the store's primary language only. A landing
|
|
97
|
+
* page is one campaign — a stack of images and one order form, written once
|
|
98
|
+
* for the ads that point at it — so it has no `/fr/lp/…` address, no language
|
|
99
|
+
* menu and no alternate links.
|
|
100
|
+
*/
|
|
101
|
+
export const SINGLE_LANGUAGE_PAGE_TYPES = ["landing"];
|
|
102
|
+
/** Whether a pathname (without its language prefix) is a page that exists in the primary language only. */
|
|
103
|
+
export function isSingleLanguagePage(rawPathname) {
|
|
104
|
+
const match = matchStorefrontPath(rawPathname);
|
|
105
|
+
return match !== null && SINGLE_LANGUAGE_PAGE_TYPES.includes(match.type);
|
|
106
|
+
}
|
|
63
107
|
/** The WhatsApp widget surface each page type belongs to (the home surface is the root). */
|
|
64
108
|
const WIDGET_SURFACE = {
|
|
65
109
|
home: "home",
|
package/dist/server.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { StoreClient } from "./client/index.js";
|
|
|
3
3
|
export type { ClientResult } from "./client/core.js";
|
|
4
4
|
export { StorefrontError, isStorefrontError } from "./client/errors.js";
|
|
5
5
|
export { previewConfig, previewResponseHeaders, readPreviewRequest, verifyPreviewToken } from "./preview.js";
|
|
6
|
-
export { REVALIDATE_PATH, readRevalidateRequest } from "./revalidate.js";
|
|
6
|
+
export { PAGES_CACHE_TAG, REVALIDATE_PATH, readRevalidateRequest } from "./revalidate.js";
|
|
7
7
|
export type { RevalidateClaims, RevalidateRequest, RevalidateRequestOptions } from "./revalidate.js";
|
|
8
8
|
export type { PreviewClaims, PreviewDraft, PreviewRequest, PreviewRequestOptions, PreviewTokenCheck, PreviewTokenOptions, PreviewTokenRefusal, } from "./preview.js";
|
|
9
9
|
export { catalogPaths, sitemapXml } from "./sitemap.js";
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createStoreClient } from "./client/index.js";
|
|
2
2
|
export { StorefrontError, isStorefrontError } from "./client/errors.js";
|
|
3
3
|
export { previewConfig, previewResponseHeaders, readPreviewRequest, verifyPreviewToken } from "./preview.js";
|
|
4
|
-
export { REVALIDATE_PATH, readRevalidateRequest } from "./revalidate.js";
|
|
4
|
+
export { PAGES_CACHE_TAG, REVALIDATE_PATH, readRevalidateRequest } from "./revalidate.js";
|
|
5
5
|
export { catalogPaths, sitemapXml } from "./sitemap.js";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request lifecycle every theme's `src/middleware.ts` runs, in one call:
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* // src/middleware.ts
|
|
6
|
+
* import { getActionContext } from "astro:actions";
|
|
7
|
+
* import { createStorefrontMiddleware } from "@wocart/storefront/astro";
|
|
8
|
+
* import { THEME_PREVIEW_PATH } from "@wocart/theme";
|
|
9
|
+
*
|
|
10
|
+
* export const onRequest = createStorefrontMiddleware({
|
|
11
|
+
* client: createStore,
|
|
12
|
+
* fallback: unavailableStoreConfig({ serverUrl, locale: "ar" }),
|
|
13
|
+
* preview: { path: THEME_PREVIEW_PATH, publicKey: () => WOCART_PREVIEW_KEY, theme: THEME_KEY },
|
|
14
|
+
* context: (store) => buildStoreContext(store),
|
|
15
|
+
* getActionContext,
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* In order:
|
|
20
|
+
*
|
|
21
|
+
* - A prerendered file (the manifest, the theme's styles) and the
|
|
22
|
+
* revalidation route (`REVALIDATE_PATH`) need no store.
|
|
23
|
+
* - A request that already has a store keeps it: a page's `notFound()`
|
|
24
|
+
* rewrites to `/404` and Astro runs the middleware again, and the 404 must
|
|
25
|
+
* stay in the language the shopper asked for.
|
|
26
|
+
* - The editor's preview (`preview.path`) renders the store as the draft
|
|
27
|
+
* would leave it, after its token is verified; it never runs a form action.
|
|
28
|
+
* - Any other request resolves its store and language
|
|
29
|
+
* (`resolveStoreRequest`), answers its redirect if it has one, builds the
|
|
30
|
+
* theme's context, runs a form-called action for the page this POST renders
|
|
31
|
+
* (`runFormAction`), then renders the unprefixed route.
|
|
32
|
+
*
|
|
33
|
+
* `astro:actions` is an Astro virtual module, so the theme passes
|
|
34
|
+
* `getActionContext` in. The theme's context becomes `locals.store`; a
|
|
35
|
+
* verified preview's claims, draft and token become `locals.preview`.
|
|
36
|
+
*/
|
|
37
|
+
import type { StoreClient } from "./client/index.js";
|
|
38
|
+
import { type ActionDescriptor, type ActionHandlerResult } from "./middleware.js";
|
|
39
|
+
import { type PreviewClaims, type PreviewDraft, type PreviewRequestOptions } from "./preview.js";
|
|
40
|
+
import { type ResolvedStore } from "./store-request.js";
|
|
41
|
+
import type { StoreConfig } from "./types.js";
|
|
42
|
+
/** What a verified preview leaves on `locals.preview`. */
|
|
43
|
+
export interface PreviewLocals {
|
|
44
|
+
claims: PreviewClaims;
|
|
45
|
+
draft: PreviewDraft;
|
|
46
|
+
token: string;
|
|
47
|
+
}
|
|
48
|
+
/** What the middleware uses of Astro's `APIContext`. */
|
|
49
|
+
export interface StorefrontMiddlewareContext {
|
|
50
|
+
isPrerendered: boolean;
|
|
51
|
+
url: URL;
|
|
52
|
+
request: Request;
|
|
53
|
+
/** `App.Locals`: the middleware writes `store` and, on a preview, `preview`. */
|
|
54
|
+
locals: object;
|
|
55
|
+
redirect(location: string, status: 301 | 302): Response;
|
|
56
|
+
}
|
|
57
|
+
/** What the middleware uses of `getActionContext(context)`. */
|
|
58
|
+
export interface StorefrontActionContext<TResult extends ActionHandlerResult, TSerialized> {
|
|
59
|
+
action?: ActionDescriptor<TResult> | undefined;
|
|
60
|
+
setActionResult: (name: string, serialized: TSerialized) => void;
|
|
61
|
+
serializeActionResult: (result: TResult) => TSerialized;
|
|
62
|
+
}
|
|
63
|
+
export interface StorefrontMiddlewareOptions<TContext extends StorefrontMiddlewareContext, TStore, TResult extends ActionHandlerResult, TSerialized> {
|
|
64
|
+
/** The theme's one client factory: in `locale`, or in the store's primary language when absent. */
|
|
65
|
+
client: (locale?: string) => StoreClient;
|
|
66
|
+
/** What renders when the store's config cannot be read: `unavailableStoreConfig(…)`. */
|
|
67
|
+
fallback: StoreConfig;
|
|
68
|
+
/**
|
|
69
|
+
* The editor's preview route (`THEME_PREVIEW_PATH`) and how its token is
|
|
70
|
+
* checked; no `publicKey`: the route answers 404. Pass the key as a
|
|
71
|
+
* function to read it on each request: a secret from `astro:env/server`
|
|
72
|
+
* can be set after the module loads.
|
|
73
|
+
*/
|
|
74
|
+
preview: {
|
|
75
|
+
path: string;
|
|
76
|
+
publicKey: PreviewRequestOptions["publicKey"] | (() => PreviewRequestOptions["publicKey"]);
|
|
77
|
+
} & Pick<PreviewRequestOptions, "theme" | "maxBytes">;
|
|
78
|
+
/** The theme's context for a resolved store; it becomes `locals.store`. */
|
|
79
|
+
context: (store: ResolvedStore) => TStore;
|
|
80
|
+
/** Astro's `getActionContext`, from `astro:actions`. */
|
|
81
|
+
getActionContext: (context: TContext) => StorefrontActionContext<TResult, TSerialized>;
|
|
82
|
+
/** Whether a path serves part of a page rather than a page. Default: `/api/` and `/_server-islands/`. */
|
|
83
|
+
embedded?: (pathname: string) => boolean;
|
|
84
|
+
}
|
|
85
|
+
/** An Astro middleware handler (`onRequest`). */
|
|
86
|
+
export type StorefrontMiddleware<TContext extends StorefrontMiddlewareContext> = (context: TContext, next: (rewrite?: string) => Promise<Response>) => Promise<Response>;
|
|
87
|
+
/** The theme's `onRequest`: store, language, redirects, preview and form actions, as described above. */
|
|
88
|
+
export declare function createStorefrontMiddleware<TContext extends StorefrontMiddlewareContext, TStore, TResult extends ActionHandlerResult, TSerialized>(options: StorefrontMiddlewareOptions<TContext, TStore, TResult, TSerialized>): StorefrontMiddleware<TContext>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runFormAction } from "./middleware.js";
|
|
2
|
+
import { readPreviewRequest } from "./preview.js";
|
|
3
|
+
import { REVALIDATE_PATH } from "./revalidate.js";
|
|
4
|
+
import { resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
|
|
5
|
+
/** The theme's `onRequest`: store, language, redirects, preview and form actions, as described above. */
|
|
6
|
+
export function createStorefrontMiddleware(options) {
|
|
7
|
+
const store = { client: options.client, fallback: options.fallback, ...(options.embedded ? { embedded: options.embedded } : {}) };
|
|
8
|
+
const { path: previewPath, publicKey, ...previewOptions } = options.preview;
|
|
9
|
+
return async (context, next) => {
|
|
10
|
+
const locals = context.locals;
|
|
11
|
+
if (context.isPrerendered)
|
|
12
|
+
return next();
|
|
13
|
+
if (context.url.pathname === REVALIDATE_PATH)
|
|
14
|
+
return next();
|
|
15
|
+
if (locals.store !== undefined)
|
|
16
|
+
return next();
|
|
17
|
+
if (context.url.pathname === previewPath) {
|
|
18
|
+
const preview = await readPreviewRequest(context.request, { ...previewOptions, publicKey: typeof publicKey === "function" ? publicKey() : publicKey });
|
|
19
|
+
if (!preview.ok)
|
|
20
|
+
return new Response(null, { status: preview.status, headers: { "Cache-Control": "no-store" } });
|
|
21
|
+
locals.store = options.context(await resolvePreviewStore(preview.draft, store));
|
|
22
|
+
locals.preview = { claims: preview.claims, draft: preview.draft, token: preview.token };
|
|
23
|
+
return next();
|
|
24
|
+
}
|
|
25
|
+
const request = await resolveStoreRequest({ url: context.url, referer: context.request.headers.get("Referer") }, store);
|
|
26
|
+
if (request.kind === "redirect")
|
|
27
|
+
return context.redirect(request.location, request.status);
|
|
28
|
+
locals.store = options.context(request);
|
|
29
|
+
const { action, setActionResult, serializeActionResult } = options.getActionContext(context);
|
|
30
|
+
await runFormAction({
|
|
31
|
+
...(action ? { action: { calledFrom: action.calledFrom, name: action.name, handler: action.handler } } : {}),
|
|
32
|
+
setActionResult,
|
|
33
|
+
serializeActionResult,
|
|
34
|
+
});
|
|
35
|
+
return request.rewrite === null ? next() : next(request.rewrite);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thank-you page's address for a placed order, and its reading back. The
|
|
3
|
+
* query carries the order's number, total and id — never the shopper's
|
|
4
|
+
* details — so the page can thank, show the total and fire a conversion
|
|
5
|
+
* without reading the order again. One spelling, here: a theme writes it
|
|
6
|
+
* after `placeOrder`, its thank-you page reads it, and an editor previews
|
|
7
|
+
* the page with a sample order by building the same address.
|
|
8
|
+
*/
|
|
9
|
+
import { type LocaleRouting } from "./locale-routing.js";
|
|
10
|
+
/** What the thank-you page reads from its address. */
|
|
11
|
+
export interface ThankYouOrder {
|
|
12
|
+
/** The order number the shopper is given; empty when the address has none. */
|
|
13
|
+
orderNumber: string;
|
|
14
|
+
/** The order's id, for a conversion's deduplication; empty when the address has none. */
|
|
15
|
+
orderId: string;
|
|
16
|
+
/** The order's total in the store's currency; null when missing or not a number. */
|
|
17
|
+
total: number | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The thank-you page for a placed order: `/thank-you?order=…&total=…&oid=…`,
|
|
21
|
+
* in the request's language when `language` is given (`/fr/thank-you?…`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function thankYouHref(order: {
|
|
24
|
+
orderNumber: string;
|
|
25
|
+
total: number;
|
|
26
|
+
orderId?: string | null;
|
|
27
|
+
}, language?: {
|
|
28
|
+
locale: string;
|
|
29
|
+
routing: LocaleRouting;
|
|
30
|
+
}): string;
|
|
31
|
+
/** The order a thank-you address names: `thankYouHref`'s query, read back and trimmed. */
|
|
32
|
+
export declare function readThankYou(url: URL | URLSearchParams): ThankYouOrder;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thank-you page's address for a placed order, and its reading back. The
|
|
3
|
+
* query carries the order's number, total and id — never the shopper's
|
|
4
|
+
* details — so the page can thank, show the total and fire a conversion
|
|
5
|
+
* without reading the order again. One spelling, here: a theme writes it
|
|
6
|
+
* after `placeOrder`, its thank-you page reads it, and an editor previews
|
|
7
|
+
* the page with a sample order by building the same address.
|
|
8
|
+
*/
|
|
9
|
+
import { localizeHref } from "./locale-routing.js";
|
|
10
|
+
import { storefrontPath } from "./routes.js";
|
|
11
|
+
const ORDER_NUMBER = "order";
|
|
12
|
+
const TOTAL = "total";
|
|
13
|
+
const ORDER_ID = "oid";
|
|
14
|
+
/**
|
|
15
|
+
* The thank-you page for a placed order: `/thank-you?order=…&total=…&oid=…`,
|
|
16
|
+
* in the request's language when `language` is given (`/fr/thank-you?…`).
|
|
17
|
+
*/
|
|
18
|
+
export function thankYouHref(order, language) {
|
|
19
|
+
const query = new URLSearchParams({ [ORDER_NUMBER]: order.orderNumber, [TOTAL]: String(order.total), [ORDER_ID]: order.orderId ?? "" });
|
|
20
|
+
const path = storefrontPath("thankYou");
|
|
21
|
+
return `${language ? localizeHref(path, language.locale, language.routing) : path}?${query.toString()}`;
|
|
22
|
+
}
|
|
23
|
+
/** The order a thank-you address names: `thankYouHref`'s query, read back and trimmed. */
|
|
24
|
+
export function readThankYou(url) {
|
|
25
|
+
const params = url instanceof URLSearchParams ? url : url.searchParams;
|
|
26
|
+
const raw = params.get(TOTAL)?.trim() ?? "";
|
|
27
|
+
const total = /^-?\d+$/.test(raw) ? Number.parseInt(raw, 10) : Number.NaN;
|
|
28
|
+
return {
|
|
29
|
+
orderNumber: (params.get(ORDER_NUMBER) ?? "").trim(),
|
|
30
|
+
orderId: (params.get(ORDER_ID) ?? "").trim(),
|
|
31
|
+
total: Number.isFinite(total) ? total : null,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/package.json
CHANGED