create-magic-storefront 0.2.0 → 0.4.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 (45) hide show
  1. package/README.md +18 -1
  2. package/dist/index.js +523 -9
  3. package/package.json +7 -1
  4. package/template/.claude/skills/theme-section/SKILL.md +157 -0
  5. package/template/.env.example +7 -1
  6. package/template/.screenshots/home-1280.png +0 -0
  7. package/template/.screenshots/home-390.png +0 -0
  8. package/template/.screenshots/home-filled-1280.png +0 -0
  9. package/template/.screenshots/home-filled-390.png +0 -0
  10. package/template/.screenshots/page-1280.png +0 -0
  11. package/template/AGENTS.md +58 -16
  12. package/template/PAGES.md +107 -30
  13. package/template/README.md +13 -1
  14. package/template/_gitignore +2 -0
  15. package/template/app/account/page.tsx +126 -10
  16. package/template/app/api/magicstore/preview/route.ts +56 -0
  17. package/template/app/globals.css +146 -4
  18. package/template/app/layout.tsx +25 -6
  19. package/template/app/page.tsx +18 -9
  20. package/template/app/pages/[handle]/page.tsx +26 -11
  21. package/template/components/sections/collections.tsx +2 -1
  22. package/template/components/sections/product-shelves.tsx +2 -1
  23. package/template/components/telegram-shell.tsx +54 -0
  24. package/template/lib/api.ts +40 -10
  25. package/template/lib/errors.ts +21 -0
  26. package/template/lib/i18n.ts +33 -0
  27. package/template/lib/preview.ts +21 -0
  28. package/template/llms.txt +175 -2
  29. package/template/package.json +8 -3
  30. package/template/scripts/theme.mjs +53 -0
  31. package/template/theme/index.ts +53 -0
  32. package/template/theme/sections/collection-list.tsx +64 -0
  33. package/template/theme/sections/featured-collection.tsx +56 -0
  34. package/template/theme/sections/hero.tsx +95 -0
  35. package/template/theme/sections/image-with-text.tsx +71 -0
  36. package/template/theme/sections/page-content.tsx +45 -0
  37. package/template/theme/sections/platform-home.tsx +37 -0
  38. package/template/theme/sections/product-shelf.tsx +96 -0
  39. package/template/theme/sections/rich-text.tsx +43 -0
  40. package/template/theme/sections/testimonials.tsx +84 -0
  41. package/template/theme/settings.ts +19 -0
  42. package/template/theme/templates/index.json +9 -0
  43. package/template/theme/templates/page.json +6 -0
  44. package/template/theme/text.ts +21 -0
  45. package/template/tsconfig.json +1 -1
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: theme-section
3
+ description: >-
4
+ Create or change a theme section — a part of a page the merchant edits in the admin's theme
5
+ editor (a hero, a size guide, a lookbook, a FAQ). Use whenever a page gets a new block of content,
6
+ when asked for a "section", or when a section's settings change. Covers the five steps, the setting
7
+ types a section may use, and a worked example.
8
+ ---
9
+
10
+ # Creating a new section
11
+
12
+ A section is a file in `theme/sections/`: its schema (`defineSection`) and its React component. The
13
+ backend has no code per section: `theme push` sends the schema, and the admin's editor, the API's
14
+ validation and the merchant's AI tools work for that section at once. **So invent any section the
15
+ design needs** (`size-guide`, `lookbook`, `faq`) — no one touches the backend.
16
+
17
+ What the backend does not know is a new **setting type**. Compose sections from the types below;
18
+ never invent one. A field no type covers is a contract change in magicbot, made there first.
19
+
20
+ `GET /home` is optional: the theme builds the home page. Data sections fetch through the catalog
21
+ operations (`productsIndex`, `collectionsProducts`, …), never through `/home`.
22
+
23
+ ## The five steps
24
+
25
+ 1. Create `theme/sections/<type>.tsx` with `defineSection` and the component (`SectionProps<typeof
26
+ section>` types its `settings` and `blocks`).
27
+ 2. Register it in `theme/index.ts`: in `sections` of `defineTheme`, and its component in `components`.
28
+ 3. Put it in a template (`theme/templates/index.json` or `page.json`) or give it `presets`, so the
29
+ merchant can add it in the editor.
30
+ 4. Every word the buyer sees comes from a setting with a default in `en`, `ru` and `uz` (or from
31
+ `lib/i18n.ts` through `message()`), so the merchant can change it. Editor labels use `label(en,
32
+ ru, uz)` from `theme/text.ts`. No copy written into the component.
33
+ 5. Run `npm run theme:check` (it must pass), then `npm run theme:push`, and bump `version` in
34
+ `theme/index.ts`. While developing, `npm run dev` with `MAGICSTORE_THEME_TOKEN` set pushes on
35
+ every change (`theme dev`).
36
+
37
+ `theme check` fails on a file in `theme/sections/` that is not registered (`SECTION_NOT_REGISTERED`),
38
+ a template that uses an unknown type, and a text setting without a default in every locale; it warns
39
+ on copy in a component (`HARDCODED_COPY`; `// theme-copy-ok` on the line opts out).
40
+
41
+ ## Setting types
42
+
43
+ The component receives the served value. Ids are camelCase (`^[a-z][a-zA-Z0-9]{0,63}$`); `SELECT`
44
+ values are `UPPER_SNAKE_CASE`; every setting takes `label`, optional `info` and `default`.
45
+
46
+ | Type | Extra members | The component receives |
47
+ | ----------------- | --------------------------------------- | --------------------------------------------------------- |
48
+ | `TEXT` | `maxLength` (≤ 500) | `string` |
49
+ | `TEXTAREA` | `maxLength` (≤ 5 000) | `string` |
50
+ | `RICHTEXT` | — | sanitised HTML `string` (the only HTML a section renders) |
51
+ | `URL` | — | `string \| null` |
52
+ | `COLOR` | — | `#rrggbb` or `null` |
53
+ | `CHECKBOX` | — | `boolean` |
54
+ | `NUMBER` | `min`, `max` (optional) | `number \| null` |
55
+ | `RANGE` | `min`, `max`, `step`, `unit` (optional) | `number` |
56
+ | `SELECT` | `options: [{ value, label }]` (≤ 100) | one option `value` |
57
+ | `IMAGE` | — | `{ url, altText: null, width, height }` or `null` |
58
+ | `COLLECTION` | — | `{ id, handle, title }` or `null` |
59
+ | `PRODUCT` | — | `{ id, handle, title }` or `null` |
60
+ | `COLLECTION_LIST` | `limit` (1–50) | `{ id, handle, title }[]` |
61
+ | `PRODUCT_LIST` | `limit` (1–50) | `{ id, handle, title }[]` |
62
+ | `HEADER` | `content` (no `id`) | — (a heading in the editor form) |
63
+ | `PARAGRAPH` | `content` (no `id`) | — (a note in the editor form) |
64
+
65
+ Text settings are stored per locale and served in the request's; the component only sees a string.
66
+ `IMAGE`, `COLLECTION`, `PRODUCT` and the lists reference shop data, so their default is empty; a
67
+ reference is not a full product — fetch what you show by `handle` through the catalog operations.
68
+ `IMAGE` has no alt text: declare a `TEXT` setting for it. Blocks (`defineBlock`) are repeatable
69
+ children with their own settings: slides, quotes, table rows (`maxBlocks` caps them).
70
+
71
+ ## Example: `size-guide`
72
+
73
+ A heading and a table of `row` blocks, in `theme/sections/size-guide.tsx`:
74
+
75
+ ```tsx
76
+ import { defineBlock, defineSection, type SectionProps } from '@magicstoreai/hydrogen/theme';
77
+
78
+ import { label } from '../text';
79
+
80
+ const row = defineBlock({
81
+ type: 'row',
82
+ name: label('Size', 'Размер', "O'lcham"),
83
+ settings: [
84
+ { id: 'size', type: 'TEXT', label: label('Size', 'Размер', "O'lcham"), default: '' },
85
+ { id: 'chest', type: 'TEXT', label: label('Chest', 'Грудь', "Ko'krak"), default: '' },
86
+ { id: 'waist', type: 'TEXT', label: label('Waist', 'Талия', 'Bel'), default: '' },
87
+ ],
88
+ });
89
+
90
+ export const sizeGuide = defineSection({
91
+ type: 'size-guide',
92
+ name: label('Size guide', 'Таблица размеров', "O'lchamlar jadvali"),
93
+ maxBlocks: 30,
94
+ settings: [
95
+ {
96
+ id: 'heading',
97
+ type: 'TEXT',
98
+ label: label('Heading', 'Заголовок', 'Sarlavha'),
99
+ default: { en: 'Size guide', ru: 'Таблица размеров', uz: "O'lchamlar jadvali" },
100
+ },
101
+ {
102
+ id: 'columns',
103
+ type: 'TEXT',
104
+ label: label('Column names', 'Названия колонок', 'Ustun nomlari'),
105
+ info: label('Separated by commas.', 'Через запятую.', 'Vergul bilan.'),
106
+ default: {
107
+ en: 'Size, Chest cm, Waist cm',
108
+ ru: 'Размер, Грудь см, Талия см',
109
+ uz: "O'lcham, Ko'krak sm, Bel sm",
110
+ },
111
+ },
112
+ ],
113
+ blocks: [row],
114
+ presets: [
115
+ {
116
+ name: label('Size guide', 'Таблица размеров', "O'lchamlar jadvali"),
117
+ blocks: [{ type: 'row' }, { type: 'row' }, { type: 'row' }],
118
+ },
119
+ ],
120
+ });
121
+
122
+ /** A size table the merchant fills row by row; nothing shows until a row has a size. */
123
+ export function SizeGuide({ settings, blocks }: SectionProps<typeof sizeGuide>) {
124
+ const rows = blocks.filter((block) => block.settings.size !== '');
125
+ if (rows.length === 0) {
126
+ return null;
127
+ }
128
+ return (
129
+ <section className="stack">
130
+ <h2>{settings.heading}</h2>
131
+ <table>
132
+ <thead>
133
+ <tr>
134
+ {settings.columns.split(',').map((name) => (
135
+ <th key={name}>{name.trim()}</th>
136
+ ))}
137
+ </tr>
138
+ </thead>
139
+ <tbody>
140
+ {rows.map(({ id, settings: cells }) => (
141
+ <tr key={id}>
142
+ <td>{cells.size}</td>
143
+ <td>{cells.chest}</td>
144
+ <td>{cells.waist}</td>
145
+ </tr>
146
+ ))}
147
+ </tbody>
148
+ </table>
149
+ </section>
150
+ );
151
+ }
152
+ ```
153
+
154
+ Then in `theme/index.ts`: `import { SizeGuide, sizeGuide } from './sections/size-guide';`, add
155
+ `sizeGuide` to `sections` and `'size-guide': SizeGuide` to `components`. The preset lets the merchant
156
+ add it from the editor; to show it by default, add `"size": { "type": "size-guide" }` to a template's
157
+ `sections` and `"size"` to its `order`. Run `npm run theme:check` and `npm run theme:push`.
@@ -10,8 +10,14 @@ MAGICSTORE_API_URL=
10
10
  # browser calls go through this app's /storefront-api proxy (app/storefront-api/[...path]).
