@voltro/cli 0.15.0 → 0.17.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 (137) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{apiBuild-CHItlwPO.js → apiBuild-ChdlLqGv.js} +2 -2
  3. package/dist/apiBuild-D5WB119b.js +2 -0
  4. package/dist/bin.js +3 -3
  5. package/dist/{commands-CPQuQRXh.js → commands-DYQbv-DG.js} +2333 -2292
  6. package/dist/dbCommand-C3R5LrBZ.js +2 -0
  7. package/dist/{dbCommand-B8F29zV0.js → dbCommand-DJSxnjPt.js} +225 -211
  8. package/dist/{dev-BHLM3RPX.js → dev-BCmoJfBm.js} +1 -1
  9. package/dist/{dev-C2lCUhYI.js → dev-DB7pbLob.js} +893 -892
  10. package/dist/{fileConventions-Cu0tM-Bl.js → fileConventions-CBNPZVOV.js} +1 -1
  11. package/dist/frameworkTableAssembly-DRJrAVr1.js +2 -0
  12. package/dist/{frameworkTableAssembly-BA2bAmYN.js → frameworkTableAssembly-sg41s14F.js} +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/{inspectMetrics-CYDSO3za.js → inspectMetrics-DWh56Qas.js} +205 -205
  15. package/dist/{serveCommand-DBzQcCuv.js → serveCommand-DO6qr5Ok.js} +7 -5
  16. package/dist/serveEntry.js +2 -2
  17. package/dist/{start-BtgatJN3.js → start-ksY0wMZG.js} +379 -379
  18. package/dist/startEntry.js +2 -2
  19. package/package.json +17 -17
  20. package/templates/AGENTS.core.md +24 -9
  21. package/templates/AGENTS.md +25 -10
  22. package/templates/agent-docs/_index.md +1 -1
  23. package/templates/agent-docs/cli.md +2 -2
  24. package/templates/agent-docs/data.md +9 -1
  25. package/templates/agent-docs/database/migrations.md +3 -3
  26. package/templates/agent-docs/internationalization.md +3 -3
  27. package/templates/agent-docs/introduction.md +2 -2
  28. package/templates/agent-docs/plugins.md +1 -1
  29. package/templates/agent-docs/reference.md +4 -4
  30. package/templates/agent-docs/routing.md +40 -40
  31. package/templates/agent-docs/templates/appshells.md +24 -24
  32. package/templates/agent-docs/whats-new.md +31 -253
  33. package/templates/apps/api-ai/package.json +7 -7
  34. package/templates/apps/api-auth/package.json +8 -8
  35. package/templates/apps/api-backend/package.json +7 -7
  36. package/templates/apps/api-backend-deactivation/package.json +7 -7
  37. package/templates/apps/api-backend-mail/package.json +8 -8
  38. package/templates/apps/api-backend-mariadb/package.json +9 -9
  39. package/templates/apps/api-backend-storage/package.json +8 -8
  40. package/templates/apps/api-data-advanced/package.json +8 -8
  41. package/templates/apps/api-durable/package.json +8 -8
  42. package/templates/apps/api-feature-flags/package.json +9 -9
  43. package/templates/apps/api-governance/package.json +8 -8
  44. package/templates/apps/api-kv/package.json +8 -8
  45. package/templates/apps/api-moderation/package.json +8 -8
  46. package/templates/apps/api-observability/package.json +8 -8
  47. package/templates/apps/api-ratelimit/package.json +8 -8
  48. package/templates/apps/api-rbac/package.json +8 -8
  49. package/templates/apps/api-rest/package.json +7 -7
  50. package/templates/apps/api-saas/package.json +11 -11
  51. package/templates/apps/api-search/package.json +8 -8
  52. package/templates/apps/api-versioning/package.json +8 -8
  53. package/templates/apps/api-webhooks/package.json +9 -9
  54. package/templates/apps/changelog/package.json +6 -6
  55. package/templates/apps/changelog/src/pages/[locale]/{[slug].page.tsx → [slug]/page.tsx} +4 -4
  56. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +2 -2
  57. package/templates/apps/changelog/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  58. package/templates/apps/changelog/src/pages/{[slug].page.test.tsx → [slug]/page.test.tsx} +1 -1
  59. package/templates/apps/changelog/src/pages/{[slug].page.tsx → [slug]/page.tsx} +2 -2
  60. package/templates/apps/changelog/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  61. package/templates/apps/edge-functions/package.json +2 -2
  62. package/templates/apps/frontend-admin/package.json +8 -8
  63. package/templates/apps/frontend-admin/src/pages/(marketing)/{login.page.test.tsx → login/page.test.tsx} +2 -2
  64. package/templates/apps/frontend-admin/src/pages/(marketing)/{login.page.tsx → login/page.tsx} +2 -2
  65. package/templates/apps/frontend-admin/src/pages/(marketing)/{index.page.test.tsx → page.test.tsx} +1 -1
  66. package/templates/apps/frontend-admin/src/pages/admin/{[entity].page.tsx → [entity]/page.tsx} +1 -1
  67. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +1 -1
  68. package/templates/apps/frontend-admin/src/pages/admin/{index.page.test.tsx → page.test.tsx} +1 -1
  69. package/templates/apps/frontend-app/package.json +8 -8
  70. package/templates/apps/frontend-app/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  71. package/templates/apps/frontend-app/src/pages/{schema-ui.page.test.tsx → schema-ui/page.test.tsx} +2 -2
  72. package/templates/apps/frontend-app/src/pages/{schema-ui.page.tsx → schema-ui/page.tsx} +1 -1
  73. package/templates/apps/frontend-blank/package.json +7 -7
  74. package/templates/apps/frontend-blank/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  75. package/templates/apps/frontend-contact/package.json +7 -7
  76. package/templates/apps/frontend-contact/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  77. package/templates/apps/frontend-contact/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  78. package/templates/apps/frontend-dashboard/package.json +7 -7
  79. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.page.test.tsx → login/page.test.tsx} +2 -2
  80. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.page.tsx → login/page.tsx} +2 -2
  81. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.page.test.tsx → page.test.tsx} +1 -1
  82. package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.page.test.tsx → page.test.tsx} +1 -1
  83. package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.page.test.tsx → settings/page.test.tsx} +2 -2
  84. package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.page.tsx → settings/page.tsx} +2 -2
  85. package/templates/apps/frontend-docs/package.json +7 -7
  86. package/templates/apps/frontend-docs/src/pages/[locale]/docs/{[...slug].page.tsx → [...slug]/page.tsx} +4 -4
  87. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +2 -2
  88. package/templates/apps/frontend-docs/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  89. package/templates/apps/frontend-docs/src/pages/docs/{[...slug].page.test.tsx → [...slug]/page.test.tsx} +3 -3
  90. package/templates/apps/frontend-docs/src/pages/docs/{[...slug].page.tsx → [...slug]/page.tsx} +1 -1
  91. package/templates/apps/frontend-docs/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  92. package/templates/apps/frontend-i18n/package.json +6 -6
  93. package/templates/apps/frontend-i18n/src/pages/[locale]/{about.page.tsx → about/page.tsx} +3 -3
  94. package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +2 -2
  95. package/templates/apps/frontend-i18n/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  96. package/templates/apps/frontend-i18n/src/pages/{about.page.test.tsx → about/page.test.tsx} +3 -3
  97. package/templates/apps/frontend-i18n/src/pages/{about.page.tsx → about/page.tsx} +1 -1
  98. package/templates/apps/frontend-i18n/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  99. package/templates/apps/frontend-landing/package.json +7 -7
  100. package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +1 -1
  101. package/templates/apps/frontend-landing/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  102. package/templates/apps/frontend-landing/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  103. package/templates/apps/frontend-spa/package.json +7 -7
  104. package/templates/apps/frontend-spa/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  105. package/templates/apps/frontend-ssr/package.json +7 -7
  106. package/templates/apps/frontend-ssr/src/pages/{feed.page.test.tsx → feed/page.test.tsx} +2 -2
  107. package/templates/apps/frontend-ssr/src/pages/{feed.page.tsx → feed/page.tsx} +1 -1
  108. package/templates/apps/frontend-ssr/src/pages/{feed-swr.page.test.tsx → feed-swr/page.test.tsx} +2 -2
  109. package/templates/apps/frontend-ssr/src/pages/{feed-swr.page.tsx → feed-swr/page.tsx} +1 -1
  110. package/templates/apps/frontend-ssr/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  111. package/templates/apps/frontend-ssr-api/package.json +8 -8
  112. package/templates/apps/frontend-ssr-api/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  113. package/templates/apps/frontend-static-blog/package.json +6 -6
  114. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/{[slug].page.tsx → [slug]/page.tsx} +4 -4
  115. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +2 -2
  116. package/templates/apps/frontend-static-blog/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  117. package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].page.test.tsx → [slug]/page.test.tsx} +3 -3
  118. package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].page.tsx → [slug]/page.tsx} +3 -3
  119. package/templates/apps/frontend-static-blog/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  120. package/dist/apiBuild-D8juhYsS.js +0 -2
  121. package/dist/dbCommand-Dt7BtdMd.js +0 -2
  122. package/dist/frameworkTableAssembly-D1zJSgOU.js +0 -2
  123. /package/templates/apps/changelog/src/pages/{index.page.tsx → page.tsx} +0 -0
  124. /package/templates/apps/frontend-admin/src/pages/(marketing)/{index.page.tsx → page.tsx} +0 -0
  125. /package/templates/apps/frontend-admin/src/pages/admin/{index.page.tsx → page.tsx} +0 -0
  126. /package/templates/apps/frontend-app/src/pages/{index.page.tsx → page.tsx} +0 -0
  127. /package/templates/apps/frontend-blank/src/pages/{index.page.tsx → page.tsx} +0 -0
  128. /package/templates/apps/frontend-contact/src/pages/{index.page.tsx → page.tsx} +0 -0
  129. /package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.page.tsx → page.tsx} +0 -0
  130. /package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.page.tsx → page.tsx} +0 -0
  131. /package/templates/apps/frontend-docs/src/pages/{index.page.tsx → page.tsx} +0 -0
  132. /package/templates/apps/frontend-i18n/src/pages/{index.page.tsx → page.tsx} +0 -0
  133. /package/templates/apps/frontend-landing/src/pages/{index.page.tsx → page.tsx} +0 -0
  134. /package/templates/apps/frontend-spa/src/pages/{index.page.tsx → page.tsx} +0 -0
  135. /package/templates/apps/frontend-ssr/src/pages/{index.page.tsx → page.tsx} +0 -0
  136. /package/templates/apps/frontend-ssr-api/src/pages/{index.page.tsx → page.tsx} +0 -0
  137. /package/templates/apps/frontend-static-blog/src/pages/{index.page.tsx → page.tsx} +0 -0
