@madojs/mado 0.11.1 → 0.12.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 (219) hide show
  1. package/AGENTS.md +102 -25
  2. package/CHANGELOG.md +158 -1
  3. package/README.md +135 -172
  4. package/dist/src/component.d.ts +3 -2
  5. package/dist/src/component.js +98 -6
  6. package/dist/src/component.js.map +1 -1
  7. package/dist/src/css.js +34 -5
  8. package/dist/src/css.js.map +1 -1
  9. package/dist/src/head.d.ts +6 -6
  10. package/dist/src/head.js +6 -6
  11. package/dist/src/html/parser.d.ts +1 -1
  12. package/dist/src/html/parser.js +1 -1
  13. package/dist/src/html/template.js +24 -10
  14. package/dist/src/html/template.js.map +1 -1
  15. package/dist/src/index.d.ts +8 -4
  16. package/dist/src/index.js +13 -3
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/page.d.ts +62 -28
  19. package/dist/src/page.js +5 -0
  20. package/dist/src/page.js.map +1 -1
  21. package/dist/src/resource.js +2 -1
  22. package/dist/src/resource.js.map +1 -1
  23. package/dist/src/router/base.d.ts +65 -0
  24. package/dist/src/router/base.js +168 -0
  25. package/dist/src/router/base.js.map +1 -0
  26. package/dist/src/router/manifest.js +75 -31
  27. package/dist/src/router/manifest.js.map +1 -1
  28. package/dist/src/router/match.d.ts +16 -2
  29. package/dist/src/router/match.js +41 -1
  30. package/dist/src/router/match.js.map +1 -1
  31. package/dist/src/router/navigation.js +58 -9
  32. package/dist/src/router/navigation.js.map +1 -1
  33. package/dist/src/static-runtime.d.ts +81 -0
  34. package/dist/src/static-runtime.js +209 -0
  35. package/dist/src/static-runtime.js.map +1 -0
  36. package/dist/src/vite/index.d.ts +31 -3
  37. package/dist/src/vite/index.js +49 -18
  38. package/dist/src/vite/index.js.map +1 -1
  39. package/docs/README.md +5 -8
  40. package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
  41. package/docs/en/00-the-mado-way.md +1 -1
  42. package/docs/en/01-quickstart.md +183 -0
  43. package/docs/en/10-pages-and-components.md +271 -0
  44. package/docs/en/11-templates-and-signals.md +211 -0
  45. package/docs/en/12-routing.md +229 -0
  46. package/docs/en/13-data.md +225 -0
  47. package/docs/en/14-forms.md +244 -0
  48. package/docs/en/15-static-snapshots.md +181 -0
  49. package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
  50. package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
  51. package/docs/en/{14-testing.md → 22-testing.md} +6 -5
  52. package/docs/en/23-cookbook.md +292 -0
  53. package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
  54. package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
  55. package/docs/en/40-llm-guide.md +776 -0
  56. package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
  57. package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
  58. package/docs/en/README.md +97 -27
  59. package/docs/recipes/nginx/Containerfile +26 -0
  60. package/docs/recipes/nginx/nginx.conf +42 -0
  61. package/llms.txt +246 -211
  62. package/package.json +17 -10
  63. package/scripts/bake.mjs +4 -569
  64. package/scripts/cli/generate.mjs +55 -5
  65. package/scripts/cli/help.mjs +11 -5
  66. package/scripts/cli/index.mjs +20 -4
  67. package/scripts/cli/init.mjs +7 -2
  68. package/scripts/cli/release.mjs +20 -68
  69. package/scripts/docs-lint.mjs +170 -0
  70. package/scripts/package-smoke.mjs +67 -9
  71. package/scripts/preview.mjs +95 -6
  72. package/scripts/size-budget.mjs +5 -1
  73. package/scripts/static/browser.mjs +654 -0
  74. package/scripts/static/discover.mjs +281 -0
  75. package/scripts/static/output.mjs +141 -0
  76. package/scripts/static/serialize.mjs +212 -0
  77. package/scripts/static/server.mjs +167 -0
  78. package/scripts/static.mjs +191 -0
  79. package/starters/default/README.md +57 -55
  80. package/starters/default/package.json +3 -9
  81. package/starters/default/src/app.routes.ts +20 -33
  82. package/starters/default/src/components/feature-card.component.ts +40 -0
  83. package/starters/default/src/components/live-counter.component.ts +57 -0
  84. package/starters/default/src/content/guides.ts +55 -0
  85. package/starters/default/src/main.ts +9 -13
  86. package/starters/default/src/pages/app.page.ts +41 -0
  87. package/starters/default/src/pages/guide.page.ts +40 -0
  88. package/starters/default/src/pages/home.page.ts +49 -0
  89. package/starters/default/src/pages/not-found.page.ts +18 -0
  90. package/starters/default/src/styles/document.css +39 -0
  91. package/starters/default/src/styles/reset.css +37 -0
  92. package/starters/default/src/styles/tokens.css +30 -0
  93. package/starters/default/src/styles.d.ts +3 -1
  94. package/starters/default/src/vite-env.d.ts +1 -1
  95. package/starters/default/vite.config.ts +14 -1
  96. package/starters/modular/README.md +142 -0
  97. package/starters/modular/index.html +13 -0
  98. package/starters/modular/package.json +30 -0
  99. package/starters/modular/src/app.routes.ts +39 -0
  100. package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
  101. package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
  102. package/starters/modular/src/main.ts +16 -0
  103. package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
  104. package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
  105. package/starters/modular/src/modules/home/not-found.page.ts +14 -0
  106. package/starters/modular/src/styles.d.ts +1 -0
  107. package/starters/modular/src/vite-env.d.ts +1 -0
  108. package/starters/modular/tsconfig.json +24 -0
  109. package/starters/modular/vite.config.ts +131 -0
  110. package/TODO.md +0 -79
  111. package/docs/en/01-routing.md +0 -152
  112. package/docs/en/02-project-layout.md +0 -124
  113. package/docs/en/03-static-bake.md +0 -249
  114. package/docs/en/04-ide-setup.md +0 -162
  115. package/docs/en/07-llm-pitfalls.md +0 -724
  116. package/docs/en/08-llm-zero-history-test.md +0 -56
  117. package/docs/en/09-shadow-vs-light-dom.md +0 -174
  118. package/docs/en/11-layouts.md +0 -113
  119. package/docs/en/12-auth-and-api.md +0 -124
  120. package/docs/en/16-bake-cookbook.md +0 -100
  121. package/docs/en/17-shadow-dom-forms.md +0 -192
  122. package/docs/fr/00-the-mado-way.md +0 -85
  123. package/docs/fr/01-routing.md +0 -120
  124. package/docs/fr/02-project-layout.md +0 -84
  125. package/docs/fr/03-static-bake.md +0 -289
  126. package/docs/fr/04-ide-setup.md +0 -162
  127. package/docs/fr/05-why-mado.md +0 -193
  128. package/docs/fr/06-for-backenders.md +0 -438
  129. package/docs/fr/07-llm-pitfalls.md +0 -625
  130. package/docs/fr/08-llm-zero-history-test.md +0 -38
  131. package/docs/fr/09-shadow-vs-light-dom.md +0 -65
  132. package/docs/fr/10-app-architecture.md +0 -138
  133. package/docs/fr/11-layouts.md +0 -47
  134. package/docs/fr/12-auth-and-api.md +0 -76
  135. package/docs/fr/13-deployment.md +0 -57
  136. package/docs/fr/14-testing.md +0 -41
  137. package/docs/fr/15-error-handling.md +0 -50
  138. package/docs/fr/16-bake-cookbook.md +0 -88
  139. package/docs/fr/17-shadow-dom-forms.md +0 -196
  140. package/docs/fr/18-api-freeze-map.md +0 -63
  141. package/docs/fr/19-reactivity-ordering.md +0 -97
  142. package/docs/fr/20-v1-stability.md +0 -88
  143. package/docs/fr/README.md +0 -27
  144. package/docs/ru/00-the-mado-way.md +0 -84
  145. package/docs/ru/01-routing.md +0 -119
  146. package/docs/ru/02-project-layout.md +0 -84
  147. package/docs/ru/03-static-bake.md +0 -250
  148. package/docs/ru/04-ide-setup.md +0 -144
  149. package/docs/ru/05-why-mado.md +0 -193
  150. package/docs/ru/06-for-backenders.md +0 -428
  151. package/docs/ru/07-llm-pitfalls.md +0 -624
  152. package/docs/ru/08-llm-zero-history-test.md +0 -57
  153. package/docs/ru/09-shadow-vs-light-dom.md +0 -63
  154. package/docs/ru/10-app-architecture.md +0 -152
  155. package/docs/ru/11-layouts.md +0 -47
  156. package/docs/ru/12-auth-and-api.md +0 -75
  157. package/docs/ru/13-deployment.md +0 -66
  158. package/docs/ru/14-testing.md +0 -50
  159. package/docs/ru/15-error-handling.md +0 -56
  160. package/docs/ru/16-bake-cookbook.md +0 -95
  161. package/docs/ru/17-shadow-dom-forms.md +0 -193
  162. package/docs/ru/18-api-freeze-map.md +0 -64
  163. package/docs/ru/19-reactivity-ordering.md +0 -95
  164. package/docs/ru/20-v1-stability.md +0 -82
  165. package/docs/ru/README.md +0 -25
  166. package/docs/uk/00-the-mado-way.md +0 -82
  167. package/docs/uk/01-routing.md +0 -76
  168. package/docs/uk/02-project-layout.md +0 -73
  169. package/docs/uk/03-static-bake.md +0 -48
  170. package/docs/uk/04-ide-setup.md +0 -26
  171. package/docs/uk/05-why-mado.md +0 -34
  172. package/docs/uk/06-for-backenders.md +0 -55
  173. package/docs/uk/07-llm-pitfalls.md +0 -145
  174. package/docs/uk/08-llm-zero-history-test.md +0 -34
  175. package/docs/uk/09-shadow-vs-light-dom.md +0 -58
  176. package/docs/uk/10-app-architecture.md +0 -97
  177. package/docs/uk/11-layouts.md +0 -47
  178. package/docs/uk/12-auth-and-api.md +0 -70
  179. package/docs/uk/13-deployment.md +0 -40
  180. package/docs/uk/14-testing.md +0 -34
  181. package/docs/uk/15-error-handling.md +0 -32
  182. package/docs/uk/16-bake-cookbook.md +0 -36
  183. package/docs/uk/17-shadow-dom-forms.md +0 -193
  184. package/docs/uk/18-api-freeze-map.md +0 -61
  185. package/docs/uk/19-reactivity-ordering.md +0 -95
  186. package/docs/uk/20-v1-stability.md +0 -83
  187. package/docs/uk/README.md +0 -27
  188. package/starters/default/src/modules/home/not-found.page.ts +0 -11
  189. /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
  190. /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
  191. /package/starters/{default → modular}/.editorconfig +0 -0
  192. /package/starters/{default → modular}/eslint.config.mjs +0 -0
  193. /package/starters/{default → modular}/public/favicon.svg +0 -0
  194. /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
  195. /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
  196. /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
  197. /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
  198. /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
  199. /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
  200. /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
  201. /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
  202. /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
  203. /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
  204. /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
  205. /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
  206. /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
  207. /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
  208. /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
  209. /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
  210. /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
  211. /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
  212. /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
  213. /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
  214. /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
  215. /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
  216. /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
  217. /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
  218. /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
  219. /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
