@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
@@ -39,7 +39,7 @@ apps/acme/web/ # dir named by the app, not the template
39
39
 
40
40
  ## The page
41
41
 
42
- `src/pages/index.page.tsx` is plain JSX with the two static-render exports:
42
+ `src/pages/page.tsx` is plain JSX with the two static-render exports:
43
43
 
44
44
  ```tsx
45
45
  export const renderMode = 'static' as const // pre-render at build time
@@ -59,7 +59,7 @@ export default function Index() {
59
59
  </section>
60
60
  <section>
61
61
  <h2>Ready to start?</h2>
62
- <p>Edit <code>src/pages/index.page.tsx</code> to make it yours.</p>
62
+ <p>Edit <code>src/pages/page.tsx</code> to make it yours.</p>
63
63
  </section>
64
64
  </main>
65
65
  )
@@ -80,7 +80,7 @@ export default island(SignupForm, { name: 'SignupForm', hydrate: 'visible' })
80
80
  ```
81
81
 
82
82
  ```tsx
83
- // src/pages/index.page.tsx
83
+ // src/pages/page.tsx
84
84
  export const interactive = 'islands' as const // was 'none'
85
85
  import SignupForm from '../components/SignupForm.island'
86
86
  // … render <SignupForm /> somewhere in the page …
@@ -185,7 +185,7 @@ If your final product is going to be a:
185
185
 
186
186
  _A documentation site with a catch-all docs router and build-time static pre-rendering of every page via getStaticPaths._
187
187
 
188
- A documentation-site starter. `src/pages/docs/[...slug].page.tsx` is a catch-all router; `getStaticPaths` enumerates every doc URL at build time so `voltro build` pre-renders them all into a static site you can deploy anywhere. Template id: **`frontend-docs`**.
188
+ A documentation-site starter. `src/pages/docs/[...slug]/page.tsx` is a catch-all router; `getStaticPaths` enumerates every doc URL at build time so `voltro build` pre-renders them all into a static site you can deploy anywhere. Template id: **`frontend-docs`**.
189
189
 
190
190
  The starter ships a tiny in-file `DOCS` array as stub content. Replace it with a real loader (a markdown reader, a CMS query, a database fetch) — the page component's contract stays the same.
191
191
 
@@ -215,7 +215,7 @@ apps/acme/docs/ # dir named by the app, not the template
215
215
 
216
216
  ## The catch-all renderer
217
217
 
218
- `src/pages/docs/[...slug].page.tsx` is the whole engine. It declares:
218
+ `src/pages/docs/[...slug]/page.tsx` is the whole engine. It declares:
219
219
 
220
220
  ```tsx
221
221
  export const renderMode = 'static' as const // pre-render at build
@@ -418,7 +418,7 @@ No rpc client, no loader, no `apis:` entry, no `@voltro/ui-shadcn` dependency. T
418
418
  The page exports `renderMode = 'spa'`, which tells `voltro build` not to pre-render it: the client renders it on load. Use this when the whole page is interactive AND its state lives in the browser — an SSR'd first paint would just be discarded on hydration, so rendering it on the server buys nothing.
419
419
 
420
420
  ```tsx
421
- // src/pages/index.page.tsx — a pure client-side SPA (a bill splitter)
421
+ // src/pages/page.tsx — a pure client-side SPA (a bill splitter)
422
422
  import type { ReactNode } from 'react'
423
423
  import { useEffect, useState } from 'react'
424
424
 
@@ -603,7 +603,7 @@ The full reference for these modes lives in the [render modes guide](/docs/routi
603
603
  `index.tsx` exports `renderMode = 'ssr'`. Its loader runs SERVER-SIDE on EVERY request under `voltro start` — the timestamp and nonce change on each refresh, and the loader can read the incoming request to personalise the HTML before it's sent. There is no caching: the response carries `x-voltro-rendered-by: ssr`.
604
604
 
605
605
  ```tsx
606
- // src/pages/index.page.tsx
606
+ // src/pages/page.tsx
607
607
  import type { ReactNode } from 'react'
608
608
  import { useLoaderData, useServerRequest, type LoaderFn, type PageMeta } from '@voltro/web'
609
609
 
