create-magic-storefront 0.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/index.js +222 -0
  4. package/package.json +31 -0
  5. package/template/.env.example +22 -0
  6. package/template/AGENTS.md +56 -0
  7. package/template/CLAUDE.md +1 -0
  8. package/template/PAGES.md +154 -0
  9. package/template/README.md +31 -0
  10. package/template/_gitignore +5 -0
  11. package/template/app/account/page.tsx +117 -0
  12. package/template/app/api/magicstore/webhook/route.ts +9 -0
  13. package/template/app/cart/page.tsx +124 -0
  14. package/template/app/checkout/page.tsx +268 -0
  15. package/template/app/collections/[handle]/page.tsx +48 -0
  16. package/template/app/error.tsx +18 -0
  17. package/template/app/globals.css +268 -0
  18. package/template/app/layout.tsx +63 -0
  19. package/template/app/not-found.tsx +10 -0
  20. package/template/app/page.tsx +16 -0
  21. package/template/app/pages/[handle]/page.tsx +28 -0
  22. package/template/app/products/[handle]/page.tsx +47 -0
  23. package/template/app/providers.tsx +49 -0
  24. package/template/app/search/page.tsx +37 -0
  25. package/template/app/sitemap.ts +36 -0
  26. package/template/app/storefront-api/[...path]/route.ts +83 -0
  27. package/template/components/analytics-views.tsx +18 -0
  28. package/template/components/buy-box.tsx +86 -0
  29. package/template/components/cart-link.tsx +9 -0
  30. package/template/components/pager.tsx +25 -0
  31. package/template/components/product-grid.tsx +32 -0
  32. package/template/components/sections/announcement-bar.tsx +13 -0
  33. package/template/components/sections/banner.tsx +41 -0
  34. package/template/components/sections/collections.tsx +67 -0
  35. package/template/components/sections/deal-of-day.tsx +48 -0
  36. package/template/components/sections/index.tsx +66 -0
  37. package/template/components/sections/product-shelves.tsx +123 -0
  38. package/template/components/sections/shoppable-stories.tsx +39 -0
  39. package/template/components/sections/store-reviews.tsx +67 -0
  40. package/template/components/sections/types.ts +10 -0
  41. package/template/lib/api.ts +35 -0
  42. package/template/lib/errors.ts +9 -0
  43. package/template/lib/upstream.ts +12 -0
  44. package/template/llms.txt +128 -0
  45. package/template/next.config.ts +9 -0
  46. package/template/package.json +26 -0
  47. package/template/tsconfig.json +33 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MagicStore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # create-magic-storefront
