@madojs/mado 0.11.0 → 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 (221) hide show
  1. package/AGENTS.md +102 -25
  2. package/CHANGELOG.md +185 -1
  3. package/README.md +137 -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/bindings.js +3 -3
  12. package/dist/src/html/bindings.js.map +1 -1
  13. package/dist/src/html/parser.d.ts +1 -1
  14. package/dist/src/html/parser.js +1 -1
  15. package/dist/src/html/template.js +24 -10
  16. package/dist/src/html/template.js.map +1 -1
  17. package/dist/src/index.d.ts +8 -4
  18. package/dist/src/index.js +13 -3
  19. package/dist/src/index.js.map +1 -1
  20. package/dist/src/page.d.ts +62 -28
  21. package/dist/src/page.js +5 -0
  22. package/dist/src/page.js.map +1 -1
  23. package/dist/src/resource.js +2 -1
  24. package/dist/src/resource.js.map +1 -1
  25. package/dist/src/router/base.d.ts +65 -0
  26. package/dist/src/router/base.js +168 -0
  27. package/dist/src/router/base.js.map +1 -0
  28. package/dist/src/router/manifest.js +75 -31
  29. package/dist/src/router/manifest.js.map +1 -1
  30. package/dist/src/router/match.d.ts +16 -2
  31. package/dist/src/router/match.js +41 -1
  32. package/dist/src/router/match.js.map +1 -1
  33. package/dist/src/router/navigation.js +58 -9
  34. package/dist/src/router/navigation.js.map +1 -1
  35. package/dist/src/static-runtime.d.ts +81 -0
  36. package/dist/src/static-runtime.js +209 -0
  37. package/dist/src/static-runtime.js.map +1 -0
  38. package/dist/src/vite/index.d.ts +31 -3
  39. package/dist/src/vite/index.js +49 -18
  40. package/dist/src/vite/index.js.map +1 -1
  41. package/docs/README.md +5 -8
  42. package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
  43. package/docs/en/00-the-mado-way.md +1 -1
  44. package/docs/en/01-quickstart.md +183 -0
  45. package/docs/en/10-pages-and-components.md +271 -0
  46. package/docs/en/11-templates-and-signals.md +211 -0
  47. package/docs/en/12-routing.md +229 -0
  48. package/docs/en/13-data.md +225 -0
  49. package/docs/en/14-forms.md +244 -0
  50. package/docs/en/15-static-snapshots.md +181 -0
  51. package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
  52. package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
  53. package/docs/en/{14-testing.md → 22-testing.md} +6 -5
  54. package/docs/en/23-cookbook.md +292 -0
  55. package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
  56. package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
  57. package/docs/en/40-llm-guide.md +776 -0
  58. package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
  59. package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
  60. package/docs/en/README.md +97 -27
  61. package/docs/recipes/nginx/Containerfile +26 -0
  62. package/docs/recipes/nginx/nginx.conf +42 -0
  63. package/llms.txt +246 -211
  64. package/package.json +17 -10
  65. package/scripts/bake.mjs +4 -568
  66. package/scripts/cli/generate.mjs +55 -5
  67. package/scripts/cli/help.mjs +11 -5
  68. package/scripts/cli/index.mjs +20 -4
  69. package/scripts/cli/init.mjs +7 -2
  70. package/scripts/cli/release.mjs +20 -68
  71. package/scripts/docs-lint.mjs +170 -0
  72. package/scripts/package-smoke.mjs +67 -9
  73. package/scripts/preview.mjs +95 -6
  74. package/scripts/size-budget.mjs +5 -1
  75. package/scripts/static/browser.mjs +654 -0
  76. package/scripts/static/discover.mjs +281 -0
  77. package/scripts/static/output.mjs +141 -0
  78. package/scripts/static/serialize.mjs +212 -0
  79. package/scripts/static/server.mjs +167 -0
  80. package/scripts/static.mjs +191 -0
  81. package/starters/default/README.md +57 -55
  82. package/starters/default/package.json +3 -9
  83. package/starters/default/src/app.routes.ts +20 -33
  84. package/starters/default/src/components/feature-card.component.ts +40 -0
  85. package/starters/default/src/components/live-counter.component.ts +57 -0
  86. package/starters/default/src/content/guides.ts +55 -0
  87. package/starters/default/src/main.ts +9 -13
  88. package/starters/default/src/pages/app.page.ts +41 -0
  89. package/starters/default/src/pages/guide.page.ts +40 -0
  90. package/starters/default/src/pages/home.page.ts +49 -0
  91. package/starters/default/src/pages/not-found.page.ts +18 -0
  92. package/starters/default/src/styles/document.css +39 -0
  93. package/starters/default/src/styles/reset.css +37 -0
  94. package/starters/default/src/styles/tokens.css +30 -0
  95. package/starters/default/src/styles.d.ts +3 -1
  96. package/starters/default/src/vite-env.d.ts +1 -1
  97. package/starters/default/vite.config.ts +14 -1
  98. package/starters/modular/README.md +142 -0
  99. package/starters/modular/index.html +13 -0
  100. package/starters/modular/package.json +30 -0
  101. package/starters/modular/src/app.routes.ts +39 -0
  102. package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
  103. package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
  104. package/starters/modular/src/main.ts +16 -0
  105. package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
  106. package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
  107. package/starters/modular/src/modules/home/not-found.page.ts +14 -0
  108. package/starters/modular/src/styles.d.ts +1 -0
  109. package/starters/modular/src/vite-env.d.ts +1 -0
  110. package/starters/modular/tsconfig.json +24 -0
  111. package/starters/modular/vite.config.ts +131 -0
  112. package/TODO.md +0 -79
  113. package/docs/en/01-routing.md +0 -152
  114. package/docs/en/02-project-layout.md +0 -124
  115. package/docs/en/03-static-bake.md +0 -249
  116. package/docs/en/04-ide-setup.md +0 -162
  117. package/docs/en/07-llm-pitfalls.md +0 -724
  118. package/docs/en/08-llm-zero-history-test.md +0 -56
  119. package/docs/en/09-shadow-vs-light-dom.md +0 -174
  120. package/docs/en/11-layouts.md +0 -113
  121. package/docs/en/12-auth-and-api.md +0 -124
  122. package/docs/en/16-bake-cookbook.md +0 -100
  123. package/docs/en/17-shadow-dom-forms.md +0 -192
  124. package/docs/fr/00-the-mado-way.md +0 -85
  125. package/docs/fr/01-routing.md +0 -120
  126. package/docs/fr/02-project-layout.md +0 -84
  127. package/docs/fr/03-static-bake.md +0 -289
  128. package/docs/fr/04-ide-setup.md +0 -162
  129. package/docs/fr/05-why-mado.md +0 -193
  130. package/docs/fr/06-for-backenders.md +0 -438
  131. package/docs/fr/07-llm-pitfalls.md +0 -625
  132. package/docs/fr/08-llm-zero-history-test.md +0 -38
  133. package/docs/fr/09-shadow-vs-light-dom.md +0 -65
  134. package/docs/fr/10-app-architecture.md +0 -138
  135. package/docs/fr/11-layouts.md +0 -47
  136. package/docs/fr/12-auth-and-api.md +0 -76
  137. package/docs/fr/13-deployment.md +0 -57
  138. package/docs/fr/14-testing.md +0 -41
  139. package/docs/fr/15-error-handling.md +0 -50
  140. package/docs/fr/16-bake-cookbook.md +0 -88
  141. package/docs/fr/17-shadow-dom-forms.md +0 -196
  142. package/docs/fr/18-api-freeze-map.md +0 -63
  143. package/docs/fr/19-reactivity-ordering.md +0 -97
  144. package/docs/fr/20-v1-stability.md +0 -88
  145. package/docs/fr/README.md +0 -27
  146. package/docs/ru/00-the-mado-way.md +0 -84
  147. package/docs/ru/01-routing.md +0 -119
  148. package/docs/ru/02-project-layout.md +0 -84
  149. package/docs/ru/03-static-bake.md +0 -250
  150. package/docs/ru/04-ide-setup.md +0 -144
  151. package/docs/ru/05-why-mado.md +0 -193
  152. package/docs/ru/06-for-backenders.md +0 -428
  153. package/docs/ru/07-llm-pitfalls.md +0 -624
  154. package/docs/ru/08-llm-zero-history-test.md +0 -57
  155. package/docs/ru/09-shadow-vs-light-dom.md +0 -63
  156. package/docs/ru/10-app-architecture.md +0 -152
  157. package/docs/ru/11-layouts.md +0 -47
  158. package/docs/ru/12-auth-and-api.md +0 -75
  159. package/docs/ru/13-deployment.md +0 -66
  160. package/docs/ru/14-testing.md +0 -50
  161. package/docs/ru/15-error-handling.md +0 -56
  162. package/docs/ru/16-bake-cookbook.md +0 -95
  163. package/docs/ru/17-shadow-dom-forms.md +0 -193
  164. package/docs/ru/18-api-freeze-map.md +0 -64
  165. package/docs/ru/19-reactivity-ordering.md +0 -95
  166. package/docs/ru/20-v1-stability.md +0 -82
  167. package/docs/ru/README.md +0 -25
  168. package/docs/uk/00-the-mado-way.md +0 -82
  169. package/docs/uk/01-routing.md +0 -76
  170. package/docs/uk/02-project-layout.md +0 -73
  171. package/docs/uk/03-static-bake.md +0 -48
  172. package/docs/uk/04-ide-setup.md +0 -26
  173. package/docs/uk/05-why-mado.md +0 -34
  174. package/docs/uk/06-for-backenders.md +0 -55
  175. package/docs/uk/07-llm-pitfalls.md +0 -145
  176. package/docs/uk/08-llm-zero-history-test.md +0 -34
  177. package/docs/uk/09-shadow-vs-light-dom.md +0 -58
  178. package/docs/uk/10-app-architecture.md +0 -97
  179. package/docs/uk/11-layouts.md +0 -47
  180. package/docs/uk/12-auth-and-api.md +0 -70
  181. package/docs/uk/13-deployment.md +0 -40
  182. package/docs/uk/14-testing.md +0 -34
  183. package/docs/uk/15-error-handling.md +0 -32
  184. package/docs/uk/16-bake-cookbook.md +0 -36
  185. package/docs/uk/17-shadow-dom-forms.md +0 -193
  186. package/docs/uk/18-api-freeze-map.md +0 -61
  187. package/docs/uk/19-reactivity-ordering.md +0 -95
  188. package/docs/uk/20-v1-stability.md +0 -83
  189. package/docs/uk/README.md +0 -27
  190. package/starters/default/src/modules/home/not-found.page.ts +0 -11
  191. /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
  192. /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
  193. /package/starters/{default → modular}/.editorconfig +0 -0
  194. /package/starters/{default → modular}/eslint.config.mjs +0 -0
  195. /package/starters/{default → modular}/public/favicon.svg +0 -0
  196. /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
  197. /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
  198. /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
  199. /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
  200. /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
  201. /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
  202. /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
  203. /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
  204. /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
  205. /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
  206. /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
  207. /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
  208. /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
  209. /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
  210. /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
  211. /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
  212. /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
  213. /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
  214. /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
  215. /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
  216. /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
  217. /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
  218. /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
  219. /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
  220. /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
  221. /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