@@ -0,0 +1,244 @@
1
+ # Forms
2
+
3
+ > `useForm()` is a thin signal-wrapper around native HTML5 form
4
+ > validation. For 90% of cases you write a `<form>`, attach
5
+ > `onInput / onBlur / onSubmit` and you are done.
6
+
7
+ ```ts
8
+ import { html, page, useForm } from "@madojs/mado";
9
+
10
+ export default page({
11
+ view: () => {
12
+ const f = useForm({
13
+ email: { required: true, type: "email" },
14
+ age: { required: true, type: "number", min: 18 },
15
+ });
16
+
17
+ return html`
18
+ <form @submit=${f.onSubmit(async (v) => { await api.save(v); f.reset(); })}>
19
+ <input name="email" type="email"
20
+ @input=${f.onInput} @blur=${f.onBlur} />
21
+ ${() =>
22
+ f.touched().email && f.errors().email
23
+ ? html`<small class="err">${f.errors().email}</small>`
24
+ : null}
25
+
26
+ <input name="age" type="number" @input=${f.onInput} />
27
+
28
+ <button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
29
+ </form>
30
+ `;
31
+ },
32
+ });
33
+ ```
34
+
35
+ ## The signals it exposes
36
+
37
+ | Reader | Value |
38
+ | ---------------------- | ---------------------------------------------------------------- |
39
+ | `f.values()` | object of current field values |
40
+ | `f.errors()` | object of `field → error string` (validators + HTML5 constraints)|
41
+ | `f.touched()` | object of `field → boolean` (true after first blur) |
42
+ | `f.isValid()` | true when `errors()` is empty |
43
+ | `f.submitting()` | true while the `onSubmit` async handler is running |
44
+ | `f.dirty()` | true when any value differs from initial |
45
+
46
+ Writers: `f.setValue(name, v)`, `f.setError(name, msg)`,
47
+ `f.reset(values?)`, `f.touch(name)`.
48
+
49
+ ## Schema
50
+
51
+ The schema mirrors HTML5 constraints, with a few extras:
52
+
53
+ ```ts
54
+ useForm({
55
+ email: { required: true, type: "email" },
56
+ age: { type: "number", min: 18, max: 120 },
57
+ name: { required: true, minLength: 2, maxLength: 80 },
58
+ website: { type: "url" },
59
+ tags: { custom: (v) => Array.isArray(v) && v.length > 0 ? null : "pick at least one" },
60
+ });
61
+ ```
62
+
63
+ For cross-field or async rules use `validate`:
64
+
65
+ ```ts
66
+ useForm({
67
+ password: { required: true, minLength: 8 },
68
+ confirm: { required: true },
69
+ }, {
70
+ validate: (values) =>
71
+ values.password !== values.confirm
72
+ ? { confirm: "passwords do not match" }
73
+ : null,
74
+ });
75
+ ```
76
+
77
+ Return `null` (everything is valid) or a partial `field → error`
78
+ object.
79
+
80
+ ## Field arrays
81
+
82
+ ```ts
83
+ const f = useForm({ tags: { custom: (v) => v.length > 0 ? null : "required" } });
84
+ const tags = f.fieldArray<string>("tags");
85
+
86
+ tags.append("rust");
87
+ tags.remove(0);
88
+ tags.move(0, 2);
89
+ tags.items(); // current array (signal)
90
+ ```
91
+
92
+ ## With `mutation()`
93
+
94
+ Forms compose with `mutation()` for typed write-paths:
95
+
96
+ ```ts
97
+ import { mutation, navigate } from "@madojs/mado";
98
+
99
+ const save = mutation((u: User) => api.save(u), {
100
+ invalidates: ["/api/users*"],
101
+ });
102
+
103
+ // inside view()
104
+ const f = useForm({ name: { required: true } });
105
+ return html`
106
+ <form @submit=${f.onSubmit(async (v) => {
107
+ await save.run(v);
108
+ navigate("/users");
109
+ })}>
110
+ <input name="name" @input=${f.onInput} />
111
+ <button ?disabled=${() => !f.isValid() || save.loading()}>Create</button>
112
+ ${() => save.error() ? html`<p class="err">${save.error()!.message}</p>` : null}
113
+ </form>
114
+ `;
115
+ ```
116
+
117
+ `save.loading()` reflects the request; `f.submitting()` reflects the
118
+ `onSubmit` handler runtime. Usually only one of them is interesting.
119
+
120
+ ## Shadow DOM inputs — when you need them
121
+
122
+ Most apps build forms from native `<input>` / `<select>` /
123
+ `<textarea>` directly inside the `<form>`. `useForm()` reads
124
+ `e.target.name` and `e.target.value` on the events, which works
125
+ out-of-the-box for native controls.
126
+
127
+ You only need a custom Web Component when the input has its own
128
+ shadow tree (a date picker, a tag editor, a rich text field, …). Two
129
+ browser-level facts make that path slightly different:
130
+
131
+ 1. **Event retargeting** — events that bubble out of an open shadow
132
+ root have their `target` retargeted to the host. `useForm()` reads
133
+ `e.target.name` and `e.target.value` — the host element must
134
+ expose both.
135
+ 2. **Form association** — a `<button type="submit">` inside a shadow
136
+ root is NOT part of the form-owner algorithm. Clicking it does
137
+ not submit the surrounding `<form>`.
138
+
139
+ The two patterns below cover both. Reach for them **only** if a real
140
+ custom input forces you to. For everything else, see "Light-DOM
141
+ inputs" further down.
142
+
143
+ ### Custom input — proxy `name` / `value`
144
+
145
+ ```ts
146
+ import { component, html } from "@madojs/mado";
147
+
148
+ component("x-input", ({ host, attr }) => {
149
+ const type = attr("type", "text");
150
+
151
+ // After Shadow DOM retargets e.target from the inner <input> to
152
+ // <x-input>, useForm reads e.target.name / e.target.value. These
153
+ // getters bridge the gap.
154
+ Object.defineProperty(host, "name", {
155
+ get: () => host.getAttribute("name") ?? "",
156
+ configurable: true,
157
+ });
158
+ Object.defineProperty(host, "value", {
159
+ get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
160
+ set: (v: string) => {
161
+ const input = host.shadowRoot?.querySelector("input");
162
+ if (input) input.value = v;
163
+ },
164
+ configurable: true,
165
+ });
166
+
167
+ return () => html`<input type=${type} />`;
168
+ });
169
+ ```
170
+
171
+ The inner `input` event already bubbles through the shadow boundary
172
+ (`composed: true` by default), so `<form @input=${f.onInput}>` keeps
173
+ working.
174
+
175
+ ### Custom submit button — `requestSubmit()`
176
+
177
+ ```ts
178
+ import { component, html } from "@madojs/mado";
179
+
180
+ component("x-button", ({ host, attr }) => {
181
+ const disabled = attr("disabled");
182
+
183
+ const onClick = () => {
184
+ const t = host.getAttribute("type");
185
+ if (t === "button" || t === "reset") return;
186
+ if (host.hasAttribute("disabled")) return;
187
+ host.closest("form")?.requestSubmit();
188
+ };
189
+
190
+ return () => html`
191
+ <button ?disabled=${() => disabled() !== ""} @click=${onClick}>
192
+ <slot></slot>
193
+ </button>
194
+ `;
195
+ });
196
+ ```
197
+
198
+ `host.closest("form")` works because the host itself lives in the
199
+ light DOM. `requestSubmit()` triggers HTML5 validation and the
200
+ `submit` event exactly like a native click.
201
+
202
+ ### Light-DOM input — the simpler alternative
203
+
204
+ If a custom element is just a styled wrapper around a native control
205
+ and does not need style encapsulation, use `{ shadow: false }`. The
206
+ native `<input>` is then part of the document; no retargeting, no
207
+ proxy properties, no submit bridge.
208
+
209
+ ```ts
210
+ component(
211
+ "x-field",
212
+ ({ attr }) => {
213
+ const label = attr("label", "");
214
+ return () => html`
215
+ <label>
216
+ <span>${label}</span>
217
+ <slot></slot>
218
+ </label>
219
+ `;
220
+ },
221
+ { shadow: false },
222
+ );
223
+ ```
224
+
225
+ This is the only situation where `shadow: false` is the recommended
226
+ default — and even then a `page()` is usually a better fit if the
227
+ wrapper is layout-shaped rather than reusable.
228
+
229
+ ## Summary
230
+
231
+ | Concern | Native form | Custom component with Shadow DOM |
232
+ | -------------------------- | ------------------------------------ | ----------------------------------------- |
233
+ | `useForm` integration | works out of the box | proxy `name` / `value` on the host |
234
+ | Submit button | `<button type="submit">` works | `host.closest("form")?.requestSubmit()` |
235
+ | Reactive attributes | not needed | `ctx.attr(name)` returns a Signal |
236
+ | Style encapsulation | global CSS | Shadow DOM CSS via `css\`\`` |
237
+
238
+ ## Further reading
239
+
240
+ - [10-pages-and-components.md](./10-pages-and-components.md) — when
241
+ to reach for `component({ shadow: false })`.
242
+ - [13-data.md](./13-data.md) — `mutation()` for typed writes.
243
+ - [21-error-handling.md](./21-error-handling.md) — surfacing form
244
+ errors from network failures.
@@ -0,0 +1,181 @@
1
+ # Static snapshots (`mado static`)
2
+
3
+ > Browser-rendered static HTML for SEO and first paint. No SSR, no
4
+ > hydration, no template-language bake.
5
+
6
+ `mado static` is a *snapshot capture*: at release time Mado runs your
7
+ app in a real headless Chromium, lets the page render to the DOM
8
+ (including any Shadow DOM), then serialises the result — Declarative
9
+ Shadow DOM and all — to one HTML file per route. On first paint the
10
+ live SPA atomically replaces the static tree; there is no hydration
11
+ protocol and no node reconciliation.
12
+
13
+ It is the right tool for content that is the same for every visitor:
14
+ public landing pages, product / catalogue pages, documentation, blog
15
+ posts. It is intentionally **not** an SSR runtime for personalised
16
+ data.
17
+
18
+ ## When `static` is suitable
19
+
20
+ - Public pages with a single canonical representation per URL.
21
+ - Pages whose data is known at build time (CMS dump, file system,
22
+ Markdown, JSON, build-time API call).
23
+ - Pages that need to appear fully rendered in `view-source:`, in
24
+ social preview crawlers, and to JS-disabled clients.
25
+ - First paint of a page that *later* upgrades into a live SPA route.
26
+
27
+ ## When `static` is **not** suitable
28
+
29
+ - Personalised content per visitor (auth, geo, A/B). The capture sees
30
+ one DOM and would ship it to everyone.
31
+ - Real-time data (chat, dashboards, prices). The snapshot is frozen
32
+ the moment of capture.
33
+ - Routes behind guards. Static pages are public by definition; the
34
+ discovery pipeline refuses to capture guarded routes.
35
+
36
+ For those cases keep the route SPA-only — Mado still serves a clean
37
+ `_mado/spa.html` fallback for them.
38
+
39
+ ## Declaring a static page
40
+
41
+ A page opts in through the `static` field on `page({ ... })`. Two
42
+ shapes are accepted:
43
+
44
+ ```ts
45
+ // Single static page (no params)
46
+ import { html, page } from "@madojs/mado";
47
+
48
+ export default page({
49
+ static: true,
50
+ title: "Pricing",
51
+ head: () => ({ description: "Plans and pricing." }),
52
+ view: () => html`<main><h1>Pricing</h1></main>`,
53
+ });
54
+ ```
55
+
56
+ ```ts
57
+ // Dynamic static route — one capture per `paths()` entry
58
+ import { html, page } from "@madojs/mado";
59
+
60
+ export default page<{ slug: string }, Guide>({
61
+ static: {
62
+ paths: async () => guides.map((g) => ({ slug: g.slug })),
63
+ initialData: async ({ slug }) => loadGuide(slug),
64
+ },
65
+ title: ({ slug }) => guides.find((g) => g.slug === slug)?.title ?? slug,
66
+ head: ({ slug }, seed) => ({
67
+ description: seed?.summary ?? "",
68
+ }),
69
+ view: (ctx) => html`<article>${ctx.data.body}</article>`,
70
+ });
71
+ ```
72
+
73
+ The seed produced by `initialData()` is JSON-serialised into the
74
+ captured HTML as `<script type="application/json" data-mado-static-data="…">`.
75
+ On first client boot Mado consumes it once so `page.load()` and
76
+ `page.head()` see the same data the snapshot saw — no extra fetch.
77
+
78
+ ## Running
79
+
80
+ In normal use `mado static` is invoked through `mado release`:
81
+
82
+ ```bash
83
+ mado release # vite build + mado static + deployment files
84
+ mado preview # serve out/ like a real static host
85
+ ```
86
+
87
+ You can also invoke `mado static` on its own (for tighter loops in
88
+ CI), but it operates on the existing `out/` artefact produced by
89
+ `vite build`, so the canonical command is `release`.
90
+
91
+ A public origin is required so static documents carry absolute
92
+ canonical URLs. Set it in `vite.config.ts`:
93
+
94
+ ```ts
95
+ import { defineConfig } from "vite";
96
+ import { mado } from "@madojs/mado/vite";
97
+
98
+ export default defineConfig({
99
+ plugins: [mado({ site: "https://your-app.example" })],
100
+ });
101
+ ```
102
+
103
+ …or override per run:
104
+
105
+ ```bash
106
+ mado release --base-url https://staging.example
107
+ MADO_SITE=https://staging.example mado release
108
+ ```
109
+
110
+ ## What the generated HTML contains
111
+
112
+ For every captured route:
113
+
114
+ - The full rendered light DOM.
115
+ - Every open shadow root serialised through Declarative Shadow DOM
116
+ (`<template shadowrootmode="open">`).
117
+ - Adopted stylesheets materialised into `<style>` tags inside the
118
+ shadow root.
119
+ - A `<link rel="canonical">` and `<meta property="og:url">` derived
120
+ from `site + base + pathname` (if the page did not declare its own).
121
+ Both are marked `data-mado-head="static"` so the runtime `applyHead`
122
+ removes them on SPA navigation into a page without explicit head
123
+ metadata.
124
+ - The seed `<script type="application/json" data-mado-static-data="…">`
125
+ for routes that declared `initialData()`.
126
+ - Vite assets resolved through the active `base`.
127
+
128
+ `<form>`, `<input>`, `<select>`, `<textarea>` and `<details>` state
129
+ is serialised through attributes so a JS-disabled browser sees the
130
+ same shape the snapshot captured.
131
+
132
+ ## Constraints and gotchas
133
+
134
+ - **`paths()` and `initialData()` run during discovery AND ship in the
135
+ client bundle.** Keep them browser-safe. Never read secrets, never
136
+ call private services.
137
+ - **Static pages cannot use guards** — route-level or layout-level.
138
+ - **Wildcard routes (`*`) cannot be static.** They are the SPA
139
+ fallback.
140
+ - **A compatible Chromium is required.** CI should install it through
141
+ Playwright:
142
+ ```bash
143
+ npx playwright install --with-deps chromium
144
+ ```
145
+ …or via the framework's installed `playwright-core` for revision
146
+ parity:
147
+ ```bash
148
+ node node_modules/playwright-core/cli.js install --with-deps chromium
149
+ ```
150
+ - **Capture is strict by default.** Any failed fetch (script, style,
151
+ resource(), custom element definition) fails the snapshot. A small
152
+ allow-list covers `/favicon.ico`, `/favicon.svg`, `/robots.txt`
153
+ and `data:` URLs.
154
+ - **All network calls happen against an internal capture server** that
155
+ serves the freshly built `out/` tree. There is no real network at
156
+ capture time.
157
+
158
+ ## Comparison
159
+
160
+ | Approach | Mado static snapshot |
161
+ | ------------------------------------- | -------------------- |
162
+ | Next-style SSR + hydration | not used |
163
+ | Astro-style island runtime | not used |
164
+ | Build-time HTML from templates only | not used |
165
+ | Browser-rendered DOM + DSD + takeover | **used** |
166
+
167
+ The result is a plain HTML file. Search engines, social preview
168
+ crawlers and `curl` see exactly what your app renders on first paint;
169
+ no JavaScript step is required to make the document meaningful.
170
+
171
+ ## Cookbook
172
+
173
+ Concrete recipes for blog, product catalogue, documentation site and
174
+ multi-locale builds live in [23-cookbook.md](./23-cookbook.md).
175
+
176
+ ## TL;DR
177
+
178
+ - Declare `static: true | { paths, initialData }` on `page({ ... })`.
179
+ - Run `mado release`.
180
+ - Ship `out/` to any static host.
181
+ - The live SPA takes over atomically on first paint.
@@ -4,7 +4,58 @@ The official starter is the canonical production shape for Mado apps. It is
4
4
  not a demo architecture and not a framework inside the framework: it is plain
