@voltro/cli 0.53.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +195 -0
- package/dist/{agentsMd-0l980yhL.js → agentsMd-BaLC10Na.js} +110 -82
- package/dist/agentsMd-DCY1RSs8.js +2 -0
- package/dist/apiBuild-CeUN55uk.js +2 -0
- package/dist/{apiBuild-CaPfoWku.js → apiBuild-DTWp0S_q.js} +2 -2
- package/dist/bin.js +1 -1
- package/dist/{build-D-OnvNMf.js → build-D4ygSbnV.js} +114 -114
- package/dist/{checkCommand-C5elt0tW.js → checkCommand-Dg1G7Gwd.js} +6 -6
- package/dist/{checkCommand-D2ZduVlh.js → checkCommand-L7DTlpIF.js} +1 -1
- package/dist/{cloudCmd-QUXh-b5w.js → cloudCmd-DzKcSYuy.js} +1 -1
- package/dist/{codegen-FEk8AZHb.js → codegen-DSLM8Su9.js} +2 -2
- package/dist/codegen-DjgxEOnD.js +2 -0
- package/dist/codegenCommand-CG_Vx4lc.js +41 -0
- package/dist/{codemodRunner-BjtB2lq6.js → codemodRunner-Cd4xkC6u.js} +9 -9
- package/dist/{commands-DyxAmhP0.js → commands-6Kzi92Np.js} +96 -73
- package/dist/{dashboardCommand-BdKTyT13.js → dashboardCommand-Cq1PWvI1.js} +3 -3
- package/dist/{dataCommand-Bab9X7s8.js → dataCommand-DYzW8vkv.js} +3 -3
- package/dist/{dbCommand-06O2finM.js → dbCommand-B4NWZtGL.js} +3 -3
- package/dist/dbCommand-CSFWs9ev.js +2 -0
- package/dist/{dev-C6LGF4iY.js → dev-CmuvUKRq.js} +2236 -2219
- package/dist/{dev-GjJWAYo2.js → dev-cKUiZZsB.js} +1 -1
- package/dist/{doctorCommand-etMkflRc.js → doctorCommand-DCiFVMtZ.js} +21 -21
- package/dist/doctorCommand-J3qu4E0Y.js +2 -0
- package/dist/{dormancyCommand-UwZ1AZzB.js → dormancyCommand-w1TrmgYP.js} +1 -1
- package/dist/{embeddingsCommand-C70zWHwo.js → embeddingsCommand-CMgPyRTr.js} +1 -1
- package/dist/{envCommand-dSyKvRkM.js → envCommand-Cyynmcfa.js} +15 -15
- package/dist/{evolveCommand-CG0_ebO5.js → evolveCommand-BwvQ8dVH.js} +2 -2
- package/dist/fileConventions-l-RIXbx8.js +36 -0
- package/dist/{fileTaxonomy-B7uxipWS.js → fileTaxonomy-CbyMQYx_.js} +37 -37
- package/dist/{frameworkTableAssembly-DKx3ba3S.js → frameworkTableAssembly-D7LJuALW.js} +5 -5
- package/dist/frameworkTableAssembly-IPD1pUnZ.js +2 -0
- package/dist/index.js +2 -2
- package/dist/{infoCommand-_53iOc_j.js → infoCommand-DlYlUPqs.js} +1 -1
- package/dist/{metaCommands-Cn2oboG4.js → metaCommands-x7RCi2AF.js} +2 -2
- package/dist/{migrate-Cko9rswM.js → migrate-BK_Bbx-_.js} +2 -2
- package/dist/mobileCommand-D9O6iq3D.js +428 -0
- package/dist/mobileCommand-DAum7tsG.js +2 -0
- package/dist/{pageConvention-C938S8oC.js → pageConvention-CMpfDN6r.js} +1 -1
- package/dist/{privacyCommand-DWTQMC6R.js → privacyCommand-BCa2OoZG.js} +2 -2
- package/dist/{probeCommand-DkGGLknv.js → probeCommand-_C0YU207.js} +1 -1
- package/dist/{projectScaffold-EzlErR4E.js → projectScaffold-CJfP-xbT.js} +1 -1
- package/dist/{projectScaffold-B4dmTlwT.js → projectScaffold-CSN0OzBV.js} +2 -2
- package/dist/renderModeScan-43yQ2opo.js +147 -0
- package/dist/{renderProfile-CskIgAfn.js → renderProfile-DvrhVJHa.js} +2 -2
- package/dist/{runtimeTrace-c0APJz7E.js → runtimeTrace-CGWx1Q6l.js} +1 -1
- package/dist/{sdkgen-BiQCgIEr.js → sdkgen-CDGHQUFj.js} +1 -1
- package/dist/serveCommand-BiPe8BJm.js +2 -0
- package/dist/{serveCommand-CueKQgzl.js → serveCommand-Bje09q1v.js} +708 -708
- package/dist/serveEntry.js +1 -1
- package/dist/start-B0bnJgxI.js +3 -0
- package/dist/{start-ekPan8BT.js → start-Clz-1BHB.js} +511 -504
- package/dist/startEntry.js +1 -1
- package/dist/{staticCommand-xlSL-IWk.js → staticCommand-ey0kYmOT.js} +1 -1
- package/dist/{subcommandNames-DpYs3DXr.js → subcommandNames-CDzfEtKV.js} +3 -3
- package/dist/{templates-BR-fb4SP.js → templates-BTWZkJJT.js} +41 -9
- package/dist/{test-BWPQcRoB.js → test-D_kW4KMj.js} +1 -1
- package/dist/updateCommand-CIoVDKnj.js +2 -0
- package/dist/{updateCommand-C_8I8Rzo.js → updateCommand-CRJlAOaM.js} +1 -1
- package/dist/{webDev-oczpugbx.js → webDev-DSI9SOhs.js} +1127 -1090
- package/dist/{webDev-C7jWJ5dX.js → webDev-DlvZO30c.js} +1 -1
- package/dist/{webhooksCommand-4SVPDjKg.js → webhooksCommand-BvzXNHji.js} +1 -1
- package/dist/workspaceDeps-RKEkX92S.js +45 -0
- package/package.json +19 -19
- package/templates/AGENTS.core.md +2 -0
- package/templates/AGENTS.md +4 -2
- package/templates/agent-docs/_index.md +2 -2
- package/templates/agent-docs/_manifest.json +1 -1
- package/templates/agent-docs/ai.md +4 -4
- package/templates/agent-docs/authentication.md +72 -0
- package/templates/agent-docs/cli.md +4 -1
- package/templates/agent-docs/data.md +61 -12
- package/templates/agent-docs/database/advancedqueries.md +1 -1
- package/templates/agent-docs/database/migrations.md +1 -1
- package/templates/agent-docs/database/seedsdialects.md +64 -2
- package/templates/agent-docs/internationalization.md +2 -0
- package/templates/agent-docs/introduction.md +25 -0
- package/templates/agent-docs/local-first-mobile.md +139 -41
- package/templates/agent-docs/observability.md +4 -2
- package/templates/agent-docs/plugins/atlassian.md +2 -2
- package/templates/agent-docs/plugins/audit.md +2 -2
- package/templates/agent-docs/plugins/billing.md +1 -1
- package/templates/agent-docs/plugins/cdc-out.md +8 -3
- package/templates/agent-docs/plugins/comments.md +22 -0
- package/templates/agent-docs/plugins/presence.md +32 -3
- package/templates/agent-docs/plugins/prometheus.md +2 -0
- package/templates/agent-docs/plugins/queue.md +47 -4
- package/templates/agent-docs/plugins.md +6 -6
- package/templates/agent-docs/reference.md +20 -1
- package/templates/agent-docs/routing.md +63 -9
- package/templates/agent-docs/scheduling.md +1 -1
- package/templates/agent-docs/schema-driven-ui.md +12 -1
- package/templates/agent-docs/templates/appshells.md +36 -4
- package/templates/agent-docs/whats-new.md +109 -135
- package/templates/apps/api-ai/package.json +6 -6
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +9 -9
- package/templates/apps/api-collab/README.md +3 -3
- package/templates/apps/api-collab/app.config.ts +1 -1
- package/templates/apps/api-collab/database/schema.ts +12 -8
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +1 -1
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +1 -1
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +1 -1
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-collab/template.json +1 -1
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +10 -2
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-row-history/package.json +8 -8
- package/templates/apps/api-saas/package.json +11 -10
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +7 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/README.md +43 -24
- package/templates/apps/frontend-collab/app.config.ts +3 -3
- package/templates/apps/frontend-collab/package.json +14 -10
- package/templates/apps/frontend-collab/src/locales/de.ts +1 -2
- package/templates/apps/frontend-collab/src/locales/en.ts +1 -2
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +72 -76
- package/templates/apps/frontend-collab/src/pages/page.tsx +45 -22
- package/templates/apps/frontend-collab/template.json +2 -2
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +8 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/README.md +48 -0
- package/templates/apps/frontend-landing/app.config.ts +28 -0
- package/templates/apps/frontend-landing/package.json +7 -6
- package/templates/apps/frontend-landing/src/assets/hero.jpg +0 -0
- package/templates/apps/frontend-landing/src/fonts/Geist-Variable.woff2 +0 -0
- package/templates/apps/frontend-landing/src/fonts/LICENSE-Geist.txt +92 -0
- package/templates/apps/frontend-landing/src/globals.css +15 -0
- package/templates/apps/frontend-landing/src/globals.d.ts +17 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +3 -2
- package/templates/apps/frontend-landing/src/locales/en.ts +5 -2
- package/templates/apps/frontend-landing/src/pages/page.test.tsx +79 -0
- package/templates/apps/frontend-landing/src/pages/page.tsx +26 -3
- package/templates/apps/frontend-landing/template.json +2 -2
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +8 -7
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +12 -11
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +29 -17
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +16 -0
- package/dist/agentsMd-SDDSkyl4.js +0 -2
- package/dist/apiBuild-DHtLXYx9.js +0 -2
- package/dist/codegen-BWpt3VgF.js +0 -2
- package/dist/codegenCommand-BOiWQ5hz.js +0 -137
- package/dist/dbCommand-B1EXBC6f.js +0 -2
- package/dist/doctorCommand-B0hX0tdz.js +0 -2
- package/dist/fileConventions-DASGEmj-.js +0 -35
- package/dist/frameworkTableAssembly-C_7Z-rMs.js +0 -2
- package/dist/renderModeScan-CUbOeOAg.js +0 -122
- package/dist/serveCommand-DsnrVN3U.js +0 -2
- package/dist/start-BJzZLbt8.js +0 -3
- package/dist/updateCommand-Bqql_rsQ.js +0 -2
|
@@ -245,6 +245,36 @@ export { searchParams } from '../../search/page'
|
|
|
245
245
|
|
|
246
246
|
One schema, no drift — the mirror page decodes exactly what the original declares.
|
|
247
247
|
|
|
248
|
+
**Two scanners read this line, and they do not agree.** The isr refusal above is a
|
|
249
|
+
source scan, and so is the route-builder codegen that brands a route's URL with its
|
|
250
|
+
searchParams type — but they recognise different spellings, which is worth knowing
|
|
251
|
+
before you pick one:
|
|
252
|
+
|
|
253
|
+
| spelling on the mirror page | typed `withQuery` on the mirror route | `isr` + schema refused |
|
|
254
|
+
| --- | --- | --- |
|
|
255
|
+
| `export const searchParams = …` | yes | yes |
|
|
256
|
+
| `export { searchParams } from '../../search/page'` | **no** | yes |
|
|
257
|
+
| `export * from '../../search/page'` | **no** | **no** |
|
|
258
|
+
| `import { searchParams as base } …` + `export const searchParams = base` | yes | yes |
|
|
259
|
+
|
|
260
|
+
The middle two are the ones to watch. A clause re-export still decodes correctly at
|
|
261
|
+
runtime and is still refused on `isr` — but the route builder does not see it, so
|
|
262
|
+
`withQuery` on the mirror's URL falls back to untyped and nothing reports it. A star
|
|
263
|
+
re-export is seen by neither: the binding is on the module at runtime (`export *`
|
|
264
|
+
forwards every named export), so the page behaves as if it declared a schema while
|
|
265
|
+
the `isr` refusal never fires.
|
|
266
|
+
|
|
267
|
+
So: prefer the last row when you want the mirror route's links type-checked, and
|
|
268
|
+
never reach a schema through `export *` on an `isr` page.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
// src/pages/[locale]/search/page.tsx — one schema, and both scanners see it
|
|
272
|
+
import { searchParams as base } from '../../search/page'
|
|
273
|
+
|
|
274
|
+
export const searchParams = base
|
|
275
|
+
export { default } from '../../search/page'
|
|
276
|
+
```
|
|
277
|
+
|
|
248
278
|
### Routes without a schema
|
|
249
279
|
|
|
250
280
|
`useSearchParams()` without an argument stays the raw `URLSearchParams` — nothing changes for a route that declares no schema:
|
|
@@ -521,7 +551,7 @@ export const renderMode = 'static' as const // 'static' | 'spa' | 'ssr' | 'isr
|
|
|
521
551
|
| `ssr` | Every request | Never | Authenticated dashboards, search results, anything cookie-driven |
|
|
522
552
|
| `isr` | First request after build, then on revalidate | Per-key in-memory or Postgres | News feeds, listings, dashboards that change but not per-user |
|
|
523
553
|
|
|
524
|
-
Those four are the **complete** set. An unrecognised value is a hard error naming the page — see [What doesn't work](#what-
|
|
554
|
+
Those four are the **complete** set. An unrecognised value is a hard error naming the page — see [What doesn't work](#what-doesn-t-work).
|
|
525
555
|
|
|
526
556
|
## static (SSG)
|
|
527
557
|
|
|
@@ -2062,7 +2092,7 @@ export const interactive = 'islands' as const
|
|
|
2062
2092
|
|
|
2063
2093
|
With `interactive: 'islands'`, the page's HTML is server-rendered and its script tag points at the page's own entry. That entry registers the page's islands, scans for island markers, and hydrates each one on its own schedule — the page component itself never runs in the browser.
|
|
2064
2094
|
|
|
2065
|
-
Looking for Astro's **"Server Islands"** — per-request-rendered holes in otherwise static pages? In Voltro that is
|
|
2095
|
+
Looking for Astro's **"Server Islands"** — per-request-rendered holes in otherwise static pages? In Voltro that is [**partial prerendering (PPR)**](/docs/routing/render-modes#partial-prerendering-ppr-cached-shell-per-request-holes): `ppr = true` on an `isr` page, a separate mechanism from islands mode. The two do not combine — ppr reveals its holes through hydration, so it requires `interactive: 'full'`.
|
|
2066
2096
|
|
|
2067
2097
|
## When to use islands
|
|
2068
2098
|
|
|
@@ -2449,9 +2479,14 @@ passthrough.
|
|
|
2449
2479
|
this path; for `?image` assets quality is baked at build time from
|
|
2450
2480
|
`images.quality`.
|
|
2451
2481
|
- **Remote images** — same: loader seam, not the build pipeline.
|
|
2452
|
-
- **Markdown-content images** (a blog's relative references) — copied
|
|
2453
|
-
|
|
2454
|
-
|
|
2482
|
+
- **Markdown-content images** (a blog's relative references) — copied into
|
|
2483
|
+
`dist/assets/content-media/<hash>.<ext>` by the content pipeline and the
|
|
2484
|
+
`src` rewritten to that URL. A relative source resolves against the markdown
|
|
2485
|
+
file that references it, and one that does not exist FAILS the build naming
|
|
2486
|
+
the path — a page that renders while its image 404s is the outcome this
|
|
2487
|
+
replaces. Absolute (`/…`) and remote sources are left untouched. Build-time
|
|
2488
|
+
TRANSFORMATION (resize / format) stays a named non-goal here: a markdown
|
|
2489
|
+
reference carries no width and no `sizes` to derive one from.
|
|
2455
2490
|
|
|
2456
2491
|
## `<Image>` without the pipeline
|
|
2457
2492
|
|
|
@@ -2730,7 +2765,7 @@ Two boundaries, stated rather than implied:
|
|
|
2730
2765
|
|
|
2731
2766
|
## A per-request CSP nonce — `cspNonce`
|
|
2732
2767
|
|
|
2733
|
-
Return `cspNonce` and the framework stamps `nonce="…"` onto every script tag of that render — the state script, the deferred registry, the shell's bundle tags, and React's own bootstrap
|
|
2768
|
+
Return `cspNonce` and the framework stamps `nonce="…"` onto every script tag of that render — the state script, the deferred registry, the shell's bundle tags, the islands entry, and React's own bootstrap and Suspense scripts (via React's nonce support). The POLICY header stays yours: set it via `responseHeaders`, with the same nonce.
|
|
2734
2769
|
|
|
2735
2770
|
```ts
|
|
2736
2771
|
import { randomBytes } from 'node:crypto'
|
|
@@ -2751,7 +2786,9 @@ export const csp = defineMiddleware({
|
|
|
2751
2786
|
```
|
|
2752
2787
|
|
|
2753
2788
|
- **`isr` + `cspNonce` refuses the render, loudly.** A cached nonce is a lie the browser enforces — the second visitor gets HTML whose nonce the policy header no longer matches. The ways out: `ssr` for nonce'd pages, or a hash-based CSP for `isr`.
|
|
2754
|
-
-
|
|
2789
|
+
- **`ppr` is refused for the same reason.** A [partial-prerendered](/docs/routing/render-modes#partial-prerendering-ppr-cached-shell-per-request-holes) page serves a cached shell whose inline registry scripts cannot carry a per-request nonce, so `cspNonce` on a `ppr` page is refused by name — same ways out as `isr`.
|
|
2790
|
+
- **Client-injected script tags carry the nonce too.** [`<Script>`](/docs/routing/third-party-scripts) propagates the DOCUMENT's own nonce onto the tag it injects, so a nonce'd `ssr` page needs no explicit `nonce` prop.
|
|
2791
|
+
- **`defer()` does not yet compose with `cspNonce`.** The settle `<script>` each [`<Await>`](/docs/routing/loaders-and-meta) boundary emits inside the streamed body is part of the RENDERED TREE — it is not one React injects, so React's nonce support does not reach it, and it is not in the `<head>` the framework stamps. Under `script-src 'nonce-…'` the browser blocks it, the deferred value is never published to the client registry, and the boundary stays on its fallback after hydration while the server HTML looks correct. Until that is closed, pick one per route: `defer()`, or a nonce'd CSP.
|
|
2755
2792
|
|
|
2756
2793
|
## `match` — where it runs
|
|
2757
2794
|
|
|
@@ -2931,5 +2968,22 @@ would any navigation.
|
|
|
2931
2968
|
|
|
2932
2969
|
`from` matches route patterns literally, so a `[locale]` mirror declares its
|
|
2933
2970
|
own: `/de/photos/[id]`'s page re-exports the base page and sets
|
|
2934
|
-
`intercept: { from: '/[locale]/photos' }
|
|
2935
|
-
|
|
2971
|
+
`intercept: { from: '/[locale]/photos' }`.
|
|
2972
|
+
|
|
2973
|
+
**Declares — not re-exports.** `intercept` is read off the page module at
|
|
2974
|
+
runtime, so any re-export forwards it, `export *` included. A mirror that
|
|
2975
|
+
forwards the base page's `intercept` therefore inherits `from: '/photos'`, and
|
|
2976
|
+
`from` is compared against the background's route PATTERN, which for a mirror is
|
|
2977
|
+
`/[locale]/photos`. The two never match, so the overlay silently never opens and
|
|
2978
|
+
the modal renders standalone — no error, no warning, just a page where a modal
|
|
2979
|
+
was expected. This is the opposite failure from the [searchParams
|
|
2980
|
+
re-export](/docs/routing/pages#mirror-routes-share-one-schema), which is silently
|
|
2981
|
+
*lost*; `intercept` is silently *inherited with the wrong pattern*.
|
|
2982
|
+
|
|
2983
|
+
```tsx
|
|
2984
|
+
// src/pages/[locale]/photos/[id]/page.tsx
|
|
2985
|
+
export { default, meta } from '../../../photos/[id]/page'
|
|
2986
|
+
|
|
2987
|
+
// NOT re-exported: the base's `from` names the un-prefixed pattern.
|
|
2988
|
+
export const intercept = { from: '/[locale]/photos' }
|
|
2989
|
+
```
|
|
@@ -214,7 +214,7 @@ The handler receives a `ScheduleContext` — the same `app` a mutation gets, plu
|
|
|
214
214
|
> tenant-scoped** — a schedule runs as `system` with no tenant. Reads see every
|
|
215
215
|
> tenant's rows, and a write to a `tenant()` table fails with
|
|
216
216
|
> `TenantScopeViolation` unless you pass `tenantId` explicitly. See
|
|
217
|
-
> [below](#a-schedule-runs-as-the-system-subject
|
|
217
|
+
> [below](#a-schedule-runs-as-the-system-subject-no-tenant). This sentence is
|
|
218
218
|
> here rather than only further down because "same shape as a mutation" is what
|
|
219
219
|
> sets the expectation that gets violated.
|
|
220
220
|
|
|
@@ -419,7 +419,18 @@ the SAME input schema the RPC path decodes:
|
|
|
419
419
|
- unknown keys are dropped
|
|
420
420
|
|
|
421
421
|
Validation runs through the same `validateFields` as the client-side
|
|
422
|
-
validation, so the error texts are identical
|
|
422
|
+
validation, so the error texts are identical — in the request's language, not
|
|
423
|
+
in English. The handler resolves the locale from THIS request through the same
|
|
424
|
+
resolver that decided the surrounding page's language ([`voltro:locale` cookie
|
|
425
|
+
› `Accept-Language` › the app's
|
|
426
|
+
`defaultLocale`](/docs/i18n/overview#locale-resolution-order)); an app that
|
|
427
|
+
configures no `locales` gets `en`. Then:
|
|
428
|
+
|
|
429
|
+
Under URL-prefix i18n the referring URL wins over the cookie chain: a form on
|
|
430
|
+
`/de/todos` renders its errors in German even if the `voltro:locale` cookie says
|
|
431
|
+
otherwise, because there the prefix is which page you are on rather than a
|
|
432
|
+
preference. A first segment that is not a declared locale falls through to the
|
|
433
|
+
cookie chain.
|
|
423
434
|
|
|
424
435
|
- **Success → `303 See Other`** (POST-redirect-GET): back to the submitting
|
|
425
436
|
page, or to `redirectTo` (same-origin relative paths only; anything else is
|
|
@@ -13,7 +13,7 @@ _A marketing landing page — hero, features, CTA. Static-rendered with zero JS
|
|
|
13
13
|
|
|
14
14
|
A marketing landing page. The page exports `renderMode = 'static'` + `interactive = 'none'`, so `voltro build` pre-renders it to HTML and `voltro start` serves the file directly — zero framework JS on the wire. Template id: **`frontend-landing`**.
|
|
15
15
|
|
|
16
|
-
It ships plain JSX (a hero, a features list, a CTA) you replace with your own copy
|
|
16
|
+
It ships plain JSX (a hero, a features list, a CTA) you replace with your own copy, bilingual out of the box (URL-prefix i18n: `/` + `/de`), plus the two asset pipelines a marketing page actually needs: a **local hero image** through `?image` + `<Image>` and a **self-hosted woff2** declared under `fonts:`. When you need a contact form or sign-up flow, switch `interactive: 'islands'` on the page and mark the interactive component with a `*.island.tsx` suffix so only that bundle ships.
|
|
17
17
|
|
|
18
18
|
## Scaffold
|
|
19
19
|
|
|
@@ -27,14 +27,21 @@ voltro add-app marketing --template=frontend-landing --to acme
|
|
|
27
27
|
|
|
28
28
|
```text
|
|
29
29
|
apps/acme/web/ # dir named by the app, not the template
|
|
30
|
-
├── app.config.ts # type:web, port:<allocated
|
|
30
|
+
├── app.config.ts # type:web, port:<allocated>, locales, fonts:
|
|
31
31
|
├── package.json
|
|
32
32
|
├── tsconfig.json
|
|
33
33
|
└── src/
|
|
34
34
|
├── globals.css
|
|
35
|
+
├── globals.d.ts # ambient '*.css' + '*?image'
|
|
36
|
+
├── assets/hero.jpg # imported with ?image (build-time pipeline)
|
|
37
|
+
├── fonts/Geist-Variable.woff2 # self-hosted, declared in app.config.ts
|
|
38
|
+
├── fonts/LICENSE-Geist.txt # the face's licence, shipped beside it
|
|
39
|
+
├── lib/locale.ts # URL-prefix i18n helpers
|
|
40
|
+
├── locales/{en,de}.ts # the two catalogs
|
|
35
41
|
└── pages/
|
|
36
42
|
├── layout.tsx # imports globals.css, renders {children}
|
|
37
|
-
|
|
43
|
+
├── page.tsx # the landing page (hero · features · CTA)
|
|
44
|
+
└── [locale]/page.tsx # the /de mirror
|
|
38
45
|
```
|
|
39
46
|
|
|
40
47
|
## The page
|
|
@@ -88,9 +95,34 @@ import SignupForm from '../components/SignupForm.island'
|
|
|
88
95
|
|
|
89
96
|
The surrounding HTML stays static; only the island hydrates.
|
|
90
97
|
|
|
98
|
+
## The hero image — `?image` + `<Image>`
|
|
99
|
+
|
|
100
|
+
`src/assets/hero.jpg` is imported with the **`?image` suffix**, the explicit opt-in to the [build-time image pipeline](/docs/routing/assets): the import resolves to an optimized-asset object instead of vite's plain hashed URL, and `<Image>` renders it as a `<picture>` with one `<source>` per modern format.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { Image } from '@voltro/web'
|
|
104
|
+
import hero from '../assets/hero.jpg?image'
|
|
105
|
+
|
|
106
|
+
<Image src={hero} alt="Abstract gradient artwork" priority sizes="(max-width: 900px) 100vw, 900px" />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`width`, `height` and the blur placeholder are **not props** — they come off the asset, which is what reserves the box (CLS ≈ 0) without hand-written numbers. `priority` marks it the LCP image (eager + high fetch priority). Nothing here needs JS, so it survives `interactive: 'none'`.
|
|
110
|
+
|
|
111
|
+
The `*?image` ambient type is declared once in `src/globals.d.ts`. A **dynamic** `src` (a URL from a loader or CMS frontmatter) cannot be seen at build time — use the loader seam (`<Image src={url} loader={cdn} />`) instead.
|
|
112
|
+
|
|
113
|
+
> **Under `voltro test`** the image plugin is not wired (it is a dev/build transform), so a `?image` specifier resolves to a plain URL string. The shipped `page.test.tsx` `vi.mock`s the import with the asset object the pipeline produces — copy that pattern rather than hand-writing `width`/`height` on the page.
|
|
114
|
+
|
|
115
|
+
## The font — self-hosted, no CDN
|
|
116
|
+
|
|
117
|
+
`app.config.ts` declares one family under [`fonts:`](/docs/routing/fonts), pointing at the woff2 committed in `src/fonts/`. The build content-hashes it and serves it from **your origin**, emits `@font-face`, computes a size-adjusted fallback face from the file's real metrics so the swap moves no text, and puts a `<link rel="preload">` in the shell.
|
|
118
|
+
|
|
119
|
+
Reference it from CSS through the `--font-geist` variable the shell defines (`globals.css` points the kit's `--font-sans` at it), or from TSX with `localFont('Geist')`.
|
|
120
|
+
|
|
121
|
+
**Swapping in your own face:** drop the `woff2` **and its licence file** into `src/fonts/`, then change `family` + `path`. The framework ships no font downloader on purpose — licence terms differ per family. The bundled Geist is SIL OFL 1.1 (`src/fonts/LICENSE-Geist.txt`).
|
|
122
|
+
|
|
91
123
|
## Styling
|
|
92
124
|
|
|
93
|
-
`globals.css`
|
|
125
|
+
`globals.css` imports `@voltro/ui-shadcn/tokens.css` (the design tokens) plus the mandatory `@source "./**/*.{tsx,ts,jsx,js}"` glob. Drop the kit import and use `@import "tailwindcss"` directly if you would rather start from nothing — but keep the `@source` line either way, and keep the `--font-sans` mapping if you keep the font declaration.
|
|
94
126
|
|
|
95
127
|
## What it doesn't ship
|
|
96
128
|
|