@@ -1,3 +1,3 @@
1
- import { $ as e } from "./inspectMetrics-CYDSO3za.js";
2
- import { t } from "./start-BtgatJN3.js";
1
+ import { Q as e } from "./inspectMetrics-DWh56Qas.js";
2
+ import { t } from "./start-ksY0wMZG.js";
3
3
  export { e as loadDotEnv, t as runStartCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -62,22 +62,22 @@
62
62
  "@effect/platform-node": "^0.107.0",
63
63
  "@effect/sql": "^0.51.1",
64
64
  "@effect/workflow": "^0.18.2",
65
- "@voltro/ai": "0.15.0",
66
- "@voltro/cache": "0.15.0",
67
- "@voltro/data-transfer": "0.15.0",
68
- "@voltro/database": "0.15.0",
69
- "@voltro/env": "0.15.0",
70
- "@voltro/kv": "0.15.0",
71
- "@voltro/logger": "0.15.0",
72
- "@voltro/plugin-auth": "0.15.0",
73
- "@voltro/plugin-broadcast": "0.15.0",
74
- "@voltro/plugin-mail": "0.15.0",
75
- "@voltro/plugin-storage": "0.15.0",
76
- "@voltro/plugin-webhooks": "0.15.0",
77
- "@voltro/protocol": "0.15.0",
78
- "@voltro/runtime": "0.15.0",
79
- "@voltro/serverless": "0.15.0",
80
- "@voltro/workflow": "0.15.0",
65
+ "@voltro/ai": "0.17.0",
66
+ "@voltro/cache": "0.17.0",
67
+ "@voltro/data-transfer": "0.17.0",
68
+ "@voltro/database": "0.17.0",
69
+ "@voltro/env": "0.17.0",
70
+ "@voltro/kv": "0.17.0",
71
+ "@voltro/logger": "0.17.0",
72
+ "@voltro/plugin-auth": "0.17.0",
73
+ "@voltro/plugin-broadcast": "0.17.0",
74
+ "@voltro/plugin-mail": "0.17.0",
75
+ "@voltro/plugin-storage": "0.17.0",
76
+ "@voltro/plugin-webhooks": "0.17.0",
77
+ "@voltro/protocol": "0.17.0",
78
+ "@voltro/runtime": "0.17.0",
79
+ "@voltro/serverless": "0.17.0",
80
+ "@voltro/workflow": "0.17.0",
81
81
  "chokidar": "^5.0.0",
82
82
  "ioredis": "^5.11.1",
83
83
  "tinyglobby": "^0.2.17",
@@ -219,7 +219,7 @@ a `web/` (frontend).
219
219
  │ │ └── rpcGroup.generated.ts # codegen — do NOT edit
220
220
  │ └── web/
221
221
  │ ├── app.config.ts # { type:'web', apis, theme, locales, … }
222
- │ └── src/pages/**/*.page.tsx # file-based routing (only *.page.tsx)
222
+ │ └── src/pages/**/page.tsx # a DIRECTORY is a route segment
223
223
  ├── packages/ # shared libs
224
224
  └── AGENTS.md / CLAUDE.md # this file
225
225
  ```
@@ -249,7 +249,7 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
249
249
  | `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
250
250
  | `*.tool.tsx` | tool an agent can call |
251
251
  | `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
252
- | `*.page.tsx` (under `src/pages/`) | web page (file-based route) + `*.page.test.tsx` |
252
+ | `page.tsx` (under `src/pages/`) | the route its DIRECTORY serves + `page.test.tsx` |
253
253
  | `*.component.tsx` | exactly ONE component (+ types) |
254
254
  | `*.component.ui.tsx` | presentational: one component, READS only — never writes |
255
255
  | `*.hook.ts` | exactly ONE `use*` hook (+ types) |
@@ -260,12 +260,27 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
260
260
  | `*.store.ts` | exactly ONE `defineStore` — client state, never server state |
261
261
  | `*.client.ts` | declares itself + its transitive imports browser-safe |
262
262
 
263
- **Only `*.page.tsx` is a route.** Everything else under `src/pages/` components,
264
- hooks, tests — is ordinary code with no URL, so put it next to the page that uses
265
- it rather than in a distant `components/` directory. The suffix decides *whether*
266
- a file is a route; the path under `src/pages/` decides *which URL*. A `*.page.tsx`
267
- outside `src/pages/` never routes. A page MUST default-export its component —
268
- without it `voltro dev` refuses to boot and names the file.
263
+ **A DIRECTORY is a route segment, and its route is `page.tsx`.**
264
+
265
+ ```
266
+ src/pages/page.tsx → /
267
+ src/pages/pricing/page.tsx → /pricing
268
+ src/pages/users/[id]/page.tsx → /users/[id]
269
+ src/pages/docs/[...slug]/page.tsx → /docs/<anything>
270
+ ```
271
+
272
+ A parameter is a DIRECTORY name (`[id]/`), never a filename. Beside `page.tsx`
273
+ sit the other reserved names its segment owns — `layout.tsx`, `error.tsx`,
274
+ `loading.tsx`, `not-found.tsx` — plus its `page.test.tsx` and any co-located
275
+ components. **Anything in that folder that is not one of the reserved names is
276
+ structurally not a route**, so put a component next to the page that uses it
277
+ rather than in a distant `components/` directory.
278
+
279
+ A `page.tsx` outside `src/pages/` never routes. A page MUST default-export its
280
+ component — without it `voltro dev` refuses to boot and names the file.
281
+
282
+ Do NOT write `x.page.tsx`, `index.page.tsx` or `[id].page.tsx` — those were the
283
+ 0.15.0 spelling and route nothing now.
269
284
 
270
285
  **Every suffix above is a CONTRACT, not a label.** Something else depends on each
271
286
  promise, and `voltro doctor` enforces all of them:
@@ -308,7 +323,7 @@ rules is that everything we author follows them.
308
323
 
309
324
  **Write the test in the same step as the code — always, for every primitive.**
310
325
  Not "afterwards", not in a cleanup pass: the filename is derivable
311
- (`users/[id].page.tsx` → `users/[id].page.test.tsx`,
326
+ (`users/[id]/page.tsx` → `users/[id]/page.test.tsx`,
312
327
  `Card.component.tsx` → `Card.component.test.tsx`,
313
328
  `order.mutation.server.ts` → `order.mutation.server.test.ts`), so there is
314
329
  nothing to decide. A change is not finished until its test exists and passes.
@@ -219,7 +219,7 @@ a `web/` (frontend).
219
219
  │ │ └── rpcGroup.generated.ts # codegen — do NOT edit
220
220
  │ └── web/
221
221
  │ ├── app.config.ts # { type:'web', apis, theme, locales, … }
222
- │ └── src/pages/**/*.page.tsx # file-based routing (only *.page.tsx)
222
+ │ └── src/pages/**/page.tsx # a DIRECTORY is a route segment
223
223
  ├── packages/ # shared libs
224
224
  └── AGENTS.md / CLAUDE.md # this file
225
225
  ```
@@ -249,7 +249,7 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
249
249
  | `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
250
250
  | `*.tool.tsx` | tool an agent can call |
251
251
  | `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
252
- | `*.page.tsx` (under `src/pages/`) | web page (file-based route) + `*.page.test.tsx` |
252
+ | `page.tsx` (under `src/pages/`) | the route its DIRECTORY serves + `page.test.tsx` |
253
253
  | `*.component.tsx` | exactly ONE component (+ types) |
254
254
  | `*.component.ui.tsx` | presentational: one component, READS only — never writes |
255
255
  | `*.hook.ts` | exactly ONE `use*` hook (+ types) |
@@ -260,12 +260,27 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
260
260
  | `*.store.ts` | exactly ONE `defineStore` — client state, never server state |
261
261
  | `*.client.ts` | declares itself + its transitive imports browser-safe |
262
262
 
263
- **Only `*.page.tsx` is a route.** Everything else under `src/pages/` components,
264
- hooks, tests — is ordinary code with no URL, so put it next to the page that uses
265
- it rather than in a distant `components/` directory. The suffix decides *whether*
266
- a file is a route; the path under `src/pages/` decides *which URL*. A `*.page.tsx`
267
- outside `src/pages/` never routes. A page MUST default-export its component —
268
- without it `voltro dev` refuses to boot and names the file.
263
+ **A DIRECTORY is a route segment, and its route is `page.tsx`.**
264
+
265
+ ```
266
+ src/pages/page.tsx → /
267
+ src/pages/pricing/page.tsx → /pricing
268
+ src/pages/users/[id]/page.tsx → /users/[id]
269
+ src/pages/docs/[...slug]/page.tsx → /docs/<anything>
270
+ ```
271
+
272
+ A parameter is a DIRECTORY name (`[id]/`), never a filename. Beside `page.tsx`
273
+ sit the other reserved names its segment owns — `layout.tsx`, `error.tsx`,
274
+ `loading.tsx`, `not-found.tsx` — plus its `page.test.tsx` and any co-located
275
+ components. **Anything in that folder that is not one of the reserved names is
276
+ structurally not a route**, so put a component next to the page that uses it
277
+ rather than in a distant `components/` directory.
278
+
279
+ A `page.tsx` outside `src/pages/` never routes. A page MUST default-export its
280
+ component — without it `voltro dev` refuses to boot and names the file.
281
+
282
+ Do NOT write `x.page.tsx`, `index.page.tsx` or `[id].page.tsx` — those were the
283
+ 0.15.0 spelling and route nothing now.
269
284
 
270
285
  **Every suffix above is a CONTRACT, not a label.** Something else depends on each
271
286
  promise, and `voltro doctor` enforces all of them:
@@ -308,7 +323,7 @@ rules is that everything we author follows them.
308
323
 
309
324
  **Write the test in the same step as the code — always, for every primitive.**
310
325
  Not "afterwards", not in a cleanup pass: the filename is derivable
311
- (`users/[id].page.tsx` → `users/[id].page.test.tsx`,
326
+ (`users/[id]/page.tsx` → `users/[id]/page.test.tsx`,
312
327
  `Card.component.tsx` → `Card.component.test.tsx`,
313
328
  `order.mutation.server.ts` → `order.mutation.server.test.ts`), so there is
314
329
  nothing to decide. A change is not finished until its test exists and passes.
@@ -526,7 +541,7 @@ each plugin's own README.
526
541
 
527
542
  | Topic | Open | Summary |
528
543
  |---|---|---|
529
- | **What's new in 0.14.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
544
+ | **What's new in 0.16.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
530
545
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
531
546
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
532
547
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.14.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
12
+ | **What's new in 0.16.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
13
13
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
14
14
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
15
15
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -1211,8 +1211,8 @@ A page is listed when ALL of these hold:
1211
1211
 
1212
1212
  ```text
1213
1213
  • renderMode:'spa' candidates (2 pages — loader-free, under a layout, currently ssr/default):
1214
- src/pages/dashboard/index.page.tsx (/dashboard) — default renderMode
1215
- src/pages/admin/settings.page.tsx (/admin/settings) — renderMode:'ssr'
1214
+ src/pages/dashboard/page.tsx (/dashboard) — default renderMode
1215
+ src/pages/admin/settings/page.tsx (/admin/settings) — renderMode:'ssr'
1216
1216
  → renderMode:'spa' skips this page's SSR compile while its layout shell still renders server-side — adopt it if the page BODY does not need SSR (internal/authenticated pages); keep 'ssr' if the page content needs SEO or server first-paint.
1217
1217
  ```
1218
1218
 
@@ -927,7 +927,15 @@ try {
927
927
 
928
928
  **Trace id for debugging.** An error caught from `useMutation` / `useAction` carries a **non-enumerable `__voltroTraceId`** — the bridge to the server logs for that exact call:
929
929
 
930
- ```ts
930
+ ```
931
+ `errorTag` lives in `@voltro/protocol` rather than in the client for a reason
932
+ worth knowing before you decide where your own error handling goes: **`_tag` is a
933
+ wire concept, and protocol owns the wire.** So a shared error handler in a package
934
+ that has no business depending on `@voltro/client` — a UI kit, an i18n layer —
935
+ can read a tag without taking that dependency. `instanceof` is the thing that
936
+ does NOT survive the wire: what arrives in the browser was decoded from JSON and
937
+ never constructed, so match on the tag, not on the class.
938
+ ts
931
939
  const traceId = (err as { __voltroTraceId?: string }).__voltroTraceId
932
940
  // → `voltro logs --trace <traceId>` to see the server-side span
933
941
  ```
@@ -2316,7 +2316,7 @@ If multiple apps target the same database, they're showing the same `_voltro_mig
2316
2316
 
2317
2317
  ## Source code
2318
2318
 
2319
- The shared component lives in `voltro/packages/devtools-ui/src/pages/MigrationsPage.page.tsx`. The local devtools wiring is in `voltro-devtools/apps/dashboard/src/pages/apps/[appId]/migrations.page.tsx`. Both repos are open to extension.
2319
+ The shared component lives in `voltro/packages/devtools-ui/src/pages/MigrationsPage/page.tsx`. The local devtools wiring is in `voltro-devtools/apps/dashboard/src/pages/apps/[appId]/migrations/page.tsx`. Both repos are open to extension.
2320
2320
 
2321
2321
 
2322
2322
 
@@ -2504,8 +2504,8 @@ server enforces this regardless of which buttons the UI renders.
2504
2504
 
2505
2505
  ## Source code
2506
2506
 
2507
- - Component: `voltro/packages/devtools-ui/src/pages/MigrationsPage.page.tsx` — shared
2508
- - Cloud wiring: `voltro-cloud/apps/voltro-cloud/dashboard/src/pages/_/p/[orgSlug]/[projectSlug]/apps/[appSlug]/migrations.page.tsx`
2507
+ - Component: `voltro/packages/devtools-ui/src/pages/MigrationsPage/page.tsx` — shared
2508
+ - Cloud wiring: `voltro-cloud/apps/voltro-cloud/dashboard/src/pages/_/p/[orgSlug]/[projectSlug]/apps/[appSlug]/migrations/page.tsx`
2509
2509
  - Cloud-api proxy: `voltro-cloud/apps/voltro-cloud/api/queries/apps.inspectMigrationsStatus.query.ts` + `.query.server.ts`
2510
2510
  - Approval schema: `voltro-cloud/apps/voltro-cloud/api/database/migrationPlans.entity.ts` + `migrationApprovals.entity.ts`
2511
2511
  - Approval rpc: `migrations.submitPlan` + `migrations.approve` (mutations), `migrations.pendingApprovals` (query), `migrations.reviewByToken` (action)
@@ -423,7 +423,7 @@ export default function Layout({ children }) {
423
423
  }
424
424
  ```
425
425
 
426
- Pair this with locale-prefixed page files: `src/pages/[locale]/index.page.tsx`, `src/pages/[locale]/docs/[...slug].page.tsx`, etc. Each re-exports the default-locale query's component, which reads `useUrlLocale()` to decide which catalog data to query.
426
+ Pair this with locale-prefixed page files: `src/pages/[locale]/page.tsx`, `src/pages/[locale]/docs/[...slug]/page.tsx`, etc. Each re-exports the default-locale query's component, which reads `useUrlLocale()` to decide which catalog data to query.
427
427
 
428
428
  Reference implementations: `voltro-dev/apps/voltro-dev/docs/` (URL-prefix on dynamic + static content) and `voltro-dev/apps/voltro-dev/landing/` (URL-prefix on a pure static marketing site — 10 default-locale pages, 10 locale-prefixed mirrors, one combined `voltro build` run).
429
429
 
@@ -441,7 +441,7 @@ dist/de/features/foo/index.html ← de mirror
441
441
  Each variant ships with the right `<I18nProvider>`-wrapped body **and** the right per-locale `<title>` / `<meta description>` / `<link rel="canonical">` / OG tags — **if** the page's `meta` is exported as a function of `({ locale })`. With a plain static `meta: PageMeta` object, the body is correctly localised but the head tags stay default-locale on every variant.
442
442
 
443
443
  ```tsx
444
- // src/pages/features/foo.page.tsx — meta as a function of locale
444
+ // src/pages/features/foo/page.tsx — meta as a function of locale
445
445
  import { getCatalog } from '../lib/locale'
446
446
  import { localeCanonicalUrl, ogTags, standardLinks } from '../lib/seo'
447
447
 
@@ -472,7 +472,7 @@ The `[locale]/…` mirror is the trigger for SSG **output** — without mirror f
472
472
  12 lines per page. Re-export the canonical page's `default`, `renderMode`, `interactive`, `meta`, plus a `getStaticPaths` that enumerates non-default locales:
473
473
 
474
474
  ```tsx
475
- // src/pages/[locale]/features/foo.page.tsx
475
+ // src/pages/[locale]/features/foo/page.tsx
476
476
  import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../../../lib/locale'
477
477
  export { default } from '../../features/foo'
478
478
  export { renderMode, interactive, meta } from '../../features/foo'
@@ -404,7 +404,7 @@ An unmarked file makes no claim, and that is fine: `*.client.ts` is for the shar
404
404
 
405
405
  | Path | What it is |
406
406
  |---|---|
407
- | `src/pages/*.tsx` | A page. URL is the file path; `[id].page.tsx` -> `/:id`, `[...slug].page.tsx` -> catch-all. |
407
+ | `src/pages/**/page.tsx` | A page. URL is the file path; `[id]/page.tsx` -> `/:id`, `[...slug]/page.tsx` -> catch-all. |
408
408
  | `src/pages/layout.tsx` | Outer layout — wraps every page. |
409
409
  | `src/pages/error.tsx` | Error boundary for the whole subtree. |
410
410
  | `src/pages/not-found.tsx` | Fallback rendered when no page matches. |
@@ -415,7 +415,7 @@ An unmarked file makes no claim, and that is fine: `*.client.ts` is for the shar
415
415
  Each page can opt into a render strategy via two exports:
416
416
 
417
417
  ```tsx
418
- // src/pages/blog/[slug].page.tsx
418
+ // src/pages/blog/[slug]/page.tsx
419
419
  export const renderMode = 'isr' as const // 'static' | 'spa' | 'ssr' | 'isr'
420
420
  export const interactive = 'islands' as const // 'none' | 'islands' | 'full'
421
421
  ```
@@ -126,7 +126,7 @@ Order matters: the framework composes outer→inner, so the rate-limit intercept
126
126
  | `extendSchema` | Contribute tables + custom SQL migrations (tracked in `_voltro_plugin_migrations`). |
127
127
  | `services` | Provide an Effect `Layer` whose Tags every handler can `yield*` (e.g. `MailService`, `StorageService`). |
128
128
  | `routes` | Register plugin-owned rpc queries / mutations / actions (alias-prefixed tags). |
129
- | `httpRoutes` | Serve public raw-HTTP endpoints on the framework listener (e.g. `GET /_voltro/storage/:id`). |
129
+ | `httpRoutes` | Serve public raw-HTTP endpoints on the framework listener (e.g. `GET /_voltro/storage/:id`). The request carries `store` — the app's DataStore — for a route that must read or write (a login endpoint minting a session row cannot be an rpc mutation). Not tenant-scoped: raw HTTP has no resolved Subject, so scope it yourself. |
130
130
  | `inspectEndpoints` | Mount tooling under `/_voltro/inspect/plugins/<alias>/…`. |
131
131
  | `onScheduleFire` / `onWorkflowStep` / `onHttpRequest` | Wrap every cron firing, every workflow `step()`, every pre-auth HTTP request. |
132
132
  | `onInstall` / `onActivate` / `onDeactivate` / `onUninstall` | Lifecycle hooks at first-install, boot, shutdown, and removal. |
@@ -482,7 +482,7 @@ className={pathname.startsWith('/dashboard') ? 'active' : ''}
482
482
  URL params from `[name]` segments. Typed via the generic.
483
483
 
484
484
  ```tsx
485
- // src/pages/users/[id].page.tsx
485
+ // src/pages/users/[id]/page.tsx
486
486
  import { useParams } from '@voltro/web'
487
487
 
488
488
  const { id } = useParams<{ id: string }>()
@@ -491,7 +491,7 @@ const { id } = useParams<{ id: string }>()
491
491
  For catch-all queries:
492
492
 
493
493
  ```tsx
494
- // src/pages/docs/[...slug].page.tsx
494
+ // src/pages/docs/[...slug]/page.tsx
495
495
  const { slug } = useParams<{ slug: string }>()
496
496
  // /docs/intro/getting-started → slug = "intro/getting-started"
497
497
  ```
@@ -499,7 +499,7 @@ const { slug } = useParams<{ slug: string }>()
499
499
  For multi-segment dynamic paths:
500
500
 
501
501
  ```tsx
502
- // src/pages/orgs/[orgId]/projects/[projectId].page.tsx
502
+ // src/pages/orgs/[orgId]/projects/[projectId]/page.tsx
503
503
  const { orgId, projectId } = useParams<{ orgId: string; projectId: string }>()
504
504
  ```
505
505
 
@@ -604,7 +604,7 @@ Idempotent — multiple calls for the same path fire one loader. The prefetched
604
604
  Page's loader output, typed.
605
605
 
606
606
  ```tsx
607
- // src/pages/notes/[id].page.tsx
607
+ // src/pages/notes/[id]/page.tsx
608
608
  import { useLoaderData } from '@voltro/web'
609
609
 
610
610
  interface Note {
@@ -11,7 +11,7 @@
11
11
 
12
12
  _Voltro's file-based router — pages, layouts, render modes, loaders, navigation, islands. The web side of the framework._
13
13
 
14
- The web side of a Voltro app uses **file-based routing**: drop a `*.page.tsx` file under `src/pages/`, the CLI discovers it on every boot + save, and the file becomes a route. No router config, no manual `<Route>` declarations, no codegen step.
14
+ The web side of a Voltro app uses **file-based routing**: drop a `page.tsx` file under `src/pages/`, the CLI discovers it on every boot + save, and the file becomes a route. No router config, no manual `<Route>` declarations, no codegen step.
15
15
 
16
16
  The suffix is what makes a file a route — the directory tree only decides *which URL*. Anything else under `src/pages/` (components, hooks, tests) is ordinary code and gets no URL, so it can live beside the page that uses it.
17
17
 
@@ -25,18 +25,18 @@ src/pages/
25
25
  ├── error.tsx # error boundary
26
26
  ├── loading.tsx # pending UI
27
27
  ├── not-found.tsx # 404 fallback
28
- ├── index.page.tsx # /
29
- ├── about.page.tsx # /about
28
+ ├── page.tsx # /
29
+ ├── about/page.tsx # /about
30
30
  ├── (marketing)/ # route group — no URL segment
31
31
  │ ├── layout.tsx # marketing-scoped layout
32
- │ └── pricing.page.tsx # /pricing
32
+ │ └── pricing/page.tsx # /pricing
33
33
  ├── users/
34
34
  │ ├── layout.tsx # users-scoped layout
35
35
  │ ├── error.tsx # users-scoped error boundary
36
- │ ├── [id].page.tsx # /users/:id
37
- │ └── index.page.tsx # /users
36
+ │ ├── [id]/page.tsx # /users/:id
37
+ │ └── page.tsx # /users
38
38
  └── docs/
39
- └── [...slug].page.tsx # /docs/<anything> (catch-all)
39
+ └── [...slug]/page.tsx # /docs/<anything> (catch-all)
40
40
  ```
41
41
 
42
42
  That's the whole router. No `<Route>`, no `<Switch>`, no `useRoutes`.
@@ -52,7 +52,7 @@ That's the whole router. No `<Route>`, no `<Switch>`, no `useRoutes`.
52
52
 
53
53
  ## Architecture in one paragraph
54
54
 
55
- The framework generates a `.framework/app.tsx` on every boot that imports each `*.page.tsx` file under `src/pages/` (excluding `node_modules`), wraps them in their layout chains, and produces a `<Router routes={…} />` element. A `mount(App, { group })` call in `.framework/main.tsx` mounts it via `react-dom/client`'s `createRoot` (or `hydrateRoot` for SSR pages). The router watches `window.location` + intercepts `<Link>` clicks for client-side nav.
55
+ The framework generates a `.framework/app.tsx` on every boot that imports each `page.tsx` file under `src/pages/` (excluding `node_modules`), wraps them in their layout chains, and produces a `<Router routes={…} />` element. A `mount(App, { group })` call in `.framework/main.tsx` mounts it via `react-dom/client`'s `createRoot` (or `hydrateRoot` for SSR pages). The router watches `window.location` + intercepts `<Link>` clicks for client-side nav.
56
56
 
57
57
  You don't write any of this. The CLI regenerates it on every save in dev; the build pipeline freezes it for production.
58
58
 
@@ -125,7 +125,7 @@ A **page** is any `*.tsx` file under `src/pages/` that's not a special file (`la
125
125
  ## A static page
126
126
 
127
127
  ```tsx
128
- // src/pages/about.page.tsx → /about
128
+ // src/pages/about/page.tsx → /about
129
129
  import type { ReactNode } from 'react'
130
130
 
131
131
  export default function About(): ReactNode {
@@ -145,9 +145,9 @@ That's it. Save the file, the CLI's discovery sees it on next save, the page is
145
145
  `index.tsx` maps to the directory's URL:
146
146
 
147
147
  ```text
148
- src/pages/index.page.tsx → /
149
- src/pages/users/index.page.tsx → /users
150
- src/pages/admin/index.page.tsx → /admin
148
+ src/pages/page.tsx → /
149
+ src/pages/users/page.tsx → /users
150
+ src/pages/admin/page.tsx → /admin
151
151
  ```
152
152
 
153
153
  ## Dynamic segments
@@ -155,7 +155,7 @@ src/pages/admin/index.page.tsx → /admin
155
155
  Brackets in the filename are dynamic. The captured value comes through `useParams<T>()`:
156
156
 
157
157
  ```tsx
158
- // src/pages/users/[id].page.tsx → /users/:id
158
+ // src/pages/users/[id]/page.tsx → /users/:id
159
159
  import { useParams } from '@voltro/web'
160
160
 
161
161
  export default function User() {
@@ -167,7 +167,7 @@ export default function User() {
167
167
  Multiple dynamic segments in one path:
168
168
 
169
169
  ```text
170
- src/pages/orgs/[orgId]/projects/[projectId].page.tsx
170
+ src/pages/orgs/[orgId]/projects/[projectId]/page.tsx
171
171
  // → /orgs/:orgId/projects/:projectId
172
172
 
173
173
  const { orgId, projectId } = useParams<{ orgId: string; projectId: string }>()
@@ -178,7 +178,7 @@ const { orgId, projectId } = useParams<{ orgId: string; projectId: string }>()
178
178
  `[...name]` captures one OR more URL segments as a single param value (joined by `/`):
179
179
 
180
180
  ```tsx
181
- // src/pages/docs/[...slug].page.tsx → /docs/<anything>
181
+ // src/pages/docs/[...slug]/page.tsx → /docs/<anything>
182
182
  const { slug } = useParams<{ slug: string }>()
183
183
  // /docs/intro/getting-started → slug = "intro/getting-started"
184
184
  ```
@@ -186,7 +186,7 @@ const { slug } = useParams<{ slug: string }>()
186
186
  **Optional** catch-all (matches the base URL too):
187
187
 
188
188
  ```tsx
189
- // src/pages/docs/[[...slug]].page.tsx
189
+ // src/pages/docs/[[...slug]]/page.tsx
190
190
  // /docs → slug = ""
191
191
  // /docs/foo → slug = "foo"
192
192
  // /docs/foo/bar → slug = "foo/bar"
@@ -201,10 +201,10 @@ When multiple files could match (static, dynamic, catch-all), priority is:
201
201
  3. Optional catch-all (`[[...slug]]`) wins over required catch-all (`[...slug]`) — the optional form scores as more specific, so it matches first.
202
202
 
203
203
  ```text
204
- src/pages/users/index.page.tsx # /users → wins for /users
205
- src/pages/users/[id].page.tsx # /users/:id → wins for /users/42
206
- src/pages/users/new.page.tsx # /users/new → wins (static beats dynamic)
207
- src/pages/[...rest].page.tsx # everything else
204
+ src/pages/users/page.tsx # /users → wins for /users
205
+ src/pages/users/[id]/page.tsx # /users/:id → wins for /users/42
206
+ src/pages/users/new/page.tsx # /users/new → wins (static beats dynamic)
207
+ src/pages/[...rest]/page.tsx # everything else
208
208
  ```
209
209
 
210
210
  ## Query strings
@@ -242,14 +242,14 @@ Parse the query string explicitly via `useServerRequest()` on the server and `wi
242
242
 
243
243
  ## Co-locating components, hooks and tests
244
244
 
245
- Only `*.page.tsx` is a route. Everything else under `src/pages/` is ordinary code and may sit next to the page that uses it:
245
+ Only `page.tsx` is a route. Everything else under `src/pages/` is ordinary code and may sit next to the page that uses it:
246
246
 
247
247
  ```text
248
248
  src/pages/
249
249
  ├── users/
250
- │ ├── index.page.tsx # → /users
251
- │ ├── index.page.test.tsx # its test
252
- │ ├── [id].page.tsx # → /users/:id
250
+ │ ├── page.tsx # → /users
251
+ │ ├── page.test.tsx # its test
252
+ │ ├── [id]/page.tsx # → /users/:id
253
253
  │ ├── UserCard.tsx # a component — no URL
254
254
  │ └── useFilters.ts # a hook — no URL
255
255
  ```
@@ -319,11 +319,11 @@ Conventions:
319
319
  ```text
320
320
  src/pages/
321
321
  ├── layout.tsx # outer (every page)
322
- ├── about.page.tsx # /about → wrapped in outer layout
322
+ ├── about/page.tsx # /about → wrapped in outer layout
323
323
  └── dashboard/
324
324
  ├── layout.tsx # nested (only /dashboard/*)
325
- ├── index.page.tsx # /dashboard
326
- └── settings.page.tsx # /dashboard/settings
325
+ ├── page.tsx # /dashboard
326
+ └── settings/page.tsx # /dashboard/settings
327
327
  ```
328
328
 
329
329
  For `/dashboard/settings`, the React tree is:
@@ -346,13 +346,13 @@ A directory in `(parentheses)` does NOT contribute a URL segment, but its layout
346
346
  src/pages/
347
347
  ├── (marketing)/
348
348
  │ ├── layout.tsx # marketing-scoped chrome
349
- │ ├── index.page.tsx # /
350
- │ ├── pricing.page.tsx # /pricing
351
- │ └── about.page.tsx # /about
349
+ │ ├── page.tsx # /
350
+ │ ├── pricing/page.tsx # /pricing
351
+ │ └── about/page.tsx # /about
352
352
  └── (app)/
353
353
  ├── layout.tsx # authenticated app chrome
354
- ├── dashboard.page.tsx # /dashboard
355
- └── settings.page.tsx # /settings
354
+ ├── dashboard/page.tsx # /dashboard
355
+ └── settings/page.tsx # /settings
356
356
  ```
357
357
 
358
358
  Marketing pages get one layout; authenticated app pages get another; the URLs stay flat.
@@ -496,7 +496,7 @@ Those four are the **complete** set. An unrecognised value is a hard error namin
496
496
  ## static (SSG)
497
497
 
498
498
  ```tsx
499
- // src/pages/about.page.tsx
499
+ // src/pages/about/page.tsx
500
500
  export const renderMode = 'static' as const
501
501
  ```
502
502
 
@@ -509,7 +509,7 @@ At `voltro build`:
509
509
  For dynamic patterns, export `getStaticPaths` to enumerate every URL to pre-render:
510
510
 
511
511
  ```tsx
512
- // src/pages/blog/[slug].page.tsx
512
+ // src/pages/blog/[slug]/page.tsx
513
513
  export const renderMode = 'static' as const
514
514
 
515
515
  export const getStaticPaths = async () => [
@@ -734,7 +734,7 @@ Both are static module exports — the framework discovers them, the build pipel
734
734
  ## A loader
735
735
 
736
736
  ```tsx
737
- // src/pages/notes/[id].page.tsx
737
+ // src/pages/notes/[id]/page.tsx
738
738
  import { useLoaderData } from '@voltro/web'
739
739
 
740
740
  interface Note {
@@ -844,7 +844,7 @@ splits the loader's result into data that blocks the shell and data that
844
844
  soon as the promise settles.
845
845
 
846
846
  ```tsx
847
- // src/pages/dashboard.page.tsx
847
+ // src/pages/dashboard/page.tsx
848
848
  import { Await, defer, useLoaderData } from '@voltro/web'
849
849
 
850
850
  export const renderMode = 'ssr' as const // required — see below
@@ -964,7 +964,7 @@ Use `signal` for any fetch that could outlive the request — pass it to `fetch(
964
964
  Instead of hand-rolling a `fetch(INTERNAL_API/...)`, a server-side loader can call the backend rpc directly through `ctx.query` — the same query tags the client subscribes to, resolved to their initial snapshot:
965
965
 
966
966
  ```tsx
967
- // src/pages/notes/[id].page.tsx
967
+ // src/pages/notes/[id]/page.tsx
968
968
  import { useSubscription } from '@voltro/client'
969
969
  import { useLoaderData, type PageMeta } from '@voltro/web'
970
970
 
@@ -1136,7 +1136,7 @@ export const meta = ({ locale }: { locale: string }): PageMeta => {
1136
1136
  ### Authenticated dashboard with cookie-driven loader
1137
1137
 
1138
1138
  ```tsx
1139
- // src/pages/dashboard/index.page.tsx
1139
+ // src/pages/dashboard/page.tsx
1140
1140
  import { useLoaderData } from '@voltro/web'
1141
1141
 
1142
1142
  export const renderMode = 'ssr' as const
@@ -1162,7 +1162,7 @@ export default function Dashboard() {
1162
1162
  `getStaticPaths` has no framework store — it reads its own content source (a CMS client, the filesystem, an API). The loader runs server-side and fetches via `query` (the backend rpc, resolved to its first snapshot):
1163
1163
 
1164
1164
  ```tsx
1165
- // src/pages/blog/[slug].page.tsx
1165
+ // src/pages/blog/[slug]/page.tsx
1166
1166
  import { listPostSlugs, type Post } from '../../content/posts'
1167
1167
 
1168
1168
  export const renderMode = 'static' as const
@@ -1563,7 +1563,7 @@ export default island(LikeButton, { name: 'LikeButton', hydrate: 'visible' })
1563
1563
  Use it in a page:
1564
1564
 
1565
1565
  ```tsx
1566
- // src/pages/blog/[slug].page.tsx
1566
+ // src/pages/blog/[slug]/page.tsx
1567
1567
  import LikeButton from '../../components/LikeButton.island'
1568
1568
 
1569
1569
  export const renderMode = 'static' as const