@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,422 @@
1
+ # Mado for Backend Developers
2
+
3
+ > You write in Go / Rust / .NET / Java / Python and you need to build a web UI.
4
+ > This page is the mental model of Mado in 10 minutes, in your language.
5
+
6
+ ---
7
+
8
+ ## The Main Analogy
9
+
10
+ Mado is structured **like an HTTP server**. Seriously:
11
+
12
+ | Server world | Mado |
13
+ |---|---|
14
+ | HTTP router (chi, axum, mux) | `routes()` — path manifest |
15
+ | Handler `func(req, resp)` | `page({ view: (ctx) => html\`...\` })` |
16
+ | Middleware | `layout` in `nested()` (wraps the handler) |
17
+ | Template engine (Jinja, Handlebars) | `html\`\`` tagged template |
18
+ | HTTP client with cache | `resource()` — fetch + cache + invalidation |
19
+ | Reactive variable / atom | `signal()` — reactive getter |
20
+ | Background goroutine / task | `effect()` — auto-reruns when a signal changes |
21
+ | `defer cleanup()` | `ctx.onDispose(fn)` in component setup |
22
+ | ENV variables | `createContext()` + `provide()`/`inject()` |
23
+
24
+ If you understand an HTTP server, you understand Mado.
25
+
26
+ ---
27
+
28
+ ## File Structure — like a regular application
29
+
30
+ ```
31
+ src/
32
+ ├── routes.ts ← path manifest (like router.go in chi)
33
+ ├── main.ts ← entry point (like main.go: setup + run)
34
+ ├── pages/ ← one file per page (like handler.go)
35
+ ├── components/ ← reusable UI (like helpers/)
36
+ ├── layouts/ ← wrappers for groups of pages (like middleware/)
37
+ └── lib/ ← business logic, API client (like service/, repo/)
38
+ ```
39
+
40
+ One file = one page. No file-based magic routing — everything is declared by hand in `routes.ts`.
41
+
42
+ ---
43
+
44
+ ## Hello World — server analogy
45
+
46
+ ### Go (chi) — for comparison
47
+
48
+ ```go
49
+ r := chi.NewRouter()
50
+ r.Get("/", func(w http.ResponseWriter, r *http.Request) {
51
+ w.Write([]byte("<h1>Hello</h1>"))
52
+ })
53
+ r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
54
+ id := chi.URLParam(r, "id")
55
+ fmt.Fprintf(w, "<h1>User %s</h1>", id)
56
+ })
57
+ http.ListenAndServe(":8080", r)
58
+ ```
59
+
60
+ ### Mado — the same thing
61
+
62
+ ```ts
63
+ // src/routes.ts
64
+ import { routes } from "@madojs/mado";
65
+
66
+ export default routes({
67
+ "/": () => import("./pages/home.js"),
68
+ "/users/:id": () => import("./pages/user.js"),
69
+ });
70
+ ```
71
+
72
+ ```ts
73
+ // src/pages/home.ts
74
+ import { page, html } from "@madojs/mado";
75
+ export default page({
76
+ view: () => html`<h1>Hello</h1>`,
77
+ });
78
+ ```
79
+
80
+ ```ts
81
+ // src/pages/user.ts
82
+ import { page, html } from "@madojs/mado";
83
+ export default page<{ id: string }>({
84
+ view: ({ params }) => html`<h1>User ${params.id}</h1>`,
85
+ });
86
+ ```
87
+
88
+ Path parameters are available in `params` — just like `chi.URLParam`.
89
+
90
+ ---
91
+
92
+ ## Signals — a reactive variable
93
+
94
+ If you've written Erlang/Elixir with `Agent`, or Rust with `Arc<Mutex<T>>`, or simply stored state in a struct and updated it — `signal` is the same thing, plus **automatic re-rendering** of the components that read that state.
95
+
96
+ ```ts
97
+ import { signal, effect } from "@madojs/mado";
98
+
99
+ // "variable" with subscription
100
+ const count = signal(0);
101
+
102
+ // read
103
+ console.log(count()); // 0
104
+
105
+ // write
106
+ count.set(5);
107
+
108
+ // "goroutine" that runs on every change
109
+ effect(() => {
110
+ console.log("count is now", count());
111
+ });
112
+ // → will print "count is now 5"
113
+
114
+ count.set(10);
115
+ // → will print "count is now 10"
116
+ ```
117
+
118
+ No rules like "can't use inside a condition". A signal is just a getter function. Wherever it is read — that's where the subscription is created.
119
+
120
+ ---
121
+
122
+ ## `resource()` — HTTP client with cache (like `cache.GetOrSet`)
123
+
124
+ This is the **most useful abstraction for a backend developer**. It's like Redis with automatic invalidation, but in the browser.
125
+
126
+ ```ts
127
+ import { resource, mutation, jsonFetcher, invalidate } from "@madojs/mado";
128
+
129
+ // "user repository"
130
+ const userId = signal(1);
131
+
132
+ const user = resource(
133
+ () => `/api/users/${userId()}`, // cache key (reactive!)
134
+ jsonFetcher<User>(), // how to load
135
+ { staleTime: 60_000 }, // 60-second cache
136
+ );
137
+
138
+ // in the component:
139
+ user.data(); // User | undefined
140
+ user.error(); // Error | null
141
+ user.loading(); // boolean
142
+
143
+ // mutation (like POST/PUT)
144
+ const save = mutation<User, User>(
145
+ (u) => fetch("/api/users", { method: "POST", body: JSON.stringify(u) }).then(r => r.json()),
146
+ { invalidates: ["/api/users*"] }, // glob invalidation — like `cache.Drop("users:*")`
147
+ );
148
+
149
+ await save.run(newUser);
150
+ // automatically: user.data() will update if glob matches
151
+ ```
152
+
153
+ If such an abstraction existed in the Go world for server-side caches — we'd all be crying with joy.
154
+
155
+ ---
156
+
157
+ ## Components = handler with its own memory
158
+
159
+ A component is a **handler** that renders its piece of UI. It has:
160
+
161
+ - parameters (attributes/properties);
162
+ - internal state (`signal`s);
163
+ - lifecycle: `connectedCallback` (like Init), `disconnectedCallback` (like Close).
164
+
165
+ ```ts
166
+ import { component, html, signal } from "@madojs/mado";
167
+
168
+ component("x-counter", () => {
169
+ const count = signal(0);
170
+
171
+ return () => html`
172
+ <button @click=${() => count.update(n => n + 1)}>
173
+ Clicks: ${count}
174
+ </button>
175
+ `;
176
+ });
177
+ ```
178
+
179
+ Usage:
180
+
181
+ ```ts
182
+ html`<x-counter></x-counter>`
183
+ ```
184
+
185
+ We register the `<x-counter>` tag in the browser — it becomes a "function" that can be inserted into HTML. This is a **native** browser mechanism (Web Components), Mado only glues it together with signals.
186
+
187
+ ---
188
+
189
+ ## Forms — like `form.Validate()` on the backend
190
+
191
+ Mado uses **native HTML5 validation**, plus adds state tracking.
192
+
193
+ ```ts
194
+ import { useForm } from "@madojs/mado";
195
+
196
+ const f = useForm({
197
+ email: { required: true, type: "email" },
198
+ age: { required: true, type: "number", min: 18 },
199
+ });
200
+
201
+ // in the template:
202
+ html`
203
+ <form @submit=${f.onSubmit(async (v) => {
204
+ await api.save(v);
205
+ f.reset();
206
+ })}>
207
+ <input name="email" .value=${() => f.values().email ?? ""}
208
+ @input=${f.onInput} @blur=${f.onBlur} />
209
+
210
+ ${() => f.errors().email && f.touched().email
211
+ ? html`<small>${f.errors().email}</small>`
212
+ : null}
213
+
214
+ <button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
215
+ </form>
216
+ `;
217
+ ```
218
+
219
+ Custom validation — `validate: (values) => errors | null`. No Yup schemas or dependencies.
220
+
221
+ ---
222
+
223
+ ## Context = DI / dependency injection
224
+
225
+ Just as you pass `context.Context` through the call stack in Go — in Mado context is propagated through the DOM tree.
226
+
227
+ ```ts
228
+ import { createContext, provide, inject } from "@madojs/mado";
229
+
230
+ // declare the "type" of the dependency
231
+ const ApiCtx = createContext<ApiClient>(defaultApiClient);
232
+
233
+ // in the root component — provide
234
+ component("x-app", ({ host }) => {
235
+ provide(host, ApiCtx, new ApiClient("https://api.example.com"));
236
+ return () => html`<x-page/>`;
237
+ });
238
+
239
+ // in any child — consume
240
+ component("x-page", ({ host }) => {
241
+ const api = inject(host, ApiCtx); // signal<ApiClient>
242
+ return () => html`<div>API version: ${() => api().version}</div>`;
243
+ });
244
+ ```
245
+
246
+ This is like `context.WithValue` / `ctx.Value` in Go, but reactive.
247
+
248
+ ---
249
+
250
+ ## SEO — not SSR, but `bake` (like `templ generate` in Go)
251
+
252
+ If you're used to server-side rendering for SEO, in Mado this is solved differently: **prerender at build time**.
253
+
254
+ ```ts
255
+ // src/pages/product.ts
256
+ export default page({
257
+ bake: {
258
+ paths: () => api.allProductSlugs(), // build-time fetch
259
+ data: ({ slug }) => api.getProduct(slug),
260
+ revalidate: 3600,
261
+ },
262
+ head: ({ slug }, data) => ({
263
+ description: data.description,
264
+ canonical: `/product/${slug}`,
265
+ og: { title: data.name, image: data.image },
266
+ }),
267
+ view: ({ params }) => html`<x-product data-slug=${params.slug}/>`,
268
+ });
269
+ ```
270
+
271
+ ```bash
272
+ npm run bake # → out/product/iphone-15/index.html (+ sitemap)
273
+ ```
274
+
275
+ The crawler sees ready-made HTML with meta tags. The user sees the same thing + interactivity after JS loads.
276
+
277
+ More details: [`03-static-bake.md`](./03-static-bake.md).
278
+
279
+ ---
280
+
281
+ ## Typical backend developer tasks — recipes
282
+
283
+ ### CRUD page with a list
284
+
285
+ ```ts
286
+ import { page, html, resource, each, signal } from "@madojs/mado";
287
+
288
+ export default page({
289
+ view: () => {
290
+ const users = resource(() => "/api/users", jsonFetcher<User[]>());
291
+
292
+ return html`
293
+ ${() => users.loading() ? html`<p>Loading…</p>` : null}
294
+ ${() => users.error() ? html`<p>Error: ${users.error()!.message}</p>` : null}
295
+ <ul>
296
+ ${() => each(users.data() ?? [], u => u.id, u => html`
297
+ <li><a href="/users/${u.id}" data-link>${u.name}</a></li>
298
+ `)}
299
+ </ul>
300
+ `;
301
+ },
302
+ });
303
+ ```
304
+
305
+ ### Form with POST
306
+
307
+ ```ts
308
+ import { useForm, mutation } from "@madojs/mado";
309
+
310
+ const createUser = mutation<NewUser, User>(
311
+ (u) => fetch("/api/users", { method: "POST", body: JSON.stringify(u) }).then(r => r.json()),
312
+ { invalidates: ["/api/users*"] },
313
+ );
314
+
315
+ // in page.view:
316
+ const f = useForm({ name: { required: true } });
317
+
318
+ html`
319
+ <form @submit=${f.onSubmit(async (v) => {
320
+ await createUser.run(v);
321
+ navigate("/users");
322
+ })}>
323
+ <input name="name" @input=${f.onInput}>
324
+ <button>Create</button>
325
+ </form>
326
+ `;
327
+ ```
328
+
329
+ ### Protected zone (auth middleware)
330
+
331
+ ```ts
332
+ // src/layouts/auth-layout.ts
333
+ import { page, html, effect } from "@madojs/mado";
334
+ import { isAuthed, navigate } from "../lib/auth.js";
335
+
336
+ export default page({
337
+ view: ({ child }) => {
338
+ effect(() => {
339
+ if (!isAuthed()) navigate("/login");
340
+ });
341
+ return html`<div class="app-shell">${child}</div>`;
342
+ },
343
+ });
344
+ ```
345
+
346
+ ```ts
347
+ // src/routes.ts
348
+ import { routes, nested } from "@madojs/mado";
349
+
350
+ export default routes({
351
+ "/login": () => import("./pages/login.js"),
352
+
353
+ "/app/*": nested({
354
+ layout: () => import("./layouts/auth-layout.js"),
355
+ routes: {
356
+ "dashboard": () => import("./pages/dashboard.js"),
357
+ "users": () => import("./pages/users.js"),
358
+ },
359
+ }),
360
+ });
361
+ ```
362
+
363
+ ### Global API client (like a singleton in Go)
364
+
365
+ ```ts
366
+ // src/lib/api.ts
367
+ export class ApiClient {
368
+ constructor(private base: string) {}
369
+ get<T>(path: string): Promise<T> {
370
+ return fetch(this.base + path).then(r => r.json());
371
+ }
372
+ }
373
+
374
+ export const api = new ApiClient("/api");
375
+ ```
376
+
377
+ Used directly via `import { api } from '...'` or through `createContext` for testability.
378
+
379
+ ---
380
+
381
+ ## What you do **not** need to learn (good news)
382
+
383
+ - **Hooks and hook rules.** Not in Mado. Signals are ordinary functions.
384
+ - **VDOM and reconciliation.** None. Signals update the DOM directly, surgically.
385
+ - **Webpack/Vite configs.** No build. `tsc → browser`.
386
+ - **`useEffect` dependency arrays.** `effect()` sees what you read on its own.
387
+ - **State management libraries** (Redux/Zustand). Signals + context.
388
+ - **CSS-in-JS transformations.** Shadow DOM + `css\`\`` + cssVars.
389
+ - **Routing v6 → v7 migration guide.** `routes()` is 500 lines, readable in 20 minutes.
390
+
391
+ ---
392
+
393
+ ## What you **will** need to learn (honestly)
394
+
395
+ These are new concepts. Not scary, but they are additions to your React/Vue base:
396
+
397
+ 1. **Custom Elements / Shadow DOM.** `<x-foo>` is not a div, it is a full-fledged element with its own DOM. Slots, scoped CSS. One evening of MDN reading.
398
+ 2. **`attribute` vs `property`.** Attribute is a string in HTML (`data-id="5"`), property is a JS property (`el.id = 5`). `?attr=${flag}` and `.prop=${value}` in templates refer to different things. Main rule: **numbers/objects/arrays — via `.prop`, flags — via `?attr`, strings — via `attr`**.
399
+ 3. **Signals.** If it's your first time — you'll get stuck for 10 minutes, then it's easier than hooks.
400
+ 4. **`html\`\``-templates.** It's just a JS function with highlighting via [lit-plugin](./04-ide-setup.md). Not magic.
401
+
402
+ Everything else — standard browser + TypeScript.
403
+
404
+ ---
405
+
406
+ ## What's missing (honestly)
407
+
408
+ - No hot reload, only full reload via SSE. Sufficient for most cases, but not like Vite.
409
+ - No browser extension dev-tools. Use `localStorage.madoDebug = '1'` + console.
410
+ - No StackBlitz starters (yet).
411
+ - No AI assistant that knows Mado as well as React. When in doubt — read `src/`, it's not scary.
412
+
413
+ ---
414
+
415
+ ## Further reading
416
+
417
+ - **[`01-routing.md`](./01-routing.md)** — the router in detail.
418
+ - **[`02-project-layout.md`](./02-project-layout.md)** — project structure.
419
+ - **[`03-static-bake.md`](./03-static-bake.md)** — SEO without SSR.
420
+ - **[`examples/showcase/`](../../examples/showcase/)** — full example (landing + admin).
421
+
422
+ If something is unclear — open an issue, or just open the source. It really is readable in an evening.