11
11
  NEXT_PUBLIC_MAGICSTORE_STOREFRONT_TOKEN=
12
12
 
13
+ # Optional. The storefront access token (msf_…) the server sends with its reads: the API then serves
14
+ # this storefront's own theme, and theme previews work (they need it). Server-only; when empty, the
15
+ # public token below is used, else the shop is found by its domain.
16
+ MAGICSTORE_STOREFRONT_TOKEN=
17
+
13
18
  # 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.
19
+ # revalidates cached pages when the merchant changes the catalog, pages or home, and
20
+ # /api/magicstore/preview verifies the theme editor's preview links with it.
15
21
  MAGICSTORE_WEBHOOK_SECRET=
16
22
 
17
23
  # The language pages render in (a locale the shop enables).
@@ -19,10 +19,10 @@ each error code means — then `PAGES.md` for how each page type is built, and
19
19
  ## Rules
20
20
 
21
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. A server component takes only components from
22
+ `@magicstoreai/hydrogen/core`, `/server`, `/seo` and `/theme`. Call the API through client
23
+ methods, never by `/api/v2/storefront/…` URL. A server component takes only components from
24
24
  `@magicstoreai/hydrogen` (a client module); functions and classes come from `/core`, `/server`,
25
- `/seo` or `@magicstoreai/storefront-client`. `npx create-magic-storefront check` must pass.
25
+ `/seo`, `/theme` or `@magicstoreai/storefront-client`. `npx create-magic-storefront check` must pass.
26
26
  - Never compute money: render `Money` values with `<Money>` / `formatMoney`, totals from the cart