@@ -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.
@@ -0,0 +1,292 @@
1
+ # Static snapshots cookbook
2
+
3
+ > Concrete recipes for `mado static`. Concept and API in
4
+ > [15-static-snapshots.md](./15-static-snapshots.md); failure modes and CI
5
+ > guidance at the end of this file.
6
+
7
+ Every recipe assumes:
8
+
9
+ - Vite is the build transport (`@madojs/mado/vite` plugin).
10
+ - A public origin is configured (`mado({ site })` or
11
+ `MADO_SITE=https://your.site mado release`).
12
+ - `mado release` is the release command (it runs `vite build` →
13
+ `mado static` → writes deployment files into `out/`).
14
+
15
+ ## 1. Single static landing page
16
+
17
+ ```ts
18
+ // src/pages/home.page.ts
19
+ import { html, page } from "@madojs/mado";
20
+
21
+ export default page({
22
+ static: true,
23
+ title: "Home",
24
+ head: () => ({
25
+ description: "Welcome to the Mado App.",
26
+ }),
27
+ view: () => html`
28
+ <main>
29
+ <h1>Mado App</h1>
30
+ <p>One component model. One page model. One release command.</p>
31
+ </main>
32
+ `,
33
+ });
34
+ ```
35
+
36
+ After `mado release`:
37
+
38
+ ```
39
+ out/
40
+ index.html ← captured snapshot
41
+ assets/...
42
+ _mado/spa.html ← SPA fallback for other routes
43
+ sitemap.xml
44
+ _headers / _redirects
45
+ ```
46
+
47
+ ## 2. Blog with file-system content
48
+
49
+ ```ts
50
+ // src/content/posts.ts
51
+ import { readdirSync, readFileSync } from "node:fs";
52
+ import { resolve } from "node:path";
53
+
54
+ export interface Post {
55
+ slug: string;
56
+ title: string;
57
+ date: string;
58
+ html: string;
59
+ }
60
+
61
+ const dir = resolve(process.cwd(), "content/posts");
62
+
63
+ export function listPosts(): Post[] {
64
+ return readdirSync(dir)
65
+ .filter((f) => f.endsWith(".md"))
66
+ .map((file) => loadPost(file));
67
+ }
68
+
69
+ export function loadPost(file: string): Post {
70
+ // Replace with your real Markdown pipeline.
71
+ return JSON.parse(readFileSync(resolve(dir, file), "utf8"));
72
+ }
73
+ ```
74
+
75
+ ```ts
76
+ // src/pages/post.page.ts
77
+ import { html, page } from "@madojs/mado";
78
+ import { listPosts, loadPost, type Post } from "../content/posts";
79
+
80
+ export default page<{ slug: string }, Post>({
81
+ static: {
82
+ paths: async () =>
83
+ listPosts().map((p) => ({ slug: p.slug })),
84
+ initialData: async ({ slug }) =>
85
+ loadPost(`${slug}.json`),
86
+ },
87
+ title: (_, seed) => seed?.title ?? "Post",
88
+ head: (_, seed) => ({
89
+ description: seed?.title,
90
+ }),
91
+ view: (ctx) => html`
92
+ <article>
93
+ <h1>${ctx.data.title}</h1>
94
+ <time>${ctx.data.date}</time>
95
+ ${html([ctx.data.html] as unknown as TemplateStringsArray)}
96
+ </article>
97
+ `,
98
+ });
99
+ ```
100
+
101
+ Output: one `out/posts/<slug>/index.html` per post, each carrying its
102
+ own canonical and `og:url`.
103
+
104
+ > File-system reads happen during *discovery* (Node side). They never
105
+ > ship in the client bundle. The captured *initial data* does ship —
106
+ > keep it free of secrets.
107
+
108
+ ## 3. Product catalogue from JSON
109
+
110
+ ```ts
111
+ import { html, page } from "@madojs/mado";
112
+ import products from "../content/products.json" with { type: "json" };
113
+
114
+ interface Product {
115
+ slug: string;
116
+ name: string;
117
+ price: number;
118
+ }
119
+
120
+ export default page<{ slug: string }, Product>({
121
+ static: {
122
+ paths: async () => products.map((p) => ({ slug: p.slug })),
123
+ initialData: async ({ slug }) =>
124
+ products.find((p) => p.slug === slug)!,
125
+ },
126
+ title: (_, seed) => seed?.name ?? "Product",
127
+ view: (ctx) => html`
128
+ <main>
129
+ <h1>${ctx.data.name}</h1>
130
+ <p>$${ctx.data.price.toFixed(2)}</p>
131
+ </main>
132
+ `,
133
+ });
134
+ ```
135
+
136
+ ## 4. Documentation site
137
+
138
+ A docs site is typically:
139
+
140
+ - Many static guides keyed by `slug`.
141
+ - A few hand-written landing pages.
142
+ - One SPA route for the search modal (kept dynamic).
143
+
144
+ ```ts
145
+ // src/app.routes.ts
146
+ import { routes } from "@madojs/mado";
147
+ import home from "./pages/home.page";
148
+ import guide from "./pages/guide.page";
149
+ import search from "./pages/search.page";
150
+
151
+ export const manifest = {
152
+ "/": home, // static
153
+ "/guides/:slug": guide, // dynamic static
154
+ "/search": search, // SPA-only (no `static` field)
155
+ "*": () => import("./pages/not-found.page"),
156
+ };
157
+
158
+ export default routes(manifest);
159
+ ```
160
+
161
+ Only `home` and `guide` are captured. `/search` is served from the
162
+ SPA fallback (`_mado/spa.html`).
163
+
164
+ ## 5. Build-time API call
165
+
166
+ ```ts
167
+ import { html, page } from "@madojs/mado";
168
+
169
+ interface Release { tag: string; date: string }
170
+
171
+ export default page<{ tag: string }, Release>({
172
+ static: {
173
+ paths: async () => {
174
+ const res = await fetch("https://api.github.com/repos/madojs/mado/releases");
175
+ const list = (await res.json()) as Release[];
176
+ return list.map((r) => ({ tag: r.tag }));
177
+ },
178
+ initialData: async ({ tag }) => {
179
+ const res = await fetch(
180
+ `https://api.github.com/repos/madojs/mado/releases/tags/${tag}`,
181
+ );
182
+ return (await res.json()) as Release;
183
+ },
184
+ },
185
+ title: (_, seed) => `Release ${seed?.tag ?? ""}`,
186
+ view: (ctx) => html`
187
+ <article>
188
+ <h1>${ctx.data.tag}</h1>
189
+ <time>${ctx.data.date}</time>
190
+ </article>
191
+ `,
192
+ });
193
+ ```
194
+
195
+ Network calls in `paths()` / `initialData()` are perfectly fine — they
196
+ run in the discovery Node process, not in the browser at capture.
197
+
198
+ ## 6. Sub-path deployment
199
+
200
+ `mado release` honours Vite's `base`. Configure it once:
201
+
202
+ ```ts
203
+ // vite.config.ts
204
+ import { defineConfig } from "vite";
205
+ import { mado } from "@madojs/mado/vite";
206
+
207
+ export default defineConfig({
208
+ base: "/docs/",
209
+ plugins: [mado({ site: "https://your.site" })],
210
+ });
211
+ ```
212
+
213
+ Every link in your app must go through `routeUrl()` so it picks up
214
+ the prefix:
215
+
216
+ ```ts
217
+ import { html, routeUrl } from "@madojs/mado";
218
+
219
+ html`<a data-link href=${routeUrl("/guides/intro")}>Intro</a>`;
220
+ html`<a data-link href=${routeUrl("/")}>Home</a>`; // → "/docs/"
221
+ ```
222
+
223
+ Captured canonicals and `og:url` will be `https://your.site/docs/...`.
224
+
225
+ ## 7. SPA-only fallback
226
+
227
+ Any route that does NOT declare `static` is served from
228
+ `out/_mado/spa.html`. That is the right place for authenticated
229
+ zones, search, dashboards, admin tools.
230
+
231
+ ```ts
232
+ // src/pages/app.page.ts
233
+ import { html, page } from "@madojs/mado";
234
+
235
+ // No `static` field — SPA only.
236
+ export default page({
237
+ title: "App",
238
+ view: () => html`<x-app-shell></x-app-shell>`,
239
+ });
240
+ ```
241
+
242
+ The fallback is automatically marked `noindex` so search engines do
243
+ not capture the SPA shell.
244
+
245
+ ## 8. Failure modes
246
+
247
+ `mado static` fails the snapshot (non-zero exit) when:
248
+
249
+ - A script, stylesheet, custom element or tracked `resource()` fetch
250
+ fails.
251
+ - A custom element is referenced in the rendered DOM but never
252
+ defined.
253
+ - A page declares `static: true` on a route with `:params` but no
254
+ `paths()`.
255
+ - Two pages produce the same generated URL.
256
+ - Any guard runs on a static route or its layout chain.
257
+ - `initialData()` returns a value that is not strictly JSON-serialisable
258
+ (Date, Map, Set, class instance, `undefined` field, NaN, Infinity,
259
+ cycles, non-plain prototypes). The validator points at the bad
260
+ field by path.
261
+
262
+ A failed snapshot leaves the previous `out/` untouched: capture
263
+ writes to a temp tree and only promotes once every route survived.
264
+
265
+ ## 9. CI guidance
266
+
267
+ ```yaml
268
+ # .github/workflows/ci.yml — extract
269
+ - name: Install Playwright Chromium
270
+ run: node node_modules/playwright-core/cli.js install --with-deps chromium
271
+
272
+ - name: Release smoke
273
+ env:
274
+ MADO_REQUIRE_BROWSER: "1"
275
+ MADO_SITE: "https://your.site"
276
+ run: npm run release
277
+ ```
278
+
279
+ `MADO_REQUIRE_BROWSER=1` forces `mado static` to fail loudly if no
280
+ Chromium is resolvable (instead of silently producing a SPA-only
281
+ build).
282
+
283
+ ## 10. Quick checklist before pushing
284
+
285
+ - [ ] `mado({ site })` set (or `MADO_SITE` / `--base-url`).
286
+ - [ ] Every static page declares `static: true | { paths, initialData }`.
287
+ - [ ] No `paths()` / `initialData()` reads secrets.
288
+ - [ ] No static page is behind a guard.
289
+ - [ ] Internal links go through `routeUrl()` + `data-link`.
290
+ - [ ] `mado release` succeeds locally with Playwright Chromium
291
+ installed.
292
+ - [ ] `mado preview` serves the artefact correctly.
@@ -30,14 +30,16 @@ These names are public and protected by SemVer once v1 ships:
30
30
  `ref`, `classMap`, `styleMap`.