5
5
  files, imports, Mado primitives, and ESLint boundaries.
6
6
 
7
- ## File Tree
7
+ ## Project layout (universal starter)
8
+
9
+ Both starters share the same top-level shape. The universal starter is the
10
+ minimum:
11
+
12
+ ```
13
+ my-app/
14
+ ├── package.json # exactly one runtime dep: @madojs/mado
15
+ ├── tsconfig.json # strict TS, ES2022, Bundler resolution
16
+ ├── vite.config.ts # mado() from @madojs/mado/vite
17
+ ├── index.html # Vite entry + SPA shell
18
+ ├── public/ # static assets (favicons, images, robots.txt)
19
+ └── src/
20
+ ├── main.ts # entry: mount router into #app
21
+ ├── app.routes.ts # one app map (default + named `manifest`)
22
+ ├── pages/ # *.page.ts files
23
+ ├── components/ # reusable <x-tag> components
24
+ └── shared/ # http/, lib/, styles/, ui/
25
+ ```
26
+
27
+ `index.html` belongs at the project root because Vite treats it as an entry
28
+ template, not a static public file. Put only copy-as-is files in `public/`.
29
+
30
+ ### The three artifact states
31
+
32
+ | Folder | What it is | Who writes | Who reads | Deploy? |
33
+ | --------- | ----------------------------------------------------------- | --------------- | ----------------------- | ------------- |
34
+ | `src/` | your TypeScript source | you | Vite, `tsc --noEmit` | ❌ no |
35
+ | `public/` | static assets copied as-is | you | Vite build | ✅ via `out/` |
36
+ | `out/` | **the deploy artifact**: SPA shell + assets + snapshots | `mado release` | nginx / CDN / CF Pages | ✅ **yes** |
37
+
38
+ One-liner: develop with `mado dev`, ship with `mado release`, upload `out/`.
39
+
40
+ ### Naming rules
41
+
42
+ | What | Style | Example |
43
+ | -------------------------- | ------------------ | ---------------------- |
44
+ | File | kebab-case | `user-profile.ts` |
45
+ | Component tag | `x-` + kebab | `<x-user-profile>` |
46
+ | Context | PascalCase + `Ctx` | `ThemeCtx`, `AuthCtx` |
47
+ | Signal | camelCase | `userId`, `isLoggedIn` |
48
+ | Page-internal element | `x-<route>-page` | `<x-posts-page>` |
49
+
50
+ ### What does NOT belong in `src/`
51
+
52
+ - ❌ Build-tool configs beyond `vite.config.ts` with `mado()`.
53
+ - ❌ `.env` files — read env in `src/shared/lib/config.ts` from
54
+ `import.meta.env` and import that one module everywhere.
55
+ - ❌ Tests mixed with code — put them in `test/`.
56
+ - ❌ `examples/` folder — keep large demos outside the app repo.
57
+
58
+ ## File tree (modular reference starter)
8
59
 
