@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 +111 -88
- package/bin/studio-consumer.mjs +29 -0
- package/cli/claude-step.mjs +48 -0
- package/cli/init.mjs +489 -0
- package/cli/preflight.mjs +103 -0
- package/cli/provisioner.mjs +103 -0
- package/cli/run.mjs +89 -0
- package/cli/scaffold.mjs +155 -0
- package/cli/state.mjs +49 -0
- package/cli/studio-api.mjs +127 -0
- package/cli/templates.mjs +134 -0
- package/cli/ui.mjs +126 -0
- package/cli/writers.mjs +148 -0
- package/package.json +33 -29
- package/skills/onboard-site/SKILL.md +125 -0
- package/src/analytics/index.ts +37 -37
- package/src/attribution/index.ts +14 -14
- package/src/brand/index.ts +66 -66
- package/src/content/index.ts +27 -27
- package/src/content/manifest-handler.ts +11 -9
- package/src/image/index.ts +14 -14
- package/src/next/index.d.mts +13 -0
- package/src/next/index.mjs +103 -0
package/cli/ui.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { Writable } from "node:stream";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How the onboarding command talks to a developer who has never done this
|
|
7
|
+
* (spec 2026-10-09 §3.0): numbered steps with one plain sentence each, a
|
|
8
|
+
* recommended default on every question, and every stop ending in the exact
|
|
9
|
+
* next command. `explain` mode answers no to everything and asks nothing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class StopError extends Error {
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} message
|
|
15
|
+
* @param {string} [nextCommand]
|
|
16
|
+
*/
|
|
17
|
+
constructor(message, nextCommand) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "StopError";
|
|
20
|
+
this.nextCommand = nextCommand;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ input: NodeJS.ReadableStream, output: NodeJS.WritableStream, explain?: boolean }} io
|
|
26
|
+
*/
|
|
27
|
+
export function createUi({ input, output, explain = false }) {
|
|
28
|
+
/** @type {string[]} */
|
|
29
|
+
const queued = [];
|
|
30
|
+
/** @type {((line: string) => void)[]} */
|
|
31
|
+
const waiting = [];
|
|
32
|
+
// Lines are read without echo from readline; a hidden answer is simply not
|
|
33
|
+
// written back. Interactive terminals echo typed characters themselves, so
|
|
34
|
+
// askSecret also turns raw mode on when it can.
|
|
35
|
+
const sink = new Writable({
|
|
36
|
+
write(_chunk, _enc, cb) {
|
|
37
|
+
cb();
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const rl = createInterface({ input, output: sink, terminal: false });
|
|
41
|
+
rl.on("line", (line) => {
|
|
42
|
+
const next = waiting.shift();
|
|
43
|
+
if (next) next(line);
|
|
44
|
+
else queued.push(line);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** @returns {Promise<string>} */
|
|
48
|
+
const readLine = () =>
|
|
49
|
+
new Promise((resolve) => {
|
|
50
|
+
const line = queued.shift();
|
|
51
|
+
if (line !== undefined) resolve(line);
|
|
52
|
+
else waiting.push(resolve);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** @param {string} s */
|
|
56
|
+
const say = (s) => output.write(`${s}\n`);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
explain,
|
|
60
|
+
/** @param {number} n @param {number} total @param {string} title @param {string} why */
|
|
61
|
+
step(n, total, title, why) {
|
|
62
|
+
say("");
|
|
63
|
+
say(`Step ${n} of ${total}: ${title}`);
|
|
64
|
+
say(` ${why}`);
|
|
65
|
+
},
|
|
66
|
+
/** @param {string} s */
|
|
67
|
+
info(s) {
|
|
68
|
+
say(` ${s}`);
|
|
69
|
+
},
|
|
70
|
+
/** @param {string} question @param {string} def */
|
|
71
|
+
async ask(question, def) {
|
|
72
|
+
if (explain) return def;
|
|
73
|
+
output.write(` ${question} (${def}): `);
|
|
74
|
+
const answer = (await readLine()).trim();
|
|
75
|
+
return answer === "" ? def : answer;
|
|
76
|
+
},
|
|
77
|
+
/** @param {string} question */
|
|
78
|
+
async askSecret(question) {
|
|
79
|
+
output.write(` ${question} (it will not show as you paste): `);
|
|
80
|
+
const tty = /** @type {NodeJS.ReadStream} */ (input);
|
|
81
|
+
const canHide = typeof tty.setRawMode === "function" && tty.isTTY;
|
|
82
|
+
if (canHide) tty.setRawMode(true);
|
|
83
|
+
try {
|
|
84
|
+
const line = await readLine();
|
|
85
|
+
// In raw mode Ctrl+C arrives as a character instead of stopping the
|
|
86
|
+
// program; honour it.
|
|
87
|
+
if (line.includes("\u0003")) {
|
|
88
|
+
throw new StopError("Stopped. Run this again whenever you are ready; it continues from here.");
|
|
89
|
+
}
|
|
90
|
+
return line.trim();
|
|
91
|
+
} finally {
|
|
92
|
+
if (canHide) tty.setRawMode(false);
|
|
93
|
+
output.write("\n");
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
/** @param {string} question @param {boolean} [defYes] */
|
|
97
|
+
async confirm(question, defYes = true) {
|
|
98
|
+
if (explain) {
|
|
99
|
+
say(` Would ask: ${question}`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
output.write(` ${question} ${defYes ? "[Y/n]" : "[y/N]"}: `);
|
|
103
|
+
const a = (await readLine()).trim().toLowerCase();
|
|
104
|
+
if (a === "") return defYes;
|
|
105
|
+
return a === "y" || a === "yes";
|
|
106
|
+
},
|
|
107
|
+
/** @param {string} message @param {string} [nextCommand] @returns {never} */
|
|
108
|
+
stop(message, nextCommand) {
|
|
109
|
+
throw new StopError(message, nextCommand);
|
|
110
|
+
},
|
|
111
|
+
/** Hand the terminal to a child process (Claude Code, the app's checks). */
|
|
112
|
+
pause() {
|
|
113
|
+
rl.pause();
|
|
114
|
+
input.pause();
|
|
115
|
+
},
|
|
116
|
+
/** Take the terminal back after the child exits. */
|
|
117
|
+
resume() {
|
|
118
|
+
rl.resume();
|
|
119
|
+
},
|
|
120
|
+
close() {
|
|
121
|
+
rl.close();
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @typedef {ReturnType<typeof createUi>} Ui */
|
package/cli/writers.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import * as t from "./templates.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The mechanical edits `init` makes (stage 2 plan, Task 4). Every writer is
|
|
8
|
+
* idempotent and never overwrites a file the app already has with different
|
|
9
|
+
* content: it reports it instead, so nothing the developer wrote is lost. The
|
|
10
|
+
* edits that need judgement (root layout, proxy, the content model) are left to
|
|
11
|
+
* the Claude Code step.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{ siteKey: string, theme: string, style: "flat" | "glass" }} SiteValues
|
|
16
|
+
* @typedef {{ written: string[], skipped: { path: string, reason: string }[] }} WriteReport
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @param {string} cwd */
|
|
20
|
+
function codeRoot(cwd) {
|
|
21
|
+
return existsSync(path.join(cwd, "src", "app")) ? "src" : ".";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @param {string} cwd */
|
|
25
|
+
function hasTestRunner(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
28
|
+
const all = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
29
|
+
return "vitest" in all || "jest" in all;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {{ cwd: string, values: SiteValues, modules: string[] }} ctx
|
|
37
|
+
* @returns {Promise<WriteReport>}
|
|
38
|
+
*/
|
|
39
|
+
export async function writeSiteFiles({ cwd, values, modules }) {
|
|
40
|
+
const root = codeRoot(cwd);
|
|
41
|
+
const content = modules.includes("content");
|
|
42
|
+
const analytics = modules.includes("analytics");
|
|
43
|
+
// From app/api/<group>/<name>/route.ts back up to the code root's lib/.
|
|
44
|
+
const toLib = "../../../../lib";
|
|
45
|
+
|
|
46
|
+
/** @type {[string, string, { keepExisting?: boolean }?][]} */
|
|
47
|
+
const files = [];
|
|
48
|
+
const at = (/** @type {string} */ p) => (root === "." ? p : `${root}/${p}`);
|
|
49
|
+
if (content || analytics) files.push([at("lib/studio.ts"), t.studioTs(values)]);
|
|
50
|
+
if (content) {
|
|
51
|
+
files.push([at("lib/brand.ts"), t.brandTs(values)]);
|
|
52
|
+
files.push([at("lib/content-manifest.ts"), t.contentManifestTs(), { keepExisting: true }]);
|
|
53
|
+
files.push([at("lib/content.ts"), t.contentTs()]);
|
|
54
|
+
files.push([at("app/api/content/manifest/route.ts"), t.manifestRouteTs(toLib)]);
|
|
55
|
+
files.push([at("app/api/content/revalidate/route.ts"), t.revalidateRouteTs()]);
|
|
56
|
+
}
|
|
57
|
+
if (analytics) files.push([at("app/api/analytics/collect/route.ts"), t.collectRouteTs(toLib, content)]);
|
|
58
|
+
|
|
59
|
+
/** @type {WriteReport} */
|
|
60
|
+
const report = { written: [], skipped: [] };
|
|
61
|
+
|
|
62
|
+
if (content) {
|
|
63
|
+
if (hasTestRunner(cwd)) files.push(["tests/studio-frame-ancestors.test.ts", t.frameAncestorsTestTs()]);
|
|
64
|
+
else {
|
|
65
|
+
report.skipped.push({
|
|
66
|
+
path: "tests/studio-frame-ancestors.test.ts",
|
|
67
|
+
reason: "this app has no test runner yet; the Claude Code step can add Vitest and this test",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const [rel, body, opts] of files) {
|
|
73
|
+
const abs = path.join(cwd, rel);
|
|
74
|
+
if (existsSync(abs)) {
|
|
75
|
+
const current = readFileSync(abs, "utf8");
|
|
76
|
+
if (current === body || opts?.keepExisting) continue;
|
|
77
|
+
report.skipped.push({ path: rel, reason: "already exists with different content; merge the Studio version by hand" });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(path.dirname(abs), { recursive: true });
|
|
81
|
+
writeFileSync(abs, body);
|
|
82
|
+
report.written.push(rel);
|
|
83
|
+
}
|
|
84
|
+
return report;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const IMPORT_LINE = 'import { withStudio } from "@web-my-money/studio-consumer/next";';
|
|
88
|
+
const CONFIG_NAMES = ["next.config.ts", "next.config.mjs", "next.config.js"];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wrap the app's next.config with withStudio. Recognises the two shapes nearly
|
|
92
|
+
* every config has (`export default name;` and `export default { … };`); any
|
|
93
|
+
* other shape is left untouched and the exact edit is returned instead.
|
|
94
|
+
*
|
|
95
|
+
* @param {{ cwd: string }} ctx
|
|
96
|
+
* @returns {Promise<"wrapped" | "already" | { manual: string }>}
|
|
97
|
+
*/
|
|
98
|
+
export async function wrapNextConfig({ cwd }) {
|
|
99
|
+
const name = CONFIG_NAMES.find((n) => existsSync(path.join(cwd, n)));
|
|
100
|
+
if (!name) {
|
|
101
|
+
writeFileSync(path.join(cwd, "next.config.mjs"), `${IMPORT_LINE}\n\nexport default withStudio({});\n`);
|
|
102
|
+
return "wrapped";
|
|
103
|
+
}
|
|
104
|
+
const file = path.join(cwd, name);
|
|
105
|
+
const src = readFileSync(file, "utf8");
|
|
106
|
+
if (src.includes("withStudio(")) return "already";
|
|
107
|
+
|
|
108
|
+
const manual = {
|
|
109
|
+
manual:
|
|
110
|
+
`Edit ${name} by hand: add\n ${IMPORT_LINE}\nat the top, and wrap the exported config: export default withStudio(yourConfig);`,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let out = null;
|
|
114
|
+
const named = /^export default ([A-Za-z_$][\w$]*);[ \t]*$/m;
|
|
115
|
+
if (named.test(src)) {
|
|
116
|
+
out = src.replace(named, "export default withStudio($1);");
|
|
117
|
+
} else if (/^export default \{/m.test(src)) {
|
|
118
|
+
const start = src.search(/^export default \{/m);
|
|
119
|
+
const open = start + "export default ".length;
|
|
120
|
+
// Find the brace that closes the exported object. Only wrap when that object
|
|
121
|
+
// is the last thing in the file: a brace inside a string, or code after the
|
|
122
|
+
// export, makes the match uncertain, and a wrong splice breaks the config.
|
|
123
|
+
let depth = 0;
|
|
124
|
+
let close = -1;
|
|
125
|
+
for (let i = open; i < src.length; i++) {
|
|
126
|
+
if (src[i] === "{") depth++;
|
|
127
|
+
else if (src[i] === "}" && --depth === 0) {
|
|
128
|
+
close = i;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const rest = close === -1 ? "x" : src.slice(close + 1).replace(/^;/, "");
|
|
133
|
+
if (close !== -1 && rest.trim() === "") {
|
|
134
|
+
out = `${src.slice(0, start)}export default withStudio(${src.slice(open, close + 1)});${rest}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!out) return manual;
|
|
138
|
+
|
|
139
|
+
// The import goes after the last existing import, or at the very top.
|
|
140
|
+
const lines = out.split("\n");
|
|
141
|
+
let last = -1;
|
|
142
|
+
lines.forEach((l, i) => {
|
|
143
|
+
if (/^import\s/.test(l)) last = i;
|
|
144
|
+
});
|
|
145
|
+
lines.splice(last + 1, 0, IMPORT_LINE);
|
|
146
|
+
writeFileSync(file, lines.join("\n"));
|
|
147
|
+
return "wrapped";
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@web-my-money/studio-consumer",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Consumer-side integration for WMM Studio: content, analytics, attribution.",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/Web-My-Money/wmm-studio.git",
|
|
9
|
-
"directory": "packages/studio-consumer"
|
|
10
|
-
},
|
|
11
|
-
"type": "module",
|
|
12
|
-
"sideEffects": false,
|
|
13
|
-
"publishConfig": {
|
|
14
|
-
"access": "public"
|
|
15
|
-
},
|
|
16
|
-
"exports": {
|
|
17
|
-
"./analytics": "./src/analytics/index.ts",
|
|
18
|
-
"./content": "./src/content/index.ts",
|
|
19
|
-
"./attribution": "./src/attribution/index.ts",
|
|
20
|
-
"./headers": "./src/content/headers.mjs",
|
|
21
|
-
"./brand": "./src/brand/index.ts",
|
|
22
|
-
"./image": "./src/image/index.ts"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@web-my-money/studio-consumer",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Consumer-side integration for WMM Studio: content, analytics, attribution.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Web-My-Money/wmm-studio.git",
|
|
9
|
+
"directory": "packages/studio-consumer"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
"./analytics": "./src/analytics/index.ts",
|
|
18
|
+
"./content": "./src/content/index.ts",
|
|
19
|
+
"./attribution": "./src/attribution/index.ts",
|
|
20
|
+
"./headers": "./src/content/headers.mjs",
|
|
21
|
+
"./brand": "./src/brand/index.ts",
|
|
22
|
+
"./image": "./src/image/index.ts",
|
|
23
|
+
"./next": "./src/next/index.mjs"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"next": ">=16",
|
|
27
|
+
"react": ">=19"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"studio-consumer": "./bin/studio-consumer.mjs"
|
|
31
|
+
},
|
|
32
|
+
"files": ["src", "bin", "cli", "skills"]
|
|
33
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onboard-site
|
|
3
|
+
description: Finish connecting this Next.js app to WMM Studio after `npx @web-my-money/studio-consumer init` has run. Wires the root layout and the proxy/middleware, then proposes which text should be editable in Studio and writes the content manifest once the developer approves. For a new site made from WMM's template, sets the client's colours and first copy instead. Use when the init command opens Claude Code on /onboard-site.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Finish connecting this app to WMM Studio
|
|
7
|
+
|
|
8
|
+
The `init` command already did the mechanical part: it installed
|
|
9
|
+
`@web-my-money/studio-consumer`, wrapped `next.config` with `withStudio`, and wrote
|
|
10
|
+
`lib/studio.ts` (the site key), `lib/brand.ts`, `lib/content.ts`, a starter
|
|
11
|
+
`lib/content-manifest.ts` and the API routes under `app/api/`. (In a `src/` app all of
|
|
12
|
+
these live under `src/`.) Your job is the part that needs judgement on someone else's
|
|
13
|
+
code.
|
|
14
|
+
|
|
15
|
+
**The developer may never have seen Studio before.** Explain each change in one plain
|
|
16
|
+
sentence, show the diff, and wait for a yes before writing it. Never rewrite a file
|
|
17
|
+
wholesale; make the smallest edit that does the job. Never remove an existing redirect,
|
|
18
|
+
rewrite, header or script.
|
|
19
|
+
|
|
20
|
+
Read `.wmm-onboarding/state.json` first: it lists the modules this site uses
|
|
21
|
+
(`content`, `analytics`, `forms`, `attribution`, `ab`, …). Skip anything for a module
|
|
22
|
+
that is not listed.
|
|
23
|
+
|
|
24
|
+
## 0. A new site from WMM's template (`"mode": "new"` in state.json)
|
|
25
|
+
|
|
26
|
+
The site was just created from `Web-My-Money/wmm-site-template`, so sections 1 and 2 are
|
|
27
|
+
already done: the layout (`app/[lang]/layout.tsx`) has the brand attributes, the editor
|
|
28
|
+
overlay and analytics, and `proxy.ts` sets the visitor cookie. Check they are there, say so in
|
|
29
|
+
one line, and skip to the client's details. Read `CLAUDE.md` for the site's rules.
|
|
30
|
+
|
|
31
|
+
1. Ask the developer, one question at a time:
|
|
32
|
+
- the business name exactly as the client writes it, and one sentence on what they do and
|
|
33
|
+
for whom;
|
|
34
|
+
- the brand's primary and accent colours as hex codes (from the client's logo or brand
|
|
35
|
+
guide; "not yet" is a fine answer: leave the magenta placeholder and say so);
|
|
36
|
+
- which language visitors should land in by default (English or Spanish);
|
|
37
|
+
- where the main button should go (the client's booking calendar or form link), if known.
|
|
38
|
+
2. Then propose, as one diff per file, and write only after a yes:
|
|
39
|
+
- `app/client-theme.css`: the colours (`--primary`, `--accent`, and a readable
|
|
40
|
+
`--primary-foreground`: white on a dark primary, near-black on a light one);
|
|
41
|
+
- `dictionaries/en.json` and `es.json`: `meta`, `hero` and `services` from what the
|
|
42
|
+
developer told you, in both languages (Spanish neutral, no regional slang). Keep every
|
|
43
|
+
key; the two files must keep identical keys;
|
|
44
|
+
- `lib/i18n.ts`: `DEFAULT_LOCALE`, only if it changes;
|
|
45
|
+
- the main button's link in `app/[lang]/page.tsx` (the `TODO` on the closing banner).
|
|
46
|
+
3. **Never invent reviews, results, prices or credentials.** Leave the review placeholders
|
|
47
|
+
and the FAQ answers you do not know, and list them as "still needed from the client".
|
|
48
|
+
4. Section 3 below is a review here, not a rebuild: `lib/content-manifest.ts` already makes
|
|
49
|
+
the home page's marketing text editable. Show the list of slots, ask whether anything should
|
|
50
|
+
be added or removed, and change it only on a yes.
|
|
51
|
+
5. Go to section 4.
|
|
52
|
+
|
|
53
|
+
## 1. Root layout (`app/layout.tsx`, or the layout that renders `<html>`)
|
|
54
|
+
|
|
55
|
+
- **Brand (content):** `import { brandRootAttributes } from "@web-my-money/studio-consumer/brand";`
|
|
56
|
+
and `import { brand } from "<relative path>/lib/brand";`, then spread
|
|
57
|
+
`{...brandRootAttributes(brand)}` on `<html>`. Keep every attribute already there.
|
|
58
|
+
- **Click-to-edit (content):** render `<WmmEditOverlay studioOrigin={process.env.NEXT_PUBLIC_STUDIO_ORIGIN} />`
|
|
59
|
+
from `@web-my-money/studio-consumer/content` once, inside `<body>`.
|
|
60
|
+
- **Analytics (analytics):** render `<FunnelAnalytics />` and `<EngagementTracking />` from
|
|
61
|
+
`@web-my-money/studio-consumer/analytics` once, inside `<body>`.
|
|
62
|
+
- **Overrides (content):** wherever the app loads its dictionary for a locale (often a
|
|
63
|
+
`[lang]` layout or a `getDictionary` helper), pass it through
|
|
64
|
+
`await applyDictOverrides(dict, locale)` from `lib/content.ts` before rendering. This is
|
|
65
|
+
what makes an edit in Studio appear on the page; without it the checklist goes green
|
|
66
|
+
and edits never show. If the app has no dictionary, say so and use `getLocalizedSlot`
|
|
67
|
+
for each editable value in step 3 instead.
|
|
68
|
+
|
|
69
|
+
## 2. Proxy / middleware (analytics)
|
|
70
|
+
|
|
71
|
+
Studio's analytics and A/B testing need the `wmm_vid` visitor cookie, which
|
|
72
|
+
`withVisitorCookie` from `@web-my-money/studio-consumer/analytics` sets.
|
|
73
|
+
|
|
74
|
+
- Next 16 uses `proxy.ts` (older apps: `middleware.ts`), at the code root.
|
|
75
|
+
- **None exists:** create `proxy.ts` that returns `withVisitorCookie(request)`, with a
|
|
76
|
+
matcher that skips `_next`, `api` and static files.
|
|
77
|
+
- **One exists:** compose, never replace. Call `withVisitorCookie(request)` for the
|
|
78
|
+
pass-through case, and keep every existing redirect and rewrite exactly as it is. If the
|
|
79
|
+
existing code returns its own `NextResponse`, show the developer the two options (set
|
|
80
|
+
the cookie on that response, or call `withVisitorCookie` first) and let them choose.
|
|
81
|
+
|
|
82
|
+
## 3. Choose the editable text (content)
|
|
83
|
+
|
|
84
|
+
This is the real decision in the whole onboarding. **Do not decide it alone.**
|
|
85
|
+
|
|
86
|
+
1. Find the copy: the per-locale dictionary (e.g. `dictionaries/en.json`, `es.json`), or,
|
|
87
|
+
if there is none, the text in the page components.
|
|
88
|
+
2. Propose a table grouped by page and section, one row per candidate:
|
|
89
|
+
|
|
90
|
+
| Key | Current text (EN) | Editable? | Why |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
|
|
93
|
+
Keys are `page.section.thing` (e.g. `home.hero.headline`). Give a one-line reason for
|
|
94
|
+
**every "no"**. Lean towards "no" for, and always justify:
|
|
95
|
+
- legal and compliance text (privacy, terms, disclaimers, medical or financial claims);
|
|
96
|
+
- form field names, validation and error messages;
|
|
97
|
+
- analytics, tracking and pixel ids;
|
|
98
|
+
- icons and component references (never editable);
|
|
99
|
+
- anything the code uses as an identifier, a route or a CSS class.
|
|
100
|
+
Lean towards "yes" for headlines, sub-headlines, body copy, calls to action, testimonials
|
|
101
|
+
and images a marketer would change.
|
|
102
|
+
3. Wait for the developer to approve or edit the table.
|
|
103
|
+
4. Then fill in `lib/content-manifest.ts`, keeping its `getManifest()` shape and the
|
|
104
|
+
`siteKey: SITE_KEY` it returns:
|
|
105
|
+
- a slot only for text the code actually renders;
|
|
106
|
+
- `type`: `text` (or `richtext`, `image`, `list`, `select` where that fits);
|
|
107
|
+
- `dictPath` for dictionary text (the override is merged into the dictionary, no
|
|
108
|
+
component changes needed); no `dictPath` for images or other non-dictionary values,
|
|
109
|
+
which are read with `getSlot` and passed down as props;
|
|
110
|
+
- `default` is read from the dictionary (`en`/`es` imports), **never retyped**, so it
|
|
111
|
+
cannot go stale;
|
|
112
|
+
- `label` in plain words a client understands ("Home: hero headline");
|
|
113
|
+
- `group` per page section, `sortOrder` in reading order.
|
|
114
|
+
|
|
115
|
+
If `lib/content-manifest.ts` already had real slots before `init` ran, keep them and only
|
|
116
|
+
add what the developer approves.
|
|
117
|
+
|
|
118
|
+
## 4. Check, then hand back
|
|
119
|
+
|
|
120
|
+
Run `npm run verify` (or the app's equivalent: typecheck, lint, test, build). Fix what it
|
|
121
|
+
reports. If the app has no test runner and `tests/studio-frame-ancestors.test.ts` was not
|
|
122
|
+
written, offer to add Vitest and that test.
|
|
123
|
+
|
|
124
|
+
When everything passes, tell the developer in one sentence what changed, and that they can
|
|
125
|
+
close Claude Code (type `/exit`) so the `init` command continues with the next step.
|
package/src/analytics/index.ts
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
/** Proves at runtime which build a consumer is actually running. */
|
|
2
|
-
export const PACKAGE_VERSION = "2.
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
-
resolveVariant,
|
|
6
|
-
bucketFor,
|
|
7
|
-
CONTROL,
|
|
8
|
-
VARIANT,
|
|
9
|
-
VISITOR_COOKIE,
|
|
10
|
-
type RunningExperiment,
|
|
11
|
-
} from "./bucketing";
|
|
12
|
-
|
|
13
|
-
export {
|
|
14
|
-
trackFunnelEvent,
|
|
15
|
-
registerExperiments,
|
|
16
|
-
flushFunnelEvents,
|
|
17
|
-
currentSessionId,
|
|
18
|
-
funnelAnalyticsEnabled,
|
|
19
|
-
consentGranted,
|
|
20
|
-
resetFunnelAnalytics,
|
|
21
|
-
type FunnelEventProps,
|
|
22
|
-
} from "./collector";
|
|
23
|
-
|
|
24
|
-
export { useFormAnalytics, type FormAnalytics, type FormAnalyticsOptions } from "./use-form-analytics";
|
|
25
|
-
|
|
26
|
-
export {
|
|
27
|
-
FunnelAnalytics,
|
|
28
|
-
EngagementTracking,
|
|
29
|
-
AbArm,
|
|
30
|
-
THRESHOLDS,
|
|
31
|
-
crossedThresholds,
|
|
32
|
-
labelFor,
|
|
33
|
-
} from "./components";
|
|
34
|
-
|
|
35
|
-
export { createCollectHandler } from "./collect-handler";
|
|
36
|
-
|
|
37
|
-
export { withVisitorCookie } from "./proxy";
|
|
1
|
+
/** Proves at runtime which build a consumer is actually running. */
|
|
2
|
+
export const PACKAGE_VERSION = "2.4.0";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
resolveVariant,
|
|
6
|
+
bucketFor,
|
|
7
|
+
CONTROL,
|
|
8
|
+
VARIANT,
|
|
9
|
+
VISITOR_COOKIE,
|
|
10
|
+
type RunningExperiment,
|
|
11
|
+
} from "./bucketing";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
trackFunnelEvent,
|
|
15
|
+
registerExperiments,
|
|
16
|
+
flushFunnelEvents,
|
|
17
|
+
currentSessionId,
|
|
18
|
+
funnelAnalyticsEnabled,
|
|
19
|
+
consentGranted,
|
|
20
|
+
resetFunnelAnalytics,
|
|
21
|
+
type FunnelEventProps,
|
|
22
|
+
} from "./collector";
|
|
23
|
+
|
|
24
|
+
export { useFormAnalytics, type FormAnalytics, type FormAnalyticsOptions } from "./use-form-analytics";
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
FunnelAnalytics,
|
|
28
|
+
EngagementTracking,
|
|
29
|
+
AbArm,
|
|
30
|
+
THRESHOLDS,
|
|
31
|
+
crossedThresholds,
|
|
32
|
+
labelFor,
|
|
33
|
+
} from "./components";
|
|
34
|
+
|
|
35
|
+
export { createCollectHandler } from "./collect-handler";
|
|
36
|
+
|
|
37
|
+
export { withVisitorCookie } from "./proxy";
|
package/src/attribution/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
/** Proves at runtime which build a consumer is actually running. */
|
|
2
|
-
export const PACKAGE_VERSION = "2.
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
-
getAttribution,
|
|
6
|
-
captureAttribution,
|
|
7
|
-
getAttributionPayload,
|
|
8
|
-
computeAttribution,
|
|
9
|
-
flattenAttribution,
|
|
10
|
-
hasCampaignSignal,
|
|
11
|
-
} from "./store";
|
|
12
|
-
export type { Attribution, Touch, AttributionPayload } from "./store";
|
|
13
|
-
|
|
14
|
-
export { sessionIdField, SESSION_ID_FIELD } from "./crm";
|
|
1
|
+
/** Proves at runtime which build a consumer is actually running. */
|
|
2
|
+
export const PACKAGE_VERSION = "2.4.0";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
getAttribution,
|
|
6
|
+
captureAttribution,
|
|
7
|
+
getAttributionPayload,
|
|
8
|
+
computeAttribution,
|
|
9
|
+
flattenAttribution,
|
|
10
|
+
hasCampaignSignal,
|
|
11
|
+
} from "./store";
|
|
12
|
+
export type { Attribution, Touch, AttributionPayload } from "./store";
|
|
13
|
+
|
|
14
|
+
export { sessionIdField, SESSION_ID_FIELD } from "./crm";
|