2
+
3
+ ```bash
4
+ npm create magic-storefront@latest my-shop -- --shop shop.example.uz
5
+ ```
6
+
7
+ Scaffolds the [storefront starter](../../examples/starter) into `my-shop`, pins the SDK to the
8
+ matching release and writes `.env.local` from the answers (`--shop`, `--token`, `--locale`). The
9
+ directory must be new or empty. Without a TTY it asks nothing and uses the flags.
10
+
11
+ ## Checking a storefront
12
+
13
+ ```bash
14
+ npx create-magic-storefront check [directory]
15
+ ```
16
+
17
+ Fails (exit 1, one line per finding) when the storefront uses the SDK outside its public API:
18
+
19
+ - `DEEP_IMPORT` — an `@magicstoreai/*` import other than `@magicstoreai/storefront-client`,
20
+ `@magicstoreai/hydrogen`, `@magicstoreai/hydrogen/core`, `/server` or `/seo` (a `src/` or `dist/`
21
+ path, an unexported subpath, the SDK's sources in this repository);
22
+ - `RAW_API_CALL` — a string that spells out an endpoint (`/api/v2/storefront/…`) instead of calling
23
+ it through the client. The bare base URL handed to `createStorefrontClient` is fine.
24
+
25
+ `node_modules`, build output and `.d.ts` files are skipped. In this repository `pnpm guard` runs it on
26
+ `examples/starter` (CI too).
package/dist/index.js ADDED
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync as readFileSync3 } from "fs";
5
+ import { dirname, relative as relative2, resolve as resolve3 } from "path";
6
+ import { createInterface } from "readline/promises";
7
+ import { fileURLToPath } from "url";
8
+ import { parseArgs } from "util";
9
+
10
+ // src/guard.ts
11
+ import { readdirSync, readFileSync } from "fs";
12
+ import { join, relative, resolve } from "path";
13
+ var PUBLIC_ENTRY_POINTS = [
14
+ "@magicstoreai/storefront-client",
15
+ "@magicstoreai/hydrogen",
16
+ "@magicstoreai/hydrogen/core",
17
+ "@magicstoreai/hydrogen/server",
18
+ "@magicstoreai/hydrogen/seo"
19
+ ];
20
+ var SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
21
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
22
+ "node_modules",
23
+ ".next",
24
+ ".git",
25
+ "dist",
26
+ "build",
27
+ "out",
28
+ "coverage"
29
+ ]);
30
+ var SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\s*\(\s*)(['"])([^'"\n]+)\1/g;
31
+ var SDK_PACKAGE = /@magicstoreai\/(?:hydrogen|storefront-client)(?:\/|$)/;
32
+ var SDK_SOURCES = /(?:^|\/)packages\/(?:hydrogen|storefront-client)(?:\/|$)/;
33
+ var RAW_API_PATH = /\/api\/v2\/storefront\/(?:[A-Za-z]|\$\{)/;
34
+ var STRING_LITERAL = /(['"`])(?:\\.|(?!\1)[^\\\n])*\1/g;
35
+ function sourceFiles(directory) {
36
+ const files = [];
37
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
38
+ if (entry.isDirectory()) {
39
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) {
40
+ files.push(...sourceFiles(join(directory, entry.name)));
41
+ }
42
+ } else if (entry.isFile() && SOURCE_EXTENSIONS.test(entry.name) && !entry.name.endsWith(".d.ts")) {
43
+ files.push(join(directory, entry.name));
44
+ }
45
+ }
46
+ return files;
47
+ }
48
+ function lineAt(source, index) {
49
+ return source.slice(0, index).split("\n").length;
50
+ }
51
+ function checkSource(file, source) {
52
+ const violations = [];
53
+ for (const match of source.matchAll(SPECIFIER)) {
54
+ const specifier = match[2];
55
+ const reachesIn = SDK_PACKAGE.test(specifier) && !PUBLIC_ENTRY_POINTS.includes(specifier) || SDK_SOURCES.test(specifier);
56
+ if (reachesIn) {
57
+ violations.push({
58
+ rule: "DEEP_IMPORT",
59
+ file,
60
+ line: lineAt(source, match.index),
61
+ text: specifier
62
+ });
63
+ }
64
+ }
65
+ for (const match of source.matchAll(STRING_LITERAL)) {
66
+ if (RAW_API_PATH.test(match[0])) {
67
+ violations.push({
68
+ rule: "RAW_API_CALL",
69
+ file,
70
+ line: lineAt(source, match.index),
71
+ text: match[0]
72
+ });
73
+ }
74
+ }
75
+ return violations.sort((a, b) => a.line - b.line);
76
+ }
77
+ function checkPublicApi(directory) {
78
+ const root = resolve(directory);
79
+ return sourceFiles(root).flatMap(
80
+ (path) => checkSource(relative(root, path).split("\\").join("/"), readFileSync(path, "utf8"))
81
+ );
82
+ }
83
+ var EXPLANATIONS = {
84
+ DEEP_IMPORT: `import only ${PUBLIC_ENTRY_POINTS.join(", ")}`,
85
+ RAW_API_CALL: "call the API through the storefront client, not by URL"
86
+ };
87
+ function formatViolations(violations) {
88
+ return violations.map((v) => `${v.file}:${v.line} ${v.rule} ${v.text} \u2014 ${EXPLANATIONS[v.rule]}`).join("\n");
89
+ }
90
+
91
+ // src/scaffold.ts
92
+ import { cpSync, existsSync, readdirSync as readdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
93
+ import { basename, join as join2, resolve as resolve2 } from "path";
94
+ function packageName(directory) {
95
+ const name = basename(resolve2(directory)).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|-+$/g, "");
96
+ return name || "magic-storefront";
97
+ }
98
+ function scaffold(options) {
99
+ const directory = resolve2(options.directory);
100
+ if (existsSync(directory) && readdirSync2(directory).length > 0) {
101
+ throw new Error(`${directory} is not empty \u2014 pick a new directory.`);
102
+ }
103
+ cpSync(options.templateDirectory, directory, { recursive: true });
104
+ if (existsSync(join2(directory, "_gitignore"))) {
105
+ renameSync(join2(directory, "_gitignore"), join2(directory, ".gitignore"));
106
+ }
107
+ const name = packageName(directory);
108
+ const manifestPath = join2(directory, "package.json");
109
+ const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
110
+ manifest.name = name;
111
+ for (const group of [manifest.dependencies, manifest.devDependencies]) {
112
+ for (const [dependency, version] of Object.entries(group ?? {})) {
113
+ if (version.startsWith("workspace:")) {
114
+ const published = options.sdkVersions[dependency];
115
+ if (!published) {
116
+ throw new Error(`No published version known for ${dependency}.`);
117
+ }
118
+ group[dependency] = `^${published}`;
119
+ }
120
+ }
121
+ }
122
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}
123
+ `);
124
+ const example = readFileSync2(join2(directory, ".env.example"), "utf8");
125
+ writeFileSync(join2(directory, ".env.local"), envFile(example, options));
126
+ return { directory, name };
127
+ }
128
+ function envFile(example, options) {
129
+ const answers = {
130
+ MAGICSTORE_SHOP_DOMAIN: options.shopDomain,
131
+ NEXT_PUBLIC_MAGICSTORE_STOREFRONT_TOKEN: options.storefrontToken,
132
+ NEXT_PUBLIC_MAGICSTORE_LOCALE: options.locale
133
+ };
134
+ return example.split("\n").map((line) => {
135
+ const match = /^([A-Z0-9_]+)=/.exec(line);
136
+ const value = match ? answers[match[1]] : void 0;
137
+ return match && value !== void 0 ? `${match[1]}=${value}` : line;
138
+ }).join("\n");
139
+ }
140
+
141
+ // src/index.ts
142
+ var HELP = `Usage: create-magic-storefront [directory] [options]
143
+ create-magic-storefront check [directory]
144
+
145
+ Scaffolds a Next.js storefront on the MagicStore Storefront API v2.
146
+
147
+ check verifies that a storefront uses the SDK only through its public entry points
148
+ (no deep @magicstoreai/* imports, no hand-written /api/v2/storefront/... calls).
149
+
150
+ Options:
151
+ --shop <domain> the shop's storefront domain (shop.example.uz)
152
+ --token <token> a storefront access token that lists this site's origin (optional)
153
+ --locale <locale> the language pages render in (default: ru)
154
+ -h, --help show this help
155
+ `;
156
+ function check(directory) {
157
+ const violations = checkPublicApi(directory);
158
+ if (violations.length > 0) {
159
+ process.stderr.write(`${formatViolations(violations)}
160
+ `);
161
+ throw new Error(`${violations.length} use(s) of the SDK outside its public API.`);
162
+ }
163
+ process.stdout.write("The storefront uses only the SDK's public API.\n");
164
+ }
165
+ async function main() {
166
+ const { values, positionals } = parseArgs({
167
+ allowPositionals: true,
168
+ options: {
169
+ shop: { type: "string" },
170
+ token: { type: "string" },
171
+ locale: { type: "string" },
172
+ help: { type: "boolean", short: "h" }
173
+ }
174
+ });
175
+ if (values.help) {
176
+ process.stdout.write(HELP);
177
+ return;
178
+ }
179
+ if (positionals[0] === "check") {
180
+ check(positionals[1] ?? ".");
181
+ return;
182
+ }
183
+ const interactive = process.stdin.isTTY === true;
184
+ const ask = async (question, fallback) => {
185
+ if (!interactive) {
186
+ return fallback;
187
+ }
188
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
189
+ const answer = (await prompt.question(`${question} (${fallback || "skip"}): `)).trim();
190
+ prompt.close();
191
+ return answer || fallback;
192
+ };
193
+ const directory = positionals[0] ?? await ask("Directory", "magic-storefront");
194
+ const shopDomain = values.shop ?? await ask("Shop domain", "");
195
+ const root = resolve3(dirname(fileURLToPath(import.meta.url)), "..");
196
+ const own = JSON.parse(readFileSync3(resolve3(root, "package.json"), "utf8"));
197
+ const { directory: created } = scaffold({
198
+ directory,
199
+ templateDirectory: resolve3(root, "template"),
200
+ // The SDK is released together with this CLI: same version.
201
+ sdkVersions: {
202
+ "@magicstoreai/hydrogen": own.version,
203
+ "@magicstoreai/storefront-client": own.version
204
+ },
205
+ shopDomain: shopDomain || void 0,
206
+ storefrontToken: values.token,
207
+ locale: values.locale
208
+ });
209
+ const where = relative2(process.cwd(), created) || ".";
210
+ process.stdout.write(`
211
+ Created ${where}.
212
+
213
+ cd ${where}
214
+ npm install
215
+ npm run dev
216
+ ${shopDomain ? "" : "\nSet MAGICSTORE_SHOP_DOMAIN in .env.local first.\n"}`);
217
+ }
218
+ main().catch((error) => {
219
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
220
+ `);
221
+ process.exit(1);
222
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "create-magic-storefront",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a Next.js storefront on the MagicStore Storefront API v2",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-magic-storefront": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "template"
13
+ ],
14
+ "engines": {
15
+ "node": ">=22.12"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^26.6.2",
19
+ "tsup": "^8.5.1",
20
+ "typescript": "5.9.3",
21
+ "vitest": "^5.0.1"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "build": "node scripts/copy-template.mjs && tsup",
28
+ "test": "vitest run",
29
+ "typecheck": "tsc --noEmit"
30
+ }
31
+ }
@@ -0,0 +1,22 @@
1
+ # The shop's storefront domain — the API is https://<domain>/api/v2/storefront.
2
+ MAGICSTORE_SHOP_DOMAIN=shop.example.uz
3
+
4
+ # Optional. The full API base URL instead — e.g. the mock server from the spec (`pnpm mock` in the
5
+ # SDK repository serves it at http://127.0.0.1:4010).
6
+ MAGICSTORE_API_URL=
7
+
8
+ # Optional. A storefront access token (Shop settings → Storefront API) that lists this site's
9
+ # origin (e.g. http://localhost:3000). With it the browser calls the API directly; without it,
10
+ # browser calls go through this app's /storefront-api proxy (app/storefront-api/[...path]).
11
+ NEXT_PUBLIC_MAGICSTORE_STOREFRONT_TOKEN=
12
+
13
+ # Optional. The signing secret of the token's webhook (whsec_…): /api/magicstore/webhook
14
+ # revalidates cached pages when the merchant changes the catalog, pages or home.
15
+ MAGICSTORE_WEBHOOK_SECRET=
16
+
17
+ # The language pages render in (a locale the shop enables).
18
+ NEXT_PUBLIC_MAGICSTORE_LOCALE=ru
19
+
20
+ # Optional. This site's public origin for sitemap URLs (https://shop.example.uz); the shop's primary
21
+ # domain when empty.
22
+ SITE_URL=
@@ -0,0 +1,56 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+
3
+ # This is NOT the Next.js you know
4
+
5
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
6
+
7
+ This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
8
+
9
+ <!-- END:nextjs-agent-rules -->
10
+
11
+ # MagicStore storefront
12
+
13
+ A Next.js App Router storefront on the MagicStore Storefront API v2. The shop's data, prices, stock,
14
+ discounts and delivery come from the API; this app renders them. Read `llms.txt` first (in the SDK
15
+ repository: its root) — entry points, credentials, wire rules, the cart → checkout flow and what
16
+ each error code means — then `PAGES.md` for how each page type is built, and
17
+ `node_modules/@magicstoreai/hydrogen/CATALOGUE.md` for every component and hook.
18
+
19
+ ## Rules
20
+
21
+ - Import the SDK only from `@magicstoreai/storefront-client`, `@magicstoreai/hydrogen`,
22
+ `@magicstoreai/hydrogen/core`, `/server` and `/seo`. Call the API through client methods, never
23
+ by `/api/v2/storefront/…` URL. `npx create-magic-storefront check` must pass.
24
+ - Never compute money: render `Money` values with `<Money>` / `formatMoney`, totals from the cart
25
+ and the checkout.
26
+ - Cart id, checkout id, tokens, OTP codes, phone numbers: never logged, never in a URL.
27
+ - Public reads render on the server through `lib/api.ts` (cached by tag, revalidated by the
28
+ webhook). Anything personal — cart, customer, wishlist — lives in client components under
29
+ `MagicStoreProvider` (`app/providers.tsx`).
30
+ - Branch on `MagicStoreError.code`, show `error.detail`.
31
+ - Every page handles its empty, error and not-found states (`orNotFound` turns `NOT_FOUND` into
32
+ the 404 page).
33
+
34
+ ## Where things go
35
+
36
+ | Path | What |
37
+ | ------------------------------ | ------------------------------------------------------------------------ |
38
+ | `app/` | Pages (server components) and route handlers |
39
+ | `app/error.tsx` | Error boundary: the API's `detail`, a retry |
40
+ | `app/sitemap.ts` | Sitemap from `GET /sitemap` |
41
+ | `app/page.tsx` | Home: renders `GET /home` sections in the merchant's order |
42
+ | `components/sections/` | One renderer per home section type; an unknown type renders nothing |
43
+ | `components/` | Shared UI; client components start with `'use client'` |
44
+ | `lib/api.ts` | The server-side client (`nextCacheFetch`), `orNotFound` |
45
+ | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist, analytics in the browser |
46
+ | `app/storefront-api/[...path]` | Same-origin proxy for browser calls without a public storefront token |
47
+ | `app/api/magicstore/webhook` | Signed webhook → `revalidateTag` |
48
+
49
+ ## Commands
50
+
51
+ ```bash
52
+ npm run dev # needs MAGICSTORE_SHOP_DOMAIN (or MAGICSTORE_API_URL) in .env.local
53
+ npm run build
54
+ npm run typecheck
55
+ npx create-magic-storefront check # SDK used only through its public API
56
+ ```
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,154 @@
1
+ # Building storefront pages
2
+
3
+ How each page of a MagicStore storefront is built on the SDK: what it reads, how it is cached, its
4
+ SEO, and the states it must handle. The files named are this starter's — the reference for each page
5
+ type. Read `llms.txt` first for the rules every page follows (entry points, money, credentials,
6
+ errors).
7
+
8
+ ## Two kinds of data
9
+
10
+ | | Public: catalog, content, shop | Personal: cart, customer, checkout, orders |
11
+ | --------------- | ------------------------------------------------------ | ----------------------------------------------------------- |
12
+ | Where it's read | Server components, through `lib/api.ts` | Client components, through hooks under `MagicStoreProvider` |
13
+ | Client | `createStorefrontClient({ fetch: nextCacheFetch() })` | `useStorefrontClient()`, `useCart()`, `useCustomer()`, … |
14
+ | Cache | Tagged by `nextCacheFetch`, revalidated by the webhook | Never cached (`no-store`; a bearer call is never tagged) |
15
+ | Not found | `orNotFound(read)` → the `not-found.tsx` page | Branch on `MagicStoreError.code`, show `detail` |
16
+
17
+ `nextCacheFetch` tags every public `GET` by its path (`tagForPath`), and
18
+ `app/api/magicstore/webhook` revalidates the tags of each webhook topic:
19
+
20
+ | Tag | Paths | Webhook topic |
21
+ | -------------------- | ----------------------------------------------------------------------------------- | ----------------- |
22
+ | `magicstore:shop` | `/shop`, `/contacts`, `/theme/section-schema` | `SHOP_UPDATED` |
23
+ | `magicstore:home` | `/home` | `HOME_UPDATED` |
24
+ | `magicstore:pages` | `/pages*`, `/menus/footer` | `PAGES_UPDATED` |
25
+ | `magicstore:catalog` | `/products*`, `/collections*`, other `/menus/*`, `/reels`, `/sitemap`, `/locations` | `CATALOG_UPDATED` |
26
+ | none (`no-store`) | `/search*`, `/reviews/site`, cart, checkout, customer, orders | — |
27
+
28
+ Every page renders per request (`dynamic = 'force-dynamic'` in `app/layout.tsx`): the shop is the
29
+ API's, not the build's. The fetch cache still spares the API.
30
+
31
+ ## Layout — `app/layout.tsx`
32
+
33
+ - Reads `api.shop()` and the navigation (`api.collectionsIndex` or `api.menusShow({ path: { handle: 'main' } })`).
34
+ - `generateMetadata`: `shop.seo.title || shop.name`, `shop.seo.description ?? shop.description`.
35
+ - Wraps the page in `Providers` (`MagicStoreProvider` with `shop` passed in, so the browser does not
36
+ load it again).
37
+
38
+ ## Home — `app/page.tsx`, `components/sections/`
39
+
40
+ `api.home()` answers `{ sections: HomeSection[] }` in the merchant's order. Render each by `type`
41
+ with one component per type; a type the storefront does not know renders **nothing** (the API can
42
+ be newer than the storefront). `GET /theme/section-schema` (`api.themeSectionSchema()`) has the
43
+ JSON Schema of every type's `settings`; the TypeScript types are `Schema<'BannerSection'>` and so
44
+ on. The server drops a section that would show nothing, but a shelf can still come back empty —
45
+ render nothing then.
46
+
47
+ | `type` | Settings | Data it reads |
48
+ | ---------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- |
49
+ | `ANNOUNCEMENT_BAR` | `text`, colours, `animation` | — |
50
+ | `BANNER` | `slides[]`: `image`, `link` (product, collection or URL) | — |
51
+ | `FEATURED_COLLECTIONS` | — | `collectionsIndex` with `filter[featured]=true` |
52
+ | `COLLECTIONS` | `cardStyle` (a styling hint) | `collectionsIndex` with `filter[root]=true` |
53
+ | `COLLECTION_GRID` | `tiles[]`: `collection`, `image` | — |
54
+ | `NEW_ARRIVALS` | `limit` | `productsIndex` with `sort=-createdAt` |
55
+ | `ON_SALE` | `limit` | `productsIndex` with `filter[onSale]=true` |
56
+ | `TRENDING` | `limit` | `productsIndex` with `filter[trending]=true` |
57
+ | `BESTSELLERS` | `limit` | `productsIndex` with `sort=-bestSellers` |
58
+ | `PRE_ORDERS` | `limit` | `productsIndex` with `filter[preOrder]=true` |
59
+ | `FOR_YOU` | `limit` | Personal: not on a cached page (see below) |
60
+ | `COLLECTION_PRODUCTS` | `collection`, `limit`, `title` | `collectionsProducts` of `collection.handle` |
61
+ | `FLASH_SALE` | `collection`, `limit`, `title`, `endsAt` | `collectionsProducts` of `collection.handle` |
62
+ | `DEAL_OF_DAY` | `product`, `image`, `price`, `compareAtPrice`, `endsAt`, `stock` | — (`stock` is the merchant's "N of M left", not real stock) |
63
+ | `SHOPPABLE_STORIES` | `stories[]`: `thumbnail`, `title`, `frames[]` (media, `product`) | — |
64
+ | `STORE_REVIEWS` | titles, `limit`, `externalRatings`, `recommendPercent`, flags | `reviewsSite` (`meta.rating` has the average and breakdown) |
65
+
66
+ `limit: null` means the storefront's own default. `FOR_YOU` has no product-independent endpoint yet
67
+ (`productsRecommendations` with `intent=FOR_YOU` needs a product) and a cached page must not be
68
+ personal; render it in a client component or not at all. A shop with no sections still needs a
69
+ home: show new arrivals.
70
+
71
+ ## Collection — `app/collections/[handle]/page.tsx`
72
+
73
+ - `api.collectionsShow({ path: { handle } })` and `api.collectionsProducts({ path: { handle }, query: { page, perPage } })`,
74
+ both through `orNotFound`.
75
+ - Filters and sorts: `api.collectionsFilters` lists the facets; pass them as `filter[…]` and `sort`
76
+ query parameters. An unknown filter or sort is a `VALIDATION_FAILED`, not ignored.
77
+ - SEO: `pageMeta({ seo: collection.seo, title, description })`, and `breadcrumbJsonLd`.
78
+ - States: an empty collection ("Nothing here yet"), a `page` past the last one (empty list), 404.
79
+
80
+ ## Product — `app/products/[handle]/page.tsx`, `components/buy-box.tsx`
81
+
82
+ - `api.productsShow({ path: { handle } })` through `orNotFound`. Recommendations:
83
+ `productsRecommendations` (`intent=RELATED` or `COMPLEMENTARY`); reviews: `productsReviews`
84
+ (`meta.rating` has the average and breakdown). Both are catalog reads, cached like the product.
85
+ - The buy box is a client component: `useVariantSelection(product)` (or `ProductProvider` +
86
+ `useProduct`), `useCart().addLine({ productId, variantId, quantity })`, `useWishlist()`, and
87
+ `useAnalytics().productView(...)`.
88
+ - Price: the selected variant's `price` / `compareAtPrice`, else the product's, with `<Money>`.
89
+ - SEO: `pageMeta({ seo, title, description, images })`, and
90
+ `jsonLdScript(productJsonLd(product, { url }))` in a `<script type="application/ld+json">`.
91
+ - States: 404; out of stock (`availableForSale` false on the product or the selected variant —
92
+ disable "add to cart", say "Sold out"); an option value that leads nowhere
93
+ (`isAvailable(name, value)` false — disable it); a failed add (`CART_LINES_UNAVAILABLE` and others:
94
+ show `detail`); no images (`<Image fallback>`).
95
+
96
+ ## Search — `app/search/page.tsx`
97
+
98
+ - `api.search({ query: { q, page, perPage } })` — not cached (no tag). Suggestions while typing:
99
+ `searchSuggestions({ query: { q } })` from the browser; popular queries: `searchTrending`.
100
+ - Facets: `searchFilters({ query: { q } })`.
101
+ - States: no query yet (show the form only), no results, pagination.
102
+ - Search pages are usually `noindex`; report `useAnalytics().search(q, total)`.
103
+
104
+ ## Cart — `app/cart/page.tsx`
105
+
106
+ - Client only: `useCart()` — `cart`, `status` (`loading` / `updating` / `idle`), `error`, and the
107
+ changes (`updateLine`, `removeLine`, `setDiscountCodes`, `setNote`, `setGift`, `setPoints`).
108
+ - Render `cart.cost` as it comes; never add prices up. Each line has `available` and `issues`.
109
+ Discount codes answer `applicable` and a `reasonCode`.
110
+ - States: loading (the stored cart is being fetched), empty or no cart, a line no longer available,
111
+ a rejected code, the last failed change (`error`, the cart is already back to the server's).
112
+ - Never put the cart id in a URL.
113
+
114
+ ## Checkout — `app/checkout/page.tsx`
115
+
116
+ - Client only. `checkoutsStore({ body: { cartId } })` (safe to repeat), then fill what
117
+ `checkout.missing` lists, in order: `CONTACT` → `DELIVERY_ADDRESS` (or a pickup point from
118
+ `locationsIndex`) → `DELIVERY_OPTION` (`checkoutsDeliveryOptions`, then select by `id`) →
119
+ `PAYMENT_METHOD`. Each step's `PUT` answers the updated checkout.
120
+ - `checkoutsCompletion` answers `{ order, payment }`; send the buyer to `payment.redirectUrl` when
121
+ `PENDING`. On their return, read `ordersPayment` (guest: header `X-Checkout-Id`) — never trust the
122
+ redirect.
123
+ - States: empty cart, preparing, each step's validation errors (`VALIDATION_FAILED` → per field),
124
+ `DELIVERY_QUOTE_EXPIRED` (list the options again), `CHECKOUT_NOT_READY`, `CHECKOUT_CLOSED`,
125
+ `STORE_UNAVAILABLE`, a payment that is still pending.
126
+ - Never put the checkout id in a URL or a log.
127
+
128
+ ## Account — `app/account/page.tsx`
129
+
130
+ - Client only. Signed out: `useCustomer().requestOtp(phone)` → `verifyOtp(phone, code)`
131
+ (`OTP_INVALID`: re-type; `OTP_EXPIRED`: ask again), or Telegram / OQ / Click sign-in in those apps.
132
+ Only offer OTP when `shop.features.otpLogin` is on.
133
+ - Signed in: `customerOrdersIndex`, `customerOrdersShow`, `customerWishlistIndex`, addresses,
134
+ rewards — through `useStorefrontClient()` (the provider adds the bearer and refreshes it).
135
+ - States: signed out, `UNAUTHENTICATED` (sign in again), no orders yet.
136
+
137
+ ## Content pages, sitemap — `app/pages/[handle]/page.tsx`, `app/sitemap.ts`
138
+
139
+ - `api.pagesShow({ path: { handle } })` (`magicstore:pages`) through `orNotFound`,
140
+ `pageMeta({ seo, title })`; `body` is the merchant's HTML (empty until written). The layout links
141
+ `api.pagesIndex()` in the footer.
142
+ - `app/sitemap.ts` reads `api.sitemap({ query: { page, perPage: 1000 } })` — `{ type, handle,
143
+ updatedAt }` rows — on `SITE_URL` or the shop's `primaryDomain`, at most 50 pages (the protocol's
144
+ 50 000 URLs). Dynamic, like
145
+ every page.
146
+
147
+ ## Every page
148
+
149
+ - Import only the SDK's entry points; `npx create-magic-storefront check` passes.
150
+ - Not found → `notFound()`; any other failure shows `describe(error)` (`lib/errors.ts`) or lets
151
+ `app/error.tsx` catch it (it shows `detail`, and "temporarily closed" on `STORE_UNAVAILABLE`).
152
+ - Images through `<Image>` (intrinsic size, lazy), prices through `<Money>`.
153
+ - Analytics: `PageViews` in `app/providers.tsx` reports each route; product, collection and search
154
+ views are reported where they render.
@@ -0,0 +1,31 @@
1
+ # MagicStore storefront starter
2
+
3
+ A Next.js (App Router) storefront on the MagicStore Storefront API v2, built only on the public SDK
4
+ (`@magicstoreai/storefront-client`, `@magicstoreai/hydrogen`). The home page from the merchant's
5
+ sections, catalog, collections, search, product with variants, cart, checkout (pickup or delivery,
6
+ payment), OTP sign-in and orders, SEO and JSON-LD, and a webhook that revalidates cached pages.
7
+
8
+ ```bash
9
+ cp .env.example .env.local # set MAGICSTORE_SHOP_DOMAIN
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ | Where | What |
15
+ | ------------------------------ | ----------------------------------------------------------------------------- |
16
+ | `PAGES.md` | How each page type is built: data calls, cache tags, SEO, required states. |
17
+ | `lib/api.ts` | The server-side client. Public reads are cached under `magicstore:*` tags. |
18
+ | `components/sections/` | One renderer per `GET /home` section type; an unknown type renders nothing. |
19
+ | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist and analytics in the browser. |
20
+ | `app/storefront-api/[...path]` | Same-origin proxy for browser calls when there is no public storefront token. |
21
+ | `app/api/magicstore/webhook` | Verifies the platform's webhook and revalidates the tags its topic covers. |
22
+
23
+ **Browser calls.** With `NEXT_PUBLIC_MAGICSTORE_STOREFRONT_TOKEN` (a token that lists this site's
24
+ origin) the browser calls the shop's API directly. Without it, calls go through the proxy route,
25
+ which forwards the buyer's address in `X-Forwarded-For`.
26
+
27
+ **Offline.** `MAGICSTORE_API_URL=http://127.0.0.1:4010` with `pnpm mock` running in the SDK repository
28
+ serves every page from the spec's example data.
29
+
30
+ **Webhook.** In Shop settings → Storefront API, set the token's webhook to
31
+ `https://<this site>/api/magicstore/webhook` and put its secret in `MAGICSTORE_WEBHOOK_SECRET`.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ .next/
3
+ .env*.local
4
+ next-env.d.ts
5
+ *.tsbuildinfo