@web-my-money/studio-consumer 2.2.0 → 2.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.
package/README.md CHANGED
@@ -1,88 +1,111 @@
1
- # @web-my-money/studio-consumer
2
-
3
- Consumer-side integration for WMM Studio: content, analytics, attribution.
4
-
5
- This is a source-only workspace package — no build step, no `dist`. Consumers
6
- are Next.js apps that already transpile workspace and scoped packages, so
7
- publishing compiled output here would only add a compile-and-publish cycle
8
- for no gain.
9
-
10
- ## Entry points
11
-
12
- - `@web-my-money/studio-consumer/analytics`
13
- - `@web-my-money/studio-consumer/content`
14
- - `@web-my-money/studio-consumer/attribution`
15
- - `@web-my-money/studio-consumer/headers`
16
- - `@web-my-money/studio-consumer/brand` (2.0.0)
17
- - `@web-my-money/studio-consumer/image` (2.1.0)
18
-
19
- Each entry point exports `PACKAGE_VERSION`, to prove at runtime that a consumer
20
- is resolving this package rather than a stale copy.
21
-
22
- ## Brand (2.0.0, breaking)
23
-
24
- A site declares its brand theme in code. Every invalid input throws, and this
25
- runs while the root layout renders, so a site with no brand, a malformed theme
26
- id, or WMM's own theme fails its build instead of silently inheriting WMM's
27
- palette.
28
-
29
- ```ts
30
- // lib/brand.ts — its own module: Next refuses unknown named exports from a layout file
31
- import { defineSiteBrand } from "@web-my-money/studio-consumer/brand";
32
-
33
- export const brand = defineSiteBrand({ theme: "acme", style: "flat" });
34
- ```
35
-
36
- ```tsx
37
- // app/layout.tsx
38
- import { brandRootAttributes } from "@web-my-money/studio-consumer/brand";
39
- import { brand } from "@/lib/brand";
40
-
41
- export default function RootLayout({ children }: { children: React.ReactNode }) {
42
- return <html lang="en" {...brandRootAttributes(brand)}><body>{children}</body></html>;
43
- }
44
- ```
45
-
46
- ```ts
47
- // app/api/content/manifest/route.ts
48
- import { brand } from "@/lib/brand";
49
-
50
- export const GET = createManifestHandler(getManifest, brand);
51
- export const dynamic = "force-dynamic";
52
- ```
53
-
54
- `createManifestHandler` now REQUIRES the brand (the breaking change in 2.0.0), so
55
- Studio records the theme the site ships. Only a WMM site may use a WMM theme
56
- (`wmm`, `site`, `light`, `dark`), by passing `wmmSite: true`.
57
-
58
- ## Image focal point (2.1.0)
59
-
60
- An image value edited in Studio can carry `focal: { x, y }` (0–1 from the
61
- top-left): where the subject is. Apply it so responsive crops keep the subject
62
- in frame. No focal point, or a malformed one, reads as the centre, which is the
63
- browser default, so existing images render exactly as before.
64
-
65
- ```tsx
66
- import { focalObjectPosition } from "@web-my-money/studio-consumer/image";
67
-
68
- <img src={value.url} alt={value.alt.en} style={{ objectFit: "cover", objectPosition: focalObjectPosition(value) }} />
69
- ```
70
-
71
- ## Analytics collector (2.2.0)
72
-
73
- `createCollectHandler` only accepts posts from the site's own pages (`Sec-Fetch-Site`,
74
- falling back to `Origin`), and it decides each event's A/B arm on the server from the
75
- `wmm_vid` cookie. The browser's own `variant` is always discarded. Pass the content
76
- client's `getRunningExperiments` so the arm can be resolved:
77
-
78
- ```ts
79
- export const POST = createCollectHandler({
80
- siteKey: SITE_KEY,
81
- ingestUrl: process.env.FUNNEL_INGEST_URL ?? "",
82
- ingestKey: process.env.FUNNEL_INGEST_TOKEN ?? "",
83
- getRunningExperiments: () => content.getRunningExperiments(),
84
- });
85
- ```
86
-
87
- Without it, events carry no arm and the site stays out of any A/B comparison.
88
-
1
+ # @web-my-money/studio-consumer
2
+
3
+ Consumer-side integration for WMM Studio: content, analytics, attribution.
4
+
5
+ This is a source-only workspace package — no build step, no `dist`. Consumers
6
+ are Next.js apps that already transpile workspace and scoped packages, so
7
+ publishing compiled output here would only add a compile-and-publish cycle
8
+ for no gain.
9
+
10
+ ## Connect a site: one command (2.3.0; new sites 2.4.0)
11
+
12
+ In Studio, open the site, Site settings, **Get the setup command**, then in the
13
+ app's folder (or, for a brand-new site, in an empty folder):
14
+
15
+ ```bash
16
+ npx @web-my-money/studio-consumer init <code>
17
+ ```
18
+
19
+ It checks the machine, installs this package, writes the routes, wraps
20
+ `next.config` with `withStudio`, links Vercel and sets the env vars, opens Claude
21
+ Code (the `onboard-site` skill shipped here) to wire the layout and choose the
22
+ editable text, verifies, opens a pull request, waits for the deploy and asks
23
+ Studio to sync. Rerun it after any stop; it resumes. `init --explain` shows the
24
+ plan without changing anything.
25
+
26
+ In an empty folder (2.4.0) it first creates the client's private repository from
27
+ `Web-My-Money/wmm-site-template`, pulls it in and puts the site's key and name in
28
+ place of the template's markers; the Claude step then sets the client's colours
29
+ and first copy instead of wiring a layout.
30
+
31
+ ## Entry points
32
+
33
+ - `@web-my-money/studio-consumer/analytics`
34
+ - `@web-my-money/studio-consumer/content`
35
+ - `@web-my-money/studio-consumer/attribution`
36
+ - `@web-my-money/studio-consumer/headers`
37
+ - `@web-my-money/studio-consumer/brand` (2.0.0)
38
+ - `@web-my-money/studio-consumer/image` (2.1.0)
39
+ - `@web-my-money/studio-consumer/next` (2.3.0): `withStudio(nextConfig)`
40
+
41
+ Each entry point exports `PACKAGE_VERSION`, to prove at runtime that a consumer
42
+ is resolving this package rather than a stale copy.
43
+
44
+ ## Brand (2.0.0, breaking)
45
+
46
+ A site declares its brand theme in code. Every invalid input throws, and this
47
+ runs while the root layout renders, so a site with no brand, a malformed theme
48
+ id, or WMM's own theme fails its build instead of silently inheriting WMM's
49
+ palette.
50
+
51
+ ```ts
52
+ // lib/brand.ts — its own module: Next refuses unknown named exports from a layout file
53
+ import { defineSiteBrand } from "@web-my-money/studio-consumer/brand";
54
+
55
+ export const brand = defineSiteBrand({ theme: "acme", style: "flat" });
56
+ ```
57
+
58
+ ```tsx
59
+ // app/layout.tsx
60
+ import { brandRootAttributes } from "@web-my-money/studio-consumer/brand";
61
+ import { brand } from "@/lib/brand";
62
+
63
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
64
+ return <html lang="en" {...brandRootAttributes(brand)}><body>{children}</body></html>;
65
+ }
66
+ ```
67
+
68
+ ```ts
69
+ // app/api/content/manifest/route.ts
70
+ import { brand } from "@/lib/brand";
71
+
72
+ export const GET = createManifestHandler(getManifest, brand);
73
+ // No `export const dynamic` (2.3.0+): the handler waits for a real request, so the
74
+ // manifest is always live, and cacheComponents apps refuse that export anyway.
75
+ ```
76
+
77
+ `createManifestHandler` now REQUIRES the brand (the breaking change in 2.0.0), so
78
+ Studio records the theme the site ships. Only a WMM site may use a WMM theme
79
+ (`wmm`, `site`, `light`, `dark`), by passing `wmmSite: true`.
80
+
81
+ ## Image focal point (2.1.0)
82
+
83
+ An image value edited in Studio can carry `focal: { x, y }` (0–1 from the
84
+ top-left): where the subject is. Apply it so responsive crops keep the subject
85
+ in frame. No focal point, or a malformed one, reads as the centre, which is the
86
+ browser default, so existing images render exactly as before.
87
+
88
+ ```tsx
89
+ import { focalObjectPosition } from "@web-my-money/studio-consumer/image";
90
+
91
+ <img src={value.url} alt={value.alt.en} style={{ objectFit: "cover", objectPosition: focalObjectPosition(value) }} />
92
+ ```
93
+
94
+ ## Analytics collector (2.2.0)
95
+
96
+ `createCollectHandler` only accepts posts from the site's own pages (`Sec-Fetch-Site`,
97
+ falling back to `Origin`), and it decides each event's A/B arm on the server from the
98
+ `wmm_vid` cookie. The browser's own `variant` is always discarded. Pass the content
99
+ client's `getRunningExperiments` so the arm can be resolved:
100
+
101
+ ```ts
102
+ export const POST = createCollectHandler({
103
+ siteKey: SITE_KEY,
104
+ ingestUrl: process.env.FUNNEL_INGEST_URL ?? "",
105
+ ingestKey: process.env.FUNNEL_INGEST_TOKEN ?? "",
106
+ getRunningExperiments: () => content.getRunningExperiments(),
107
+ });
108
+ ```
109
+
110
+ Without it, events carry no arm and the site stays out of any A/B comparison.
111
+
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+
4
+ /**
5
+ * `npx @web-my-money/studio-consumer <command>`. Plain ESM: Node will not strip
6
+ * TypeScript inside node_modules, so nothing on this path may be `.ts`.
7
+ */
8
+
9
+ const USAGE = `Usage:
10
+ npx @web-my-money/studio-consumer init <code> Connect this app to WMM Studio, step by step.
11
+ npx @web-my-money/studio-consumer init Continue where the last run stopped.
12
+ npx @web-my-money/studio-consumer init --explain Show every step without changing anything.
13
+
14
+ Get <code> in Studio: open the site, Site settings, Get the setup command.`;
15
+
16
+ const [command, ...rest] = process.argv.slice(2);
17
+
18
+ if (!command || command === "--help" || command === "-h" || command === "help") {
19
+ process.stdout.write(`${USAGE}\n`);
20
+ process.exit(0);
21
+ }
22
+
23
+ if (command !== "init") {
24
+ process.stderr.write(`Unknown command "${command}".\n\n${USAGE}\n`);
25
+ process.exit(1);
26
+ }
27
+
28
+ const { runInit } = await import("../cli/init.mjs");
29
+ process.exitCode = await runInit(rest);
@@ -0,0 +1,48 @@
1
+ // @ts-check
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ /**
7
+ * Step 8 of `init`: the edits that need judgement on someone else's code (root
8
+ * layout, proxy/middleware, which text is editable) are done by Claude Code, on
9
+ * the developer's own subscription, through the `onboard-site` skill this
10
+ * package ships. Each edit is shown and approved before it is written.
11
+ */
12
+
13
+ /** @typedef {import("./run.mjs").Runner} Runner */
14
+ /** @typedef {import("./ui.mjs").Ui} Ui */
15
+
16
+ const SKILL_SOURCE = fileURLToPath(new URL("../skills/onboard-site/SKILL.md", import.meta.url));
17
+
18
+ /**
19
+ * @param {{ cwd: string, run: Runner, ui: Ui }} ctx
20
+ */
21
+ export async function claudeStep({ cwd, run, ui }) {
22
+ const target = path.join(cwd, ".claude", "skills", "onboard-site", "SKILL.md");
23
+ const source = readFileSync(SKILL_SOURCE, "utf8");
24
+ if (!existsSync(target) || readFileSync(target, "utf8") !== source) {
25
+ mkdirSync(path.dirname(target), { recursive: true });
26
+ copyFileSync(SKILL_SOURCE, target);
27
+ }
28
+
29
+ const version = await run("claude", ["--version"], { cwd });
30
+ if (version.code !== 0) {
31
+ ui.stop(
32
+ "This step needs Claude Code, to wire your layout and choose the editable text with you. Install it, then run this command again; it continues from here.",
33
+ "Install Claude Code from https://claude.com/claude-code",
34
+ );
35
+ }
36
+
37
+ ui.info("Opening Claude Code. It will show each change and wait for your yes.");
38
+ ui.info("When it says it is done, type /exit and this command carries on.");
39
+ if (ui.explain) return;
40
+ // Claude Code owns the terminal while it runs: the setup stops reading input,
41
+ // or keystrokes would be split between the two and replayed later as answers.
42
+ ui.pause();
43
+ try {
44
+ await run("claude", ["/onboard-site"], { cwd, inherit: true });
45
+ } finally {
46
+ ui.resume();
47
+ }
48
+ }