@madojs/mado 0.11.1 → 0.13.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 (298) hide show
  1. package/AGENTS.md +116 -32
  2. package/CHANGELOG.md +216 -1
  3. package/README.md +158 -173
  4. package/dist/src/component.d.ts +6 -3
  5. package/dist/src/component.d.ts.map +1 -0
  6. package/dist/src/component.js +163 -36
  7. package/dist/src/component.js.map +1 -1
  8. package/dist/src/context.d.ts +13 -32
  9. package/dist/src/context.d.ts.map +1 -0
  10. package/dist/src/context.js +73 -48
  11. package/dist/src/context.js.map +1 -1
  12. package/dist/src/css.d.ts +1 -0
  13. package/dist/src/css.d.ts.map +1 -0
  14. package/dist/src/css.js +34 -5
  15. package/dist/src/css.js.map +1 -1
  16. package/dist/src/devtools-hook.d.ts +14 -0
  17. package/dist/src/devtools-hook.d.ts.map +1 -0
  18. package/dist/src/devtools-hook.js +27 -0
  19. package/dist/src/devtools-hook.js.map +1 -0
  20. package/dist/src/devtools.d.ts +24 -22
  21. package/dist/src/devtools.d.ts.map +1 -0
  22. package/dist/src/devtools.js +230 -58
  23. package/dist/src/devtools.js.map +1 -1
  24. package/dist/src/diagnostics.d.ts +12 -0
  25. package/dist/src/diagnostics.d.ts.map +1 -0
  26. package/dist/src/diagnostics.js +68 -8
  27. package/dist/src/diagnostics.js.map +1 -1
  28. package/dist/src/each.d.ts +1 -5
  29. package/dist/src/each.d.ts.map +1 -0
  30. package/dist/src/each.js +0 -8
  31. package/dist/src/each.js.map +1 -1
  32. package/dist/src/forms.d.ts +27 -96
  33. package/dist/src/forms.d.ts.map +1 -0
  34. package/dist/src/forms.js +184 -394
  35. package/dist/src/forms.js.map +1 -1
  36. package/dist/src/head.d.ts +7 -6
  37. package/dist/src/head.d.ts.map +1 -0
  38. package/dist/src/head.js +8 -7
  39. package/dist/src/head.js.map +1 -1
  40. package/dist/src/html/bindings.d.ts +1 -0
  41. package/dist/src/html/bindings.d.ts.map +1 -0
  42. package/dist/src/html/bindings.js.map +1 -1
  43. package/dist/src/html/parser.d.ts +4 -5
  44. package/dist/src/html/parser.d.ts.map +1 -0
  45. package/dist/src/html/parser.js +64 -15
  46. package/dist/src/html/parser.js.map +1 -1
  47. package/dist/src/html/template-types.d.ts +1 -0
  48. package/dist/src/html/template-types.d.ts.map +1 -0
  49. package/dist/src/html/template-types.js.map +1 -1
  50. package/dist/src/html/template.d.ts +5 -1
  51. package/dist/src/html/template.d.ts.map +1 -0
  52. package/dist/src/html/template.js +102 -13
  53. package/dist/src/html/template.js.map +1 -1
  54. package/dist/src/index.d.ts +15 -12
  55. package/dist/src/index.d.ts.map +1 -0
  56. package/dist/src/index.js +18 -10
  57. package/dist/src/index.js.map +1 -1
  58. package/dist/src/json.d.ts +3 -0
  59. package/dist/src/json.d.ts.map +1 -0
  60. package/dist/src/json.js +14 -0
  61. package/dist/src/json.js.map +1 -0
  62. package/dist/src/lifecycle.d.ts +1 -0
  63. package/dist/src/lifecycle.d.ts.map +1 -0
  64. package/dist/src/lifecycle.js +3 -2
  65. package/dist/src/lifecycle.js.map +1 -1
  66. package/dist/src/page.d.ts +63 -28
  67. package/dist/src/page.d.ts.map +1 -0
  68. package/dist/src/page.js +5 -0
  69. package/dist/src/page.js.map +1 -1
  70. package/dist/src/persisted.d.ts +6 -1
  71. package/dist/src/persisted.d.ts.map +1 -0
  72. package/dist/src/persisted.js +17 -11
  73. package/dist/src/persisted.js.map +1 -1
  74. package/dist/src/resource.d.ts +2 -1
  75. package/dist/src/resource.d.ts.map +1 -0
  76. package/dist/src/resource.js +128 -50
  77. package/dist/src/resource.js.map +1 -1
  78. package/dist/src/router/base.d.ts +66 -0
  79. package/dist/src/router/base.d.ts.map +1 -0
  80. package/dist/src/router/base.js +168 -0
  81. package/dist/src/router/base.js.map +1 -0
  82. package/dist/src/router/manifest.d.ts +1 -0
  83. package/dist/src/router/manifest.d.ts.map +1 -0
  84. package/dist/src/router/manifest.js +156 -171
  85. package/dist/src/router/manifest.js.map +1 -1
  86. package/dist/src/router/match.d.ts +17 -2
  87. package/dist/src/router/match.d.ts.map +1 -0
  88. package/dist/src/router/match.js +60 -6
  89. package/dist/src/router/match.js.map +1 -1
  90. package/dist/src/router/navigation.d.ts +1 -0
  91. package/dist/src/router/navigation.d.ts.map +1 -0
  92. package/dist/src/router/navigation.js +58 -9
  93. package/dist/src/router/navigation.js.map +1 -1
  94. package/dist/src/signal.d.ts +1 -0
  95. package/dist/src/signal.d.ts.map +1 -0
  96. package/dist/src/signal.js +46 -17
  97. package/dist/src/signal.js.map +1 -1
  98. package/dist/src/static-runtime.d.ts +82 -0
  99. package/dist/src/static-runtime.d.ts.map +1 -0
  100. package/dist/src/static-runtime.js +209 -0
  101. package/dist/src/static-runtime.js.map +1 -0
  102. package/dist/src/vite/index.d.ts +32 -3
  103. package/dist/src/vite/index.d.ts.map +1 -0
  104. package/dist/src/vite/index.js +49 -17
  105. package/dist/src/vite/index.js.map +1 -1
  106. package/docs/README.md +5 -8
  107. package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
  108. package/docs/architecture/v1-roadmap.md +19 -0
  109. package/docs/en/00-the-mado-way.md +1 -1
  110. package/docs/en/01-quickstart.md +183 -0
  111. package/docs/en/10-pages-and-components.md +279 -0
  112. package/docs/en/11-templates-and-signals.md +211 -0
  113. package/docs/en/12-routing.md +229 -0
  114. package/docs/en/13-data.md +227 -0
  115. package/docs/en/14-forms.md +91 -0
  116. package/docs/en/15-static-snapshots.md +181 -0
  117. package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +56 -4
  118. package/docs/en/{13-deployment.md → 20-deployment.md} +79 -17
  119. package/docs/en/{15-error-handling.md → 21-error-handling.md} +9 -6
  120. package/docs/en/{14-testing.md → 22-testing.md} +6 -5
  121. package/docs/en/23-cookbook.md +292 -0
  122. package/docs/en/24-devtools-and-diagnostics.md +45 -0
  123. package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +17 -9
  124. package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +5 -5
  125. package/docs/en/33-migration-0.12-0.13.md +56 -0
  126. package/docs/en/40-llm-guide.md +774 -0
  127. package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +28 -25
  128. package/docs/en/{05-why-mado.md → 42-why-mado.md} +3 -3
  129. package/docs/en/README.md +99 -27
  130. package/docs/recipes/nginx/Containerfile +26 -0
  131. package/docs/recipes/nginx/nginx.conf +42 -0
  132. package/llms.txt +263 -210
  133. package/package.json +51 -23
  134. package/scripts/clean.mjs +8 -0
  135. package/scripts/cli/generate.mjs +57 -10
  136. package/scripts/cli/help.mjs +13 -5
  137. package/scripts/cli/index.mjs +31 -7
  138. package/scripts/cli/init.mjs +19 -19
  139. package/scripts/cli/release.mjs +38 -83
  140. package/scripts/cli/run.mjs +5 -3
  141. package/scripts/logger.mjs +117 -0
  142. package/scripts/output-guard.mjs +128 -0
  143. package/scripts/preview.mjs +105 -24
  144. package/scripts/static/browser.mjs +695 -0
  145. package/scripts/static/discover.mjs +284 -0
  146. package/scripts/static/output.mjs +141 -0
  147. package/scripts/static/serialize.mjs +212 -0
  148. package/scripts/static/server.mjs +167 -0
  149. package/scripts/static.mjs +205 -0
  150. package/starters/default/README.md +57 -55
  151. package/starters/default/package.json +6 -12
  152. package/starters/default/src/app.routes.ts +20 -33
  153. package/starters/default/src/components/feature-card.component.ts +40 -0
  154. package/starters/default/src/components/live-counter.component.ts +57 -0
  155. package/starters/default/src/content/guides.ts +55 -0
  156. package/starters/default/src/main.ts +9 -13
  157. package/starters/default/src/pages/app.page.ts +41 -0
  158. package/starters/default/src/pages/guide.page.ts +40 -0
  159. package/starters/default/src/pages/home.page.ts +49 -0
  160. package/starters/default/src/pages/not-found.page.ts +18 -0
  161. package/starters/default/src/styles/document.css +39 -0
  162. package/starters/default/src/styles/reset.css +37 -0
  163. package/starters/default/src/styles/tokens.css +30 -0
  164. package/starters/default/src/styles.d.ts +3 -1
  165. package/starters/default/src/vite-env.d.ts +1 -1
  166. package/starters/default/tsconfig.node.json +13 -0
  167. package/starters/default/vite.config.ts +14 -4
  168. package/starters/modular/README.md +142 -0
  169. package/starters/modular/index.html +13 -0
  170. package/starters/modular/package.json +30 -0
  171. package/starters/modular/src/app.routes.ts +39 -0
  172. package/starters/{default → modular}/src/layouts/app-shell.layout.ts +9 -7
  173. package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
  174. package/starters/modular/src/main.ts +16 -0
  175. package/starters/{default → modular}/src/modules/auth/login.page.ts +5 -12
  176. package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +3 -4
  177. package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +4 -4
  178. package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
  179. package/starters/modular/src/modules/home/not-found.page.ts +14 -0
  180. package/starters/{default → modular}/src/shared/styles/reset.css +21 -1
  181. package/starters/modular/src/styles.d.ts +1 -0
  182. package/starters/modular/src/vite-env.d.ts +1 -0
  183. package/starters/modular/tsconfig.json +33 -0
  184. package/starters/modular/tsconfig.node.json +13 -0
  185. package/starters/modular/vite.config.ts +132 -0
  186. package/TODO.md +0 -79
  187. package/dist/src/lazy.d.ts +0 -38
  188. package/dist/src/lazy.js +0 -73
  189. package/dist/src/lazy.js.map +0 -1
  190. package/docs/en/01-routing.md +0 -152
  191. package/docs/en/02-project-layout.md +0 -124
  192. package/docs/en/03-static-bake.md +0 -249
  193. package/docs/en/04-ide-setup.md +0 -162
  194. package/docs/en/07-llm-pitfalls.md +0 -724
  195. package/docs/en/08-llm-zero-history-test.md +0 -56
  196. package/docs/en/09-shadow-vs-light-dom.md +0 -174
  197. package/docs/en/11-layouts.md +0 -113
  198. package/docs/en/12-auth-and-api.md +0 -124
  199. package/docs/en/16-bake-cookbook.md +0 -100
  200. package/docs/en/17-shadow-dom-forms.md +0 -192
  201. package/docs/fr/00-the-mado-way.md +0 -85
  202. package/docs/fr/01-routing.md +0 -120
  203. package/docs/fr/02-project-layout.md +0 -84
  204. package/docs/fr/03-static-bake.md +0 -289
  205. package/docs/fr/04-ide-setup.md +0 -162
  206. package/docs/fr/05-why-mado.md +0 -193
  207. package/docs/fr/06-for-backenders.md +0 -438
  208. package/docs/fr/07-llm-pitfalls.md +0 -625
  209. package/docs/fr/08-llm-zero-history-test.md +0 -38
  210. package/docs/fr/09-shadow-vs-light-dom.md +0 -65
  211. package/docs/fr/10-app-architecture.md +0 -138
  212. package/docs/fr/11-layouts.md +0 -47
  213. package/docs/fr/12-auth-and-api.md +0 -76
  214. package/docs/fr/13-deployment.md +0 -57
  215. package/docs/fr/14-testing.md +0 -41
  216. package/docs/fr/15-error-handling.md +0 -50
  217. package/docs/fr/16-bake-cookbook.md +0 -88
  218. package/docs/fr/17-shadow-dom-forms.md +0 -196
  219. package/docs/fr/18-api-freeze-map.md +0 -63
  220. package/docs/fr/19-reactivity-ordering.md +0 -97
  221. package/docs/fr/20-v1-stability.md +0 -88
  222. package/docs/fr/README.md +0 -27
  223. package/docs/ru/00-the-mado-way.md +0 -84
  224. package/docs/ru/01-routing.md +0 -119
  225. package/docs/ru/02-project-layout.md +0 -84
  226. package/docs/ru/03-static-bake.md +0 -250
  227. package/docs/ru/04-ide-setup.md +0 -144
  228. package/docs/ru/05-why-mado.md +0 -193
  229. package/docs/ru/06-for-backenders.md +0 -428
  230. package/docs/ru/07-llm-pitfalls.md +0 -624
  231. package/docs/ru/08-llm-zero-history-test.md +0 -57
  232. package/docs/ru/09-shadow-vs-light-dom.md +0 -63
  233. package/docs/ru/10-app-architecture.md +0 -152
  234. package/docs/ru/11-layouts.md +0 -47
  235. package/docs/ru/12-auth-and-api.md +0 -75
  236. package/docs/ru/13-deployment.md +0 -66
  237. package/docs/ru/14-testing.md +0 -50
  238. package/docs/ru/15-error-handling.md +0 -56
  239. package/docs/ru/16-bake-cookbook.md +0 -95
  240. package/docs/ru/17-shadow-dom-forms.md +0 -193
  241. package/docs/ru/18-api-freeze-map.md +0 -64
  242. package/docs/ru/19-reactivity-ordering.md +0 -95
  243. package/docs/ru/20-v1-stability.md +0 -82
  244. package/docs/ru/README.md +0 -25
  245. package/docs/uk/00-the-mado-way.md +0 -82
  246. package/docs/uk/01-routing.md +0 -76
  247. package/docs/uk/02-project-layout.md +0 -73
  248. package/docs/uk/03-static-bake.md +0 -48
  249. package/docs/uk/04-ide-setup.md +0 -26
  250. package/docs/uk/05-why-mado.md +0 -34
  251. package/docs/uk/06-for-backenders.md +0 -55
  252. package/docs/uk/07-llm-pitfalls.md +0 -145
  253. package/docs/uk/08-llm-zero-history-test.md +0 -34
  254. package/docs/uk/09-shadow-vs-light-dom.md +0 -58
  255. package/docs/uk/10-app-architecture.md +0 -97
  256. package/docs/uk/11-layouts.md +0 -47
  257. package/docs/uk/12-auth-and-api.md +0 -70
  258. package/docs/uk/13-deployment.md +0 -40
  259. package/docs/uk/14-testing.md +0 -34
  260. package/docs/uk/15-error-handling.md +0 -32
  261. package/docs/uk/16-bake-cookbook.md +0 -36
  262. package/docs/uk/17-shadow-dom-forms.md +0 -193
  263. package/docs/uk/18-api-freeze-map.md +0 -61
  264. package/docs/uk/19-reactivity-ordering.md +0 -95
  265. package/docs/uk/20-v1-stability.md +0 -83
  266. package/docs/uk/README.md +0 -27
  267. package/scripts/bake.mjs +0 -570
  268. package/scripts/package-smoke.mjs +0 -77
  269. package/scripts/release-notes.mjs +0 -66
  270. package/scripts/size-budget.mjs +0 -91
  271. package/starters/default/src/modules/home/not-found.page.ts +0 -11
  272. package/starters/default/src/shared/ui/x-button.component.ts +0 -49
  273. /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
  274. /package/starters/{default → modular}/.editorconfig +0 -0
  275. /package/starters/{default → modular}/eslint.config.mjs +0 -0
  276. /package/starters/{default → modular}/public/favicon.svg +0 -0
  277. /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
  278. /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
  279. /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
  280. /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
  281. /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
  282. /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
  283. /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
  284. /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
  285. /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
  286. /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
  287. /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
  288. /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
  289. /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
  290. /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
  291. /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
  292. /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
  293. /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
  294. /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
  295. /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
  296. /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
  297. /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
  298. /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