9
60
  ```txt
10
61
  src/
@@ -68,7 +119,7 @@ export default routes(manifest);
68
119
 
69
120
  Rules:
70
121
 
71
- - Export `manifest` so `mado bake` can discover bakeable pages.
122
+ - Export `manifest` so `mado static` can discover bakeable pages.
72
123
  - Modules never call `layout()`.
73
124
  - Layouts describe app zones, not domains.
74
125
  - Do not hide the router inside a custom element or a second shell in
@@ -8,11 +8,12 @@
8
8
 
9
9
  ```
10
10
  out/
11
- ├── index.html ← SPA shell or baked HTML for /
11
+ ├── index.html ← captured snapshot for / (or SPA shell)
12
12
  ├── assets/ ← Vite hashed assets
13
13
  │ ├── *.gz ← precompressed gzip (gzip_static / Accept-Encoding)
14
14
  │ └── *.br ← precompressed brotli (brotli_static / Accept-Encoding)
15
- ├── <route>/index.html ← prerendered SEO HTML for baked routes
15
+ ├── <route>/index.html ← captured snapshot per static route
16
+ ├── _mado/spa.html ← SPA fallback shell (noindex)
16
17
  ├── sitemap.xml ← generated sitemap
17
18
  ├── favicon.svg ← your public/ assets copied verbatim
