@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.
- package/AGENTS.md +102 -25
- package/CHANGELOG.md +185 -1
- package/README.md +137 -172
- package/dist/src/component.d.ts +3 -2
- package/dist/src/component.js +98 -6
- package/dist/src/component.js.map +1 -1
- package/dist/src/css.js +34 -5
- package/dist/src/css.js.map +1 -1
- package/dist/src/head.d.ts +6 -6
- package/dist/src/head.js +6 -6
- package/dist/src/html/bindings.js +3 -3
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html/parser.d.ts +1 -1
- package/dist/src/html/parser.js +1 -1
- package/dist/src/html/template.js +24 -10
- package/dist/src/html/template.js.map +1 -1
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +13 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +62 -28
- package/dist/src/page.js +5 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/resource.js +2 -1
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/base.d.ts +65 -0
- package/dist/src/router/base.js +168 -0
- package/dist/src/router/base.js.map +1 -0
- package/dist/src/router/manifest.js +75 -31
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +16 -2
- package/dist/src/router/match.js +41 -1
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.js +58 -9
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/static-runtime.d.ts +81 -0
- package/dist/src/static-runtime.js +209 -0
- package/dist/src/static-runtime.js.map +1 -0
- package/dist/src/vite/index.d.ts +31 -3
- package/dist/src/vite/index.js +49 -18
- package/dist/src/vite/index.js.map +1 -1
- package/docs/README.md +5 -8
- package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
- package/docs/en/00-the-mado-way.md +1 -1
- package/docs/en/01-quickstart.md +183 -0
- package/docs/en/10-pages-and-components.md +271 -0
- package/docs/en/11-templates-and-signals.md +211 -0
- package/docs/en/12-routing.md +229 -0
- package/docs/en/13-data.md +225 -0
- package/docs/en/14-forms.md +244 -0
- package/docs/en/15-static-snapshots.md +181 -0
- package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
- package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
- package/docs/en/{14-testing.md → 22-testing.md} +6 -5
- package/docs/en/23-cookbook.md +292 -0
- package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
- package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
- package/docs/en/40-llm-guide.md +776 -0
- package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
- package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
- package/docs/en/README.md +97 -27
- package/docs/recipes/nginx/Containerfile +26 -0
- package/docs/recipes/nginx/nginx.conf +42 -0
- package/llms.txt +246 -211
- package/package.json +17 -10
- package/scripts/bake.mjs +4 -568
- package/scripts/cli/generate.mjs +55 -5
- package/scripts/cli/help.mjs +11 -5
- package/scripts/cli/index.mjs +20 -4
- package/scripts/cli/init.mjs +7 -2
- package/scripts/cli/release.mjs +20 -68
- package/scripts/docs-lint.mjs +170 -0
- package/scripts/package-smoke.mjs +67 -9
- package/scripts/preview.mjs +95 -6
- package/scripts/size-budget.mjs +5 -1
- package/scripts/static/browser.mjs +654 -0
- package/scripts/static/discover.mjs +281 -0
- package/scripts/static/output.mjs +141 -0
- package/scripts/static/serialize.mjs +212 -0
- package/scripts/static/server.mjs +167 -0
- package/scripts/static.mjs +191 -0
- package/starters/default/README.md +57 -55
- package/starters/default/package.json +3 -9
- package/starters/default/src/app.routes.ts +20 -33
- package/starters/default/src/components/feature-card.component.ts +40 -0
- package/starters/default/src/components/live-counter.component.ts +57 -0
- package/starters/default/src/content/guides.ts +55 -0
- package/starters/default/src/main.ts +9 -13
- package/starters/default/src/pages/app.page.ts +41 -0
- package/starters/default/src/pages/guide.page.ts +40 -0
- package/starters/default/src/pages/home.page.ts +49 -0
- package/starters/default/src/pages/not-found.page.ts +18 -0
- package/starters/default/src/styles/document.css +39 -0
- package/starters/default/src/styles/reset.css +37 -0
- package/starters/default/src/styles/tokens.css +30 -0
- package/starters/default/src/styles.d.ts +3 -1
- package/starters/default/src/vite-env.d.ts +1 -1
- package/starters/default/vite.config.ts +14 -1
- package/starters/modular/README.md +142 -0
- package/starters/modular/index.html +13 -0
- package/starters/modular/package.json +30 -0
- package/starters/modular/src/app.routes.ts +39 -0
- package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
- package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
- package/starters/modular/src/main.ts +16 -0
- package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
- package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
- package/starters/modular/src/modules/home/not-found.page.ts +14 -0
- package/starters/modular/src/styles.d.ts +1 -0
- package/starters/modular/src/vite-env.d.ts +1 -0
- package/starters/modular/tsconfig.json +24 -0
- package/starters/modular/vite.config.ts +131 -0
- package/TODO.md +0 -79
- package/docs/en/01-routing.md +0 -152
- package/docs/en/02-project-layout.md +0 -124
- package/docs/en/03-static-bake.md +0 -249
- package/docs/en/04-ide-setup.md +0 -162
- package/docs/en/07-llm-pitfalls.md +0 -724
- package/docs/en/08-llm-zero-history-test.md +0 -56
- package/docs/en/09-shadow-vs-light-dom.md +0 -174
- package/docs/en/11-layouts.md +0 -113
- package/docs/en/12-auth-and-api.md +0 -124
- package/docs/en/16-bake-cookbook.md +0 -100
- package/docs/en/17-shadow-dom-forms.md +0 -192
- package/docs/fr/00-the-mado-way.md +0 -85
- package/docs/fr/01-routing.md +0 -120
- package/docs/fr/02-project-layout.md +0 -84
- package/docs/fr/03-static-bake.md +0 -289
- package/docs/fr/04-ide-setup.md +0 -162
- package/docs/fr/05-why-mado.md +0 -193
- package/docs/fr/06-for-backenders.md +0 -438
- package/docs/fr/07-llm-pitfalls.md +0 -625
- package/docs/fr/08-llm-zero-history-test.md +0 -38
- package/docs/fr/09-shadow-vs-light-dom.md +0 -65
- package/docs/fr/10-app-architecture.md +0 -138
- package/docs/fr/11-layouts.md +0 -47
- package/docs/fr/12-auth-and-api.md +0 -76
- package/docs/fr/13-deployment.md +0 -57
- package/docs/fr/14-testing.md +0 -41
- package/docs/fr/15-error-handling.md +0 -50
- package/docs/fr/16-bake-cookbook.md +0 -88
- package/docs/fr/17-shadow-dom-forms.md +0 -196
- package/docs/fr/18-api-freeze-map.md +0 -63
- package/docs/fr/19-reactivity-ordering.md +0 -97
- package/docs/fr/20-v1-stability.md +0 -88
- package/docs/fr/README.md +0 -27
- package/docs/ru/00-the-mado-way.md +0 -84
- package/docs/ru/01-routing.md +0 -119
- package/docs/ru/02-project-layout.md +0 -84
- package/docs/ru/03-static-bake.md +0 -250
- package/docs/ru/04-ide-setup.md +0 -144
- package/docs/ru/05-why-mado.md +0 -193
- package/docs/ru/06-for-backenders.md +0 -428
- package/docs/ru/07-llm-pitfalls.md +0 -624
- package/docs/ru/08-llm-zero-history-test.md +0 -57
- package/docs/ru/09-shadow-vs-light-dom.md +0 -63
- package/docs/ru/10-app-architecture.md +0 -152
- package/docs/ru/11-layouts.md +0 -47
- package/docs/ru/12-auth-and-api.md +0 -75
- package/docs/ru/13-deployment.md +0 -66
- package/docs/ru/14-testing.md +0 -50
- package/docs/ru/15-error-handling.md +0 -56
- package/docs/ru/16-bake-cookbook.md +0 -95
- package/docs/ru/17-shadow-dom-forms.md +0 -193
- package/docs/ru/18-api-freeze-map.md +0 -64
- package/docs/ru/19-reactivity-ordering.md +0 -95
- package/docs/ru/20-v1-stability.md +0 -82
- package/docs/ru/README.md +0 -25
- package/docs/uk/00-the-mado-way.md +0 -82
- package/docs/uk/01-routing.md +0 -76
- package/docs/uk/02-project-layout.md +0 -73
- package/docs/uk/03-static-bake.md +0 -48
- package/docs/uk/04-ide-setup.md +0 -26
- package/docs/uk/05-why-mado.md +0 -34
- package/docs/uk/06-for-backenders.md +0 -55
- package/docs/uk/07-llm-pitfalls.md +0 -145
- package/docs/uk/08-llm-zero-history-test.md +0 -34
- package/docs/uk/09-shadow-vs-light-dom.md +0 -58
- package/docs/uk/10-app-architecture.md +0 -97
- package/docs/uk/11-layouts.md +0 -47
- package/docs/uk/12-auth-and-api.md +0 -70
- package/docs/uk/13-deployment.md +0 -40
- package/docs/uk/14-testing.md +0 -34
- package/docs/uk/15-error-handling.md +0 -32
- package/docs/uk/16-bake-cookbook.md +0 -36
- package/docs/uk/17-shadow-dom-forms.md +0 -193
- package/docs/uk/18-api-freeze-map.md +0 -61
- package/docs/uk/19-reactivity-ordering.md +0 -95
- package/docs/uk/20-v1-stability.md +0 -83
- package/docs/uk/README.md +0 -27
- package/starters/default/src/modules/home/not-found.page.ts +0 -11
- /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
- /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
- /package/starters/{default → modular}/.editorconfig +0 -0
- /package/starters/{default → modular}/eslint.config.mjs +0 -0
- /package/starters/{default → modular}/public/favicon.svg +0 -0
- /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
- /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
- /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
- /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
- /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
- /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
- /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Pages and Components
|
|
2
|
+
|
|
3
|
+
> Mado has exactly two primitives. You decide which one to use by
|
|
4
|
+
> looking at the URL bar, not at the DOM tree.
|
|
5
|
+
|
|
6
|
+
This is the document that removes the most common Mado design
|
|
7
|
+
question. By the end of it you should never again have to think
|
|
8
|
+
"should this be a page or a component?", "should this be Shadow DOM
|
|
9
|
+
or Light DOM?", or "how do I share styles across components?". You
|
|
10
|
+
just write the thing.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## The one rule
|
|
15
|
+
|
|
16
|
+
| If the unit is… | Use |
|
|
17
|
+
| -------------------------------------------------------- | ----------- |
|
|
18
|
+
| Something the URL points to (a route, a layout, a 404) | `page()` |
|
|
19
|
+
| Something you would copy-paste under multiple URLs | `component()` |
|
|
20
|
+
|
|
21
|
+
That's it. Pages are the URLs your app exposes. Components are
|
|
22
|
+
re-usable Web Components.
|
|
23
|
+
|
|
24
|
+
A page can render any number of components. A component can never
|
|
25
|
+
participate in routing. Pages live in `src/pages/` (universal
|
|
26
|
+
starter) or in `src/modules/<name>/pages/` (modular starter).
|
|
27
|
+
Components live in `src/components/` or `src/modules/<name>/components/`.
|
|
28
|
+
|
|
29
|
+
You **never** wrap a page in `component()` — that breaks form
|
|
30
|
+
participation, shared CSS and the static snapshot capture. You
|
|
31
|
+
**never** route to a `component()` — only `page()` shows up in the
|
|
32
|
+
router manifest.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## You write the same TypeScript either way
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { html, page, signal } from "@madojs/mado";
|
|
40
|
+
|
|
41
|
+
// PAGE — has a URL, owns route + load + head + view
|
|
42
|
+
export default page({
|
|
43
|
+
title: "Counter",
|
|
44
|
+
view: () => {
|
|
45
|
+
const n = signal(0);
|
|
46
|
+
return html`
|
|
47
|
+
<main>
|
|
48
|
+
<h1>Counter</h1>
|
|
49
|
+
<button @click=${() => n.set(n() + 1)}>Clicks: ${n}</button>
|
|
50
|
+
</main>
|
|
51
|
+
`;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { component, css, html, signal } from "@madojs/mado";
|
|
58
|
+
|
|
59
|
+
// COMPONENT — reusable, has no URL
|
|
60
|
+
component(
|
|
61
|
+
"x-counter",
|
|
62
|
+
() => {
|
|
63
|
+
const n = signal(0);
|
|
64
|
+
return () => html`
|
|
65
|
+
<button @click=${() => n.set(n() + 1)}>Clicks: ${n}</button>
|
|
66
|
+
`;
|
|
67
|
+
},
|
|
68
|
+
{ styles: css`button { padding: .5rem 1rem; }` },
|
|
69
|
+
);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Same signals. Same `html\`\`` templates. Same lifecycle. Same data
|
|
73
|
+
model (`resource()`, `mutation()`, `useForm()`, `effect()`). The
|
|
74
|
+
only thing that changes is *where* the DOM lives.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## What changes under the hood
|
|
79
|
+
|
|
80
|
+
| Property | `page()` | `component()` (default) |
|
|
81
|
+
| ---------------------------------------------- | ------------ | ------------------------------ |
|
|
82
|
+
| DOM location | Light DOM | Open Shadow DOM |
|
|
83
|
+
| Sees global CSS (`shell.css`, `content.css`) | Yes | No (Shadow boundary) |
|
|
84
|
+
| Can use `<slot>` | No (uses `child`) | Yes |
|
|
85
|
+
| Participates in a parent `<form>` | Yes | Only via `attachInternals()` or `shadow:false` |
|
|
86
|
+
| Is captured by `mado static` | If `static: true \| { ... }` | Yes (DSD serialised) |
|
|
87
|
+
| Has a hyphenated custom-element tag | No | Yes (`x-foo`, `my-bar`) |
|
|
88
|
+
| Owns its own CSS isolation | No | Yes (via `css\`\``) |
|
|
89
|
+
|
|
90
|
+
That is the entire decision matrix. Anything beyond it is detail.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## `{ shadow: false }` — the only escape hatch you need
|
|
95
|
+
|
|
96
|
+
Sometimes a custom element MUST live in the light DOM. Two real
|
|
97
|
+
cases:
|
|
98
|
+
|
|
99
|
+
1. **Form participation without `attachInternals()`.** A custom
|
|
100
|
+
input that should be submitted as part of a regular `<form>` and
|
|
101
|
+
whose author does not want to wire up `ElementInternals`.
|
|
102
|
+
2. **Host-level CSS that the document must address by tag name**
|
|
103
|
+
(rare; usually solved by passing class attributes instead).
|
|
104
|
+
|
|
105
|
+
For both, declare `{ shadow: false }`:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
component(
|
|
109
|
+
"x-custom-input",
|
|
110
|
+
() => () => html`<input name="email" />`,
|
|
111
|
+
{ shadow: false, styles: css`x-custom-input input { width: 100%; }` },
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
That is the *only* place this option is justified. If you reach for
|
|
116
|
+
`shadow: false` because you want to share document-level CSS classes,
|
|
117
|
+
**stop** — you wanted a `page()`, not a component.
|
|
118
|
+
|
|
119
|
+
Page-shaped wrappers (layouts, route shells) are written with
|
|
120
|
+
`page({ view: ({ child }) => ... })`. They are not components.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Decision table
|
|
125
|
+
|
|
126
|
+
| You are writing… | Reach for |
|
|
127
|
+
| --------------------------------------------- | --------------------------------------------- |
|
|
128
|
+
| A landing page | `page({ static: true, view })` |
|
|
129
|
+
| A dynamic SEO page (`/product/:slug`) | `page({ static: { paths, initialData } })` |
|
|
130
|
+
| An app screen behind auth | `page({ view })` (no `static`) |
|
|
131
|
+
| A shared shell that wraps several pages | `page({ view: ({ child }) => html\`...\` })` |
|
|
132
|
+
| A reusable button / badge / card / modal | `component("x-foo", setup, { styles })` |
|
|
133
|
+
| A custom form input | `component("x-input", setup, { shadow: false })` |
|
|
134
|
+
| A small inline render helper (no state) | a plain `(arg) => html\`...\`` function |
|
|
135
|
+
|
|
136
|
+
When in doubt, ask: **does this thing have a URL?** Yes → page.
|
|
137
|
+
No → component.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Anti-patterns
|
|
142
|
+
|
|
143
|
+
These are the four mistakes Mado generators and AI assistants tend
|
|
144
|
+
to produce. Avoid them.
|
|
145
|
+
|
|
146
|
+
### 1. Page inside a `component()`
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// ❌ Don't
|
|
150
|
+
component(
|
|
151
|
+
"x-home-page",
|
|
152
|
+
() => () => html`<h1>Home</h1><p>...</p>`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// ✅ Do
|
|
156
|
+
export default page({
|
|
157
|
+
static: true,
|
|
158
|
+
title: "Home",
|
|
159
|
+
view: () => html`<h1>Home</h1><p>...</p>`,
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Why: the page becomes invisible to the router, the static snapshot
|
|
164
|
+
pipeline cannot find it, and shared CSS (`content.css`) does not
|
|
165
|
+
cross the Shadow boundary.
|
|
166
|
+
|
|
167
|
+
### 2. Component used as a route
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// ❌ Don't
|
|
171
|
+
import "./components/billing-screen.component.js";
|
|
172
|
+
const manifest = { "/billing": () => html`<x-billing-screen/>` };
|
|
173
|
+
|
|
174
|
+
// ✅ Do
|
|
175
|
+
import billing from "./pages/billing.page.js";
|
|
176
|
+
const manifest = { "/billing": billing };
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Why: the router gives you `page.head()`, `page.load()`, the static
|
|
180
|
+
snapshot pipeline, the `data-mado-head` lifecycle and the seed
|
|
181
|
+
contract. A component does not.
|
|
182
|
+
|
|
183
|
+
### 3. `shadow: false` to "fix" CSS in a route layout
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// ❌ Don't — this is a page wearing the wrong hat
|
|
187
|
+
component(
|
|
188
|
+
"x-app-shell",
|
|
189
|
+
() => ({ child }) => html`<header>...</header><main>${child}</main>`,
|
|
190
|
+
{ shadow: false, styles: css`...` },
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// ✅ Do
|
|
194
|
+
export default page({
|
|
195
|
+
view: ({ child }) => html`<header>...</header><main>${child}</main>`,
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Why: light-DOM components do not get the route `child`, are not
|
|
200
|
+
recognised by `layout()`, and the manifest cannot wrap them.
|
|
201
|
+
|
|
202
|
+
### 4. Route-id state in layout view locals
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// ❌ Don't — `current` is shared across every page rendered by this layout
|
|
206
|
+
const current = signal<User | null>(null);
|
|
207
|
+
export default page({
|
|
208
|
+
view: ({ child }) => {
|
|
209
|
+
effect(() => loadUser(routePath()).then(current.set));
|
|
210
|
+
return html`<x-shell .user=${current}>${child}</x-shell>`;
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ✅ Do — put the resource in the page that needs it
|
|
215
|
+
const user = resource(() => `/api/users/${userId()}`, jsonFetcher<User>());
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Layouts are stateless wrappers. Per-page state belongs in the page
|
|
219
|
+
itself or in a `resource()` whose key is the page's identity.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## What about styles?
|
|
224
|
+
|
|
225
|
+
- `page()` lives in the light DOM. Use `content.css` / `shell.css`
|
|
226
|
+
from `src/styles/` (universal starter) or `src/shared/styles/`
|
|
227
|
+
(modular starter). Class selectors work normally.
|
|
228
|
+
- `component()` carries its own `css\`\``. CSS custom properties
|
|
229
|
+
(`--accent`, `--bg`) cross the Shadow boundary; class selectors
|
|
230
|
+
from the document do not.
|
|
231
|
+
- The shared design tokens (colours, spacing, type) live in
|
|
232
|
+
`tokens.css`. They are CSS custom properties and reach both pages
|
|
233
|
+
and components through `var(--...)`.
|
|
234
|
+
|
|
235
|
+
For the long form on style boundaries, see
|
|
236
|
+
[20-deployment.md](./20-deployment.md) (production tuning) and the
|
|
237
|
+
starter README files.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## What about routing and links?
|
|
242
|
+
|
|
243
|
+
Internal navigation is base-aware. Always use `routeUrl()` and
|
|
244
|
+
`data-link`:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { html, routeUrl } from "@madojs/mado";
|
|
248
|
+
|
|
249
|
+
html`<a data-link href=${routeUrl("/billing/invoices")}>Invoices</a>`;
|
|
250
|
+
html`<a data-link href=${routeUrl("/")}>Home</a>`; // → "/mado/" under base
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`data-link` opts the anchor into SPA navigation. A bare `<a href>`
|
|
254
|
+
performs a full document load — that is intentional for foreign
|
|
255
|
+
links and downloads. The router intercepts links inside Shadow DOM
|
|
256
|
+
too (it uses `event.composedPath()`).
|
|
257
|
+
|
|
258
|
+
Full router contract: [12-routing.md](./12-routing.md).
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Further reading
|
|
263
|
+
|
|
264
|
+
- [11-templates-and-signals.md](./11-templates-and-signals.md) — the
|
|
265
|
+
`html\`\`` parser, signals, `each`, reactive bindings.
|
|
266
|
+
- [12-routing.md](./12-routing.md) — `routes()`, layouts, guards,
|
|
267
|
+
`routeUrl`, `navigate`, prefetch.
|
|
268
|
+
- [15-static-snapshots.md](./15-static-snapshots.md) — when to mark
|
|
269
|
+
a page `static: true`.
|
|
270
|
+
- [14-forms.md](./14-forms.md) — `useForm()` + the only Shadow DOM
|
|
271
|
+
case where `shadow: false` is right.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Templates and signals
|
|
2
|
+
|
|
3
|
+
> Reactivity in Mado is one primitive — `signal()` — composed into
|
|
4
|
+
> three reading patterns. Templates are tagged `html\`\`` literals
|
|
5
|
+
> the browser understands directly.
|
|
6
|
+
|
|
7
|
+
By the end of this page you should be able to read and write any
|
|
8
|
+
Mado view without surprises.
|
|
9
|
+
|
|
10
|
+
## Signals
|
|
11
|
+
|
|
12
|
+
A signal is a getter function with a `.set()` method.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { signal, computed, effect } from "@madojs/mado";
|
|
16
|
+
|
|
17
|
+
const count = signal(0);
|
|
18
|
+
|
|
19
|
+
count(); // 0 — read
|
|
20
|
+
count.set(5); // — write
|
|
21
|
+
count.update((n) => n + 1); // 6 — derived write
|
|
22
|
+
count.peek(); // 6 — read without subscribing
|
|
23
|
+
|
|
24
|
+
const doubled = computed(() => count() * 2);
|
|
25
|
+
doubled(); // 12
|
|
26
|
+
|
|
27
|
+
effect(() => console.log("count =", count()));
|
|
28
|
+
// → "count = 6"
|
|
29
|
+
count.set(7);
|
|
30
|
+
// → "count = 7"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Rules of thumb:
|
|
34
|
+
|
|
35
|
+
- A signal is a **function**: `count()`, not `count.value`.
|
|
36
|
+
- `effect()` and `computed()` track every signal read **during their
|
|
37
|
+
callback**. There is no dependency array.
|
|
38
|
+
- `computed()` is lazy and de-duplicates: if its result is `Object.is`-
|
|
39
|
+
equal to the previous one, subscribers do not re-run.
|
|
40
|
+
- `untracked(() => ...)` reads without subscribing. Use it inside
|
|
41
|
+
async callbacks created from a synchronous `view()` so the router's
|
|
42
|
+
render effect does not accidentally subscribe.
|
|
43
|
+
- `batch(() => { a.set(...); b.set(...); })` flushes once after the
|
|
44
|
+
callback returns. `flushSync()` flushes pending effects immediately.
|
|
45
|
+
|
|
46
|
+
`effect()` cleanups run before the next run and on disposal. Inside a
|
|
47
|
+
component or a page they happen automatically when the host leaves
|
|
48
|
+
the DOM; outside, register the disposer through
|
|
49
|
+
`getCurrentLifecycle()` or pass an explicit one to a
|
|
50
|
+
`createLifecycle()` you own.
|
|
51
|
+
|
|
52
|
+
## Templates
|
|
53
|
+
|
|
54
|
+
`html\`\`` parses once per template literal into a static fragment +
|
|
55
|
+
binding indices, then patches only the changing slots on each
|
|
56
|
+
re-render.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { html } from "@madojs/mado";
|
|
60
|
+
|
|
61
|
+
html`<h1>Hello, ${name}</h1>`;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Five binding shapes — and only these:
|
|
65
|
+
|
|
66
|
+
| Shape | Meaning |
|
|
67
|
+
| -------------------- | ----------------------------------------------------------------------- |
|
|
68
|
+
| `${value}` | Child content: text, nodes, arrays, nested `html`, `each()` results. |
|
|
69
|
+
| `attr=${v}` | HTML attribute. String / number / falsy (`false`/`null`/`undefined` → remove). |
|
|
70
|
+
| `.prop=${v}` | DOM property. Use for `.value` of inputs, arrays, objects, numbers. |
|
|
71
|
+
| `?attr=${flag}` | Boolean attribute. `true` → present, `false` → absent (`?disabled`). |
|
|
72
|
+
| `@event=${fn}` | Event listener. Removed automatically on re-render. |
|
|
73
|
+
|
|
74
|
+
### The single most common mistake
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const count = signal(0);
|
|
78
|
+
|
|
79
|
+
// ❌ NOT REACTIVE — count() is read once at render
|
|
80
|
+
html`<div>${count() * 2}</div>`
|
|
81
|
+
|
|
82
|
+
// ✅ REACTIVE — the function is re-invoked on signal change
|
|
83
|
+
html`<div>${() => count() * 2}</div>`
|
|
84
|
+
|
|
85
|
+
// ✅ ALSO OK — the signal itself IS a function
|
|
86
|
+
html`<div>${count}</div>`
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Rule of thumb: if you wrote `signal()` (with parens) inside `${...}`,
|
|
90
|
+
wrap the whole expression in `() => ...`.
|
|
91
|
+
|
|
92
|
+
### Lists — `each(items, keyFn, renderFn)`
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { each } from "@madojs/mado";
|
|
96
|
+
|
|
97
|
+
html`<ul>
|
|
98
|
+
${() =>
|
|
99
|
+
each(
|
|
100
|
+
users(),
|
|
101
|
+
(u) => u.id,
|
|
102
|
+
(u) => html`<li>${u.name}</li>`,
|
|
103
|
+
)}
|
|
104
|
+
</ul>`;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`each()` is keyed: rows keep DOM identity across re-orders, so input
|
|
108
|
+
focus, scroll position and component state survive. A plain `.map()`
|
|
109
|
+
works but is not keyed; avoid it for anything but throwaway lists.
|
|
110
|
+
|
|
111
|
+
### Directives
|
|
112
|
+
|
|
113
|
+
Inline helpers you import alongside `html`:
|
|
114
|
+
|
|
115
|
+
- `unsafeHTML(string)` — interpolate trusted HTML.
|
|
116
|
+
- `ref((el) => …)` — get a callback when the element mounts.
|
|
117
|
+
- `classMap({ active: isActive(), error: hasError() })` — toggle
|
|
118
|
+
class names declaratively.
|
|
119
|
+
- `styleMap({ color: theme().fg, "--bg": theme().bg })` — set
|
|
120
|
+
inline styles, including custom properties.
|
|
121
|
+
|
|
122
|
+
### Parser hard errors
|
|
123
|
+
|
|
124
|
+
Mado refuses templates it cannot represent safely. Two cases you
|
|
125
|
+
will hit:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// ❌ slots inside RAW_TEXT elements
|
|
129
|
+
html`<textarea>${draft}</textarea>`;
|
|
130
|
+
html`<title>${title}</title>`;
|
|
131
|
+
|
|
132
|
+
// ✅ use properties / page APIs
|
|
133
|
+
html`<textarea .value=${draft}></textarea>`;
|
|
134
|
+
page({ title: ({ id }) => `User ${id}`, view: () => html`<main></main>` });
|
|
135
|
+
|
|
136
|
+
// ❌ nested SVG-only templates (namespace context is lost)
|
|
137
|
+
html`<svg>${html`<circle r="5"></circle>`}</svg>`;
|
|
138
|
+
|
|
139
|
+
// ✅ keep SVG internals in one template
|
|
140
|
+
html`<svg viewBox="0 0 10 10"><circle r="5"></circle></svg>`;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
No dynamic `${...}` child slots inside `<script>`, `<style>`,
|
|
144
|
+
`<textarea>` or `<title>`.
|
|
145
|
+
|
|
146
|
+
## How the two compose
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { html, page, signal } from "@madojs/mado";
|
|
150
|
+
|
|
151
|
+
export default page({
|
|
152
|
+
title: "Counter",
|
|
153
|
+
view: () => {
|
|
154
|
+
// 1. local state
|
|
155
|
+
const n = signal(0);
|
|
156
|
+
|
|
157
|
+
// 2. event handlers
|
|
158
|
+
const inc = () => n.update((x) => x + 1);
|
|
159
|
+
|
|
160
|
+
// 3. view — values that change are wrapped in arrow functions
|
|
161
|
+
return html`
|
|
162
|
+
<main>
|
|
163
|
+
<h1>Counter</h1>
|
|
164
|
+
<button @click=${inc} ?disabled=${() => n() >= 10}>${n}</button>
|
|
165
|
+
</main>
|
|
166
|
+
`;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
A page's `view()` runs **once** when the route commits; the returned
|
|
172
|
+
template stays mounted until the route changes. Every reactive slot
|
|
173
|
+
re-evaluates independently — Mado never re-runs the whole view.
|
|
174
|
+
|
|
175
|
+
## Lifecycle reads
|
|
176
|
+
|
|
177
|
+
Inside a `component(setup, …)` you get the lifecycle through `ctx`:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
component("x-timer", (ctx) => {
|
|
181
|
+
const tick = signal(0);
|
|
182
|
+
const id = setInterval(() => tick.update((n) => n + 1), 1000);
|
|
183
|
+
ctx.onDispose(() => clearInterval(id));
|
|
184
|
+
return () => html`<span>${tick}</span>`;
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Inside a `page({ view })` use the same `onDispose` from the view
|
|
189
|
+
context:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
export default page({
|
|
193
|
+
view: ({ onDispose }) => {
|
|
194
|
+
const handler = () => { /* … */ };
|
|
195
|
+
window.addEventListener("resize", handler);
|
|
196
|
+
onDispose?.(() => window.removeEventListener("resize", handler));
|
|
197
|
+
return html`<main>…</main>`;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`resource()`, `effect()` and `mutation()` subscribe to the active
|
|
203
|
+
lifecycle automatically — you do not write cleanup for them.
|
|
204
|
+
|
|
205
|
+
## Further reading
|
|
206
|
+
|
|
207
|
+
- [10-pages-and-components.md](./10-pages-and-components.md) — the
|
|
208
|
+
page vs component decision.
|
|
209
|
+
- [13-data.md](./13-data.md) — `resource()` and `mutation()`.
|
|
210
|
+
- [31-reactivity-ordering.md](./31-reactivity-ordering.md) — signal
|
|
211
|
+
scheduling, equality and teardown guarantees.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
> One app map. No folder scanners. No special path syntax. Layouts
|
|
4
|
+
> are pages that render `${child}`.
|
|
5
|
+
|
|
6
|
+
Mado does not infer routes from files. Route composition lives in
|
|
7
|
+
one file — `src/app.routes.ts` — so the whole map is readable at a
|
|
8
|
+
glance.
|
|
9
|
+
|
|
10
|
+
## The app manifest
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// src/app.routes.ts
|
|
14
|
+
import { layout, routes } from "@madojs/mado";
|
|
15
|
+
import { requireAuth } from "./modules/auth/auth.public";
|
|
16
|
+
import { authRoutes } from "./modules/auth/auth.routes";
|
|
17
|
+
import { billingRoutes } from "./modules/billing/billing.routes";
|
|
18
|
+
|
|
19
|
+
export const manifest = {
|
|
20
|
+
"/": () => import("./modules/home/home.page"),
|
|
21
|
+
"/login": layout({
|
|
22
|
+
layout: () => import("./layouts/auth-shell.layout"),
|
|
23
|
+
routes: authRoutes,
|
|
24
|
+
}),
|
|
25
|
+
"/billing": layout({
|
|
26
|
+
layout: () => import("./layouts/app-shell.layout"),
|
|
27
|
+
guard: requireAuth,
|
|
28
|
+
routes: billingRoutes,
|
|
29
|
+
}),
|
|
30
|
+
"*": () => import("./modules/home/not-found.page"),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default routes(manifest);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Opening `app.routes.ts` shows the whole app: public pages, auth
|
|
37
|
+
zone, protected zones, guards and shells.
|
|
38
|
+
|
|
39
|
+
Exporting `manifest` is **required**: `mado static` discovers
|
|
40
|
+
static routes by reading it.
|
|
41
|
+
|
|
42
|
+
## Module routes
|
|
43
|
+
|
|
44
|
+
Modules export plain route maps. They do not decide which shell
|
|
45
|
+
wraps them; that happens in `app.routes.ts`.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// src/modules/billing/billing.routes.ts
|
|
49
|
+
export const billingRoutes = {
|
|
50
|
+
"/invoices": () => import("./pages/invoices-list.page"),
|
|
51
|
+
"/invoices/:id": () => import("./pages/invoice-detail.page"),
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When mounted under `"/billing"` the public URLs become
|
|
56
|
+
`/billing/invoices` and `/billing/invoices/:id`.
|
|
57
|
+
|
|
58
|
+
## Layouts (the one blessed path)
|
|
59
|
+
|
|
60
|
+
A layout is a `page({ view })` that renders `${ctx.child}` somewhere:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// src/layouts/app-shell.layout.ts
|
|
64
|
+
import { html, page } from "@madojs/mado";
|
|
65
|
+
|
|
66
|
+
export default page({
|
|
67
|
+
view: ({ child }) => html`
|
|
68
|
+
<header class="app-header">…</header>
|
|
69
|
+
<main class="app-main">${child}</main>
|
|
70
|
+
`,
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Mount it through `layout({ layout, routes, guard? })` in
|
|
75
|
+
`app.routes.ts`. That is the **only** canonical place to declare a
|
|
76
|
+
layout.
|
|
77
|
+
|
|
78
|
+
Two corollaries that prevent the classic "navigation appears below
|
|
79
|
+
content" bug:
|
|
80
|
+
|
|
81
|
+
- **Order matters.** Outer manifest entries wrap inner ones.
|
|
82
|
+
- **One shell per group.** If a subtree needs a different shell,
|
|
83
|
+
open a new `layout({...})` group; do not branch inside one shell.
|
|
84
|
+
|
|
85
|
+
Two acceptable alternatives exist but are escape hatches, not the
|
|
86
|
+
default:
|
|
87
|
+
|
|
88
|
+
- *Single shell in `main.ts`* — `render(html\`<x-shell>${routes.view}</x-shell>\`)`.
|
|
89
|
+
Caveat: every route lives inside one shell; no centred login page,
|
|
90
|
+
no marketing landing without admin chrome.
|
|
91
|
+
- *Per-page wrapping inside `view`* — each page repeats the shell.
|
|
92
|
+
Caveat: the first time someone forgets it the layout disappears.
|
|
93
|
+
Do not start with this.
|
|
94
|
+
|
|
95
|
+
Layouts are stateless wrappers. Per-page state belongs in the page
|
|
96
|
+
or in a `resource()` keyed by the page's identity — never in layout
|
|
97
|
+
view locals.
|
|
98
|
+
|
|
99
|
+
## What goes on the right side
|
|
100
|
+
|
|
101
|
+
### Lazy page import (preferred)
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
"/users/:id": () => import("./modules/users/pages/user-profile.page"),
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Vite emits a separate chunk per dynamic import in production.
|
|
108
|
+
|
|
109
|
+
### Eager page
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import home from "./modules/home/home.page";
|
|
113
|
+
|
|
114
|
+
export const manifest = { "/": home };
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use only for tiny critical pages.
|
|
118
|
+
|
|
119
|
+
### Layout group
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
"/admin": layout({
|
|
123
|
+
layout: () => import("./layouts/app-shell.layout"),
|
|
124
|
+
guard: requireAuth,
|
|
125
|
+
routes: adminRoutes,
|
|
126
|
+
}),
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Page contract
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { html, page } from "@madojs/mado";
|
|
133
|
+
|
|
134
|
+
export default page<{ id: string }>({
|
|
135
|
+
title: ({ id }) => `User ${id}`,
|
|
136
|
+
view: ({ params }) => html`<h1>${params.id}</h1>`,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If a lazy route does not default-export `page({...})`, `routes()`
|
|
141
|
+
throws a clear error at load time.
|
|
142
|
+
|
|
143
|
+
## Guards
|
|
144
|
+
|
|
145
|
+
A guard is a function returning `true | false | string | { redirect, replace? } | { halt }`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
export function requireAuth(): boolean | string {
|
|
149
|
+
return isAuthed() ? true : "/login";
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Guards apply outer → inner: layout-group guards run before page
|
|
154
|
+
guards. Static routes refuse all guards (they are public by
|
|
155
|
+
definition).
|
|
156
|
+
|
|
157
|
+
## Internal links — `routeUrl()` + `data-link`
|
|
158
|
+
|
|
159
|
+
Vite's `base` flows into `appBase` and `routeUrl()` automatically.
|
|
160
|
+
Internal links MUST use both:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { html, routeUrl } from "@madojs/mado";
|
|
164
|
+
|
|
165
|
+
html`<a data-link href=${routeUrl("/users/42")}>User 42</a>`;
|
|
166
|
+
html`<a data-link href=${routeUrl("/")}>Home</a>`; // → "/mado/" under base
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- `routeUrl(path)` returns a base-prefixed URL (preserves query and
|
|
170
|
+
hash).
|
|
171
|
+
- `data-link` opts the anchor into SPA navigation. A bare
|
|
172
|
+
`<a href>` performs a full document load — intentional for
|
|
173
|
+
foreign URLs and downloads.
|
|
174
|
+
|
|
175
|
+
The router intercepts links inside open Shadow DOM (it uses
|
|
176
|
+
`event.composedPath()`).
|
|
177
|
+
|
|
178
|
+
## Programmatic navigation
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import appRoutes from "./app.routes";
|
|
182
|
+
import { navigate } from "@madojs/mado";
|
|
183
|
+
|
|
184
|
+
appRoutes.navigate("/billing/invoices");
|
|
185
|
+
appRoutes.navigate("/billing/invoices?page=2");
|
|
186
|
+
appRoutes.navigate("/login", { replace: true });
|
|
187
|
+
|
|
188
|
+
// Standalone helper — dispatches popstate, every active router updates.
|
|
189
|
+
navigate("/users/42");
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Both accept route paths (no base prefix). The active base is
|
|
193
|
+
re-applied internally before `history.pushState`.
|
|
194
|
+
|
|
195
|
+
## Query parameters
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { queryParam } from "@madojs/mado";
|
|
199
|
+
|
|
200
|
+
const page = queryParam("page", "1");
|
|
201
|
+
page(); // current value (reactive)
|
|
202
|
+
page.set("2"); // history.replaceState
|
|
203
|
+
page.set("3", { push: true }); // history.pushState
|
|
204
|
+
page.set(null); // delete the parameter
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`queryParam()` returns a `Signal<string>`. Reading inside a
|
|
208
|
+
template subscribes; updating triggers a re-render.
|
|
209
|
+
|
|
210
|
+
## Prefetch
|
|
211
|
+
|
|
212
|
+
Hover-prefetch is on by default for `<a data-link>`. Programmatic
|
|
213
|
+
prefetch:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { prefetchPath } from "@madojs/mado";
|
|
217
|
+
prefetchPath("/billing/invoices/123");
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Foreign-base links (outside the active prefix) are ignored — the
|
|
221
|
+
manifest only knows base-free route paths.
|
|
222
|
+
|
|
223
|
+
## What is intentionally absent
|
|
224
|
+
|
|
225
|
+
- No auto-scan of page folders.
|
|
226
|
+
- No filesystem route syntax (`[id]`, `(group)`, `_layout`).
|
|
227
|
+
- No server routes in the client manifest.
|
|
228
|
+
- No hidden layout discovery — app zones are explicit in
|
|
229
|
+
`app.routes.ts`.
|