31
31
  - Components and CSS: `component`, `css`, `cssVars`.
32
32
  - Routing and pages: `routes`, `router`, `page`, `layout`,
33
- `navigate`, `queryParam`, `prefetchPath`.
33
+ `navigate`, `queryParam`, `prefetchPath`, `routeUrl`, `appBase`.
34
34
  - Data: `resource`, `mutation`, `invalidate`, `jsonFetcher`, `HttpError`.
35
35
  - Forms: `useForm`.
36
36
  - Head and persistence: `applyHead`, `persisted`.
37
37
  - Context: `createContext`, `provide`, `inject`.
38
38
  - Advanced lifecycle helpers: `createLifecycle`, `runInLifecycle`,
39
39
  `getCurrentLifecycle`.
40
- - Public TypeScript types exported from `@madojs/mado`.
40
+ - Public TypeScript types exported from `@madojs/mado`, including
41
+ `StaticPageConfig` (the `static: true | { ... }` option on `page()`)
42
+ and `HeadMeta` (the head metadata shape returned by pages).
41
43
 
42
44
  ## Internal or unstable
43
45
 
@@ -48,7 +50,14 @@ These are not public API:
48
50
  - Template parser/binding internals such as `html/parser.js`,
49
51
  `html/bindings.js`, `ChildState`, and `EachEntry`.