@@ -646,7 +646,7 @@ Use SSR for anything that varies per request: a logged-in dashboard, a page that
646
646
  `feed.tsx` takes the SAME render path as SSR, but `voltro start` CACHES the HTML and serves the cached copy to every request within the `revalidate` window. After the window expires the next request re-renders and replaces the cache — cache hits return in well under a millisecond. `tenantAware: true` folds the `x-tenant` request header into the cache key, so tenant A's render is never served to tenant B:
647
647
 
648
648
  ```tsx
649
- // src/pages/feed.page.tsx
649
+ // src/pages/feed/page.tsx
650
650
  import type { ReactNode } from 'react'
651
651
  import { useLoaderData, type LoaderFn, type PageMeta } from '@voltro/web'
652
652
 
@@ -685,7 +685,7 @@ The loader's `headers` is populated server-side (lowercased keys) — that's how
685
685
  `feed-swr.tsx` is the same ISR page plus a `staleWhileRevalidate` window. When `revalidate` expires, instead of blocking the next visitor on a fresh render, the cache serves the STALE HTML immediately AND kicks off a background re-render. The visitor never waits; the cache catches up out-of-band. Beyond `revalidate + staleWhileRevalidate` the next request blocks on a fresh render, like a cold MISS:
686
686
 
687
687
  ```tsx
688
- // src/pages/feed-swr.page.tsx
688
+ // src/pages/feed-swr/page.tsx
689
689
  import type { ReactNode } from 'react'
690
690
  import { useLoaderData, type LoaderFn, type PageMeta } from '@voltro/web'
691
691
 