@@ -0,0 +1,91 @@
1
+ # Forms
2
+
3
+ `useForm()` adds reactive state to the browser's form and constraint-validation
4
+ model. HTML remains the source of truth for `required`, `type`, `min`, `max`,
5
+ `minlength`, `maxlength` and `pattern`.
6
+
7
+ ```ts
8
+ import { html, useForm } from "@madojs/mado";
9
+
10
+ const form = useForm({
11
+ initial: { email: "", age: "" as number | "", newsletter: false },
12
+ validate: async (values, { signal }) => {
13
+ const available = await api.emailAvailable(values.email, { signal });
14
+ return available ? null : { email: "Email is already registered" };
15
+ },
16
+ });
17
+
18
+ html`
19
+ <form @submit=${form.onSubmit(async (values, event) => {
20
+ await api.save(values, new FormData(event.currentTarget as HTMLFormElement));
21
+ })}>
22
+ <label>
23
+ Email
24
+ <input name="email" type="email" required
25
+ @input=${form.onInput} @blur=${form.onBlur} />
26
+ </label>
27
+ ${() => form.touched().email && form.errors().email
28
+ ? html`<small role="alert">${form.errors().email}</small>`
29
+ : null}
30
+
31
+ <input name="age" type="number" min="18" max="120" @input=${form.onInput} />
32
+ <label><input name="newsletter" type="checkbox" @input=${form.onInput} /> Subscribe</label>
33
+ <button type="submit" ?disabled=${() => form.submitting() || form.validating()}>
34
+ Save
35
+ </button>
36
+ </form>
37
+ `;
38
+ ```
39
+
40
+ ## API
41
+
42
+ `useForm<T>({ initial, validate? })` returns:
43
+
44
+ | Member | Meaning |
45
+ | --- | --- |
46
+ | `values()` | current typed values |
47
+ | `errors()` | native and custom errors by field; `$form` is available for form-wide errors |
48
+ | `touched()` | fields that have blurred or participated in submit |
49
+ | `dirty()` | whether values differ from the current initial values |
50
+ | `isValid()` | whether the merged error map is empty |
51
+ | `submitting()` / `validating()` | current async activity |
52
+ | `setField(name, value)` | update one field programmatically |
53
+ | `validate(form?)` | run native and custom validation; returns `Promise<boolean>` |
54
+ | `reset(nextInitial?)` | abort validation, clear state and reset native controls |
55
+ | `onInput`, `onBlur`, `onSubmit(handler)` | DOM event handlers |
56
+
57
+ Custom validation receives `{ signal, form }`. Starting another validation or
58
+ calling `reset()` aborts the previous run. Treat abort as normal cancellation.
59
+
60
+ ## Native controls
61
+
62
+ - Checkbox groups produce an array when the initial field value is an array;
63
+ a single checkbox produces a boolean.
64
+ - Radio groups produce the checked value.
65
+ - `<select multiple>` produces a string array.
66
+ - Number/range inputs produce a number, or `""` when empty.
67
+ - File inputs produce `File | null`, or `File[]` when `multiple` is present.
68
+ - The submit callback receives the native `SubmitEvent`; construct `FormData`
69
+ from its form when the backend expects multipart or repeated fields.
70
+
71
+ Use a native `<button type="submit">`. A button inside another element's
72
+ Shadow DOM is not a submit control for the outer form and keyboard submission
73
+ will not discover it.
74
+
75
+ ## Mutations
76
+
77
+ ```ts
78
+ const save = mutation((values: Profile) => api.save(values), {
79
+ invalidates: ["/api/profile"],
80
+ });
81
+ const form = useForm({ initial: { displayName: "" } });
82
+
83
+ html`<form @submit=${form.onSubmit((values) => save.run(values))}>
84
+ <input name="displayName" required minlength="2" @input=${form.onInput} />
85
+ <button type="submit" ?disabled=${() => form.submitting()}>Save</button>
86
+ </form>`;
87
+ ```
88
+
89
+ Write failures belong to the mutation; field and cross-field validation belong
90
+ to the form. See the [0.12 → 0.13 migration guide](./33-migration-0.12-0.13.md)
91
+ for the removed schema and field-array APIs.
@@ -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 must target the internal capture origin** serving the
155
+ freshly built `out/` tree. Undeclared external HTTP(S) requests are blocked;
156
+ provide deterministic `static.initialData()` instead.
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
@@ -139,8 +190,9 @@ Reusable leaf components keep their own styles in ``css`...` `` inside
139
190
  `component()` options. They should depend on tokens (`var(--color-text)`)