27
27
  and the checkout.
28
28
  - Cart id, checkout id, tokens, OTP codes, phone numbers: never logged, never in a URL.
@@ -34,6 +34,10 @@ each error code means — then `PAGES.md` for how each page type is built, and
34
34
  every language it has (`ru`, `uz`, `en`).
35
35
  - Every page handles its empty, error and not-found states (`orNotFound` turns `NOT_FOUND` into
36
36
  the 404 page).
37
+ - The storefront also runs as a Telegram Mini App (`PAGES.md`, "Running as a Telegram Mini App").
38
+ App-wide Telegram behaviour lives in `components/telegram-shell.tsx` only: one automatic
39
+ `useTelegramSignIn()` there, `{ auto: false }` anywhere else. Never put `initData` in a URL, a log
40
+ or analytics.
37
41
 
38
42
  ## Design
39
43
 
@@ -51,19 +55,55 @@ screenshot every page type at 390 and 1280px, fix what you see. Interactive UI f
51
55
 
52
56
  ## Where things go
53
57
 
54
- | Path | What |
55
- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
56
- | `app/` | Pages (server components) and route handlers |
57
- | `app/error.tsx` | Error boundary: the API's `detail`, a retry |
58
- | `app/blog/` | Blog: index, category, author, tag and article pages (`PAGES.md`, Blog) |
59
- | `app/sitemap.ts` | Sitemap from `GET /sitemap` |
60
- | `app/page.tsx` | Home: renders `GET /home` sections in the merchant's order. Optional: a custom home composes its own (`PAGES.md`) |
61
- | `components/sections/` | One renderer per home section type; an unknown type renders nothing |
62
- | `components/` | Shared UI; client components start with `'use client'` |
63
- | `lib/api.ts` | The server-side client (`nextCacheFetch`), `orNotFound` |
64
- | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist, analytics in the browser |
65
- | `app/storefront-api/[...path]` | Same-origin proxy for browser calls without a public storefront token |
66
- | `app/api/magicstore/webhook` | Signed webhook → `revalidateTag` |
58
+ | Path | What |
59
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------- |
60
+ | `app/` | Pages (server components) and route handlers |
61
+ | `app/error.tsx` | Error boundary: the API's `detail`, a retry |
62
+ | `app/blog/` | Blog: index, category, author, tag and article pages (`PAGES.md`, Blog) |
63
+ | `app/sitemap.ts` | Sitemap from `GET /sitemap` |
64
+ | `app/page.tsx` | Home: the theme's `index` template (`loadTemplate` + `ThemeSections`, `PAGES.md`) |
65
+ | `theme/` | The theme: `sections/` (schema + component), `templates/*.json`, `settings.ts`, registered in `index.ts` |
66
+ | `components/sections/` | One renderer per `GET /home` section type (the `platform-home` theme section); an unknown type renders nothing |
67
+ | `components/` | Shared UI; client components start with `'use client'` |
68
+ | `components/telegram-shell.tsx` | Telegram Mini App: theme marks, back arrow, the one automatic Telegram sign-in (`PAGES.md`) |
69
+ | `lib/api.ts` | The server-side client (`nextCacheFetch`), `orNotFound` |
70
+ | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist, analytics in the browser |
71
+ | `app/storefront-api/[...path]` | Same-origin proxy for browser calls without a public storefront token |
72
+ | `app/api/magicstore/webhook` | Signed webhook → `revalidateTag` |
73
+ | `app/api/magicstore/preview` | Theme editor preview: signed token → draft mode; pages pass `themePreview()` (`lib/preview.ts`) as `preview` |
74
+
75
+ ## Creating a new section
76
+
77
+ A page part the merchant edits in the admin is a theme section. Invent any section the design needs
78
+ (`size-guide`, `lookbook`): the backend has no code per section, so a type it has never seen is fine.
79
+ A **setting type** it has never seen is not — compose the types below, never invent one; a missing
80
+ type is a contract change in magicbot. `GET /home` is optional. Full recipe and a worked example
81
+ (`size-guide`): `.claude/skills/theme-section/SKILL.md`.
82
+
83
+ 1. `theme/sections/<type>.tsx`: `defineSection` + the component (`SectionProps<typeof section>`).
84
+ 2. Register it in `theme/index.ts` (`sections` and `components`).
85
+ 3. Put it in `theme/templates/*.json` or give it `presets`.
86
+ 4. Every buyer-facing word comes from a setting with an `en`/`ru`/`uz` default. No copy in the component.
87
+ 5. `npm run theme:check`, then `npm run theme:push` (bump `version`); `theme dev` pushes while developing.
88
+
89
+ | Type | Extra members | The component receives |
90
+ | ----------------- | --------------------------------------- | --------------------------------------------------------- |
91
+ | `TEXT` | `maxLength` (≤ 500) | `string` |
92
+ | `TEXTAREA` | `maxLength` (≤ 5 000) | `string` |
93
+ | `RICHTEXT` | — | sanitised HTML `string` (the only HTML a section renders) |
94
+ | `URL` | — | `string \| null` |
95
+ | `COLOR` | — | `#rrggbb` or `null` |
96
+ | `CHECKBOX` | — | `boolean` |
97
+ | `NUMBER` | `min`, `max` (optional) | `number \| null` |
98
+ | `RANGE` | `min`, `max`, `step`, `unit` (optional) | `number` |
99
+ | `SELECT` | `options: [{ value, label }]` (≤ 100) | one option `value` |
100
+ | `IMAGE` | — | `{ url, altText: null, width, height }` or `null` |
101
+ | `COLLECTION` | — | `{ id, handle, title }` or `null` |
102
+ | `PRODUCT` | — | `{ id, handle, title }` or `null` |
103
+ | `COLLECTION_LIST` | `limit` (1–50) | `{ id, handle, title }[]` |
104
+ | `PRODUCT_LIST` | `limit` (1–50) | `{ id, handle, title }[]` |
105
+ | `HEADER` | `content` (no `id`) | — (a heading in the editor form) |
106
+ | `PARAGRAPH` | `content` (no `id`) | — (a note in the editor form) |
67
107
 