50
52
  - Router implementation modules such as `router/match.js`,
51
- `router/navigation.js`, and `router/manifest.js`.
53
+ `router/navigation.js`, `router/manifest.js`, and `router/base.js`.
54
+ - Base-path helpers other than `routeUrl` / `appBase`: `normalizeBase`,
55
+ `stripBase` and `withBase` are intentionally not exported from
56
+ `@madojs/mado`. Use `routeUrl()` for `<a href>` values and `appBase`
57
+ if you need the raw active prefix.
58
+ - The static snapshot pipeline (`scripts/static.mjs`, the
59
+ `_mado/build.json` bridge, the temp capture server). `mado static` is
60
+ a CLI command; its internals can change between minor versions.
52
61
  - Diagnostics internals and all `_testHooks`.
53
62
  - Exact generated bundle text, chunk names, and internal file layout.
54
63
 
@@ -8,8 +8,8 @@ copy, or diagnostic string is frozen forever.
8
8
 
9
9
  Read this together with:
10
10
 
11
- - [API freeze map](./18-api-freeze-map.md)
12
- - [Reactivity ordering](./19-reactivity-ordering.md)
11
+ - [API freeze map](./30-api-freeze-map.md)
12
+ - [Reactivity ordering](./31-reactivity-ordering.md)
13
13
 
14
14
  ## Stable under SemVer
15
15
 
@@ -25,7 +25,7 @@ After v1, Mado treats these as SemVer-protected:
25
25
  teardown for same-tick moves, cleanup via `ctx.onDispose`.
26
26
  - Router/page/resource/form contracts documented in the English docs.
27
27
  - CLI command names and broad command intent (`build`, `dev`, `release`,
28
- `bake`, `preview`, `init`, `new`).
28
+ `static`, `preview`, `init`, `new`).
29
29
 
30
30
  Breaking these requires a major version.
31
31