18
19
  ├── _redirects ← Cloudflare Pages / Netlify SPA fallback
@@ -89,8 +90,9 @@ Build command: npm ci && npx mado release
89
90
  Output directory: out
90
91
  ```
91
92
 
92
- For catalogs too big to bake at build time, keep edge prerender experiments in
93
- the external examples workspace rather than in the core package.
93
+ For catalogs too big to snapshot at release time, keep on-demand snapshot
94
+ experiments in the external examples workspace rather than in the core
95
+ package.
94
96
 
95
97
  ---
96
98
 
@@ -182,10 +184,57 @@ jobs:
182
184
  - **`/assets/*` files change but the browser keeps the old one.** They
183
185
  should not — the filename is hashed by Vite during `mado release`. If you bypassed
184
186
  build and shipped your own unhashed JS, give it a hash or short cache.
185
- - **Baked SEO page shows `[object Object]`.** Should never happen after the
186
- v1 bake update bake now raises a loud error in that case. If you see it,
187
- upgrade `@madojs/mado` and re-run `mado bake`.
187
+ - **Captured snapshot shows `[object Object]`.** `mado static` rejects
188
+ non-JSON-serialisable seeds at build time and points at the bad
189
+ field. If you still see it, upgrade `@madojs/mado` and re-run
190
+ `mado release`.
191
+
192
+ ## Sub-path deployments and `base`
193
+
194
+ A deployment under a sub-path (`https://your.site/docs/`, GitHub
195
+ Pages project URL, an admin panel mounted under `/admin/`, …) only
196
+ requires two changes:
197
+
198
+ ```ts
199
+ // vite.config.ts
200
+ import { defineConfig } from "vite";
201
+ import { mado } from "@madojs/mado/vite";
202
+
203
+ export default defineConfig({
204
+ base: "/docs/", // ← active prefix
205
+ plugins: [mado({ site: "https://your.site" })],
206
+ });
207
+ ```
208
+
209
+ Then make every internal anchor go through `routeUrl()` + `data-link`
210
+ so the URL is correct under any base:
211
+
212
+ ```ts
213
+ import { html, routeUrl } from "@madojs/mado";
214
+
215
+ html`<a data-link href=${routeUrl("/")}>Home</a>`; // → "/docs/"
216
+ html`<a data-link href=${routeUrl("/guides/intro")}>Intro</a>`;
217
+ ```
218
+
219
+ `mado release` honours `base` end-to-end:
220
+
221
+ - captured snapshots load assets through `/docs/assets/...`;
222
+ - sitemap URLs are `https://your.site/docs/...`;
223
+ - canonicals and `og:url` include the prefix;
224
+ - `mado preview` redirects bare `/` to `/docs/` and serves SPA
225
+ fallback under `/docs/<anything>`.
226
+
227
+ On the production host:
228
+
229
+ - **nginx** — keep `try_files $uri $uri/ /index.html;` but mount the
230
+ app under the matching prefix (`location /docs/ { ... }`).
231
+ - **Cloudflare Pages / Netlify** — `_redirects` is generated with the
232
+ base prefix already baked in.
233
+ - **GitHub Pages (project site)** — set `base: "/<repo>/"` and use
234
+ the `gh-pages` deploy path; canonical URLs land at
235
+ `https://<user>.github.io/<repo>/...`.
188
236
 
189
- See also: [`02-project-layout.md`](./02-project-layout.md) for the
190
- `src/`/`public/`/`out/` model and [`03-static-bake.md`](./03-static-bake.md)
191
- for the SEO bake mechanics.
237
+ See also: [`16-app-architecture.md`](./16-app-architecture.md) for the
238
+ `src/` / `public/` / `out/` model and
239
+ [`15-static-snapshots.md`](./15-static-snapshots.md) for the static
240
+ snapshot mechanics.
@@ -48,7 +48,7 @@ Test behavior, not internal implementation details:
48
48
  - route guards, redirects, scroll/focus behavior, error boundaries;
49
49
  - forms: validation, async validation races, field arrays;
50
50
  - resources/mutations: cache keys, invalidation, lifecycle cleanup;
51
- - CLI flows: `mado release`, `mado bake`, `mado preview`.
51
+ - CLI flows: `mado release`, `mado static`, `mado preview`.
52
52
 
53
53
  ## Browser smoke tests
54
54
 
@@ -74,9 +74,10 @@ services from framework tests. For app tests, make the API client injectable via
74
74
  npm run typecheck
75
75
  npm run build
76
76
  npm test
77
- npm run bake
77
+ npm run release
78
78
  ```
79
79
 
80
- `mado release` runs the production path for an app: typecheck, Vite build,
81
- bake, compression and deploy helper files. In the framework repository,
82
- `npm run build` still emits `dist/src` for package tests and publishing.
80
+ `mado release` runs the production path for an app: typecheck, Vite
81
+ build, static-snapshot capture, compression and deploy helper files.
82
+ In the framework repository, `npm run build` still emits `dist/src`
83
+ for package tests and publishing.