@madojs/mado 0.5.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 (162) hide show
  1. package/AGENTS.md +291 -0
  2. package/CHANGELOG.md +23 -0
  3. package/LICENSE +21 -0
  4. package/README.md +371 -0
  5. package/ROADMAP.md +52 -0
  6. package/dist/src/component.d.ts +48 -0
  7. package/dist/src/component.js +140 -0
  8. package/dist/src/component.js.map +1 -0
  9. package/dist/src/context.d.ts +40 -0
  10. package/dist/src/context.js +67 -0
  11. package/dist/src/context.js.map +1 -0
  12. package/dist/src/css.d.ts +54 -0
  13. package/dist/src/css.js +137 -0
  14. package/dist/src/css.js.map +1 -0
  15. package/dist/src/devtools.d.ts +22 -0
  16. package/dist/src/devtools.js +63 -0
  17. package/dist/src/devtools.js.map +1 -0
  18. package/dist/src/diagnostics.d.ts +11 -0
  19. package/dist/src/diagnostics.js +28 -0
  20. package/dist/src/diagnostics.js.map +1 -0
  21. package/dist/src/each.d.ts +39 -0
  22. package/dist/src/each.js +35 -0
  23. package/dist/src/each.js.map +1 -0
  24. package/dist/src/forms.d.ts +71 -0
  25. package/dist/src/forms.js +161 -0
  26. package/dist/src/forms.js.map +1 -0
  27. package/dist/src/head.d.ts +19 -0
  28. package/dist/src/head.js +97 -0
  29. package/dist/src/head.js.map +1 -0
  30. package/dist/src/html/bindings.d.ts +78 -0
  31. package/dist/src/html/bindings.js +304 -0
  32. package/dist/src/html/bindings.js.map +1 -0
  33. package/dist/src/html/parser.d.ts +64 -0
  34. package/dist/src/html/parser.js +521 -0
  35. package/dist/src/html/parser.js.map +1 -0
  36. package/dist/src/html/template-types.d.ts +27 -0
  37. package/dist/src/html/template-types.js +8 -0
  38. package/dist/src/html/template-types.js.map +1 -0
  39. package/dist/src/html/template.d.ts +45 -0
  40. package/dist/src/html/template.js +119 -0
  41. package/dist/src/html/template.js.map +1 -0
  42. package/dist/src/html.d.ts +16 -0
  43. package/dist/src/html.js +16 -0
  44. package/dist/src/html.js.map +1 -0
  45. package/dist/src/index.d.ts +35 -0
  46. package/dist/src/index.js +39 -0
  47. package/dist/src/index.js.map +1 -0
  48. package/dist/src/lazy.d.ts +38 -0
  49. package/dist/src/lazy.js +73 -0
  50. package/dist/src/lazy.js.map +1 -0
  51. package/dist/src/lifecycle.d.ts +45 -0
  52. package/dist/src/lifecycle.js +66 -0
  53. package/dist/src/lifecycle.js.map +1 -0
  54. package/dist/src/page.d.ts +161 -0
  55. package/dist/src/page.js +38 -0
  56. package/dist/src/page.js.map +1 -0
  57. package/dist/src/persisted.d.ts +47 -0
  58. package/dist/src/persisted.js +119 -0
  59. package/dist/src/persisted.js.map +1 -0
  60. package/dist/src/resource.d.ts +120 -0
  61. package/dist/src/resource.js +275 -0
  62. package/dist/src/resource.js.map +1 -0
  63. package/dist/src/router/manifest.d.ts +56 -0
  64. package/dist/src/router/manifest.js +302 -0
  65. package/dist/src/router/manifest.js.map +1 -0
  66. package/dist/src/router/match.d.ts +62 -0
  67. package/dist/src/router/match.js +117 -0
  68. package/dist/src/router/match.js.map +1 -0
  69. package/dist/src/router/navigation.d.ts +89 -0
  70. package/dist/src/router/navigation.js +263 -0
  71. package/dist/src/router/navigation.js.map +1 -0
  72. package/dist/src/router.d.ts +13 -0
  73. package/dist/src/router.js +13 -0
  74. package/dist/src/router.js.map +1 -0
  75. package/dist/src/signal.d.ts +67 -0
  76. package/dist/src/signal.js +238 -0
  77. package/dist/src/signal.js.map +1 -0
  78. package/docs/README.md +12 -0
  79. package/docs/en/00-the-mado-way.md +106 -0
  80. package/docs/en/01-routing.md +204 -0
  81. package/docs/en/02-project-layout.md +58 -0
  82. package/docs/en/03-static-bake.md +251 -0
  83. package/docs/en/04-ide-setup.md +162 -0
  84. package/docs/en/05-why-mado.md +193 -0
  85. package/docs/en/06-for-backenders.md +422 -0
  86. package/docs/en/07-llm-pitfalls.md +486 -0
  87. package/docs/en/08-llm-zero-history-test.md +56 -0
  88. package/docs/en/09-shadow-vs-light-dom.md +122 -0
  89. package/docs/en/README.md +16 -0
  90. package/docs/fr/00-the-mado-way.md +108 -0
  91. package/docs/fr/01-routing.md +202 -0
  92. package/docs/fr/02-project-layout.md +58 -0
  93. package/docs/fr/03-static-bake.md +290 -0
  94. package/docs/fr/04-ide-setup.md +162 -0
  95. package/docs/fr/05-why-mado.md +193 -0
  96. package/docs/fr/06-for-backenders.md +432 -0
  97. package/docs/fr/07-llm-pitfalls.md +487 -0
  98. package/docs/fr/08-llm-zero-history-test.md +60 -0
  99. package/docs/fr/09-shadow-vs-light-dom.md +121 -0
  100. package/docs/fr/README.md +16 -0
  101. package/docs/ru/00-the-mado-way.md +93 -0
  102. package/docs/ru/01-routing.md +194 -0
  103. package/docs/ru/02-project-layout.md +57 -0
  104. package/docs/ru/03-static-bake.md +251 -0
  105. package/docs/ru/04-ide-setup.md +144 -0
  106. package/docs/ru/05-why-mado.md +193 -0
  107. package/docs/ru/06-for-backenders.md +422 -0
  108. package/docs/ru/07-llm-pitfalls.md +485 -0
  109. package/docs/ru/08-llm-zero-history-test.md +56 -0
  110. package/docs/ru/09-shadow-vs-light-dom.md +122 -0
  111. package/docs/ru/README.md +14 -0
  112. package/docs/uk/00-the-mado-way.md +54 -0
  113. package/docs/uk/01-routing.md +82 -0
  114. package/docs/uk/02-project-layout.md +46 -0
  115. package/docs/uk/03-static-bake.md +49 -0
  116. package/docs/uk/04-ide-setup.md +26 -0
  117. package/docs/uk/05-why-mado.md +34 -0
  118. package/docs/uk/06-for-backenders.md +50 -0
  119. package/docs/uk/07-llm-pitfalls.md +82 -0
  120. package/docs/uk/08-llm-zero-history-test.md +31 -0
  121. package/docs/uk/09-shadow-vs-light-dom.md +40 -0
  122. package/docs/uk/README.md +16 -0
  123. package/llms.txt +155 -0
  124. package/package.json +81 -0
  125. package/scripts/bake.mjs +406 -0
  126. package/scripts/bundle.mjs +146 -0
  127. package/scripts/cli.mjs +382 -0
  128. package/scripts/new.mjs +80 -0
  129. package/scripts/preview.mjs +176 -0
  130. package/scripts/release-notes.mjs +66 -0
  131. package/scripts/showcase-regression.mjs +392 -0
  132. package/server/serve.mjs +292 -0
  133. package/starters/crud/README.md +21 -0
  134. package/starters/crud/index.html +20 -0
  135. package/starters/crud/package.json +17 -0
  136. package/starters/crud/src/components/app-shell.ts +51 -0
  137. package/starters/crud/src/components/ticket-detail.ts +33 -0
  138. package/starters/crud/src/components/ticket-form.ts +69 -0
  139. package/starters/crud/src/components/ticket-list.ts +66 -0
  140. package/starters/crud/src/lib/api.ts +76 -0
  141. package/starters/crud/src/main.ts +12 -0
  142. package/starters/crud/src/pages/home.ts +18 -0
  143. package/starters/crud/src/pages/not-found.ts +12 -0
  144. package/starters/crud/src/pages/ticket-detail.ts +6 -0
  145. package/starters/crud/src/pages/ticket-new.ts +6 -0
  146. package/starters/crud/src/pages/tickets.ts +6 -0
  147. package/starters/crud/src/routes.ts +9 -0
  148. package/starters/crud/src/styles/global.ts +155 -0
  149. package/starters/crud/tsconfig.json +15 -0
  150. package/starters/minimal/README.md +19 -0
  151. package/starters/minimal/index.html +20 -0
  152. package/starters/minimal/package.json +17 -0
  153. package/starters/minimal/src/components/app-counter.ts +31 -0
  154. package/starters/minimal/src/main.ts +9 -0
  155. package/starters/minimal/src/pages/home.ts +18 -0
  156. package/starters/minimal/src/pages/not-found.ts +14 -0
  157. package/starters/minimal/src/routes.ts +6 -0
  158. package/starters/minimal/src/styles/global.ts +60 -0
  159. package/starters/minimal/tsconfig.json +15 -0
  160. package/templates/page-detail.ts +63 -0
  161. package/templates/page-form.ts +94 -0
  162. package/templates/page-list.ts +79 -0