140
191
  rather than global classes.
141
192
 
142
- `vite.config.ts` opts into Vite's Lightning CSS transformer. Mado does not own
143
- prefixing, CSS lowering or minification.
193
+ Vite 8's standard CSS pipeline owns transforms and minification. Mado adds no
194
+ CSS transformer; applications can configure Vite directly when they have a
195
+ measured need.
144
196
 
145
197
  ## Growth
146
198
 
@@ -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
@@ -61,7 +62,7 @@ Key lines of the recipe `nginx.conf`:
61
62
  - `gzip_static on;` — serves the precompressed `.gz` files written by
62
63
  `mado release`. Zero CPU at request time.
63
64
  - `/assets/*` should be cached immutable; Vite filenames are content hashed.
64
- - `try_files $uri $uri/ /index.html;` — SPA fallback so deep links work
65
+ - `try_files $uri $uri/ /_mado/spa.html;` — SPA fallback so deep links work
65
66
  after a hard refresh.
66
67
 
67
68
  Enable HTTPS with Let's Encrypt / Certbot. Add HSTS once you have it.
@@ -75,7 +76,7 @@ mado release
75
76
  npx wrangler pages deploy out --project-name=myapp
76
77
  ```
77
78
 
78
- - The generated `_redirects` (`/* /index.html 200`) gives you SPA fallback.
79
+ - The generated `_redirects` (`/* /_mado/spa.html 200`) gives you SPA fallback.
79
80
  - The generated `_headers` (immutable cache for `/assets/*`, `no-cache` for
80
81
  HTML) is honored by CF Pages.
81
82
  - Baked routes are promoted to real route files (`out/<route>/index.html`),
@@ -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
 
@@ -114,16 +116,15 @@ aws s3 sync out/ s3://my-bucket/ \
114
116
  --cache-control "no-cache, must-revalidate" --include '*.html'
115
117
  ```
116
118
  Configure CloudFront's "Default root object" to `index.html` and add a custom
117
- error response: 403/404 → `/index.html` with status 200 (SPA fallback).
119
+ error response: 403/404 → `/_mado/spa.html` with status 200 (SPA fallback).
118
120
 
119
121
  **GitHub Pages**
120
122
  ```bash
121
123
  mado release
122
124
  # Push out/ into the gh-pages branch (or use actions/upload-pages-artifact)
123
125
  ```
124
- Pages handles `index.html` automatically. There is no native SPA fallback;
125
- add a `404.html` that loads the SPA, or use the
126
- [`spa-github-pages`](https://github.com/rafgraph/spa-github-pages) trick.
126
+ Pages handles route `index.html` files automatically. `mado release` also writes
127
+ `404.html` from the noindex SPA shell for client-only routes.
127
128
 
128
129
  ---
129
130
 
@@ -175,17 +176,78 @@ jobs:
175
176
 
176
177
  - **404 on hard refresh of a deep link.** Your host did not pick up SPA
177
178
  fallback. nginx: check `try_files`. CF/Netlify: `_redirects` is present?
178
- S3+CloudFront: configure the 404 → `/index.html` (200) error response.
179
+ S3+CloudFront: configure the 404 → `/_mado/spa.html` (200) error response.
179
180
  - **HTML is cached forever.** Either your host sent a default
180
181
  `Cache-Control: public, max-age=...` or you are sitting behind a CDN that
181
182
  ignores `no-cache`. Add an explicit rule mirroring the matrix above.
182
183
  - **`/assets/*` files change but the browser keeps the old one.** They
183
184
  should not — the filename is hashed by Vite during `mado release`. If you bypassed
184
185
  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`.
186
+ - **Captured snapshot shows `[object Object]`.** `mado static` rejects
187
+ non-JSON-serialisable seeds at build time and points at the bad
188
+ field. If you still see it, upgrade `@madojs/mado` and re-run
189
+ `mado release`.
190
+
191
+ ## CSP and trusted content
192
+
193
+ Mado serialises JSON-LD and static seed JSON with `<`, U+2028 and U+2029
194
+ escaped, so data cannot close its script element. `unsafeHTML()` is different:
195
+ it is an explicit trust boundary and must receive only audited or sanitised
196
+ markup.
197
+
198
+ Start with a restrictive policy such as `default-src 'self'; script-src
199
+ 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'`
200
+ and tighten styles with nonces/hashes when your deployment supports them.
201
+ Trusted Types can enforce the same application boundary around any code that
202
+ produces trusted HTML. The devtools overlay is development-only and should not
203
+ be imported in production entrypoints.
204
+
205
+ ## Sub-path deployments and `base`
206
+
207
+ A deployment under a sub-path (`https://your.site/docs/`, GitHub
208
+ Pages project URL, an admin panel mounted under `/admin/`, …) only
209
+ requires two changes:
210
+
211
+ ```ts
212
+ // vite.config.ts
213
+ import { defineConfig } from "vite";
214
+ import { mado } from "@madojs/mado/vite";
215
+
216
+ export default defineConfig({
217
+ base: "/docs/", // ← active prefix
218
+ plugins: [mado({ site: "https://your.site" })],
219
+ });
220
+ ```
221
+
222
+ Then make every internal anchor go through `routeUrl()` + `data-link`
223
+ so the URL is correct under any base:
224
+
225
+ ```ts
226
+ import { html, routeUrl } from "@madojs/mado";
227
+
228
+ html`<a data-link href=${routeUrl("/")}>Home</a>`; // → "/docs/"
229
+ html`<a data-link href=${routeUrl("/guides/intro")}>Intro</a>`;
230
+ ```
231
+
232
+ `mado release` honours `base` end-to-end:
233
+
234
+ - captured snapshots load assets through `/docs/assets/...`;
235
+ - sitemap URLs are `https://your.site/docs/...`;
236
+ - canonicals and `og:url` include the prefix;
237
+ - `mado preview` redirects bare `/` to `/docs/` and serves SPA
238
+ fallback under `/docs/<anything>`.
239
+
240
+ On the production host:
241
+
242
+ - **nginx** — keep `try_files $uri $uri/ /_mado/spa.html;` but mount the
243
+ app under the matching prefix (`location /docs/ { ... }`).
244
+ - **Cloudflare Pages / Netlify** — `_redirects` is generated with the
245
+ base prefix already baked in.
246
+ - **GitHub Pages (project site)** — set `base: "/<repo>/"` and use
247
+ the `gh-pages` deploy path; canonical URLs land at
248
+ `https://<user>.github.io/<repo>/...`.
188
249
 
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.
250
+ See also: [`16-app-architecture.md`](./16-app-architecture.md) for the
251
+ `src/` / `public/` / `out/` model and
252
+ [`15-static-snapshots.md`](./15-static-snapshots.md) for the static
253
+ snapshot mechanics.
@@ -24,7 +24,7 @@ For a specific page, `page({ errorView })` wins over the global route boundary.
24
24
 
25
25
  ```ts
26
26
  export default page({
27
- load: async () => api.get("/reports"),
27
+ load: () => resource(() => "/reports", jsonFetcher<Report[]>()),
28
28
  errorView: (err) => html`<x-report-error .error=${err}></x-report-error>`,
29
29
  view: ({ data }) => html`<x-report .data=${data}></x-report>`,
30
30
  });
@@ -53,10 +53,10 @@ Validation errors belong in `useForm()`. Server errors from writes belong near
53
53
  the submit button.
54
54
 
55
55
  ```ts
56
- const form = useForm(
57
- { email: { required: true, type: "email" } },
58
- { validateAsync: (values) => api.validateUser(values) },
59
- );
56
+ const form = useForm({
57
+ initial: { email: "" },
58
+ validate: (values, { signal }) => api.validateUser(values, { signal }),
59
+ });
60
60
 
61
61
  const save = mutation((values) => api.post("/users", values), {
62
62
  invalidates: ["/api/users*"],
@@ -66,6 +66,7 @@ html`
66
66
  <form @submit=${form.onSubmit(async values => {
67
67
  await save.run(values);
68
68
  })}>
69
+ <input name="email" type="email" required @input=${form.onInput} />
69
70
  <button ?disabled=${() => form.validating() || form.submitting()}>
70
71
  Save
71
72
  </button>
@@ -97,4 +98,6 @@ component("x-online", (ctx) => {
97
98
 
98
99
  Log once at the boundary that owns recovery. Avoid logging the same failure in
99
100
  the API client, resource, page and component. The user should get one visible
100
- message and developers should get one useful console error.
101
+ message and developers should get one structured diagnostic. Runtime records
102
+ are visible in the devtools Timeline/Errors panel and as `mado:diagnostic`
103
+ events; CLI automation can select `--log-format=json`.
@@ -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.