@@ -878,10 +878,10 @@ export const posts: ReadonlyArray<Post> = [
878
878
 
879
879
  ## SSG dynamic routes — `getStaticPaths` + `loader` + per-post `meta`
880
880
 
881
- A dynamic route like `src/pages/blog/[slug].page.tsx` matches infinitely many URLs. To pre-render it statically the build needs to know WHICH slugs exist — that is what `getStaticPaths` answers, by mapping over the content source. For each enumerated `params` the build runs the `loader` (server-side, at build time) and renders one HTML file. The `meta` export is a FUNCTION of the loader data, so each post gets its own correct `<title>` / `<description>` baked into the HTML — exactly what you want for SEO and social cards.
881
+ A dynamic route like `src/pages/blog/[slug]/page.tsx` matches infinitely many URLs. To pre-render it statically the build needs to know WHICH slugs exist — that is what `getStaticPaths` answers, by mapping over the content source. For each enumerated `params` the build runs the `loader` (server-side, at build time) and renders one HTML file. The `meta` export is a FUNCTION of the loader data, so each post gets its own correct `<title>` / `<description>` baked into the HTML — exactly what you want for SEO and social cards.
882
882
 
883
883
  ```tsx
884
- // src/pages/blog/[slug].page.tsx
884
+ // src/pages/blog/[slug]/page.tsx
885
885
  import type { ReactNode } from 'react'
886
886
  import { useLoaderData, notFound, type LoaderFn, type PageMeta } from '@voltro/web'
887
887
  import { posts, type Post } from '../../content/posts'
@@ -972,7 +972,7 @@ export default island(ReadingProgress, { name: 'ReadingProgress', hydrate: 'load
972
972
  The index at `/` is pure content: a list of posts with no interactivity. It declares `interactive: 'none'`, which makes the framework strip EVERY `<script>` it would otherwise emit — the HTML ships with no JS bundle at all. It reads the same content source the `[slug]` page + `getStaticPaths` use:
973
973
 
974
974
  ```tsx
975
- // src/pages/index.page.tsx
975
+ // src/pages/page.tsx
976
976
  import type { ReactNode } from 'react'
977
977
  import type { PageMeta } from '@voltro/web'
978
978
  import { posts } from '../content/posts'
@@ -1106,10 +1106,10 @@ The serverless function lives next to the web app but is a separate deploy artif
1106
1106
 
1107
1107
  ## The static page + the island form
1108
1108
 
1109
- `src/pages/index.page.tsx` is `renderMode: 'static'` (pre-rendered, servable from a CDN) and `interactive: 'islands'` — so only the `<ContactForm>` island hydrates; the headline + copy ship as inert HTML with no React lifecycle:
1109
+ `src/pages/page.tsx` is `renderMode: 'static'` (pre-rendered, servable from a CDN) and `interactive: 'islands'` — so only the `<ContactForm>` island hydrates; the headline + copy ship as inert HTML with no React lifecycle:
1110
1110
 
1111
1111
  ```tsx
1112
- // src/pages/index.page.tsx
1112
+ // src/pages/page.tsx
1113
1113
  import type { ReactNode } from 'react'
1114
1114
  import type { PageMeta } from '@voltro/web'
1115
1115
  import ContactForm from '../components/ContactForm.island'
@@ -1358,7 +1358,7 @@ apps/demo/web/
1358
1358
  ├── README.md
1359
1359
  └── src/pages/
1360
1360
  ├── layout.tsx # root shell (header + <main>)
1361
- └── index.page.tsx # the reactive page — useSubscription + useMutation
1361
+ └── page.tsx # the reactive page — useSubscription + useMutation
1362
1362
  ```
1363
1363
 
1364
1364
  ## The `apis` wiring
@@ -1390,7 +1390,7 @@ The `package` resolves the api workspace package; its port is auto-discovered fr
1390
1390
  ## The reactive loop
1391
1391
 
1392
1392
  ```tsx
1393
- // src/pages/index.page.tsx
1393
+ // src/pages/page.tsx
1394
1394
  import { useMutation, useSubscription } from '@voltro/client'
1395
1395
 
1396
1396
  const TENANT = 'acme'
@@ -1418,7 +1418,7 @@ The subscription builder also exposes `revision`, `emittedAt`, `error`, and `pen
1418
1418
 
1419
1419
  ## Pairing with a different api
1420
1420
 
1421
- This page is wired to api-backend's `notes` domain. To point it at your own api, change `apis.app.package` in `app.config.ts`, then swap the rpc tags + the row type in `src/pages/index.page.tsx` to match your descriptors. The hook shapes are identical for any api.
1421
+ This page is wired to api-backend's `notes` domain. To point it at your own api, change `apis.app.package` in `app.config.ts`, then swap the rpc tags + the row type in `src/pages/page.tsx` to match your descriptors. The hook shapes are identical for any api.
1422
1422
 
1423
1423
  ## When to use
1424
1424
 
@@ -1533,7 +1533,7 @@ export const withLocalePrefix = (path: string, locale: string): string =>
1533
1533
  Each page has a one-line **mirror** under `src/pages/[locale]/`. The mirror re-exports the bare page verbatim (the component reads its locale from the URL via the layout's provider) and uses `getStaticPaths` to enumerate the *non-default* locales — which is what tells `voltro build` to emit the `/de/...` HTML. Without the mirror, only the default-locale URL is built.
1534
1534
 
1535
1535
  ```tsx
1536
- // src/pages/[locale]/index.page.tsx — the entire file
1536
+ // src/pages/[locale]/page.tsx — the entire file
1537
1537
  import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../../lib/locale'
1538
1538
 
1539
1539
  export { default } from '../index'
@@ -1550,7 +1550,7 @@ export const getStaticPaths = async (): Promise<Array<{ params: { locale: string
1550
1550
  The body of each page is localised automatically (the SSG pipeline wraps every variant in the right catalog's provider). The document head is NOT — unless you export `meta` as a **function of `{ locale }`**. The framework drives `locale` from `params.locale` on the `[locale]` route (and `defaultLocale` on the bare path), so each variant bakes the right `<title>` / `<meta description>` into its HTML — exactly what you want for SEO and social cards. A plain `meta` object would leave the head English on `/de`.
1551
1551
 
1552
1552
  ```tsx
1553
- // src/pages/index.page.tsx
1553
+ // src/pages/page.tsx
1554
1554
  import { T, useT, useLocale } from '@voltro/i18n'
1555
1555
  import { getCatalog } from '../lib/locale'
1556
1556
 
@@ -1662,7 +1662,7 @@ The `[locale]` mirrors pick it up automatically — `getStaticPaths` reads `SUPP
1662
1662
 
1663
1663
  ## Anti-patterns
1664
1664
 
1665
- - **Forgetting the `[locale]` mirror for a new page.** Create `src/pages/foo.page.tsx` AND a one-line mirror `src/pages/[locale]/foo.page.tsx`. Without the mirror, only the default-locale URL is emitted — `/de/foo` 404s.
1665
+ - **Forgetting the `[locale]` mirror for a new page.** Create `src/pages/foo/page.tsx` AND a one-line mirror `src/pages/[locale]/foo/page.tsx`. Without the mirror, only the default-locale URL is emitted — `/de/foo` 404s.
1666
1666
  - **Exporting `meta` as a plain object.** The body localises, but the `<title>` / `<meta description>` stay English on `/de`. Export `meta` as a function of `{ locale }` and read the catalog via `getCatalog(locale)`.
1667
1667
  - **Skipping `defineLocale<typeof en>()` for a non-base catalog.** Plain `defineCatalog(...)` compiles but loses the parity check — the one feature that stops translations drifting. Always run non-base locales through `defineLocale<typeof en>()`.
1668
1668
  - **Angle brackets in catalog strings.** react-intl's ICU parser treats `<tag>` as a rich-text element and fails (`FORMAT_ERROR`) if there's no matching close tag. Write "page titles", not "`<title>` tags", in a message.
@@ -1759,7 +1759,7 @@ export default function DashboardLayout({ children }) {
1759
1759
  The gate must run **per request** — it reads the request's cookie and may redirect. A `static` page is pre-rendered once at build time with no per-visitor render, so it can't redirect per-visitor; in fact `voltro build` **rejects** a `RedirectError` thrown during static pre-render. So the authed pages declare `renderMode: 'ssr'`:
1760
1760
 
1761
1761
  ```tsx
1762
- // src/pages/dashboard/index.page.tsx
1762
+ // src/pages/dashboard/page.tsx
1763
1763
  export const renderMode = 'ssr' as const
1764
1764
  export const meta = { title: 'Overview' }
1765
1765
  export default function Overview() { /* … */ }
@@ -1884,7 +1884,7 @@ export default {
1884
1884
  `query` is present ONLY server-side (`ssr` / `isr`). It invokes the api's rpc directly over `POST /rpc`, **forwarding the request's session cookie** so the same Subject + tenant resolve as the WebSocket path. A streaming query is drained to its FIRST snapshot — here, the current notes for this tenant. `meta` reads the result, so the `<title>` reflects real data in the server-rendered HTML:
1885
1885
 
1886
1886
  ```tsx
1887
- // src/pages/index.page.tsx
1887
+ // src/pages/page.tsx
1888
1888
  import { useLoaderData, type LoaderFn, type PageMeta } from '@voltro/web'
1889
1889
  import { useSubscription } from '@voltro/client'
1890
1890
 
@@ -1994,7 +1994,7 @@ const entities = manifest ? deriveEntityAdmins(manifest) : []
1994
1994
  ## Per-entity CRUD
1995
1995
 
1996
1996
  ```tsx
1997
- // src/pages/admin/[entity].page.tsx (abridged)
1997
+ // src/pages/admin/[entity]/page.tsx (abridged)
1998
1998
  const canCreate = useCan(spec.createScope) // <table>:create
1999
1999
  {spec.createTag && canCreate ? (
2000
2000
  <AutoForm api="app" mutation={spec.createTag} submitLabel={`Add ${spec.table}`} />
@@ -2041,7 +2041,7 @@ apps/acme/web/
2041
2041
 
2042
2042
  ## Make it yours
2043
2043
 
2044
- Everything under `src/pages/admin/` is yours. Swap the cookie gate (`lib/auth.ts`) + demo scopes (`lib/admin.ts`) for real auth; replace the generic `[entity].page.tsx` with a hand-built page for any entity that needs more than CRUD.
2044
+ Everything under `src/pages/admin/` is yours. Swap the cookie gate (`lib/auth.ts`) + demo scopes (`lib/admin.ts`) for real auth; replace the generic `[entity]/page.tsx` with a hand-built page for any entity that needs more than CRUD.
2045
2045
 
2046
2046
  ## When to use frontend-admin vs. the other web templates
2047
2047