@@ -0,0 +1,106 @@
1
+ # The Mado Way
2
+
3
+ > One right way. Strict contracts. No magic.
4
+
5
+ Mado is not just a framework — it is a **set of conventions**. If you follow them,
6
+ the project stays understandable even with 200 screens and 5 developers. If you
7
+ break them — types and the linter will tell you immediately.
8
+
9
+ ## Principles
10
+
11
+ 1. **One way.** For every task there is one right path, not five. If you write
12
+ something unusual — ask yourself whether an idiomatic helper already exists.
13
+ 2. **Explicitness over magic.** No file-system scanners, no implicit globals, no
14
+ hidden side-effects. Everything the framework does can be read in a single file.
15
+ 3. **Platform first.** If the browser already has a feature — use it directly.
16
+ No custom abstractions over `fetch`, `<form>`, the History API, or Shadow DOM.
17
+ 4. **Strict types.** `tsc --strict --noUncheckedIndexedAccess` always. If
18
+ something cannot be typed — that is a signal the API is wrong.
19
+ 5. **No runtime dependencies.** Every dependency is a years-long commitment; the
20
+ Web Components ecosystem does not require it.
21
+
22
+ ## Conventions
23
+
24
+ ### Project structure
25
+
26
+ ```
27
+ src/
28
+ ├── routes.ts ← route manifest, one file per project
29
+ ├── main.ts ← entry point: providers + mount <x-app>
30
+ ├── pages/ ← one page = one file = `export default page({...})`
31
+ ├── components/ ← reusable components, side-effect registration
32
+ ├── lib/ ← contexts, API clients, business logic without UI
33
+ └── styles/ ← shared styles (if needed), .ts files with css``
34
+ ```
35
+
36
+ This is **mandatory**, not optional. If a project has 10 developers — they must
37
+ all write the same way.
38
+
39
+ ### One component = one file
40
+
41
+ ```ts
42
+ // src/components/user-card.ts
43
+ import { component, html, css } from '@madojs/mado';
44
+
45
+ component('x-user-card', () => {
46
+ return () => html`<div class="card"><slot/></div>`;
47
+ }, {
48
+ styles: css`.card { padding: 1rem; }`,
49
+ });
50
+ ```
51
+
52
+ `import './components/user-card.js'` **registers** the component via
53
+ `customElements.define`. This is a side effect. Import where the component is needed.
54
+
55
+ ### One way to load data
56
+
57
+ ❌ Do not call `fetch()` directly from a component. Always use:
58
+
59
+ ```ts
60
+ // reading → resource
61
+ const user = resource(() => `/api/users/${id()}`, jsonFetcher());
62
+
63
+ // writing → mutation
64
+ const save = mutation(api.save, { invalidates: ['/api/users*'] });
65
+ ```
66
+
67
+ This provides caching, cancellation, error handling, and auto-invalidation.
68
+
69
+ ### One way to describe a page
70
+
71
+ ```ts
72
+ // src/pages/user-profile.ts
73
+ import { page, html, resource, jsonFetcher } from '@madojs/mado';
74
+
75
+ export default page({
76
+ title: ({ id }) => `User #${id}`,
77
+ view: ({ params }) => html`...`,
78
+ });
79
+ ```
80
+
81
+ Three slots — `title`, `load`, `view`. No others. Want something else — that is
82
+ a component or a helper.
83
+
84
+ ### One way to declare routes
85
+
86
+ See [`01-routing.md`](./01-routing.md).
87
+
88
+ ## What we do NOT do
89
+
90
+ - ❌ Do not write components without a hyphen. This is the browser rule for
91
+ custom elements: `user-card` is ok, `usercard` is not.
92
+ - `x-*` is only a convention for Mado examples and tests, not a brand standard.
93
+ In production use a domain prefix: `app-*`, `crm-*`, `ticket-*`, `admin-*`.
94
+ - ❌ Do not use `innerHTML` directly. Only via `html\`\``.
95
+ - ❌ Do not call `setTimeout`/`setInterval` without cleanup. Only inside `effect()`.
96
+ - ❌ Do not store global mutable state. Use signals and `context`.
97
+ - ❌ Do not add packages without discussion. Every dependency is a commitment.
98
+
99
+ ## When in doubt
100
+
101
+ If you are asking "what's the best way here?" — that is a signal that:
102
+ 1. Either there is a built-in helper you don't know about (check `docs/`).
103
+ 2. Or this is a new situation — discuss it and **record** it in this document
104
+ as one more convention.
105
+
106
+ "A consistent okay beats a varied ideal."
@@ -0,0 +1,204 @@
1
+ # Routing
2
+
3
+ > One manifest file. No folder scanners. No special characters.
4
+
5
+ ## Why not file-based
6
+
7
+ In Next/SvelteKit/SolidStart routes appear "magically" from file names. This has
8
+ advantages (URL structure visible in `pages/`), but in production it means:
9
+
10
+ - An invisible plugin-scanner in the build. Without it the files are just files.
11
+ - Special characters in paths: `[id]`, `(group)`, `_layout`, `+page.svelte`, `...slug`.
12
+ - Server-routes and client-routes get confused.
13
+ - Testing routing is a pain: you need a build-tool emulator.
14
+
15
+ Mado considers this **too much magic**. We do it differently.
16
+
17
+ ## Manifest
18
+
19
+ One file — `src/routes.ts`. One object. Read top to bottom.
20
+
21
+ ```ts
22
+ // src/routes.ts
23
+ import { routes } from '@madojs/mado';
24
+
25
+ export default routes({
26
+ '/': () => import('./pages/home.js'),
27
+ '/about': () => import('./pages/about.js'),
28
+ '/users/:id': () => import('./pages/user-profile.js'),
29
+ '/users/:id/edit':() => import('./pages/user-edit.js'),
30
+ '*': () => import('./pages/not-found.js'),
31
+ });
32
+ ```
33
+
34
+ Want to see all routes? Open `routes.ts`. No surprises.
35
+
36
+ ## What goes on the right side of a path
37
+
38
+ Every entry is **one of three things**:
39
+
40
+ ### 1. Lazy import (recommended)
41
+
42
+ ```ts
43
+ '/posts': () => import('./pages/posts.js'),
44
+ ```
45
+
46
+ - The browser makes its own chunk when bundling (esbuild --bundle --splitting).
47
+ - The module is loaded only when the user visits the route.
48
+ - Subsequent navigations use the cached result.
49
+
50
+ ### 2. Ready Page (eager)
51
+
52
+ ```ts
53
+ import about from './pages/about.js';
54
+
55
+ '/about': about,
56
+ ```
57
+
58
+ In the bundle immediately, no delay. Use for critical pages (home, login).
59
+
60
+ ### 3. Nested with layout
61
+
62
+ ```ts
63
+ import { routes, nested } from '@madojs/mado';
64
+
65
+ export default routes({
66
+ '/': () => import('./pages/home.js'),
67
+
68
+ '/admin/*': nested({
69
+ layout: () => import('./layouts/admin.js'),
70
+ routes: {
71
+ '': () => import('./pages/admin/dashboard.js'),
72
+ 'users': () => import('./pages/admin/users.js'),
73
+ 'logs': () => import('./pages/admin/logs.js'),
74
+ },
75
+ }),
76
+ });
77
+ ```
78
+
79
+ A layout is just a regular `page({...})` that renders `ctx.child` wherever it wants:
80
+
81
+ ```ts
82
+ // src/layouts/admin.ts
83
+ import { page, html, css, component } from '@madojs/mado';
84
+
85
+ export default page({
86
+ view: ({ child }) => html`
87
+ <div class="admin">
88
+ <aside><nav>...</nav></aside>
89
+ <main>${child}</main>
90
+ </div>
91
+ `,
92
+ });
93
+ ```
94
+
95
+ ## Page contract
96
+
97
+ ```ts
98
+ import { page, html, resource, jsonFetcher } from '@madojs/mado';
99
+
100
+ export default page({
101
+ title: ({ id }) => `User #${id}`, // string | (params) => string
102
+ load: ({ id }) => resource(...), // optional, returns Resource or data
103
+ view: ({ params, data, path, child }) => html`...`, // REQUIRED
104
+ });
105
+ ```
106
+
107
+ Three slots, that's all. If you export something other than `page({...})`, a plain
108
+ function for instance — `routes()` throws a clear error:
109
+
110
+ ```
111
+ [Mado] Lazy route did not return page({...}) as the default export.
112
+ ```
113
+
114
+ ## URL parameters
115
+
116
+ ```ts
117
+ '/users/:id': () => import('./pages/user.js'),
118
+ ```
119
+
120
+ ```ts
121
+ export default page<{ id: string }>({
122
+ title: ({ id }) => `User ${id}`,
123
+ view: ({ params }) => html`<h1>${params.id}</h1>`,
124
+ });
125
+ ```
126
+
127
+ Types are passed in `page<Params>` — `tsc` verifies that you don't access
128
+ `params.foo` which doesn't exist in the route.
129
+
130
+ ## Global options
131
+
132
+ ```ts
133
+ export default routes(
134
+ { '/': home, '/about': about, '*': nf },
135
+ {
136
+ titleSuffix: ' · MyApp', // → "Home · MyApp"
137
+ loading: () => html`<x-spinner/>`, // while module loads
138
+ error: (err) => html`<x-fatal-error .err=${err}/>`,
139
+ },
140
+ );
141
+ ```
142
+
143
+ ## Programmatic navigation
144
+
145
+ ```ts
146
+ import route from './routes.js';
147
+
148
+ route.navigate('/posts');
149
+ route.navigate('/posts?page=2');
150
+ route.navigate('/posts', { replace: true });
151
+ ```
152
+
153
+ Clicks on `<a href="/foo" data-link>` are intercepted globally (without the
154
+ attribute — the browser does a full reload, as expected for external links).
155
+
156
+ ## Query parameters
157
+
158
+ ```ts
159
+ import { queryParam } from '@madojs/mado';
160
+
161
+ const page = queryParam('page', '1');
162
+ page(); // '1'
163
+ page.set('2'); // history.replaceState + re-render
164
+ page.set(null); // delete the parameter
165
+ page.set('3', { push: true }); // history.pushState
166
+ ```
167
+
168
+ `queryParam` is a normal signal. Use it anywhere: in pages, components, computed.
169
+
170
+ ## What is intentionally absent
171
+
172
+ - ❌ Auto-scan of `pages/`. **One explicit manifest file**.
173
+ - ❌ Special characters in paths (`[id]`, `(group)`, `_layout`). **Parameters are
174
+ `:name` only, nothing else**.
175
+ - ❌ Server-side routing in the same manifest. Mado is a client-side framework.
176
+ - ❌ Auto-prefetch on hover. If you really need it — do it manually:
177
+ `link.addEventListener('mouseenter', loader)`. Usually unnecessary.
178
+
179
+ ## FAQ
180
+
181
+ **What if I have 100 routes? Won't the file get huge?**
182
+ It will grow to ~150 lines. That is still **one source of truth** versus a hundred
183
+ files in `pages/` with magic names. In practice even large projects (1000+ pages)
184
+ can split into feature manifests:
185
+
186
+ ```ts
187
+ import { routes } from '@madojs/mado';
188
+ import adminRoutes from './features/admin/routes.js';
189
+ import billingRoutes from './features/billing/routes.js';
190
+
191
+ export default routes({
192
+ ...adminRoutes,
193
+ ...billingRoutes,
194
+ '*': () => import('./pages/not-found.js'),
195
+ });
196
+ ```
197
+
198
+ **How do I test routing?**
199
+ Import `routes.ts` — it is just an object. Substitute your mock router. No build
200
+ tool emulation needed.
201
+
202
+ **Does code splitting work?**
203
+ Yes. With `esbuild --bundle --splitting --format=esm` every
204
+ `() => import('./pages/x.js')` becomes its own chunk.
@@ -0,0 +1,58 @@
1
+ # Project layout
2
+
3
+ Every new Mado project has the same structure. This is a **mandatory** convention.
4
+
5
+ ```
6
+ my-app/
7
+ ├── package.json # exactly 1 dep: typescript (esbuild optional)
8
+ ├── tsconfig.json # with paths "@madojs/mado" → import without relative paths
9
+ ├── Dockerfile + nginx.conf # copied from Mado/ on scaffold
10
+ ├── .gitlab-ci.yml | .github/workflows/ci.yml
11
+ ├── server/serve.mjs # dev-server from Mado, no deps
12
+ ├── scripts/
13
+ │ ├── bundle.mjs # esbuild prod bundle
14
+ │ └── new.mjs # page scaffolder
15
+ ├── templates/ # templates for new.mjs
16
+ ├── docs/ # project docs (can copy our guides)
17
+ ├── public/ # static assets (favicon, manifests)
18
+ └── src/
19
+ ├── main.ts # entry: providers + mount <x-app>
20
+ ├── routes.ts # route manifest
21
+ ├── pages/ # one page = one file = `export default page({...})`
22
+ ├── components/ # reusable components (x-*)
23
+ ├── layouts/ # layout pages (for nested)
24
+ └── lib/
25
+ ├── api.ts # all fetch wrappers
26
+ ├── contexts.ts # createContext(...)
27
+ ├── theme.ts # themes
28
+ └── ... # utilities, types, business rules
29
+ ```
30
+
31
+ ## Where to put a new file?
32
+
33
+ | What | Where |
34
+ |---|---|
35
+ | Page for a new URL | `src/pages/foo.ts` + add to `src/routes.ts` |
36
+ | Reusable UI widget | `src/components/foo-bar.ts` |
37
+ | API wrapper | `src/lib/api.ts` (add a method) |
38
+ | Global context (theme, user, i18n) | `src/lib/<name>.ts` |
39
+ | Pure function without UI | `src/lib/util/<name>.ts` |
40
+
41
+ If you don't know where — that is a signal that **the architecture is suffering**.
42
+ Ask the team, **record** the answer in `docs/`.
43
+
44
+ ## Naming rules
45
+
46
+ | What | Style | Example |
47
+ |---|---|---|
48
+ | File | kebab-case | `user-profile.ts` |
49
+ | Component tag | `x-` + kebab | `<x-user-profile>` |
50
+ | Context | PascalCase + `Ctx` | `ThemeCtx`, `AuthCtx` |
51
+ | Signal | camelCase | `userId`, `isLoggedIn` |
52
+ | Page function (internal component) | `x-<route>-page` | `<x-posts-page>` |
53
+
54
+ ## What does NOT go in src/
55
+
56
+ - ❌ Build tool configs (webpack, rollup, vite) — we don't have any.
57
+ - ❌ `.env` files — env is read from `process.env`/`import.meta.env` in `lib/config.ts`.
58
+ - ❌ Tests mixed with code — all in `test/`.
@@ -0,0 +1,251 @@
1
+ # Smart Static (`bake`)
2
+
3
+ > Baking HTML for SEO without runtime SSR. **Idea: data and view in one file, static output.**
4
+
5
+ `bake` is a **build-time prerender**, not SSR. The output is static `*.html` files that any nginx/Cloudflare serves as regular static content. On the client, Web Components come alive and SPA navigation continues to work.
6
+
7
+ ---
8
+
9
+ ## When `bake` is suitable
10
+
11
+ - **Marketing pages**: landing pages, sub-landings for advertising campaigns, product pages.
12
+ - **Catalog with a relatively stable set of pages**: blog, documentation, portfolio, e-commerce up to **~10k SKUs** with updates less than once an hour.
13
+ - **Content that is the same for all users**: the entire page is identical for a guest and an authenticated user (or authentication is rendered on the client via `effect()`).
14
+ - You need **good SEO** (rich snippets, OG, JSON-LD) and **fast first paint** without running node servers in production.
15
+
16
+ ## When `bake` is **not** suitable
17
+
18
+ - **Hundreds of thousands of pages with frequent changes**. `bake` traverses all `paths` synchronously in one run. For 100k+ pages this means minutes of rebuild, and invalidating one page either requires a full rebake or separate CI logic (see below on targeted revalidation).
19
+ - **Personalized content in HTML**. If the page should show "Hello, Ivan" in the `<title>` or in meta — that's not for `bake`. Authenticated dashboards, personal feeds, a cart with real prices for the user — keep these as SPA.
20
+ - **Server-only APIs are needed in render**: cookies, headers, real network requests to private APIs. On the bake side only `linkedom` is available, no Node environment for components.
21
+ - **A/B tests and flags that change markup on the first paint**. `bake` will lock in one variant. Handle dynamic behavior on the client via `effect()`.
22
+ - **Real-time / frequently changing data** (stock quotes, warehouse stock by the minute). `bake.revalidate` is metadata, not runtime: the framework does not re-bake anything itself.
23
+ - **Content behind authentication** (admin, internal tools). No need; use SPA mode.
24
+
25
+ ---
26
+
27
+ ## Concept
28
+
29
+ `page({...})` has four optional slots related to `bake`:
30
+
31
+ - `head` — meta, OG, JSON-LD.
32
+ - `bake.paths` — list of URL parameters for generation (build-time, can be `async`).
33
+ - `bake.data` — data for a specific URL (build-time, can be `async`).
34
+ - `bake.revalidate` — after how many seconds the cache is stale (written to `<meta>`, real invalidation is handled by your CI/CDN).
35
+
36
+ The `npm run bake` command traverses all `page` entries with `bake`, generates HTML via `linkedom`, and places it in `out/<path>/index.html`. **No Chromium needed** — `linkedom` is ~50 KB.
37
+
38
+ ---
39
+
40
+ ## Example
41
+
42
+ ```ts
43
+ // src/pages/product.ts
44
+ import { page, component, html } from "@madojs/mado";
45
+ import { findProduct, products, type Product } from "../lib/products.js";
46
+
47
+ component("x-product-page", ({ host }) => {
48
+ return () => {
49
+ const p = findProduct(host.dataset.slug);
50
+ return p
51
+ ? html`<h1>${p.name}</h1><p>${p.description}</p>`
52
+ : html`<p>Not found.</p>`;
53
+ };
54
+ });
55
+
56
+ export default page<{ slug: string }, Product | undefined>({
57
+ title: ({ slug }) => `${findProduct(slug)?.name} — MyShop`,
58
+
59
+ head: ({ slug }, baked) => {
60
+ const p = baked ?? findProduct(slug);
61
+ if (!p) return {};
62
+ return {
63
+ description: p.description,
64
+ canonical: `/product/${p.slug}`,
65
+ og: { title: p.name, image: p.image, type: "product" },
66
+ jsonLd: {
67
+ "@context": "https://schema.org",
68
+ "@type": "Product",
69
+ name: p.name,
70
+ offers: { "@type": "Offer", price: p.price, priceCurrency: p.currency },
71
+ },
72
+ };
73
+ },
74
+
75
+ bake: {
76
+ paths: () => products.map((p) => ({ slug: p.slug })),
77
+ data: ({ slug }) => findProduct(slug),
78
+ revalidate: 3600,
79
+ },
80
+
81
+ view: ({ params }) =>
82
+ html`<x-product-page data-slug=${params.slug}></x-product-page>`,
83
+ });
84
+ ```
85
+
86
+ ```ts
87
+ // src/routes.ts
88
+ import { routes, type RoutesMap } from "@madojs/mado";
89
+
90
+ // Export BOTH default (RouterApi for runtime) AND manifest (for the bake script).
91
+ export const manifest: RoutesMap = {
92
+ "/": () => import("./pages/home.js"),
93
+ "/product/:slug": () => import("./pages/product.js"),
94
+ "*": () => import("./pages/not-found.js"),
95
+ };
96
+
97
+ export default routes(manifest, { titleSuffix: " · MyShop" });
98
+ ```
99
+
100
+ ## Running
101
+
102
+ ```bash
103
+ npm install -D linkedom esbuild
104
+ npm run build
105
+ npm run bake
106
+ ```
107
+
108
+ You get:
109
+
110
+ ```
111
+ out/
112
+ ├── product/
113
+ │ ├── mado-mug/index.html ← HTML with meta + JSON-LD
114
+ │ ├── raw-bundler/index.html
115
+ │ └── shadow-dom/index.html
116
+ └── sitemap.xml
117
+ ```
118
+
119
+ ## What's Inside the Generated HTML
120
+
121
+ ```html
122
+ <head>
123
+ <title>Mado Mug — MyShop</title>
124
+ <meta name="description" content="..." data-mado-head="baked">
125
+ <link rel="canonical" href="/product/mado-mug" data-mado-head="baked">
126
+ <meta property="og:title" content="Mado Mug" data-mado-head="baked">
127
+ <meta property="og:image" content="..." data-mado-head="baked">
128
+ <script type="application/ld+json" data-mado-head="baked">
129
+ {"@context":"https://schema.org","@type":"Product","..."}
130
+ </script>
131
+ <meta name="bake-revalidate" content="3600" data-mado-head="baked">
132
+ <meta name="bake-stamp" content="1234567890" data-mado-head="baked">
133
+ </head>
134
+ <body>
135
+ <div id="app">
136
+ <x-product-page data-slug="mado-mug">
137
+ <h1>Mado Mug</h1>
138
+ <p>A mug with the inscription "zero dependencies".</p>
139
+ <strong>12 EUR</strong>
140
+ </x-product-page>
141
+ </div>
142
+
143
+ <!-- preloaded data for hydration -->
144
+ <script id="bake" type="application/json">
145
+ {"slug":"mado-mug","name":"Mado Mug","price":12,"..."}
146
+ </script>
147
+
148
+ <script type="module" src="/dist/examples/main.js"></script>
149
+ </body>
150
+ ```
151
+
152
+ After JS loads:
153
+
154
+ 1. Web Components `<x-product-page>` come alive (the browser already knows the custom elements).
155
+ 2. `page.load()` receives `baked` as initialData — no unnecessary `fetch` calls.
156
+ 3. SPA navigation continues to work as normal.
157
+
158
+ ---
159
+
160
+ ## Cookbook: Typical Scenarios
161
+
162
+ ### Blog (Markdown → bake)
163
+
164
+ ```ts
165
+ // src/lib/posts.ts
166
+ import { readdirSync, readFileSync } from "node:fs";
167
+ // (this module is imported only from the bake script,
168
+ // it must not end up in the browser graph — put it in lib/server/ or ifdef it out)
169
+
170
+ export const allPosts = () =>
171
+ readdirSync("content/blog").map((file) => ({
172
+ slug: file.replace(/\.md$/, ""),
173
+ ...parseFrontmatter(readFileSync(`content/blog/${file}`, "utf-8")),
174
+ }));
175
+ ```
176
+
177
+ ```ts
178
+ // src/pages/blog-post.ts
179
+ import { page, html } from "@madojs/mado";
180
+ import { allPosts } from "../lib/posts.js";
181
+
182
+ export default page<{ slug: string }>({
183
+ title: ({ slug }) => allPosts().find((p) => p.slug === slug)?.title ?? slug,
184
+ head: ({ slug }, post) => ({
185
+ description: post?.excerpt,
186
+ canonical: `/blog/${slug}`,
187
+ og: { title: post?.title, type: "article" },
188
+ }),
189
+ bake: {
190
+ paths: () => allPosts().map(({ slug }) => ({ slug })),
191
+ data: ({ slug }) => allPosts().find((p) => p.slug === slug),
192
+ },
193
+ view: ({ params }) =>
194
+ html`<x-blog-post data-slug=${params.slug}></x-blog-post>`,
195
+ });
196
+ ```
197
+
198
+ ### Product Catalog (e-commerce, ≤ 10k SKUs)
199
+
200
+ - `bake.paths` → SELECT slug FROM products
201
+ - `bake.data` → SELECT * FROM products WHERE slug=?
202
+ - Full invalidation every N hours via cron + `npm run bake`
203
+ - Targeted invalidation of a single product: webhook → rebuild only that `paths` entry
204
+
205
+ ### Documentation
206
+
207
+ - `bake.paths` — traversal of the file system `docs/**/*.md`
208
+ - `bake.data` — Markdown parsing
209
+ - `head.jsonLd` — `TechArticle`
210
+
211
+ ---
212
+
213
+ ## Revalidate / CDN
214
+
215
+ `bake.revalidate: 3600` writes `<meta name="bake-revalidate" content="3600">` and `bake-stamp` to the HTML. This is **metadata** — the framework does not re-bake anything itself. Strategies:
216
+
217
+ 1. **Simplest option**: cron in CI — `npm run bake && rsync out/ origin:/var/www/`.
218
+ 2. **Via CDN** (Cloudflare/Fastly): serve HTML with `Cache-Control: max-age=3600`. CDN invalidates itself.
219
+ 3. **Webhook trigger**: shop API → POST `/_revalidate?path=/product/mado-mug` → CI re-bakes only that page (you can implement `bake.paths` to return a targeted list based on an env parameter).
220
+
221
+ ---
222
+
223
+ ## Comparison with Alternatives
224
+
225
+ | | Next.js SSG/ISR | playwright-prerender | **mado bake** |
226
+ |--------------------------|--------------------|----------------------|-----------------|
227
+ | Chrome required in CI | no | **yes** (~300 MB) | **no** |
228
+ | Node required in production | for ISR | no | **no** |
229
+ | Time for 1000 pages | minutes | minutes | **seconds** |
230
+ | Magic level | high | low | **zero** |
231
+ | HTML parser | React renderer | browser | linkedom (~50 KB) |
232
+ | Configuration | next.config.js + … | single script | single script |
233
+ | Source of truth view+data | separate | page | **one page** |
234
+
235
+ ---
236
+
237
+ ## Limitations and Gotchas
238
+
239
+ - **There is no browser on the bake side.** The following do not work: `setTimeout` (technically it works, but bake finishes before it fires), `fetch` to relative URLs, any `effect()`/`signal()` side effects, real `requestAnimationFrame`. The render function must be deterministic based on `params` / `data`.
240
+ - **`linkedom` ≠ browser.** Not all DOM APIs are supported (for example, `HTMLElement.click()` behaves more simply). Heavy logic in Web Components will only execute in the browser after `connectedCallback`; only what rendered synchronously on the first pass will end up in the baked HTML.
241
+ - **Render dynamic content on the client.** Current time, A/B tests, geo-banners, the user's cart — these must not be in the baked HTML. Use `effect()` for client-side rendering.
242
+ - **Server-side imports must not end up in the client graph.** If `lib/posts.ts` imports `node:fs`, it cannot be imported from `view`. Keep such modules in a separate folder (`lib/build/`) and use them only from `bake.paths`/`bake.data`.
243
+ - **`paths` and `data` are executed on every bake run.** If they involve a heavy database query — cache at the script level.
244
+
245
+ ---
246
+
247
+ ## TL;DR
248
+
249
+ If a page is **the same for all users**, has **a relatively stable set of URLs**, and **SEO + first paint** matter — add `bake: { paths, data }` and get static HTML with meta/JSON-LD/sitemap in milliseconds. No node server, no Chrome, no magic.
250
+
251
+ If the page is personalized, or there are millions of URLs, or content changes in real time — `bake` is not your tool. Keep it as SPA or bring in a separate SSR framework.