68
108
  ## Commands
69
109
 
@@ -72,4 +112,6 @@ npm run dev # needs MAGICSTORE_SHOP_DOMAIN (or MAGICSTORE_API_URL) in .
72
112
  npm run build
73
113
  npm run typecheck
74
114
  npx create-magic-storefront check # SDK used only through its public API
115
+ npm run theme:check # theme sections and templates; npm run build runs it too
116
+ npm run theme:push # send the theme to the shop (MAGICSTORE_THEME_TOKEN, mtt_…)
75
117
  ```
package/template/PAGES.md CHANGED
@@ -17,13 +17,15 @@ errors).
17
17
  `nextCacheFetch` tags every public `GET` by its path (`tagForPath`), and
18
18
  `app/api/magicstore/webhook` revalidates the tags of each webhook topic:
19
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*`, `/blog/*`, `/menus/footer`, `/sitemap` | `PAGES_UPDATED` |
25
- | `magicstore:catalog` | `/products*`, `/collections*`, other `/menus/*`, `/reels`, `/sitemap` (both tags), `/locations` | `CATALOG_UPDATED` |
26
- | none (`no-store`) | `/search*`, `/reviews/site`, cart, checkout, customer, orders | — |
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*`, `/blog/*`, `/menus/footer`, `/sitemap` | `PAGES_UPDATED` |
25
+ | `magicstore:catalog` | `/products*`, `/collections*`, other `/menus/*`, `/reels`, `/sitemap` (both tags), `/locations` | `CATALOG_UPDATED` |
26
+ | `magicstore:theme` | `/theme`, `/theme/settings`, `/templates/*` (both tags) | `THEME_UPDATED` |
27
+ | `magicstore:template:<name>` | `/templates/<name>` | `TEMPLATE_UPDATED` (`data.name`) |
28
+ | none (`no-store`) | `/search*`, `/reviews/site`, cart, checkout, customer, orders, theme previews | — |
27
29
 
28
30
  Every page renders per request (`dynamic = 'force-dynamic'` in `app/layout.tsx`): the shop is the
29
31
  API's, not the build's. The fetch cache still spares the API.
@@ -38,26 +40,37 @@ API's, not the build's. The fetch cache still spares the API.
38
40
  - Wraps the page in `Providers` (`MagicStoreProvider` with `shop` passed in, so the browser does not
39
41
  load it again).
40
42
  - Links the blog only when `shop.features.blog` is on (an article is readable in this language).
41
-
42
- ## Home — `app/page.tsx`, `components/sections/`
43
-
44
- The home page belongs to the storefront. `GET /home` is one way to fill it, not a requirement.
45
- Pick one per project:
46
-
47
- - **Merchant-managed** (this starter's default): render `GET /home`, described below. The merchant
48
- arranges the page in the admin and the storefront follows.
49
- - **Custom**: compose the page yourself from any API data — `productsIndex` with its sorts and
50
- filters, `collectionsIndex`, `collectionsProducts`, `reviewsSite`, `pagesShow`, `shop.branding` —
51
- in whatever layout the design calls for. Not calling `api.home()` at all is fine, and so is
52
- taking only some sections from it (the merchant's `BANNER` slides, the `ANNOUNCEMENT_BAR` text)
53
- and placing them where the design wants. `components/sections/` can then be deleted.
54
-
55
- A custom home is a trade-off: the merchant's home editor in the admin no longer controls the page.
56
- Tell whoever owns the store. Everything else still holds: data only from the API, nothing invented,
57
- and the page is revalidated by the tags of what it reads (`magicstore:catalog`, `magicstore:shop`),
58
- not by `HOME_UPDATED` unless it calls `api.home()`.
59
-
60
- ### Rendering `GET /home`
43
+ - Telegram: when `shop.features.telegramBot` is on, loads `TELEGRAM_WEB_APP_SCRIPT` (from
44
+ `@magicstoreai/hydrogen/core` — a plain string a server component may import) with
45
+ `<Script strategy="beforeInteractive">`; a shop without a bot pays nothing for it. Renders
46
+ `TelegramShell` (`components/telegram-shell.tsx`) inside `Providers` (see "Running as a Telegram
47
+ Mini App").
48
+
49
+ ## Home — `app/page.tsx`, `theme/`
50
+
51
+ The home page belongs to the storefront. This starter is a **theme** (Online Store 2.0 style):
52
+
53
+ - `theme/sections/<type>.tsx` holds one section: its schema (`defineSection`) next to its component.
54
+ `theme/index.ts` registers every section (`defineTheme`) and its component (`components`).
55
+ - `theme/templates/index.json` is the home's default content; `theme/templates/page.json` lays out
56
+ content pages; `theme/settings.ts` holds global settings (the announcement above the header).
57
+ - `create-magic-storefront theme push` sends the theme to the shop, and the merchant edits texts,
58
+ images, products and the order of sections in the admin's theme editor. `app/page.tsx` reads the
59
+ merchant's template with `loadTemplate(api, 'index', theme)` and renders it with `ThemeSections`;
60
+ until the theme is pushed, the bundled `templates/index.json` renders instead.
61
+ - Every buyer-facing string in a section comes from a setting with a default (from `lib/i18n.ts`),
62
+ so the merchant can change it. `npm run theme:check` flags copy written into a component.
63
+
64
+ `GET /home` is optional: the default template never calls it. The `platform-home` section renders
65
+ the platform's home editor layout (below) when the merchant adds it in the theme editor. Theme reads
66
+ are cached under `magicstore:theme` and `magicstore:template:<name>` (table above).
67
+
68
+ A storefront may still drop the theme and compose a **custom** home from any API data —
69
+ `productsIndex` with its sorts and filters, `collectionsIndex`, `collectionsProducts`,
70
+ `reviewsSite`, `pagesShow`, `shop.branding`. The merchant's editor then no longer controls the page:
71
+ tell whoever owns the store. Everything else still holds: data only from the API, nothing invented.
72
+
73
+ ### Rendering `GET /home` — `components/sections/`, the `platform-home` section
61
74
 
62
75
  `api.home()` answers `{ sections: HomeSection[] }` in the merchant's order. Render each by `type`
63
76
  with one component per type; a type the storefront does not know renders **nothing** (the API can
@@ -90,6 +103,15 @@ render nothing then.
90
103
  personal; render it in a client component or not at all. A shop with no sections still needs a
91
104
  home: show new arrivals.
92
105
 
106
+ ## Creating a new section
107
+
108
+ A section is one file in `theme/sections/`, registered in `theme/index.ts`, placed in a template or
109
+ given `presets`, with every buyer-facing word in a setting, then `npm run theme:check` and
110
+ `npm run theme:push`. The backend needs nothing for a new section type — only a new setting type is
111
+ a contract change. The steps, the setting types and a worked example (`size-guide`) are in
112
+ `AGENTS.md` and `.claude/skills/theme-section/SKILL.md`; `theme/sections/testimonials.tsx` is a
113
+ section with blocks to copy from.
114
+
93
115
  ## Collection — `app/collections/[handle]/page.tsx`
94
116
 
95
117
  - `api.collectionsShow({ path: { handle } })` and `api.collectionsProducts({ path: { handle }, query: { page, perPage } })`,
@@ -149,13 +171,68 @@ home: show new arrivals.
149
171
 
150
172
  ## Account — `app/account/page.tsx`
151
173
 
152
- - Client only. Signed out: `useCustomer().requestOtp(phone)` → `verifyOtp(phone, code)`
153
- (`OTP_INVALID`: re-type; `OTP_EXPIRED`: ask again), or Telegram / OQ / Click sign-in in those apps.
154
- Only offer OTP when `shop.features.otpLogin` is on.
174
+ - Client only. Signed out:
175
+ - Phone: `useCustomer().requestOtp(phone)` → `verifyOtp(phone, code)` (`OTP_INVALID`: re-type;
176
+ `OTP_EXPIRED`: ask again). On a shop whose sign-in code is off
177
+ (`shop.features.otpLogin === false`) the same form calls `signInWithPhone(phone)` — no code
178
+ step.
179
+ - Inside a Telegram Mini App (`useTelegramWebApp().isTelegram`), above the phone form: "Sign in
180
+ with Telegram" — `useTelegramSignIn({ auto: false }).signIn()` (the automatic sign-in lives in
181
+ `components/telegram-shell.tsx`, never here). The hook shares the attempt's state, so this
182
+ block also shows the shell's automatic attempt: "Signing in…" while it runs, its failure
183
+ without a tap. `status: 'failed'` with `UNAUTHENTICATED` → the launch data is stale: ask to
184
+ close the shop and reopen it from the bot; `FORBIDDEN` → the shop has no bot: hide the button.
185
+ The phone form stays as the fallback.
186
+ - While `useTelegramWebApp().status` is `unknown` (server render and first paint) on a shop with
187
+ a bot (`shop.features.telegramBot`), an invisible copy of that block holds its place, so the
188
+ phone form does not jump down after mount. A shop without a bot gets no gap.
189
+ - OQ / Click sign-in in those apps.
155
190
  - Signed in: `customerOrdersIndex`, `customerOrdersShow`, `customerWishlistIndex`, addresses,
156
191
  rewards — through `useStorefrontClient()` (the provider adds the bearer and refreshes it).
192
+ - Phone proof: signed in inside a Mini App with no `customer.phone` (a Telegram-only customer) and
193
+ `useTelegramWebApp().canRequestContact` (Bot API 6.9+; older clients cannot share) → a
194
+ "Share phone number" button → `useCustomer().shareTelegramPhone()` (null: the buyer declined —
195
+ nothing to show). Errors through `describePhoneShare` (`lib/errors.ts`): `CONTACT_EXPIRED` → share
196
+ again; `CONTACT_INVALID`, `CONTACT_NOT_OWN`, `INVALID_PHONE`, `TAKEN` → the API's message for
197
+ `telegramContact`. Never offer it to a signed-out visitor (`UNAUTHENTICATED`).
157
198
  - States: signed out, `UNAUTHENTICATED` (sign in again), no orders yet.
158
199
 
200
+ ## Running as a Telegram Mini App
201
+
202
+ The same app opens inside Telegram when the shop's bot points at it: in BotFather, set the bot's
203
+ Mini App (or menu button) URL to this site's `https://` address. Nothing else changes — same pages,
204
+ same API.
205
+
206
+ - **Script.** `app/layout.tsx` loads it only for a shop with a bot (above). It defines
207
+ `window.Telegram.WebApp` and writes `--tg-theme-*` / `--tg-viewport-*` / `--tg-safe-area-*` CSS
208
+ variables on `<html>`.
209
+ - **Shell.** `components/telegram-shell.tsx` (`'use client'`, renders nothing) holds every
210
+ app-wide Telegram behaviour:
211
+ - `useTelegramWebApp({ documentAttributes: true })` marks `<html>` with `data-telegram` and
212
+ `data-color-scheme`;
213
+ - `useTelegramBackButton(pathname !== '/', onBack)` shows the header's back arrow off the home
214
+ page. `onBack` calls `router.back()` while there is app history behind the page, and
215
+ `router.push('/')` on the history entry the Mini App opened on (a deep link from the bot, the
216
+ return from a payment page), where the history before it is not the app's. The entry is told by
217
+ its Navigation API history index, so revisiting the first page through a link still goes back;
218
+ without that API, by its path;
219
+ - `useTelegramSignIn()` signs the buyer in with the launch data — the **only** automatic sign-in
220
+ in the app. A failure keeps them browsing as a guest; `/account` shows it (the hook's state is
221
+ shared). A referral in the `startapp` parameter is read by the server: pass no `referralCode`
222
+ and never parse `start_param`.
223
+ - **Theme.** `app/globals.css` maps Telegram's colors onto the starter's tokens under
224
+ `html[data-telegram]` (`--bg`, `--fg`, `--muted`, `--surface`, `--placeholder`, `--accent`,
225
+ `--on-accent`, `--danger`), with the web values as fallbacks, and darkens `--line` for
226
+ `data-color-scheme="dark"`. The merchant's own accent (`brandStyle`, inline on `<html>`) still
227
+ wins over Telegram's button color. The merchant's `--nav-*` and `--label-*` brand tokens are not
228
+ overridden in Telegram's dark theme either: brand colors are the merchant's choice.
229
+ - **Viewport.** `body` takes `--tg-viewport-stable-height` (else `100dvh`, never `100vh`) and the
230
+ safe-area insets; tap targets are at least 44px.
231
+ - **MainButton.** `useTelegramMainButton` exists in the SDK; the starter does not use it.
232
+ - **Credentials.** `initData`, tokens and the cart id never go into a URL, a log or analytics.
233
+ - **Check by hand** with the shop's bot: opened from the bot → signed in, back arrow off the home
234
+ page, Telegram's colors in light and dark.
235
+
159
236
  ## Blog — `app/blog/`, `components/article-body.tsx`, `components/blog-listing.tsx`
160
237
 
161
238
  All blog reads are `magicstore:pages`. Rules: the backend's `docs/api/v2/blog.md`.
@@ -3,7 +3,8 @@
3
3
  A Next.js (App Router) storefront on the MagicStore Storefront API v2, built only on the public SDK
4
4
  (`@magicstoreai/storefront-client`, `@magicstoreai/hydrogen`). The home page from the merchant's
5
5
  sections (or your own composition: `GET /home` is optional), 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.
6
+ payment), OTP sign-in and orders, SEO and JSON-LD, a webhook that revalidates cached pages — and it
7
+ runs as a Telegram Mini App (sign-in from the bot, back button, Telegram's theme, phone sharing).
7
8
 
8
9
  ```bash
9
10
  cp .env.example .env.local # set MAGICSTORE_SHOP_DOMAIN
@@ -19,13 +20,24 @@ npm run dev
19
20
  | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist and analytics in the browser. |
20
21
  | `app/storefront-api/[...path]` | Same-origin proxy for browser calls when there is no public storefront token. |
21
22
  | `app/api/magicstore/webhook` | Verifies the platform's webhook and revalidates the tags its topic covers. |
23
+ | `app/api/magicstore/preview` | The admin's theme preview: verifies its token, turns on draft mode. |
22
24
 
23
25
  **Browser calls.** With `NEXT_PUBLIC_MAGICSTORE_STOREFRONT_TOKEN` (a token that lists this site's
24
26
  origin) the browser calls the shop's API directly. Without it, calls go through the proxy route,
25
27
  which forwards the buyer's address in `X-Forwarded-For`.
26
28
 
29
+ **Telegram Mini App.** For a shop with a connected bot, set the bot's Mini App URL in BotFather to
30
+ this site's `https://` address. The layout loads Telegram's script only for such a shop;
31
+ `components/telegram-shell.tsx` signs the buyer in, shows the back arrow and applies Telegram's
32
+ theme. Details: `PAGES.md`, "Running as a Telegram Mini App".
33
+
27
34
  **Offline.** `MAGICSTORE_API_URL=http://127.0.0.1:4010` with `pnpm mock` running in the SDK repository
28
35
  serves every page from the spec's example data.
29
36
 
30
37
  **Webhook.** In Shop settings → Storefront API, set the token's webhook to
31
38
  `https://<this site>/api/magicstore/webhook` and put its secret in `MAGICSTORE_WEBHOOK_SECRET`.
39
+ The same secret signs the theme editor's preview links: the admin opens
40
+ `https://<this site>/api/magicstore/preview?token=…&path=/`, which turns on draft mode for ten minutes
41
+ and serves the draft theme to that browser only (`lib/preview.ts`); `?exit=1` ends it. Previews also need
42
+ `MAGICSTORE_STOREFRONT_TOKEN`, the storefront token that webhook belongs to: the API previews only a
43
+ read that names its token.
@@ -4,3 +4,5 @@ node_modules/
4
4
  next-env.d.ts
5
5
  *.tsbuildinfo
6
6
  .screenshots/
7
+ .magicstore/
8
+ .tmp/