@shop-on/create-theme 0.4.0 → 0.6.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/package.json +1 -1
- package/template/.claude/skills/theme-dev/SKILL.md +12 -0
- package/template/.claude/skills/theme-dev/recipes/dynamic-sources.md +16 -7
- package/template/.claude/skills/theme-dev/recipes/quality.md +7 -2
- package/template/README.md +11 -6
- package/template/package.json +12 -11
- package/template/src/ai/guide.md +27 -0
- package/template/src/server/settings.ts +7 -1
package/package.json
CHANGED
|
@@ -64,6 +64,7 @@ src/
|
|
|
64
64
|
config/ settings-*.json theme-wide settings
|
|
65
65
|
components/ template-renderer.tsx core resolver (don't usually touch)
|
|
66
66
|
server/ settings.ts (route server fns), enrich-adapter.ts (fetch-depth policy), template-loader.ts (loader config)
|
|
67
|
+
ai/ guide.md + recipes/*.json what the editor's AI tools read — see below
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
## Core rules — read before authoring
|
|
@@ -264,6 +265,17 @@ A theme change is not finished when it type-checks — the artifact carries a ve
|
|
|
264
265
|
|
|
265
266
|
Write the notes **for the merchant, not the diff**: what changes in their store, in their words. Match the file — plain bullets under the heading, no `### Fixed` subsections, no file names, no component names. "Prices now say when tax is included", not "added `taxIncludedCaption` to the price block".
|
|
266
267
|
|
|
268
|
+
## AI guide + recipes (`src/ai/`)
|
|
269
|
+
|
|
270
|
+
AI assistants connected to a store (Claude, ChatGPT), `shop-on theme mcp` and the theme editor's AI chat edit a theme through one set of editor tools. What they know about _this_ theme comes from `src/ai/`: `get_theme_guide` serves `guide.md`, `list_recipes` / `apply_recipe` serve `recipes/<handle>.json`.
|
|
271
|
+
|
|
272
|
+
- **`guide.md`** — a designer's brief for the model, plain Markdown, ≤ 16 KB: what the theme is for, which section does which job, the settings that matter, what to avoid. Section, block and setting handles in backticks.
|
|
273
|
+
- **`recipes/<handle>.json`** — named page builds, ≤ 20, each ≤ 32 KB, handle `[a-z0-9-]`. Shape: `{ "name", "description", "params": { "<id>": { "type": "text" | "collection" | "image" | …, "label", "required" } }, "position": "append" | "prepend" | "replace", "sections": [{ "type", "settings", "blocks" }] }`; a param is used as `"{{id}}"` inside a setting value.
|
|
274
|
+
- **The rule is strict:** `ai/` holds exactly `guide.md` and `recipes/*.json`. No other file, subdir or dotfile — the build and the upload both refuse it.
|
|
275
|
+
- **Recipes are checked at build — but only shallowly.** Every section/block type and setting id must exist, or `pnpm build` fails. Setting values are checked only when a merchant applies the recipe: a select value outside its options, an out-of-range number or a `palette:` ref the shop no longer has makes `apply_recipe` refuse the whole recipe. Keep literal values to the schema's options; leave colours to the theme's defaults.
|
|
276
|
+
- **Optional params drop their line.** The editor tools store a rich-text value with nothing to show as `""` — `<p>{{tagline}}</p>` or `<a href="tel:{{phone}}">{{phone}}</a>` with the param omitted, or `<p> </p>` — so a theme that hides empty rich text drops it. A plain `text` setting, a button label or an FAQ row's question is not emptied for you: make that param required, or check the block renders nothing when it is blank.
|
|
277
|
+
- **Keep it in step with the sections.** Adding, renaming or removing a section or block, or changing a setting the guide names, means editing `guide.md` and any recipe that uses it in the same change.
|
|
278
|
+
|
|
267
279
|
## Building in the editor (Save to source)
|
|
268
280
|
|
|
269
281
|
You can also build a theme **visually in the real admin theme-editor** and persist the result to `src/`, instead of hand-editing JSON: `pnpm theme:editor` runs the theme's dev server + a dev-host gateway (`@shop-on/theme-dev-host`) that serves the editor API from `src/` with drafts held in memory.
|
|
@@ -130,9 +130,9 @@ templates only; `null` elsewhere blanks it. Each key has one form:
|
|
|
130
130
|
stripped, `data-dyn=` attributes removed) so the last inline pass can't turn
|
|
131
131
|
description content into a live token.
|
|
132
132
|
|
|
133
|
-
The theme supplies `collection:
|
|
134
|
-
`DynamicSourceCollection
|
|
135
|
-
`
|
|
133
|
+
The theme supplies `collection: routeCollectionSource(meta, locale)` (a
|
|
134
|
+
`DynamicSourceCollection`, from `@shop-on/theme-server/collection`, off the
|
|
135
|
+
`fetchCollectionMeta` read); the provider runs only when
|
|
136
136
|
`collectCollectionAttributeBindings(template)` includes `description`, so the
|
|
137
137
|
lean meta read stays lean.
|
|
138
138
|
|
|
@@ -145,8 +145,16 @@ only**, in text/textarea/richtext like a product attribute.
|
|
|
145
145
|
There is no whole-value page source and no body attribute: the page body is
|
|
146
146
|
rendered from the route page by the theme's own section or block (the starter's
|
|
147
147
|
`main-page`), so a binding would be a second path to the same HTML. The theme
|
|
148
|
-
supplies `page:
|
|
149
|
-
makes, so the
|
|
148
|
+
supplies `page: routePageSource(page, locale)` (`@shop-on/theme-server/page`)
|
|
149
|
+
off the `fetchPage` read its page loader already makes, so the title costs no
|
|
150
|
+
extra request.
|
|
151
|
+
|
|
152
|
+
Always pass a route resource through its builder, never an inline
|
|
153
|
+
`{ title }`: the builder also carries the lazy `getMetafields` provider behind
|
|
154
|
+
the `collection_metafield` / `page_metafield` / `blog_metafield` /
|
|
155
|
+
`article_metafield` sources (a merchant's own fields on that resource, same
|
|
156
|
+
grammar and sinks as `product_metafield`). An inline object type-checks and
|
|
157
|
+
silently renders every such binding's literal.
|
|
150
158
|
|
|
151
159
|
### Blog and article attributes
|
|
152
160
|
|
|
@@ -156,8 +164,9 @@ blanks everywhere else. Same shape as the page source: one key, `title`,
|
|
|
156
164
|
inline token only — the body, the cover and the post list are rendered by the
|
|
157
165
|
theme's own blocks and sections.
|
|
158
166
|
|
|
159
|
-
Pass them through the theme's `TemplateRequest` as
|
|
160
|
-
`
|
|
167
|
+
Pass them through the theme's `TemplateRequest` as
|
|
168
|
+
`article: routeArticleSource(article, locale)` (off the `fetchArticle` read)
|
|
169
|
+
and `blog: routeBlogSource(blog, locale)` (both `@shop-on/theme-server/blog`). **Enrich after the resource
|
|
161
170
|
resolves**: a loader that runs `loadTemplate` (load + enrich) in a
|
|
162
171
|
`Promise.all` beside the fetch resolves the token against `null`. Split the load
|
|
163
172
|
into a raw read and an `enrich` call (a `loadRawTemplate` helper, say). When the
|
|
@@ -16,14 +16,15 @@ it will be this file's **review** lines.
|
|
|
16
16
|
## Run the tools
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
pnpm theme:check # every check rule; exit 1 on any finding
|
|
19
|
+
pnpm theme:check # every check rule; exit 1 on any finding but a warning
|
|
20
20
|
pnpm theme:check --release # also theme-info: run before you hand the theme off
|
|
21
21
|
pnpm theme:check --json # for CI
|
|
22
22
|
pnpm build # postbuild enforces the build lines
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
The output is one line per finding, `file:line rule message`, followed by the
|
|
26
|
-
recipe for each rule that fired.
|
|
26
|
+
recipe for each rule that fired. A `(warning)` after the rule id prints but
|
|
27
|
+
doesn't fail the run.
|
|
27
28
|
|
|
28
29
|
The **audit** lines come from `shop-on theme audit`, in the `@shop-on/theme-cli`
|
|
29
30
|
package:
|
|
@@ -96,12 +97,16 @@ anyone else installs the theme.
|
|
|
96
97
|
| no default for `collection`, `product`, `product_list`, `metaobject(_list)`, `review(_list)` pickers | check: `tenant-picker-default` | [picker-data.md](picker-data.md#no-defaults-for-tenant-specific-pickers) |
|
|
97
98
|
| no literal colors in class names; tokens only | check: `literal-color` | [styling.md](styling.md#rule-tokens-never-literals) |
|
|
98
99
|
| no raw `<a href="/…">` to an internal path | check: `raw-internal-link` | [SKILL.md § Don't do](../SKILL.md#dont-do) |
|
|
100
|
+
| each `closest_*` resource has one unambiguous provider | check: `closest-provider` (warning) | [dynamic-sources.md](dynamic-sources.md#closest-resources--closest_) |
|
|
99
101
|
| a section with nothing picked still looks intentional | review | [section.md](section.md#preview-readiness) |
|
|
100
102
|
| the layout holds at 375px | review: the editor's mobile preview | [styling.md](styling.md#responsive) |
|
|
101
103
|
| settings read the way a merchant would name them | review | [input-settings.md](input-settings.md) |
|
|
102
104
|
|
|
103
105
|
`schema-label-key` skips labels with nothing to translate (`16px`, `/`).
|
|
104
106
|
`tenant-picker-default` also catches a preset that fills a tenant picker.
|
|
107
|
+
`closest-provider` warns on a second picker of one resource with no
|
|
108
|
+
`provides` for it (only the first provides), one static block in two `each`
|
|
109
|
+
lists, and a `provides` whose `from` or `each` names nothing on the schema.
|
|
105
110
|
|
|
106
111
|
## Suppressing a finding
|
|
107
112
|
|
package/template/README.md
CHANGED
|
@@ -20,8 +20,8 @@ never commit it. A store holds at most 5 of these credentials;
|
|
|
20
20
|
`npx @shop-on/theme-cli theme credentials revoke <client-id>` frees one.
|
|
21
21
|
|
|
22
22
|
`theme dev` runs the theme behind a `cloudflared` tunnel (install it first)
|
|
23
|
-
and opens the hosted theme editor on a developer preview of it
|
|
24
|
-
|
|
23
|
+
and opens the hosted theme editor on a developer preview of it, with nothing
|
|
24
|
+
to confirm. Keep admin on the same store. `pnpm dev` still runs the
|
|
25
25
|
storefront on its own.
|
|
26
26
|
|
|
27
27
|
Without logging in, scaffold with `npm create` and fill in `.env` yourself:
|
|
@@ -49,10 +49,15 @@ with a "missing" message naming the variable.
|
|
|
49
49
|
| `pnpm theme:check` | the quality bar's static rules; `--release` before handing off |
|
|
50
50
|
| `pnpm build` | the theme artifact in `.output/`, with `release-notes.md` alongside it |
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
To sell the theme on the Lite theme store, register at the developer portal
|
|
53
|
+
and submit its source: `npx @shop-on/theme-cli theme source` writes the
|
|
54
|
+
tarball to upload. Lite builds it and reviews the result; see the developer
|
|
55
|
+
portal guide.
|
|
56
|
+
|
|
57
|
+
`npx @shop-on/theme-cli theme dev` runs the theme behind the gateway and a
|
|
58
|
+
tunnel, and opens the hosted admin's theme editor on your local theme. It makes
|
|
59
|
+
a dev install in your store for this, the only kind of install the editor
|
|
60
|
+
connects to a local theme.
|
|
56
61
|
|
|
57
62
|
## Layout
|
|
58
63
|
|
package/template/package.json
CHANGED
|
@@ -10,21 +10,21 @@
|
|
|
10
10
|
"dev": "vite dev",
|
|
11
11
|
"prebuild": "node scripts/backfill-defaults.mjs && oxfmt src",
|
|
12
12
|
"build": "NITRO_PRESET=cloudflare_module vite build",
|
|
13
|
-
"postbuild": "node scripts/postbuild.mjs",
|
|
13
|
+
"postbuild": "node --import tsx scripts/postbuild.mjs",
|
|
14
14
|
"test": "vitest run --passWithNoTests",
|
|
15
15
|
"theme:check": "node scripts/theme-check.mjs",
|
|
16
16
|
"theme:editor": "theme-dev-host"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@bprogress/core": "1.3.4",
|
|
20
|
-
"@shop-on/i18n": "^0.
|
|
21
|
-
"@shop-on/theme-contract": "^0.
|
|
22
|
-
"@shop-on/theme-loader": "^0.
|
|
23
|
-
"@shop-on/theme-react": "^0.
|
|
24
|
-
"@shop-on/theme-routes": "^0.
|
|
25
|
-
"@shop-on/theme-runtime": "^0.
|
|
26
|
-
"@shop-on/theme-server": "^0.
|
|
27
|
-
"@shop-on/theme-system": "^0.
|
|
20
|
+
"@shop-on/i18n": "^0.6.0",
|
|
21
|
+
"@shop-on/theme-contract": "^0.6.0",
|
|
22
|
+
"@shop-on/theme-loader": "^0.6.0",
|
|
23
|
+
"@shop-on/theme-react": "^0.6.0",
|
|
24
|
+
"@shop-on/theme-routes": "^0.6.0",
|
|
25
|
+
"@shop-on/theme-runtime": "^0.6.0",
|
|
26
|
+
"@shop-on/theme-server": "^0.6.0",
|
|
27
|
+
"@shop-on/theme-system": "^0.6.0",
|
|
28
28
|
"@tailwindcss/vite": "4.3.3",
|
|
29
29
|
"@tanstack/react-router": "1.170.45",
|
|
30
30
|
"@tanstack/react-start": "1.169.3",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"tailwindcss": "4.3.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@shop-on/theme-config": "^0.
|
|
39
|
-
"@shop-on/theme-dev-host": "^0.
|
|
38
|
+
"@shop-on/theme-config": "^0.6.0",
|
|
39
|
+
"@shop-on/theme-dev-host": "^0.6.0",
|
|
40
40
|
"@spiriit/vite-plugin-svg-spritemap": "7.2.0",
|
|
41
41
|
"@types/node": "26.6.5",
|
|
42
42
|
"@types/react": "19.3.0",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"gql.tada": "1.11.4",
|
|
46
46
|
"happy-dom": "21.0.4",
|
|
47
47
|
"oxfmt": "^0.74.0",
|
|
48
|
+
"tsx": "^4.23.15",
|
|
48
49
|
"typescript": "7.0.2",
|
|
49
50
|
"vite": "8.3.5",
|
|
50
51
|
"vitest": "5.0.5"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Starter: the designer's guide
|
|
2
|
+
|
|
3
|
+
Starter is a new theme in a neutral light palette. Rewrite this guide
|
|
4
|
+
as the theme grows: say what it is for, which section does which job, and what
|
|
5
|
+
to avoid. The editor's AI tools read this file, so a section missing from it is
|
|
6
|
+
one they won't reach for.
|
|
7
|
+
|
|
8
|
+
## How a page is put together
|
|
9
|
+
|
|
10
|
+
- Templates: `index` (home), `product`, `collection`, `page`, `blog`,
|
|
11
|
+
`article`, `cart`, `search`, `list-collections`, `404`, `password`,
|
|
12
|
+
`policy`. Every template except `404` has its own `main-*` section, fixed to
|
|
13
|
+
it; `404` starts empty.
|
|
14
|
+
- Groups: `header` holds the `header` section and `footer` holds the `footer`
|
|
15
|
+
section. They can be edited but not added to a page.
|
|
16
|
+
|
|
17
|
+
## Sections by job
|
|
18
|
+
|
|
19
|
+
**Open a page (hero)**
|
|
20
|
+
|
|
21
|
+
- `hero`: an eyebrow line, a headline, body text and one button
|
|
22
|
+
(`cta_label`, `cta_url`). Keep the headline short; leave the button out
|
|
23
|
+
rather than pointing it nowhere.
|
|
24
|
+
|
|
25
|
+
## What to avoid
|
|
26
|
+
|
|
27
|
+
- Don't stack two `hero` sections.
|
|
@@ -107,7 +107,13 @@ export const getThemeSettingsFn = createServerFn({ method: "GET" })
|
|
|
107
107
|
// return `null` when the handle misses so the route `notFound()`s. A resource
|
|
108
108
|
// whose template is merchant-assignable (product / collection / page) should
|
|
109
109
|
// load through the package's `resolveTemplateVariant` instead of `loadTemplate`,
|
|
110
|
-
// so an unshipped `product.<variant>` falls back to the base document.
|
|
110
|
+
// so an unshipped `product.<variant>` falls back to the base document. A
|
|
111
|
+
// template's dynamic sources read its route resource: pass
|
|
112
|
+
// `collection: routeCollectionSource(meta, locale)`, `page: routePageSource(page,
|
|
113
|
+
// locale)`, `blog: routeBlogSource(blog, locale)` and `article:
|
|
114
|
+
// routeArticleSource(article, locale)` (theme-server's `collection`, `page` and
|
|
115
|
+
// `blog` subpaths) rather than building the object inline, so the theme picks
|
|
116
|
+
// up every provider (descriptions, metafields) on upgrade.
|
|
111
117
|
//
|
|
112
118
|
// biome-ignore lint/suspicious/noExplicitAny: serializer requires it
|
|
113
119
|
type TemplatePayload = Promise<any>;
|