@voltro/cli 0.1.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 (626) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +57 -0
  3. package/README.md +26 -0
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +20236 -0
  6. package/bin/voltro.mjs +43 -0
  7. package/dist/bin.d.ts +1 -0
  8. package/dist/bin.js +9 -0
  9. package/dist/commands-CfPH2Wf4.js +18061 -0
  10. package/dist/frameworkInspectState-CX2250XB.js +86 -0
  11. package/dist/index.d.ts +48 -0
  12. package/dist/index.js +5 -0
  13. package/dist/inspectState.d.ts +116 -0
  14. package/dist/inspectState.js +2 -0
  15. package/dist/startup.d.ts +26 -0
  16. package/dist/startup.js +2 -0
  17. package/dist/startupRunner-CRhuUl91.js +71 -0
  18. package/package.json +88 -0
  19. package/templates/AGENTS.core.md +258 -0
  20. package/templates/AGENTS.md +351 -0
  21. package/templates/agent-docs/_index.md +93 -0
  22. package/templates/agent-docs/_manifest.json +655 -0
  23. package/templates/agent-docs/ai.md +1845 -0
  24. package/templates/agent-docs/authentication.md +1788 -0
  25. package/templates/agent-docs/caching.md +624 -0
  26. package/templates/agent-docs/cli.md +1650 -0
  27. package/templates/agent-docs/configuration.md +295 -0
  28. package/templates/agent-docs/data.md +2172 -0
  29. package/templates/agent-docs/database/advancedqueries.md +1583 -0
  30. package/templates/agent-docs/database/columntypes.md +1200 -0
  31. package/templates/agent-docs/database/hosting.md +881 -0
  32. package/templates/agent-docs/database/migrations.md +2938 -0
  33. package/templates/agent-docs/database/misc.md +270 -0
  34. package/templates/agent-docs/database/overview.md +108 -0
  35. package/templates/agent-docs/database/querying.md +1622 -0
  36. package/templates/agent-docs/database/scaling.md +331 -0
  37. package/templates/agent-docs/database/schema.md +1458 -0
  38. package/templates/agent-docs/database/seedsdialects.md +1235 -0
  39. package/templates/agent-docs/database/transactions.md +285 -0
  40. package/templates/agent-docs/deployment.md +999 -0
  41. package/templates/agent-docs/internationalization.md +359 -0
  42. package/templates/agent-docs/introduction.md +438 -0
  43. package/templates/agent-docs/multi-tenancy.md +610 -0
  44. package/templates/agent-docs/observability.md +350 -0
  45. package/templates/agent-docs/plugins.md +1273 -0
  46. package/templates/agent-docs/reference.md +978 -0
  47. package/templates/agent-docs/routing.md +1553 -0
  48. package/templates/agent-docs/scheduling.md +666 -0
  49. package/templates/agent-docs/schema-driven-ui.md +607 -0
  50. package/templates/agent-docs/security.md +42 -0
  51. package/templates/agent-docs/templates/apibackends.md +3214 -0
  52. package/templates/agent-docs/templates/appshells.md +2062 -0
  53. package/templates/agent-docs/templates/custom.md +128 -0
  54. package/templates/agent-docs/templates/overview.md +122 -0
  55. package/templates/agent-docs/templates/serverless.md +315 -0
  56. package/templates/agent-docs/testing.md +376 -0
  57. package/templates/agent-docs/workflows.md +1351 -0
  58. package/templates/apps/api-ai/README.md +105 -0
  59. package/templates/apps/api-ai/actions/summarize.action.server.tsx +32 -0
  60. package/templates/apps/api-ai/actions/summarize.action.ts +33 -0
  61. package/templates/apps/api-ai/agents/support.agent.server.tsx +31 -0
  62. package/templates/apps/api-ai/agents/support.agent.tsx +27 -0
  63. package/templates/apps/api-ai/app.config.ts +57 -0
  64. package/templates/apps/api-ai/database/schema.ts +67 -0
  65. package/templates/apps/api-ai/package.json +27 -0
  66. package/templates/apps/api-ai/seeds/docs.seed.ts +67 -0
  67. package/templates/apps/api-ai/template.json +6 -0
  68. package/templates/apps/api-ai/tests/summarize.test.ts +50 -0
  69. package/templates/apps/api-ai/tools/searchDocs.tool.tsx +60 -0
  70. package/templates/apps/api-ai/tsconfig.json +5 -0
  71. package/templates/apps/api-auth/.env +17 -0
  72. package/templates/apps/api-auth/README.md +109 -0
  73. package/templates/apps/api-auth/actions/me.action.server.ts +20 -0
  74. package/templates/apps/api-auth/actions/me.action.ts +19 -0
  75. package/templates/apps/api-auth/app.config.ts +59 -0
  76. package/templates/apps/api-auth/database/schema.ts +26 -0
  77. package/templates/apps/api-auth/package.json +28 -0
  78. package/templates/apps/api-auth/template.json +6 -0
  79. package/templates/apps/api-auth/tests/me.test.ts +37 -0
  80. package/templates/apps/api-auth/tsconfig.json +5 -0
  81. package/templates/apps/api-backend/README.md +25 -0
  82. package/templates/apps/api-backend/app.config.ts +39 -0
  83. package/templates/apps/api-backend/database/schema.ts +57 -0
  84. package/templates/apps/api-backend/mutations/notes.create.mutation.server.ts +19 -0
  85. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +37 -0
  86. package/templates/apps/api-backend/package.json +28 -0
  87. package/templates/apps/api-backend/queries/notes.query.server.ts +14 -0
  88. package/templates/apps/api-backend/queries/notes.query.ts +20 -0
  89. package/templates/apps/api-backend/template.json +6 -0
  90. package/templates/apps/api-backend/tests/notes.create.test.ts +50 -0
  91. package/templates/apps/api-backend/tsconfig.json +5 -0
  92. package/templates/apps/api-backend-deactivation/README.md +49 -0
  93. package/templates/apps/api-backend-deactivation/actions/users.get.action.server.ts +17 -0
  94. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +18 -0
  95. package/templates/apps/api-backend-deactivation/app.config.ts +20 -0
  96. package/templates/apps/api-backend-deactivation/database/schema.ts +41 -0
  97. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.server.ts +6 -0
  98. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +16 -0
  99. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.server.ts +10 -0
  100. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +19 -0
  101. package/templates/apps/api-backend-deactivation/package.json +28 -0
  102. package/templates/apps/api-backend-deactivation/template.json +6 -0
  103. package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +47 -0
  104. package/templates/apps/api-backend-deactivation/tsconfig.json +5 -0
  105. package/templates/apps/api-backend-mail/README.md +39 -0
  106. package/templates/apps/api-backend-mail/actions/sendWelcome.action.server.ts +15 -0
  107. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +21 -0
  108. package/templates/apps/api-backend-mail/app.config.ts +34 -0
  109. package/templates/apps/api-backend-mail/database/schema.ts +57 -0
  110. package/templates/apps/api-backend-mail/emails/welcome.email.tsx +106 -0
  111. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.server.ts +19 -0
  112. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +37 -0
  113. package/templates/apps/api-backend-mail/package.json +30 -0
  114. package/templates/apps/api-backend-mail/queries/notes.query.server.ts +14 -0
  115. package/templates/apps/api-backend-mail/queries/notes.query.ts +20 -0
  116. package/templates/apps/api-backend-mail/template.json +6 -0
  117. package/templates/apps/api-backend-mail/tests/notes.create.test.ts +50 -0
  118. package/templates/apps/api-backend-mail/tsconfig.json +5 -0
  119. package/templates/apps/api-backend-mariadb/.env.example +35 -0
  120. package/templates/apps/api-backend-mariadb/README.md +31 -0
  121. package/templates/apps/api-backend-mariadb/app.config.ts +68 -0
  122. package/templates/apps/api-backend-mariadb/database/schema.ts +57 -0
  123. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.server.ts +19 -0
  124. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +37 -0
  125. package/templates/apps/api-backend-mariadb/package.json +30 -0
  126. package/templates/apps/api-backend-mariadb/queries/notes.query.server.ts +14 -0
  127. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +20 -0
  128. package/templates/apps/api-backend-mariadb/template.json +6 -0
  129. package/templates/apps/api-backend-mariadb/tests/notes.create.test.ts +50 -0
  130. package/templates/apps/api-backend-mariadb/tsconfig.json +5 -0
  131. package/templates/apps/api-backend-storage/README.md +86 -0
  132. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.server.ts +21 -0
  133. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +21 -0
  134. package/templates/apps/api-backend-storage/actions/uploadDocument.action.server.ts +21 -0
  135. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +20 -0
  136. package/templates/apps/api-backend-storage/app.config.ts +38 -0
  137. package/templates/apps/api-backend-storage/database/schema.ts +57 -0
  138. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.server.ts +19 -0
  139. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +37 -0
  140. package/templates/apps/api-backend-storage/package.json +27 -0
  141. package/templates/apps/api-backend-storage/queries/notes.query.server.ts +14 -0
  142. package/templates/apps/api-backend-storage/queries/notes.query.ts +20 -0
  143. package/templates/apps/api-backend-storage/template.json +6 -0
  144. package/templates/apps/api-backend-storage/tests/notes.create.test.ts +50 -0
  145. package/templates/apps/api-backend-storage/tsconfig.json +5 -0
  146. package/templates/apps/api-data-advanced/.env +17 -0
  147. package/templates/apps/api-data-advanced/README.md +88 -0
  148. package/templates/apps/api-data-advanced/app.config.ts +56 -0
  149. package/templates/apps/api-data-advanced/database/actors.entity.ts +14 -0
  150. package/templates/apps/api-data-advanced/database/authors.entity.ts +30 -0
  151. package/templates/apps/api-data-advanced/database/authors.relations.ts +15 -0
  152. package/templates/apps/api-data-advanced/database/books.entity.ts +59 -0
  153. package/templates/apps/api-data-advanced/database/books.relations.ts +10 -0
  154. package/templates/apps/api-data-advanced/database/index.ts +23 -0
  155. package/templates/apps/api-data-advanced/database/tenants.entity.ts +11 -0
  156. package/templates/apps/api-data-advanced/package.json +28 -0
  157. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.server.ts +18 -0
  158. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +40 -0
  159. package/templates/apps/api-data-advanced/queries/books.search.query.server.ts +17 -0
  160. package/templates/apps/api-data-advanced/queries/books.search.query.ts +39 -0
  161. package/templates/apps/api-data-advanced/seeds/catalog.seed.ts +101 -0
  162. package/templates/apps/api-data-advanced/template.json +6 -0
  163. package/templates/apps/api-data-advanced/tests/queries.test.ts +124 -0
  164. package/templates/apps/api-data-advanced/tsconfig.json +5 -0
  165. package/templates/apps/api-durable/README.md +81 -0
  166. package/templates/apps/api-durable/aggregates/orderStats.aggregate.ts +49 -0
  167. package/templates/apps/api-durable/app.config.ts +41 -0
  168. package/templates/apps/api-durable/database/schema.ts +65 -0
  169. package/templates/apps/api-durable/mutations/orders.approve.mutation.server.ts +43 -0
  170. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +23 -0
  171. package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +45 -0
  172. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +40 -0
  173. package/templates/apps/api-durable/package.json +28 -0
  174. package/templates/apps/api-durable/schedules/nightlyReport.cron.tsx +37 -0
  175. package/templates/apps/api-durable/startup/warm.startup.tsx +33 -0
  176. package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +27 -0
  177. package/templates/apps/api-durable/template.json +6 -0
  178. package/templates/apps/api-durable/tests/orders.place.test.ts +91 -0
  179. package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +21 -0
  180. package/templates/apps/api-durable/tsconfig.json +5 -0
  181. package/templates/apps/api-durable/workflows/order.fulfill.workflow.server.tsx +103 -0
  182. package/templates/apps/api-durable/workflows/order.fulfill.workflow.tsx +25 -0
  183. package/templates/apps/api-feature-flags/README.md +63 -0
  184. package/templates/apps/api-feature-flags/actions/notes.export.action.server.ts +11 -0
  185. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +18 -0
  186. package/templates/apps/api-feature-flags/app.config.ts +41 -0
  187. package/templates/apps/api-feature-flags/database/schema.ts +37 -0
  188. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.server.ts +29 -0
  189. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +28 -0
  190. package/templates/apps/api-feature-flags/package.json +29 -0
  191. package/templates/apps/api-feature-flags/template.json +6 -0
  192. package/templates/apps/api-feature-flags/tests/notes.create.test.ts +67 -0
  193. package/templates/apps/api-feature-flags/tsconfig.json +5 -0
  194. package/templates/apps/api-governance/.env +4 -0
  195. package/templates/apps/api-governance/README.md +61 -0
  196. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -0
  197. package/templates/apps/api-governance/actions/profiles.get.action.ts +17 -0
  198. package/templates/apps/api-governance/app.config.ts +35 -0
  199. package/templates/apps/api-governance/database/schema.ts +41 -0
  200. package/templates/apps/api-governance/mutations/profiles.create.mutation.server.ts +17 -0
  201. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +21 -0
  202. package/templates/apps/api-governance/package.json +29 -0
  203. package/templates/apps/api-governance/template.json +6 -0
  204. package/templates/apps/api-governance/tests/profiles.create.test.ts +59 -0
  205. package/templates/apps/api-governance/tsconfig.json +5 -0
  206. package/templates/apps/api-kv/README.md +100 -0
  207. package/templates/apps/api-kv/actions/sync.pull.action.server.ts +75 -0
  208. package/templates/apps/api-kv/actions/sync.pull.action.ts +25 -0
  209. package/templates/apps/api-kv/actions/sync.reset.action.server.ts +25 -0
  210. package/templates/apps/api-kv/actions/sync.reset.action.ts +22 -0
  211. package/templates/apps/api-kv/actions/sync.status.action.server.ts +28 -0
  212. package/templates/apps/api-kv/actions/sync.status.action.ts +17 -0
  213. package/templates/apps/api-kv/app.config.ts +48 -0
  214. package/templates/apps/api-kv/database/schema.ts +62 -0
  215. package/templates/apps/api-kv/package.json +28 -0
  216. package/templates/apps/api-kv/queries/events.list.query.server.ts +14 -0
  217. package/templates/apps/api-kv/queries/events.list.query.ts +23 -0
  218. package/templates/apps/api-kv/template.json +6 -0
  219. package/templates/apps/api-kv/tests/sync.test.ts +103 -0
  220. package/templates/apps/api-kv/tsconfig.json +5 -0
  221. package/templates/apps/api-moderation/README.md +46 -0
  222. package/templates/apps/api-moderation/app.config.ts +33 -0
  223. package/templates/apps/api-moderation/database/schema.ts +45 -0
  224. package/templates/apps/api-moderation/mutations/comments.create.mutation.server.ts +8 -0
  225. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +20 -0
  226. package/templates/apps/api-moderation/mutations/posts.create.mutation.server.ts +9 -0
  227. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +25 -0
  228. package/templates/apps/api-moderation/package.json +29 -0
  229. package/templates/apps/api-moderation/template.json +6 -0
  230. package/templates/apps/api-moderation/tests/posts.create.test.ts +79 -0
  231. package/templates/apps/api-moderation/tsconfig.json +5 -0
  232. package/templates/apps/api-observability/README.md +106 -0
  233. package/templates/apps/api-observability/app.config.ts +32 -0
  234. package/templates/apps/api-observability/database/schema.ts +38 -0
  235. package/templates/apps/api-observability/mutations/notes.create.mutation.server.ts +33 -0
  236. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +15 -0
  237. package/templates/apps/api-observability/package.json +29 -0
  238. package/templates/apps/api-observability/queries/notes.list.query.server.ts +7 -0
  239. package/templates/apps/api-observability/queries/notes.list.query.ts +14 -0
  240. package/templates/apps/api-observability/template.json +6 -0
  241. package/templates/apps/api-observability/tests/notes.create.test.ts +36 -0
  242. package/templates/apps/api-observability/tsconfig.json +5 -0
  243. package/templates/apps/api-ratelimit/README.md +49 -0
  244. package/templates/apps/api-ratelimit/app.config.ts +42 -0
  245. package/templates/apps/api-ratelimit/database/schema.ts +37 -0
  246. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.server.ts +16 -0
  247. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +34 -0
  248. package/templates/apps/api-ratelimit/package.json +29 -0
  249. package/templates/apps/api-ratelimit/template.json +6 -0
  250. package/templates/apps/api-ratelimit/tests/notes.create.test.ts +69 -0
  251. package/templates/apps/api-ratelimit/tsconfig.json +5 -0
  252. package/templates/apps/api-rbac/README.md +59 -0
  253. package/templates/apps/api-rbac/app.config.ts +48 -0
  254. package/templates/apps/api-rbac/database/schema.ts +37 -0
  255. package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +22 -0
  256. package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +26 -0
  257. package/templates/apps/api-rbac/package.json +29 -0
  258. package/templates/apps/api-rbac/template.json +6 -0
  259. package/templates/apps/api-rbac/tests/notes.create.test.ts +80 -0
  260. package/templates/apps/api-rbac/tsconfig.json +5 -0
  261. package/templates/apps/api-rest/README.md +85 -0
  262. package/templates/apps/api-rest/app.config.ts +70 -0
  263. package/templates/apps/api-rest/database/schema.ts +52 -0
  264. package/templates/apps/api-rest/lib/product.ts +30 -0
  265. package/templates/apps/api-rest/package.json +27 -0
  266. package/templates/apps/api-rest/routes/v1/products.create.route.tsx +45 -0
  267. package/templates/apps/api-rest/routes/v1/products.delete.route.tsx +25 -0
  268. package/templates/apps/api-rest/routes/v1/products.get.route.tsx +29 -0
  269. package/templates/apps/api-rest/routes/v1/products.list.route.tsx +43 -0
  270. package/templates/apps/api-rest/template.json +6 -0
  271. package/templates/apps/api-rest/tests/products.create.test.ts +53 -0
  272. package/templates/apps/api-rest/tsconfig.json +5 -0
  273. package/templates/apps/api-saas/README.md +106 -0
  274. package/templates/apps/api-saas/app.config.ts +51 -0
  275. package/templates/apps/api-saas/database/schema.ts +50 -0
  276. package/templates/apps/api-saas/mutations/projects.create.mutation.server.ts +52 -0
  277. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +18 -0
  278. package/templates/apps/api-saas/package.json +31 -0
  279. package/templates/apps/api-saas/queries/projects.list.query.server.ts +7 -0
  280. package/templates/apps/api-saas/queries/projects.list.query.ts +18 -0
  281. package/templates/apps/api-saas/template.json +6 -0
  282. package/templates/apps/api-saas/tests/projects.create.test.ts +47 -0
  283. package/templates/apps/api-saas/tsconfig.json +5 -0
  284. package/templates/apps/api-search/README.md +68 -0
  285. package/templates/apps/api-search/app.config.ts +31 -0
  286. package/templates/apps/api-search/database/schema.ts +47 -0
  287. package/templates/apps/api-search/lib/search.ts +27 -0
  288. package/templates/apps/api-search/mutations/articles.create.mutation.server.ts +19 -0
  289. package/templates/apps/api-search/mutations/articles.create.mutation.ts +35 -0
  290. package/templates/apps/api-search/package.json +29 -0
  291. package/templates/apps/api-search/queries/articles.list.query.server.ts +14 -0
  292. package/templates/apps/api-search/queries/articles.list.query.ts +19 -0
  293. package/templates/apps/api-search/seeds/articles.seed.ts +41 -0
  294. package/templates/apps/api-search/startup/searchBackfill.startup.tsx +24 -0
  295. package/templates/apps/api-search/template.json +6 -0
  296. package/templates/apps/api-search/tests/articles.create.test.ts +69 -0
  297. package/templates/apps/api-search/tsconfig.json +5 -0
  298. package/templates/apps/api-versioning/README.md +51 -0
  299. package/templates/apps/api-versioning/actions/documents.asOf.action.server.ts +13 -0
  300. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +14 -0
  301. package/templates/apps/api-versioning/actions/documents.history.action.server.ts +15 -0
  302. package/templates/apps/api-versioning/actions/documents.history.action.ts +17 -0
  303. package/templates/apps/api-versioning/app.config.ts +24 -0
  304. package/templates/apps/api-versioning/database/schema.ts +38 -0
  305. package/templates/apps/api-versioning/mutations/documents.create.mutation.server.ts +8 -0
  306. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +21 -0
  307. package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +9 -0
  308. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +21 -0
  309. package/templates/apps/api-versioning/package.json +29 -0
  310. package/templates/apps/api-versioning/template.json +6 -0
  311. package/templates/apps/api-versioning/tests/documents.create.test.ts +37 -0
  312. package/templates/apps/api-versioning/tsconfig.json +5 -0
  313. package/templates/apps/api-webhooks/.env +6 -0
  314. package/templates/apps/api-webhooks/README.md +106 -0
  315. package/templates/apps/api-webhooks/app.config.ts +16 -0
  316. package/templates/apps/api-webhooks/database/schema.ts +49 -0
  317. package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +22 -0
  318. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.server.ts +37 -0
  319. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +15 -0
  320. package/templates/apps/api-webhooks/package.json +28 -0
  321. package/templates/apps/api-webhooks/queries/orders.list.query.server.ts +7 -0
  322. package/templates/apps/api-webhooks/queries/orders.list.query.ts +15 -0
  323. package/templates/apps/api-webhooks/template.json +6 -0
  324. package/templates/apps/api-webhooks/tests/orders.fulfill.test.ts +51 -0
  325. package/templates/apps/api-webhooks/tsconfig.json +5 -0
  326. package/templates/apps/api-webhooks/webhooks/orders.webhook.tsx +35 -0
  327. package/templates/apps/changelog/README.md +77 -0
  328. package/templates/apps/changelog/app.config.ts +27 -0
  329. package/templates/apps/changelog/content/releases/0.1.0.mdx +19 -0
  330. package/templates/apps/changelog/content/releases/0.2.0.mdx +28 -0
  331. package/templates/apps/changelog/package.json +30 -0
  332. package/templates/apps/changelog/scripts/generate-rss.mjs +38 -0
  333. package/templates/apps/changelog/src/globals.css +66 -0
  334. package/templates/apps/changelog/src/globals.d.ts +16 -0
  335. package/templates/apps/changelog/src/lib/locale.test.ts +72 -0
  336. package/templates/apps/changelog/src/lib/locale.ts +55 -0
  337. package/templates/apps/changelog/src/lib/releases.ts +21 -0
  338. package/templates/apps/changelog/src/locales/de.ts +22 -0
  339. package/templates/apps/changelog/src/locales/en.ts +30 -0
  340. package/templates/apps/changelog/src/pages/[locale]/[slug].tsx +24 -0
  341. package/templates/apps/changelog/src/pages/[locale]/index.tsx +13 -0
  342. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +95 -0
  343. package/templates/apps/changelog/src/pages/[slug].test.tsx +127 -0
  344. package/templates/apps/changelog/src/pages/[slug].tsx +52 -0
  345. package/templates/apps/changelog/src/pages/index.test.tsx +112 -0
  346. package/templates/apps/changelog/src/pages/index.tsx +55 -0
  347. package/templates/apps/changelog/src/pages/layout.tsx +84 -0
  348. package/templates/apps/changelog/template.json +6 -0
  349. package/templates/apps/changelog/tsconfig.json +11 -0
  350. package/templates/apps/edge-functions/README.md +55 -0
  351. package/templates/apps/edge-functions/functions/aiComplete.serverless.ts +58 -0
  352. package/templates/apps/edge-functions/functions/currencyConvert.serverless.ts +47 -0
  353. package/templates/apps/edge-functions/functions/geoGreeting.serverless.ts +44 -0
  354. package/templates/apps/edge-functions/functions/health.serverless.ts +29 -0
  355. package/templates/apps/edge-functions/functions/resolveLink.serverless.ts +32 -0
  356. package/templates/apps/edge-functions/functions/shareLink.serverless.ts +30 -0
  357. package/templates/apps/edge-functions/functions/slackNotify.serverless.ts +40 -0
  358. package/templates/apps/edge-functions/functions/verifySignature.serverless.ts +52 -0
  359. package/templates/apps/edge-functions/package.json +21 -0
  360. package/templates/apps/edge-functions/template.json +6 -0
  361. package/templates/apps/edge-functions/tsconfig.json +10 -0
  362. package/templates/apps/frontend-admin/README.md +40 -0
  363. package/templates/apps/frontend-admin/app.config.ts +37 -0
  364. package/templates/apps/frontend-admin/package.json +30 -0
  365. package/templates/apps/frontend-admin/src/config.ts +8 -0
  366. package/templates/apps/frontend-admin/src/globals.css +76 -0
  367. package/templates/apps/frontend-admin/src/globals.d.ts +6 -0
  368. package/templates/apps/frontend-admin/src/lib/admin.ts +16 -0
  369. package/templates/apps/frontend-admin/src/lib/auth.ts +24 -0
  370. package/templates/apps/frontend-admin/src/locales/de.ts +67 -0
  371. package/templates/apps/frontend-admin/src/locales/en.ts +79 -0
  372. package/templates/apps/frontend-admin/src/locales/index.ts +15 -0
  373. package/templates/apps/frontend-admin/src/pages/(marketing)/index.test.tsx +55 -0
  374. package/templates/apps/frontend-admin/src/pages/(marketing)/index.tsx +32 -0
  375. package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +29 -0
  376. package/templates/apps/frontend-admin/src/pages/(marketing)/login.test.tsx +73 -0
  377. package/templates/apps/frontend-admin/src/pages/(marketing)/login.tsx +35 -0
  378. package/templates/apps/frontend-admin/src/pages/admin/[entity].tsx +121 -0
  379. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +119 -0
  380. package/templates/apps/frontend-admin/src/pages/admin/error.tsx +20 -0
  381. package/templates/apps/frontend-admin/src/pages/admin/fallbacks.test.tsx +68 -0
  382. package/templates/apps/frontend-admin/src/pages/admin/index.test.tsx +88 -0
  383. package/templates/apps/frontend-admin/src/pages/admin/index.tsx +65 -0
  384. package/templates/apps/frontend-admin/src/pages/admin/layout.test.tsx +114 -0
  385. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +97 -0
  386. package/templates/apps/frontend-admin/src/pages/admin/loading.tsx +15 -0
  387. package/templates/apps/frontend-admin/src/pages/admin/not-found.tsx +15 -0
  388. package/templates/apps/frontend-admin/src/pages/layout.tsx +12 -0
  389. package/templates/apps/frontend-admin/src/pages/layouts.test.tsx +54 -0
  390. package/templates/apps/frontend-admin/template.json +6 -0
  391. package/templates/apps/frontend-admin/tsconfig.json +11 -0
  392. package/templates/apps/frontend-app/README.md +98 -0
  393. package/templates/apps/frontend-app/app.config.ts +43 -0
  394. package/templates/apps/frontend-app/package.json +30 -0
  395. package/templates/apps/frontend-app/src/locales/de.ts +36 -0
  396. package/templates/apps/frontend-app/src/locales/en.ts +43 -0
  397. package/templates/apps/frontend-app/src/locales/index.ts +15 -0
  398. package/templates/apps/frontend-app/src/pages/index.test.tsx +167 -0
  399. package/templates/apps/frontend-app/src/pages/index.tsx +136 -0
  400. package/templates/apps/frontend-app/src/pages/layout.tsx +41 -0
  401. package/templates/apps/frontend-app/src/pages/schema-ui.test.tsx +99 -0
  402. package/templates/apps/frontend-app/src/pages/schema-ui.tsx +74 -0
  403. package/templates/apps/frontend-app/template.json +6 -0
  404. package/templates/apps/frontend-app/tsconfig.json +11 -0
  405. package/templates/apps/frontend-blank/README.md +18 -0
  406. package/templates/apps/frontend-blank/app.config.ts +29 -0
  407. package/templates/apps/frontend-blank/package.json +29 -0
  408. package/templates/apps/frontend-blank/src/locales/de.ts +15 -0
  409. package/templates/apps/frontend-blank/src/locales/en.ts +22 -0
  410. package/templates/apps/frontend-blank/src/locales/index.ts +15 -0
  411. package/templates/apps/frontend-blank/src/pages/index.test.tsx +55 -0
  412. package/templates/apps/frontend-blank/src/pages/index.tsx +27 -0
  413. package/templates/apps/frontend-blank/src/pages/layout.test.tsx +54 -0
  414. package/templates/apps/frontend-blank/src/pages/layout.tsx +35 -0
  415. package/templates/apps/frontend-blank/template.json +6 -0
  416. package/templates/apps/frontend-blank/tsconfig.json +11 -0
  417. package/templates/apps/frontend-contact/README.md +65 -0
  418. package/templates/apps/frontend-contact/app.config.ts +25 -0
  419. package/templates/apps/frontend-contact/functions/sendMessage.serverless.ts +69 -0
  420. package/templates/apps/frontend-contact/package.json +33 -0
  421. package/templates/apps/frontend-contact/src/components/ContactForm.island.test.tsx +142 -0
  422. package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +104 -0
  423. package/templates/apps/frontend-contact/src/config.ts +12 -0
  424. package/templates/apps/frontend-contact/src/globals.css +84 -0
  425. package/templates/apps/frontend-contact/src/lib/locale.ts +55 -0
  426. package/templates/apps/frontend-contact/src/locales/de.ts +26 -0
  427. package/templates/apps/frontend-contact/src/locales/en.ts +29 -0
  428. package/templates/apps/frontend-contact/src/pages/[locale]/index.tsx +14 -0
  429. package/templates/apps/frontend-contact/src/pages/index.test.tsx +70 -0
  430. package/templates/apps/frontend-contact/src/pages/index.tsx +63 -0
  431. package/templates/apps/frontend-contact/src/pages/layout.tsx +65 -0
  432. package/templates/apps/frontend-contact/template.json +6 -0
  433. package/templates/apps/frontend-contact/tsconfig.json +11 -0
  434. package/templates/apps/frontend-dashboard/README.md +54 -0
  435. package/templates/apps/frontend-dashboard/app.config.ts +39 -0
  436. package/templates/apps/frontend-dashboard/package.json +29 -0
  437. package/templates/apps/frontend-dashboard/src/config.ts +8 -0
  438. package/templates/apps/frontend-dashboard/src/globals.css +72 -0
  439. package/templates/apps/frontend-dashboard/src/globals.d.ts +6 -0
  440. package/templates/apps/frontend-dashboard/src/lib/auth.ts +27 -0
  441. package/templates/apps/frontend-dashboard/src/locales/de.ts +49 -0
  442. package/templates/apps/frontend-dashboard/src/locales/en.ts +60 -0
  443. package/templates/apps/frontend-dashboard/src/locales/index.ts +15 -0
  444. package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.test.tsx +55 -0
  445. package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.tsx +34 -0
  446. package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +32 -0
  447. package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.test.tsx +74 -0
  448. package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.tsx +40 -0
  449. package/templates/apps/frontend-dashboard/src/pages/dashboard/error.tsx +21 -0
  450. package/templates/apps/frontend-dashboard/src/pages/dashboard/fallbacks.test.tsx +68 -0
  451. package/templates/apps/frontend-dashboard/src/pages/dashboard/index.test.tsx +55 -0
  452. package/templates/apps/frontend-dashboard/src/pages/dashboard/index.tsx +38 -0
  453. package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.test.tsx +88 -0
  454. package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +69 -0
  455. package/templates/apps/frontend-dashboard/src/pages/dashboard/loading.tsx +21 -0
  456. package/templates/apps/frontend-dashboard/src/pages/dashboard/not-found.tsx +19 -0
  457. package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.test.tsx +69 -0
  458. package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.tsx +35 -0
  459. package/templates/apps/frontend-dashboard/src/pages/layout.tsx +12 -0
  460. package/templates/apps/frontend-dashboard/src/pages/layouts.test.tsx +54 -0
  461. package/templates/apps/frontend-dashboard/template.json +6 -0
  462. package/templates/apps/frontend-dashboard/tsconfig.json +11 -0
  463. package/templates/apps/frontend-docs/README.md +19 -0
  464. package/templates/apps/frontend-docs/app.config.ts +25 -0
  465. package/templates/apps/frontend-docs/package.json +29 -0
  466. package/templates/apps/frontend-docs/src/globals.css +32 -0
  467. package/templates/apps/frontend-docs/src/lib/locale.test.ts +72 -0
  468. package/templates/apps/frontend-docs/src/lib/locale.ts +56 -0
  469. package/templates/apps/frontend-docs/src/locales/de.ts +24 -0
  470. package/templates/apps/frontend-docs/src/locales/en.ts +28 -0
  471. package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug].tsx +18 -0
  472. package/templates/apps/frontend-docs/src/pages/[locale]/index.tsx +13 -0
  473. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +67 -0
  474. package/templates/apps/frontend-docs/src/pages/docs/[...slug].test.tsx +115 -0
  475. package/templates/apps/frontend-docs/src/pages/docs/[...slug].tsx +82 -0
  476. package/templates/apps/frontend-docs/src/pages/index.test.tsx +90 -0
  477. package/templates/apps/frontend-docs/src/pages/index.tsx +45 -0
  478. package/templates/apps/frontend-docs/src/pages/layout.test.tsx +84 -0
  479. package/templates/apps/frontend-docs/src/pages/layout.tsx +66 -0
  480. package/templates/apps/frontend-docs/template.json +6 -0
  481. package/templates/apps/frontend-docs/tsconfig.json +11 -0
  482. package/templates/apps/frontend-i18n/README.md +61 -0
  483. package/templates/apps/frontend-i18n/app.config.ts +33 -0
  484. package/templates/apps/frontend-i18n/package.json +28 -0
  485. package/templates/apps/frontend-i18n/src/globals.css +46 -0
  486. package/templates/apps/frontend-i18n/src/globals.d.ts +6 -0
  487. package/templates/apps/frontend-i18n/src/lib/locale.test.ts +72 -0
  488. package/templates/apps/frontend-i18n/src/lib/locale.ts +55 -0
  489. package/templates/apps/frontend-i18n/src/locales/de.ts +24 -0
  490. package/templates/apps/frontend-i18n/src/locales/en.ts +25 -0
  491. package/templates/apps/frontend-i18n/src/pages/[locale]/about.tsx +11 -0
  492. package/templates/apps/frontend-i18n/src/pages/[locale]/index.tsx +13 -0
  493. package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +50 -0
  494. package/templates/apps/frontend-i18n/src/pages/about.test.tsx +75 -0
  495. package/templates/apps/frontend-i18n/src/pages/about.tsx +31 -0
  496. package/templates/apps/frontend-i18n/src/pages/index.test.tsx +102 -0
  497. package/templates/apps/frontend-i18n/src/pages/index.tsx +43 -0
  498. package/templates/apps/frontend-i18n/src/pages/layout.test.tsx +86 -0
  499. package/templates/apps/frontend-i18n/src/pages/layout.tsx +70 -0
  500. package/templates/apps/frontend-i18n/template.json +6 -0
  501. package/templates/apps/frontend-i18n/tsconfig.json +11 -0
  502. package/templates/apps/frontend-landing/README.md +17 -0
  503. package/templates/apps/frontend-landing/app.config.ts +25 -0
  504. package/templates/apps/frontend-landing/package.json +29 -0
  505. package/templates/apps/frontend-landing/src/globals.css +23 -0
  506. package/templates/apps/frontend-landing/src/lib/locale.test.ts +72 -0
  507. package/templates/apps/frontend-landing/src/lib/locale.ts +55 -0
  508. package/templates/apps/frontend-landing/src/locales/de.ts +24 -0
  509. package/templates/apps/frontend-landing/src/locales/en.ts +26 -0
  510. package/templates/apps/frontend-landing/src/pages/[locale]/index.tsx +13 -0
  511. package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +37 -0
  512. package/templates/apps/frontend-landing/src/pages/index.test.tsx +110 -0
  513. package/templates/apps/frontend-landing/src/pages/index.tsx +75 -0
  514. package/templates/apps/frontend-landing/src/pages/layout.test.tsx +84 -0
  515. package/templates/apps/frontend-landing/src/pages/layout.tsx +66 -0
  516. package/templates/apps/frontend-landing/template.json +6 -0
  517. package/templates/apps/frontend-landing/tsconfig.json +11 -0
  518. package/templates/apps/frontend-spa/README.md +45 -0
  519. package/templates/apps/frontend-spa/app.config.ts +27 -0
  520. package/templates/apps/frontend-spa/package.json +29 -0
  521. package/templates/apps/frontend-spa/src/globals.css +84 -0
  522. package/templates/apps/frontend-spa/src/locales/de.ts +22 -0
  523. package/templates/apps/frontend-spa/src/locales/en.ts +29 -0
  524. package/templates/apps/frontend-spa/src/locales/index.ts +15 -0
  525. package/templates/apps/frontend-spa/src/pages/index.test.tsx +137 -0
  526. package/templates/apps/frontend-spa/src/pages/index.tsx +123 -0
  527. package/templates/apps/frontend-spa/src/pages/layout.tsx +27 -0
  528. package/templates/apps/frontend-spa/template.json +6 -0
  529. package/templates/apps/frontend-spa/tsconfig.json +11 -0
  530. package/templates/apps/frontend-ssr/README.md +68 -0
  531. package/templates/apps/frontend-ssr/app.config.ts +32 -0
  532. package/templates/apps/frontend-ssr/package.json +29 -0
  533. package/templates/apps/frontend-ssr/src/globals.css +67 -0
  534. package/templates/apps/frontend-ssr/src/locales/de.ts +41 -0
  535. package/templates/apps/frontend-ssr/src/locales/en.ts +54 -0
  536. package/templates/apps/frontend-ssr/src/locales/index.ts +16 -0
  537. package/templates/apps/frontend-ssr/src/pages/feed-swr.test.tsx +69 -0
  538. package/templates/apps/frontend-ssr/src/pages/feed-swr.tsx +54 -0
  539. package/templates/apps/frontend-ssr/src/pages/feed.test.tsx +73 -0
  540. package/templates/apps/frontend-ssr/src/pages/feed.tsx +64 -0
  541. package/templates/apps/frontend-ssr/src/pages/index.test.tsx +89 -0
  542. package/templates/apps/frontend-ssr/src/pages/index.tsx +72 -0
  543. package/templates/apps/frontend-ssr/src/pages/layout.tsx +37 -0
  544. package/templates/apps/frontend-ssr/template.json +6 -0
  545. package/templates/apps/frontend-ssr/tsconfig.json +11 -0
  546. package/templates/apps/frontend-ssr-api/README.md +50 -0
  547. package/templates/apps/frontend-ssr-api/app.config.ts +43 -0
  548. package/templates/apps/frontend-ssr-api/package.json +30 -0
  549. package/templates/apps/frontend-ssr-api/src/globals.css +38 -0
  550. package/templates/apps/frontend-ssr-api/src/globals.d.ts +6 -0
  551. package/templates/apps/frontend-ssr-api/src/locales/de.ts +20 -0
  552. package/templates/apps/frontend-ssr-api/src/locales/en.ts +31 -0
  553. package/templates/apps/frontend-ssr-api/src/locales/index.ts +16 -0
  554. package/templates/apps/frontend-ssr-api/src/pages/index.test.tsx +105 -0
  555. package/templates/apps/frontend-ssr-api/src/pages/index.tsx +83 -0
  556. package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +28 -0
  557. package/templates/apps/frontend-ssr-api/template.json +6 -0
  558. package/templates/apps/frontend-ssr-api/tsconfig.json +11 -0
  559. package/templates/apps/frontend-static-blog/README.md +49 -0
  560. package/templates/apps/frontend-static-blog/app.config.ts +34 -0
  561. package/templates/apps/frontend-static-blog/package.json +28 -0
  562. package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.test.tsx +65 -0
  563. package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +35 -0
  564. package/templates/apps/frontend-static-blog/src/content/posts.ts +64 -0
  565. package/templates/apps/frontend-static-blog/src/globals.css +75 -0
  566. package/templates/apps/frontend-static-blog/src/lib/locale.test.ts +72 -0
  567. package/templates/apps/frontend-static-blog/src/lib/locale.ts +55 -0
  568. package/templates/apps/frontend-static-blog/src/locales/de.ts +19 -0
  569. package/templates/apps/frontend-static-blog/src/locales/en.ts +26 -0
  570. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug].tsx +20 -0
  571. package/templates/apps/frontend-static-blog/src/pages/[locale]/index.tsx +13 -0
  572. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +62 -0
  573. package/templates/apps/frontend-static-blog/src/pages/blog/[slug].test.tsx +116 -0
  574. package/templates/apps/frontend-static-blog/src/pages/blog/[slug].tsx +69 -0
  575. package/templates/apps/frontend-static-blog/src/pages/index.test.tsx +100 -0
  576. package/templates/apps/frontend-static-blog/src/pages/index.tsx +58 -0
  577. package/templates/apps/frontend-static-blog/src/pages/layout.tsx +63 -0
  578. package/templates/apps/frontend-static-blog/template.json +6 -0
  579. package/templates/apps/frontend-static-blog/tsconfig.json +11 -0
  580. package/templates/baselines/bare/.env.example +37 -0
  581. package/templates/baselines/bare/README.md +47 -0
  582. package/templates/baselines/bare/baseline.json +31 -0
  583. package/templates/baselines/bare/deploy/README.md +43 -0
  584. package/templates/baselines/bare/deploy/voltro.service.example +36 -0
  585. package/templates/baselines/compose/.env.example +46 -0
  586. package/templates/baselines/compose/README.md +69 -0
  587. package/templates/baselines/compose/baseline.json +51 -0
  588. package/templates/baselines/compose/docker/.dockerignore +38 -0
  589. package/templates/baselines/compose/docker/api.Dockerfile +57 -0
  590. package/templates/baselines/compose/docker/dev.Dockerfile +35 -0
  591. package/templates/baselines/compose/docker/web.Dockerfile +59 -0
  592. package/templates/baselines/compose/docker-compose.dev.yml +89 -0
  593. package/templates/baselines/compose/docker-compose.prod.yml +87 -0
  594. package/templates/baselines/compose/docker-compose.yml +41 -0
  595. package/templates/baselines/compose-mariadb/.env.example +57 -0
  596. package/templates/baselines/compose-mariadb/README.md +78 -0
  597. package/templates/baselines/compose-mariadb/baseline.json +51 -0
  598. package/templates/baselines/compose-mariadb/docker/.dockerignore +38 -0
  599. package/templates/baselines/compose-mariadb/docker/api.Dockerfile +57 -0
  600. package/templates/baselines/compose-mariadb/docker/dev.Dockerfile +35 -0
  601. package/templates/baselines/compose-mariadb/docker/mariadb-init.sql +6 -0
  602. package/templates/baselines/compose-mariadb/docker/web.Dockerfile +59 -0
  603. package/templates/baselines/compose-mariadb/docker-compose.dev.yml +117 -0
  604. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +114 -0
  605. package/templates/baselines/compose-mariadb/docker-compose.yml +79 -0
  606. package/templates/baselines/helm/.env.example +39 -0
  607. package/templates/baselines/helm/README.md +98 -0
  608. package/templates/baselines/helm/baseline.json +53 -0
  609. package/templates/baselines/helm/charts/voltro-app/.helmignore +10 -0
  610. package/templates/baselines/helm/charts/voltro-app/Chart.yaml +10 -0
  611. package/templates/baselines/helm/charts/voltro-app/templates/_helpers.tpl +36 -0
  612. package/templates/baselines/helm/charts/voltro-app/templates/configmap.yaml +13 -0
  613. package/templates/baselines/helm/charts/voltro-app/templates/deployment-api.yaml +120 -0
  614. package/templates/baselines/helm/charts/voltro-app/templates/deployment-web.yaml +45 -0
  615. package/templates/baselines/helm/charts/voltro-app/templates/ingress.yaml +37 -0
  616. package/templates/baselines/helm/charts/voltro-app/templates/postgres-service.yaml +19 -0
  617. package/templates/baselines/helm/charts/voltro-app/templates/postgres-statefulset.yaml +73 -0
  618. package/templates/baselines/helm/charts/voltro-app/templates/secret.yaml +33 -0
  619. package/templates/baselines/helm/charts/voltro-app/templates/service-api.yaml +19 -0
  620. package/templates/baselines/helm/charts/voltro-app/templates/service-web.yaml +19 -0
  621. package/templates/baselines/helm/charts/voltro-app/values-dev.yaml +21 -0
  622. package/templates/baselines/helm/charts/voltro-app/values-prod.yaml +58 -0
  623. package/templates/baselines/helm/charts/voltro-app/values-staging.yaml +25 -0
  624. package/templates/baselines/helm/charts/voltro-app/values.yaml +109 -0
  625. package/templates/baselines/helm/deploy/README.md +94 -0
  626. package/templates/patches/@effect__cluster@0.59.0.patch +262 -0
@@ -0,0 +1,1553 @@
1
+ # Routing
2
+
3
+ > Voltro's file-based router — pages, layouts, render modes, loaders, navigation, islands. The web side of the framework.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/routing/overview.md -->
10
+ ## Overview
11
+
12
+ _Voltro's file-based router — pages, layouts, render modes, loaders, navigation, islands. The web side of the framework._
13
+
14
+ The web side of a Voltro app uses **file-based routing**: drop a 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
+
16
+ This section covers everything about how URLs map to React + how Voltro decides when to render, what to ship to the browser, and how to navigate between pages.
17
+
18
+ ## The shape of it
19
+
20
+ ```text
21
+ src/pages/
22
+ ├── layout.tsx # outer shell (wraps every page)
23
+ ├── error.tsx # error boundary
24
+ ├── loading.tsx # pending UI
25
+ ├── not-found.tsx # 404 fallback
26
+ ├── index.tsx # /
27
+ ├── about.tsx # /about
28
+ ├── (marketing)/ # route group — no URL segment
29
+ │ ├── layout.tsx # marketing-scoped layout
30
+ │ └── pricing.tsx # /pricing
31
+ ├── users/
32
+ │ ├── layout.tsx # users-scoped layout
33
+ │ ├── error.tsx # users-scoped error boundary
34
+ │ ├── [id].tsx # /users/:id
35
+ │ └── index.tsx # /users
36
+ └── docs/
37
+ └── [...slug].tsx # /docs/<anything> (catch-all)
38
+ ```
39
+
40
+ That's the whole router. No `<Route>`, no `<Switch>`, no `useRoutes`.
41
+
42
+ ## What's in this section
43
+
44
+ - [Pages & dynamic segments](/docs/routing/pages) — file → URL mapping, `[id]`, `[...slug]`, query params
45
+ - [Layouts & route groups](/docs/routing/layouts) — `layout.tsx`, error boundaries, `(group)/` directories
46
+ - [Render modes](/docs/routing/render-modes) — `static` vs `ssr` vs `isr`, when to use each
47
+ - [Loaders & meta](/docs/routing/loaders-and-meta) — server-side data fetch + `<head>` injection
48
+ - [Navigation](/docs/routing/navigation) — `Link`, `useNavigate`, prefetch on hover
49
+ - [Islands](/docs/routing/islands) — hydrating only the interactive bits, JS-free pages
50
+
51
+ ## Architecture in one paragraph
52
+
53
+ The framework generates a `.framework/app.tsx` on every boot that imports each `*.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.
54
+
55
+ You don't write any of this. The CLI regenerates it on every save in dev; the build pipeline freezes it for production.
56
+
57
+ ## Embedding the runtime in a foreign host
58
+
59
+ To mount Voltro's reactive runtime *inside* an app that owns its own routing (a Next.js / Remix / existing React shell), skip the generated boot and use the embeddable provider from the light `@voltro/web/runtime` subpath:
60
+
61
+ ```tsx
62
+ import { VoltroRuntimeProvider, type MountedApi } from '@voltro/web/runtime'
63
+
64
+ const apis: ReadonlyArray<MountedApi> = [
65
+ { name: 'app', group, descriptors, wsUrl: 'wss://your-api.example.com/ws' },
66
+ ]
67
+
68
+ export const Providers = ({ children }: { children: React.ReactNode }) => (
69
+ <VoltroRuntimeProvider apis={apis}>{children}</VoltroRuntimeProvider>
70
+ )
71
+ ```
72
+
73
+ Inside it, every `@voltro/client` hook (`useSubscription`, `useMutation`, `useAction`, …) resolves exactly as in a native Voltro app — it builds + supervises the per-api RpcClient-over-WebSocket + runtime + subscription cache and handles reconnect. `children` render immediately (a pending api reads `undefined` until connected — no spinner gate), so a prerendered host hydrates without a mismatch. The `@voltro/web/runtime` subpath pulls only the rpc/socket/runtime graph, not the router/mount/SSR — keeping the host bundle small. (`FrameworkBoot`, the framework's own web boot, is a thin wrapper over this provider.) Consume the framework as the **published** package — a `file:` link to an unbuilt checkout resolves the raw `src` export, which a foreign bundler can't handle.
74
+
75
+ ## Conventions
76
+
77
+ | Pattern | Behaviour |
78
+ |---|---|
79
+ | `index.tsx` in a directory | Maps to the directory's URL (no segment for the filename). |
80
+ | `[name].tsx` | Dynamic single segment. Available via `useParams<{ name: string }>()`. |
81
+ | `[...rest].tsx` | Catch-all. `useParams<{ rest: string }>()` joins the captured path with `/`. |
82
+ | `[[...rest]].tsx` | Optional catch-all. Matches both `/foo` and `/foo/bar/baz`. |
83
+ | `(name)/` | Route group. Strips the segment from the URL but layouts inside still apply. |
84
+ | `_*.tsx` | Private — discovery skips it. Useful for helpers next to pages. |
85
+ | `*.island.tsx` | Hydration island. Bundles as a separate chunk; hydrated only when imported. |
86
+
87
+ ## When NOT to use file-based routing
88
+
89
+ The pattern works for ~99% of apps. Edge cases:
90
+
91
+ - **Programmatically generated routes** — when you can't know the URL ahead of time. Use `[...slug].tsx` + match inside.
92
+ - **i18n with URL-segment locale** — see [URL strategies](/docs/i18n/url-strategies) for the shipped URL-prefix approach (`/de/docs/foo`).
93
+ - **Cross-tenant subdomains** — handled at the reverse-proxy layer; the file-based router serves one host's paths.
94
+
95
+ For everything else, drop a file + done.
96
+
97
+ ## Where to next — building the UI inside the page
98
+
99
+ Routing gets you to a page; the **[Schema-driven UI](/docs/ui/overview)** section
100
+ fills it. It's the other half of the frontend story:
101
+
102
+ - **[Forms & tables](/docs/ui/forms-and-tables)** — `<AutoForm>` binds to a
103
+ mutation, `<DataTable>` to a query; fields + columns come from the
104
+ descriptors' `effect/Schema`, validation + live auto-optimistic for free.
105
+ - **[Reactive components](/docs/ui/reactive-components)** — drop-in workflow
106
+ progress, presence/multiplayer, and AI chat over the durable backend.
107
+ - **[Client utilities](/docs/ui/client-utilities/use-can)** — the bound-hook toolbox
108
+ beyond `useSubscription`/`useMutation`: `useCan`, `useDerived`, `usePreview`,
109
+ `useUndo`, `useProvenance`, `useAsyncValidation`, `useOutbox`,
110
+ `useWindowedSubscription`, and more.
111
+
112
+
113
+
114
+ ---
115
+
116
+ <!-- source: en/routing/pages.md -->
117
+ ## Pages & dynamic segments
118
+
119
+ _Filesystem → URL mapping, dynamic [id] segments, catch-all [...slug] queries, query params, and private files._
120
+
121
+ A **page** is any `*.tsx` file under `src/pages/` that's not a special file (`layout.tsx`, `error.tsx`, `loading.tsx`, `not-found.tsx`) and doesn't start with `_`. Its default export is the page component; the URL comes from the file path.
122
+
123
+ ## A static page
124
+
125
+ ```tsx
126
+ // src/pages/about.tsx → /about
127
+ import type { ReactNode } from 'react'
128
+
129
+ export default function About(): ReactNode {
130
+ return (
131
+ <div className="max-w-2xl mx-auto py-12 px-6">
132
+ <h1 className="text-3xl font-bold">About us</h1>
133
+ <p>Voltro Cloud is a framework for shipping multi-tenant SaaS.</p>
134
+ </div>
135
+ )
136
+ }
137
+ ```
138
+
139
+ That's it. Save the file, the CLI's discovery sees it on next save, the page is live at `/about`.
140
+
141
+ ## index files
142
+
143
+ `index.tsx` maps to the directory's URL:
144
+
145
+ ```text
146
+ src/pages/index.tsx → /
147
+ src/pages/users/index.tsx → /users
148
+ src/pages/admin/index.tsx → /admin
149
+ ```
150
+
151
+ ## Dynamic segments
152
+
153
+ Brackets in the filename are dynamic. The captured value comes through `useParams<T>()`:
154
+
155
+ ```tsx
156
+ // src/pages/users/[id].tsx → /users/:id
157
+ import { useParams } from '@voltro/web'
158
+
159
+ export default function User() {
160
+ const { id } = useParams<{ id: string }>()
161
+ return <h1>User {id}</h1>
162
+ }
163
+ ```
164
+
165
+ Multiple dynamic segments in one path:
166
+
167
+ ```text
168
+ src/pages/orgs/[orgId]/projects/[projectId].tsx
169
+ // → /orgs/:orgId/projects/:projectId
170
+
171
+ const { orgId, projectId } = useParams<{ orgId: string; projectId: string }>()
172
+ ```
173
+
174
+ ## Catch-all queries
175
+
176
+ `[...name]` captures one OR more URL segments as a single param value (joined by `/`):
177
+
178
+ ```tsx
179
+ // src/pages/docs/[...slug].tsx → /docs/<anything>
180
+ const { slug } = useParams<{ slug: string }>()
181
+ // /docs/intro/getting-started → slug = "intro/getting-started"
182
+ ```
183
+
184
+ **Optional** catch-all (matches the base URL too):
185
+
186
+ ```tsx
187
+ // src/pages/docs/[[...slug]].tsx
188
+ // /docs → slug = ""
189
+ // /docs/foo → slug = "foo"
190
+ // /docs/foo/bar → slug = "foo/bar"
191
+ ```
192
+
193
+ ## Priority
194
+
195
+ When multiple files could match (static, dynamic, catch-all), priority is:
196
+
197
+ 1. Static segments win over dynamic.
198
+ 2. Dynamic single (`[id]`) wins over catch-all (`[...slug]`).
199
+ 3. Optional catch-all (`[[...slug]]`) wins over required catch-all (`[...slug]`) — the optional form scores as more specific, so it matches first.
200
+
201
+ ```text
202
+ src/pages/users/index.tsx # /users → wins for /users
203
+ src/pages/users/[id].tsx # /users/:id → wins for /users/42
204
+ src/pages/users/new.tsx # /users/new → wins (static beats dynamic)
205
+ src/pages/[...rest].tsx # everything else
206
+ ```
207
+
208
+ ## Query strings
209
+
210
+ Voltro doesn't bake query params into the query — they're orthogonal to the URL pattern:
211
+
212
+ ```tsx
213
+ import { useLocation } from '@voltro/web'
214
+
215
+ const Page = () => {
216
+ const pathname = useLocation()
217
+ // …
218
+ // For the search string, parse it from the request URL via useServerRequest()
219
+ // (SSR) or window.location.search (client after hydration).
220
+ }
221
+ ```
222
+
223
+ For SSR pages that need server-side query parsing:
224
+
225
+ ```tsx
226
+ import { useServerRequest } from '@voltro/web'
227
+
228
+ export const renderMode = 'ssr' as const
229
+
230
+ export default function SearchPage() {
231
+ const req = useServerRequest()
232
+ const q = req
233
+ ? new URL(req.url, 'http://x').searchParams.get('q') ?? ''
234
+ : new URLSearchParams(window.location.search).get('q') ?? ''
235
+ // …
236
+ }
237
+ ```
238
+
239
+ Parse the query string explicitly via `useServerRequest()` on the server and `window.location.search` on the client, as shown above.
240
+
241
+ ## Private files
242
+
243
+ Files starting with `_` are skipped by discovery — they're helpers next to pages:
244
+
245
+ ```text
246
+ src/pages/
247
+ ├── _components/
248
+ │ └── UserCard.tsx # NOT a route — import from siblings
249
+ ├── users/
250
+ │ ├── _helpers.ts # NOT a route
251
+ │ └── [id].tsx # → /users/:id
252
+ ```
253
+
254
+ You can also use the colocation pattern: keep page-specific components in a directory named with `_` prefix.
255
+
256
+ ## Trailing slashes
257
+
258
+ The canonical form is **no trailing slash** — always link with `<Link to="/about">`, not `<Link to="/about/">`.
259
+
260
+ The framework does NOT emit a trailing-slash redirect on its own. If you need `/about/` → `/about` normalisation (for SEO), configure a 301 redirect at your reverse proxy.
261
+
262
+ ## What pages CAN'T do
263
+
264
+ - **Live outside `src/pages/`.** Discovery walks one root. Helpers + components go elsewhere; pages go here.
265
+ - **Have multiple default exports.** One page per file.
266
+ - **Be `.ts` files.** Pages must be `.tsx` — React components only.
267
+ - **Be discovered via dynamic import.** The CLI generates the import statements at boot; runtime adds need a re-discover (which `voltro dev` does on save).
268
+
269
+ ## Where to read next
270
+
271
+ - [Layouts & route groups](/docs/routing/layouts) — wrap pages in shared chrome
272
+ - [Loaders & meta](/docs/routing/loaders-and-meta) — server-side data + `<head>` tags
273
+ - [Render modes](/docs/routing/render-modes) — `static` / `ssr` / `isr`
274
+
275
+
276
+
277
+ ---
278
+
279
+ <!-- source: en/routing/layouts.md -->
280
+ ## Layouts & route groups
281
+
282
+ _layout.tsx, error.tsx, loading.tsx, not-found.tsx, route groups (group)/, and how the chain composes._
283
+
284
+ A **layout** wraps every page in its subtree. Drop a `layout.tsx` in a directory and every page below it gets wrapped — outer layouts compose around inner ones automatically.
285
+
286
+ The same pattern handles error boundaries (`error.tsx`), pending UI (`loading.tsx`), and 404 fallbacks (`not-found.tsx`). These are the four **special files**; discovery treats them differently from regular pages.
287
+
288
+ ## Root layout
289
+
290
+ ```tsx
291
+ // src/pages/layout.tsx — wraps EVERY page
292
+ import type { ReactNode } from 'react'
293
+ import './globals.css'
294
+
295
+ export default function Layout({ children }: { readonly children: ReactNode }): ReactNode {
296
+ return (
297
+ <div className="min-h-screen bg-background text-foreground">
298
+ <header>{/* topbar */}</header>
299
+ <main>{children}</main>
300
+ <footer>{/* footer */}</footer>
301
+ </div>
302
+ )
303
+ }
304
+ ```
305
+
306
+ Conventions:
307
+
308
+ - **Don't render `<html>`/`<head>`/`<body>`.** The framework's `index.html` shell owns those. Rendering them inside React puts them under `#root` + the browser unwraps them, breaking the document structure.
309
+ - **Set `<html class>` via `theme:` in `app.config.ts`.** Bakes the dark/light class into the shell before first paint.
310
+ - **Set `<title>` + meta via the page's `meta` export.** See [Loaders & meta](/docs/routing/loaders-and-meta).
311
+
312
+ ## Nested layouts
313
+
314
+ ```text
315
+ src/pages/
316
+ ├── layout.tsx # outer (every page)
317
+ ├── about.tsx # /about → wrapped in outer layout
318
+ └── dashboard/
319
+ ├── layout.tsx # nested (only /dashboard/*)
320
+ ├── index.tsx # /dashboard
321
+ └── settings.tsx # /dashboard/settings
322
+ ```
323
+
324
+ For `/dashboard/settings`, the React tree is:
325
+
326
+ ```text
327
+ <OuterLayout>
328
+ <DashboardLayout>
329
+ <Settings />
330
+ </DashboardLayout>
331
+ </OuterLayout>
332
+ ```
333
+
334
+ Outer layouts compose around inner ones. Each layout's state survives navigation **within** its scope — moving from `/dashboard` to `/dashboard/settings` doesn't unmount `DashboardLayout`.
335
+
336
+ ## Route groups
337
+
338
+ A directory in `(parentheses)` does NOT contribute a URL segment, but its layout still applies. Useful when you want a layout for a logical group of pages without nesting their URLs.
339
+
340
+ ```text
341
+ src/pages/
342
+ ├── (marketing)/
343
+ │ ├── layout.tsx # marketing-scoped chrome
344
+ │ ├── index.tsx # /
345
+ │ ├── pricing.tsx # /pricing
346
+ │ └── about.tsx # /about
347
+ └── (app)/
348
+ ├── layout.tsx # authenticated app chrome
349
+ ├── dashboard.tsx # /dashboard
350
+ └── settings.tsx # /settings
351
+ ```
352
+
353
+ Marketing pages get one layout; authenticated app pages get another; the URLs stay flat.
354
+
355
+ Use this when:
356
+
357
+ - The marketing landing + product app share root URL paths but have completely different chrome.
358
+ - You want layout state to NOT persist across logical sections (moving from `/about` to `/dashboard` unmounts everything).
359
+
360
+ ## Error boundaries
361
+
362
+ ```tsx
363
+ // src/pages/error.tsx — catches errors from any page below
364
+ import type { ReactNode } from 'react'
365
+
366
+ interface ErrorProps {
367
+ readonly error: Error
368
+ readonly reset: () => void
369
+ }
370
+
371
+ export default function ErrorPage({ error, reset }: ErrorProps): ReactNode {
372
+ return (
373
+ <div className="text-center py-12">
374
+ <h1 className="text-2xl font-bold mb-2">Something went wrong</h1>
375
+ <p className="text-muted-foreground mb-6">{error.message}</p>
376
+ <button onClick={reset}>Try again</button>
377
+ </div>
378
+ )
379
+ }
380
+ ```
381
+
382
+ - Catches errors from page renders + loaders + any descendant's React tree.
383
+ - `reset()` re-renders the boundary — call after fixing whatever caused the throw.
384
+ - Scoped: `src/pages/dashboard/error.tsx` only catches errors from `/dashboard/*`.
385
+
386
+ ## Pending UI (opt-in)
387
+
388
+ Navigation is **deferred** by default: clicking a link keeps the CURRENT page on screen until the target route's `loader`s settle, then swaps. There is **no full-screen loading overlay**. Background progress shows in the devtools button (dev) or a minimal corner indicator (production) — both read the same status bus.
389
+
390
+ `loading.tsx` is an **opt-in** override for one route: export it (or a page-level `Pending`) ONLY when you want that route to swap in immediately and show a skeleton instead of holding the previous page.
391
+
392
+ ```tsx
393
+ // src/pages/dashboard/loading.tsx — opt-in skeleton for /dashboard/*
394
+ export default function Loading(): ReactNode {
395
+ return <div className="animate-pulse">Loading…</div>
396
+ }
397
+ ```
398
+
399
+ `loading.tsx` is driven by in-flight loaders, not by subscriptions — a `useSubscription` returns `data: undefined` until its first snapshot rather than suspending. Render that empty state inside the page itself.
400
+
401
+ Scoped like `error.tsx`.
402
+
403
+ ## Not-found
404
+
405
+ ```tsx
406
+ // src/pages/not-found.tsx — 404 fallback
407
+ export default function NotFound(): ReactNode {
408
+ return (
409
+ <div className="text-center py-12">
410
+ <h1>Page not found</h1>
411
+ <a href="/">← Home</a>
412
+ </div>
413
+ )
414
+ }
415
+ ```
416
+
417
+ Scoped: `src/pages/dashboard/not-found.tsx` catches 404s only for URLs starting with `/dashboard/`. Useful for tenant-specific 404 messaging.
418
+
419
+ ## The default fallback chrome (and localizing it)
420
+
421
+ Until you supply your own `error.tsx` / `not-found.tsx`, the router renders a built-in diagnostic chrome — a runtime-error card (error name, stack, Retry / Reload / Copy) and a 404 card. It's a dev-diagnostic surface, not app UI, so it ships full detail regardless of environment. Two ways to change it:
422
+
423
+ - **Replace it** — export your own `error.tsx` / `not-found.tsx` (above), or pass `errorFallback` / `notFound` components to the `<Router>`. Total control.
424
+ - **Just relabel it** — wrap the app in `<FallbackStringsProvider>` to override the built-in chrome's English strings without rebuilding the components (e.g. to localize "Retry" / "Reload page" / "No page for this URL"):
425
+
426
+ ```tsx
427
+ import { FallbackStringsProvider } from '@voltro/web'
428
+
429
+ <FallbackStringsProvider
430
+ strings={{
431
+ error: { retry: 'Wiederholen', reload: 'Seite neu laden', copy: 'Fehler kopieren' },
432
+ notFound: { heading: 'Keine Seite für diese URL' },
433
+ }}
434
+ >
435
+ {/* your app */}
436
+ </FallbackStringsProvider>
437
+ ```
438
+
439
+ Overrides deep-merge onto the English defaults — supply only the keys you change; the rest stay English. A per-component `strings` prop on `DefaultErrorFallback` / `DefaultNotFound` wins over the provider for a one-off. (Mirrors `<UiStringsProvider>` for the `@voltro/ui` kit.)
440
+
441
+ ## How the framework composes them
442
+
443
+ For a request to `/dashboard/settings`, the framework walks the page tree:
444
+
445
+ 1. Find the matching leaf — `dashboard/settings.tsx`.
446
+ 2. Walk up the directory tree, collecting every directory's special files in order.
447
+ 3. Build the chain: outermost layout → next layout → … → leaf page.
448
+ 4. Wrap each layer's `error.tsx` as a React Error Boundary around the next layer.
449
+ 5. Render.
450
+
451
+ The generated `.framework/app.tsx` records this chain explicitly per route — you can `cat` it to see the result.
452
+
453
+ ## Limits
454
+
455
+ - **One `layout.tsx` per directory.** Multiple would be ambiguous.
456
+ - **Layouts can't be async functions.** Use a `loader` for data + read it via `useLoaderData`.
457
+ - **Error boundaries don't catch loader errors.** Loader errors render the page's `error.tsx`; throwing inside the page's render does too. Both flow through the same boundary.
458
+ - **Layouts can call `useLocation()`, `useParams()`, `useServerRequest()`.** They're React components like any other.
459
+
460
+ ## Where to read next
461
+
462
+ - [Render modes](/docs/routing/render-modes) — how `static` / `ssr` / `isr` interact with layouts
463
+ - [Loaders & meta](/docs/routing/loaders-and-meta) — server-side data + page-level `<head>` tags
464
+
465
+
466
+
467
+ ---
468
+
469
+ <!-- source: en/routing/render-modes.md -->
470
+ ## Render modes
471
+
472
+ _static (SSG) vs ssr vs isr — when each runs, what it caches, and how to pick._
473
+
474
+ Every page declares a `renderMode`. The mode controls **when** the HTML is produced — at build time, on every request, or build-once-revalidate-occasionally.
475
+
476
+ ```tsx
477
+ export const renderMode = 'static' as const // 'static' | 'ssr' | 'isr'
478
+ ```
479
+
480
+ ## The three modes
481
+
482
+ | Mode | When HTML is produced | Cached? | Best for |
483
+ |---|---|---|---|
484
+ | `static` *(default)* | `voltro build` time | Forever | Marketing pages, docs, anything that doesn't change per-request |
485
+ | `ssr` | Every request | Never | Authenticated dashboards, search results, anything cookie-driven |
486
+ | `isr` | First request after build, then on revalidate | Per-key in-memory or Postgres | News feeds, listings, dashboards that change but not per-user |
487
+
488
+ ## static (SSG)
489
+
490
+ ```tsx
491
+ // src/pages/about.tsx
492
+ export const renderMode = 'static' as const
493
+ ```
494
+
495
+ At `voltro build`:
496
+
497
+ 1. The framework runs the page's render once (with `useServerRequest()` returning `null`).
498
+ 2. The output HTML lands at `dist/about/index.html`.
499
+ 3. `voltro start` serves the file directly — no React runs on the server.
500
+
501
+ For dynamic patterns, export `getStaticPaths` to enumerate every URL to pre-render:
502
+
503
+ ```tsx
504
+ // src/pages/blog/[slug].tsx
505
+ export const renderMode = 'static' as const
506
+
507
+ export const getStaticPaths = async () => [
508
+ { params: { slug: 'first-post' } },
509
+ { params: { slug: 'second-post' } },
510
+ ]
511
+ ```
512
+
513
+ One HTML file per entry lands in `dist/blog/first-post/index.html` etc.
514
+
515
+ Pages that don't enumerate (dynamic without `getStaticPaths`) fall through to the SPA shell — the client-side router takes over.
516
+
517
+ ## ssr
518
+
519
+ ```tsx
520
+ export const renderMode = 'ssr' as const
521
+ ```
522
+
523
+ On every request:
524
+
525
+ 1. `voltro start` matches the URL → finds your page.
526
+ 2. Calls the loader (if any) with the request's params + headers + cookies.
527
+ 3. Renders the React tree to HTML.
528
+ 4. Returns it.
529
+
530
+ Use SSR for:
531
+
532
+ - **Authenticated pages** that read the session cookie via `useServerRequest()`.
533
+ - **Personalised content** — recommendations, "your" anything.
534
+ - **Search result pages** — the query string changes per request.
535
+
536
+ Cost: every request triggers a fresh render. For very high-traffic pages, prefer ISR.
537
+
538
+ ### Streaming SSR
539
+
540
+ By default an `ssr` page renders with `renderToString` — the whole document is built in memory, then sent. The server-side SSR helpers in `@voltro/web/ssr` also expose a **streaming** path, `renderPageToStream`, built on React's `renderToPipeableStream`. It flushes the shell (and each `<Suspense>` boundary as it resolves) instead of buffering the whole page, so the browser gets the first bytes sooner:
541
+
542
+ ```ts
543
+ import { renderPageToStream } from '@voltro/web/ssr'
544
+
545
+ const { stream, meta } = renderPageToStream({
546
+ descriptor,
547
+ params,
548
+ pathname,
549
+ loaderData,
550
+ bootstrapModules: ['/assets/client.js'],
551
+ onShellReady: () => {
552
+ // meta is resolved synchronously — write <head> before piping the body.
553
+ res.setHeader('content-type', 'text/html')
554
+ res.write(`<!doctype html><html><head>${renderMetaToHtml(meta)}</head><body><div id="root">`)
555
+ stream.pipe(res)
556
+ },
557
+ onAllReady: () => res.end('</div></body></html>'),
558
+ onShellError: (err) => { res.statusCode = 500; res.end('Internal error') },
559
+ })
560
+ ```
561
+
562
+ `renderPageToStream` takes the same options as `renderPageToHtml` plus the stream callbacks (`onShellReady` / `onAllReady` / `onShellError`) and `bootstrapModules`. Both paths compose the same providers and resolve `meta` synchronously, so the sync string render and the streaming render produce the same tree — the difference is time-to-first-byte and native Suspense streaming. Most apps never call these directly; the `voltro start` pipeline uses them under the hood.
563
+
564
+ ## isr (incremental static regeneration)
565
+
566
+ ```tsx
567
+ export const renderMode = 'isr' as const
568
+ export const revalidate = '60 seconds' // re-render when older than this
569
+ ```
570
+
571
+ A bare number is read as **seconds** (Next.js compat) — `revalidate = 60` means 60 seconds, NOT milliseconds. Use the string form (`'60 seconds'`, `'5 minutes'`, `'1 hour'`) for clarity.
572
+
573
+ Behaviour:
574
+
575
+ | Request | Action |
576
+ |---|---|
577
+ | First | MISS — render, store in cache, serve. |
578
+ | Subsequent (cache fresh) | HIT — serve from cache. |
579
+ | After revalidate window | MISS — re-render, store, serve. |
580
+ | With `staleWhileRevalidate` | STALE — serve cached HTML immediately, kick off background refresh. |
581
+
582
+ ```tsx
583
+ export const renderMode = 'isr' as const
584
+ export const revalidate = '60 seconds'
585
+ export const staleWhileRevalidate = '60 seconds' // serve stale while refreshing in bg
586
+ ```
587
+
588
+ Cache backends:
589
+
590
+ - `memory` *(default)* — in-process, doesn't survive restarts.
591
+ - `postgres` — `SSR_CACHE=postgres`. Survives restarts, shared across api instances.
592
+
593
+ ## Tenant-aware ISR
594
+
595
+ For multi-tenant ISR (each tenant gets its own cache entry):
596
+
597
+ ```tsx
598
+ export const renderMode = 'isr' as const
599
+ export const revalidate = '60 seconds'
600
+ export const tenantAware = true
601
+ ```
602
+
603
+ The cache key becomes `${pathname}|tenant=${tenant}`, where `tenant` is the request's `x-tenant` header (falling back to `anonymous`). Tenant A's cached HTML never serves to tenant B.
604
+
605
+ ## CDC-invalidated ISR
606
+
607
+ When a specific DB write should invalidate the cache (instead of waiting for the revalidate window):
608
+
609
+ ```tsx
610
+ export const renderMode = 'isr' as const
611
+ export const cacheInvalidatesOn = ['posts', 'comments'] // tables to watch
612
+ ```
613
+
614
+ The framework reads Postgres logical replication; writes to `posts` or `comments` invalidate every cached HTML for this query. New requests rebuild the page from the current data.
615
+
616
+ Requires `SSR_CACHE=postgres` and a `wal_level=logical` Postgres.
617
+
618
+ ## Picking between them
619
+
620
+ | You have… | Use |
621
+ |---|---|
622
+ | A truly static page (marketing copy) | `static` |
623
+ | A list of known pre-publishable URLs (blog posts) | `static` + `getStaticPaths` |
624
+ | A page that changes per user (dashboard, account) | `ssr` |
625
+ | A search results page (URL query → result) | `ssr` |
626
+ | A blog index that changes when posts are added | `isr` + `cacheInvalidatesOn: ['posts']` |
627
+ | A multi-tenant marketing site (`acme.com/[tenant]/pricing`) | `isr` + `tenantAware: true` |
628
+ | A status page with 30s-stale acceptable | `isr` + `revalidate = '30 seconds'` |
629
+
630
+ ## What about `interactive`?
631
+
632
+ `interactive` is **orthogonal** to `renderMode` — it controls how much JS runs in the browser. See [Islands](/docs/routing/islands).
633
+
634
+ | `interactive` | What's hydrated |
635
+ |---|---|
636
+ | `'none'` | Nothing — pure HTML. |
637
+ | `'islands'` | Only `*.island.tsx` files. |
638
+ | `'full'` *(default)* | Whole page. |
639
+
640
+ Combinations:
641
+
642
+ | `renderMode` × `interactive` | When |
643
+ |---|---|
644
+ | `static` + `none` | Marketing pages, blog posts. Zero JS. |
645
+ | `static` + `full` | SSG with full client-side nav. Docs sites. |
646
+ | `ssr` + `full` | Dashboards. The most "Next.js-like" mode. |
647
+ | `isr` + `islands` | News feeds with a "like" button island. |
648
+
649
+ ## What gets served when
650
+
651
+ A request to `/foo`:
652
+
653
+ 1. **Pre-rendered HTML exists at `dist/foo/index.html`?** Serve it. (static + isr-already-cached.)
654
+ 2. **No pre-render, page is `ssr`?** Render fresh, serve.
655
+ 3. **No pre-render, page is `isr`?** Cache lookup → MISS → render → store → serve.
656
+ 4. **No pre-render, page is `static`?** Serve the SPA shell — the client router takes over.
657
+
658
+ That last case is how dynamic `static` routes work in dev / when `getStaticPaths` didn't include the URL.
659
+
660
+ ## What doesn't work
661
+
662
+ - **Switching `renderMode` per request.** It's a static module export — one value per build.
663
+ - **`renderMode: 'ssr'` without `voltro start`.** SSR requires the production server. Dev mode runs everything client-side regardless.
664
+ - **`isr` with `cacheInvalidatesOn` against memory cache.** Memory cache is per-process; CDC events fire across processes. Use `SSR_CACHE=postgres`.
665
+
666
+ ## Where to read next
667
+
668
+ - [Loaders & meta](/docs/routing/loaders-and-meta) — fetch data before render, inject `<head>` tags
669
+ - [Islands](/docs/routing/islands) — pages that ship 0 JS except for explicit islands
670
+
671
+
672
+
673
+ ---
674
+
675
+ <!-- source: en/routing/loaders-and-meta.md -->
676
+ ## Loaders & meta
677
+
678
+ _Server-side data fetch via `loader`, page-level `<head>` tags via `meta`, and how the build pipeline runs both._
679
+
680
+ A **loader** is the page's server-side data hook. It runs before the React render (during SSR, during SSG, or per-request for ISR/SSR), and its result lands in `useLoaderData<T>()`. **Meta** is a sibling export that produces `<title>` + `<meta>` tags.
681
+
682
+ Both are static module exports — the framework discovers them, the build pipeline runs them.
683
+
684
+ ## A loader
685
+
686
+ ```tsx
687
+ // src/pages/notes/[id].tsx
688
+ import { useLoaderData } from '@voltro/web'
689
+
690
+ interface Note {
691
+ readonly id: string
692
+ readonly title: string
693
+ readonly body: string
694
+ }
695
+
696
+ export const renderMode = 'ssr' as const
697
+
698
+ export const loader = async ({ params, headers }: {
699
+ params: { id: string }
700
+ headers: Readonly<Record<string, string>>
701
+ }): Promise<Note> => {
702
+ // Server-side fetch — runs on the Node side, never in the browser.
703
+ const res = await fetch(`${INTERNAL_API}/notes/${params.id}`, {
704
+ headers: { cookie: headers.cookie ?? '' },
705
+ })
706
+ if (!res.ok) throw new Error(`note ${params.id}: ${res.status}`)
707
+ return await res.json()
708
+ }
709
+
710
+ export default function NotePage(): ReactNode {
711
+ const note = useLoaderData<Note>()
712
+ return (
713
+ <article>
714
+ <h1>{note.title}</h1>
715
+ <p>{note.body}</p>
716
+ </article>
717
+ )
718
+ }
719
+ ```
720
+
721
+ `useLoaderData<T>()` returns the loader's resolved value, typed via the generic.
722
+
723
+ ## When loaders run
724
+
725
+ | renderMode | When loader runs |
726
+ |---|---|
727
+ | `static` | At `voltro build` time, once. Result baked into HTML. |
728
+ | `ssr` | Every request. |
729
+ | `isr` | On cache MISS (re-runs when cache stale). |
730
+
731
+ For `static` pages with `getStaticPaths`, the loader runs once per enumerated path.
732
+
733
+ ## Loader arguments
734
+
735
+ ```ts
736
+ export const loader = async (ctx: {
737
+ readonly params: Readonly<Record<string, string>> // URL params from [name] segments
738
+ readonly pathname: string // matched path (no query string)
739
+ readonly signal: AbortSignal // Aborts if the client disconnects mid-render
740
+ readonly headers?: Readonly<Record<string, string>> // Request headers (SSR/ISR only — empty for SSG/client)
741
+ // Call the backend rpc directly — present ONLY when the loader runs
742
+ // server-side (`voltro start` / `voltro dev` SSR); `undefined`
743
+ // client-side. Resolves a query's FIRST (initial) snapshot.
744
+ readonly query?: <T = unknown>(tag: string, input?: Record<string, unknown>) => Promise<T>
745
+ }) => Promise<unknown>
746
+ ```
747
+
748
+ The loader context carries `pathname`, not a `request` object. For the query string during SSR, read it from `useServerRequest().url` inside the component.
749
+
750
+ Use `signal` for any fetch that could outlive the request — pass it to `fetch(url, { signal })` so cancelled requests don't waste CPU.
751
+
752
+ ## Fetching backend data with `ctx.query`
753
+
754
+ 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:
755
+
756
+ ```tsx
757
+ // src/pages/notes/[id].tsx
758
+ import { useSubscription } from '@voltro/client'
759
+ import { useLoaderData, type PageMeta } from '@voltro/web'
760
+
761
+ interface Note { readonly id: string; readonly title: string; readonly body: string }
762
+
763
+ export const renderMode = 'ssr' as const
764
+
765
+ export const loader = async ({ params, query }: {
766
+ params: { id: string }
767
+ query?: <T>(tag: string, input?: Record<string, unknown>) => Promise<T>
768
+ }) => {
769
+ // `query` is undefined client-side — guard it. SSR forwards the
770
+ // request's cookie, so the api resolves the SAME Subject + tenant
771
+ // as the WebSocket path.
772
+ const note = query ? await query<Note>('notes.get', { id: params.id }) : undefined
773
+ return { note }
774
+ }
775
+
776
+ export const meta = ({ loaderData }: { loaderData: { note?: Note } }): PageMeta => ({
777
+ title: loaderData.note ? `${loaderData.note.title} — Notes` : 'Notes',
778
+ description: loaderData.note?.body.slice(0, 140) ?? '',
779
+ })
780
+
781
+ export default function NotePage() {
782
+ const { note: ssrNote } = useLoaderData<{ note?: Note }>()
783
+ // Live updates after hydration: useSubscription takes over from the
784
+ // SSR snapshot. The loader gave us first-paint HTML + correct meta;
785
+ // the subscription keeps it fresh.
786
+ const { data } = useSubscription<Note>('app', 'notes.get', { id: ssrNote?.id ?? '' }, { skip: !ssrNote })
787
+ const note = data ?? ssrNote
788
+ if (!note) return null
789
+ return <article><h1>{note.title}</h1><p>{note.body}</p></article>
790
+ }
791
+ ```
792
+
793
+ Two rules that fall out of this:
794
+
795
+ - **`query` is server-only.** It's `undefined` for client-side loader invocations (SPA navigation re-runs the loader in the browser). Guard it (`query ? … : undefined`) and use `useSubscription` in the component for the reactive, after-hydration path. The loader's `query` is for SSR first-paint + `meta`.
796
+ - **It forwards the request cookie.** The HTTP rpc resolves the same Subject + tenant as the WebSocket connection would, so tenant-scoped queries return the right rows during SSR.
797
+
798
+ Under the hood, `ctx.query` is a one-shot `POST /rpc` call (see [Wire protocol](/docs/data/wire-protocol#http-one-shot-rpc-post-rpc)).
799
+
800
+ ## Errors from loaders
801
+
802
+ If the loader throws, the framework:
803
+
804
+ 1. Catches the throw.
805
+ 2. Renders the page's `error.tsx` (or the nearest ancestor's) with the error.
806
+ 3. Serves the resulting HTML.
807
+
808
+ For 404s, throw a `NotFoundError`:
809
+
810
+ ```ts
811
+ import { NotFoundError } from '@voltro/web'
812
+
813
+ export const loader = async ({ params, query }) => {
814
+ const note = query ? await query('notes.get', { id: params.id }) : undefined
815
+ if (!note) throw new NotFoundError(`note ${params.id}`)
816
+ return note
817
+ }
818
+ ```
819
+
820
+ The framework returns a 404 status + renders `not-found.tsx` for that subtree. The `notFound()` helper is throwing sugar for the same thing — `const note = (await load()) ?? notFound('note ' + params.id)` reads well when the not-found is inline.
821
+
822
+ ## Meta
823
+
824
+ ```tsx
825
+ import type { PageMeta } from '@voltro/web'
826
+
827
+ export const meta: PageMeta = {
828
+ title: 'Notes — Voltro',
829
+ description: 'All your notes, in one place.',
830
+ tags: [
831
+ { property: 'og:title', content: 'Voltro Notes' },
832
+ { property: 'og:description', content: 'All your notes, in one place.' },
833
+ { property: 'og:image', content: '/og.svg' },
834
+ { name: 'twitter:card', content: 'summary_large_image' },
835
+ ],
836
+ }
837
+ ```
838
+
839
+ The framework injects these into the HTML's `<head>` at build / SSR time:
840
+
841
+ ```html
842
+ <head>
843
+ <title>Notes — Voltro</title>
844
+ <meta name="description" content="All your notes, in one place." />
845
+ <meta property="og:title" content="Voltro Notes" />
846
+ <meta property="og:image" content="/og.svg" />
847
+
848
+ </head>
849
+ ```
850
+
851
+ ### The default document title
852
+
853
+ A page's `meta.title` overrides the tab title on navigation. Before any page
854
+ sets one — the initial HTML shell, a route with no `meta`, an error page — the
855
+ browser tab shows the app's **default title**, set in `app.config.ts`:
856
+
857
+ ```ts
858
+ export default {
859
+ type: 'web' as const,
860
+ name: 'AcmeDashboard', // internal identifier (package/port lookup) — PascalCase by convention
861
+ title: 'Acme', // human document title baked into the HTML shell
862
+ }
863
+ ```
864
+
865
+ `title` is the default `<title>`. It is distinct from `name`, the app's internal
866
+ identifier — leaking that PascalCase identifier into the tab reads as a dev
867
+ artefact. When `title` is unset the shell falls back to `name`, so set a real
868
+ product title on any app users actually see. Per-page `meta.title` still wins
869
+ wherever a page provides one.
870
+
871
+ ## Dynamic meta from params + loader data + locale
872
+
873
+ When the meta depends on the URL or on what the loader fetched, export `meta` as a function. It receives a single object `{ params, loaderData, locale }` and runs at build / SSR time after the loader resolves:
874
+
875
+ ```tsx
876
+ export const meta = ({ params }: { params: { id: string } }): PageMeta => ({
877
+ title: `Note ${params.id} — Voltro`,
878
+ description: '…',
879
+ })
880
+ ```
881
+
882
+ Reading the loader's result lets the title/description reflect fetched fields — the canonical "page title is the note's title" case:
883
+
884
+ ```tsx
885
+ export const loader = async ({ params, query }) => ({
886
+ note: query ? await query('notes.get', { id: params.id }) : undefined,
887
+ })
888
+
889
+ export const meta = ({ loaderData }: { loaderData: { note?: { title: string; body: string } } }): PageMeta => ({
890
+ title: loaderData.note ? `${loaderData.note.title} — Voltro` : 'Voltro',
891
+ description: loaderData.note?.body.slice(0, 140) ?? '',
892
+ })
893
+ ```
894
+
895
+ The third context field — `locale: string` — is the active i18n locale for this render. For `[locale]/…` routes it carries the URL-prefix locale (`'de'` on `/de/notes/42`). For bare-path routes it carries the active locale from the framework's `voltro:lang` cookie when set — so **cookie-based i18n works too** (an authed dashboard with no `[locale]` URL still gets a translated `<title>` that tracks the language switch) — otherwise the app's `defaultLocale`. Use it to localise title / description / canonical / OG per locale at SSG time so search engines see translated head tags on every variant, and to give cookie-i18n pages a translated tab title:
896
+
897
+ ```tsx
898
+ import { getCatalog } from '../lib/locale'
899
+ import { localeCanonicalUrl } from '../lib/seo'
900
+
901
+ export const meta = ({ locale }: { locale: string }): PageMeta => {
902
+ const c = getCatalog(locale)
903
+ return {
904
+ title: c['seo.notes.title'] as string,
905
+ description: c['seo.notes.description'] as string,
906
+ canonical: localeCanonicalUrl('/notes', locale),
907
+ }
908
+ }
909
+ ```
910
+
911
+ `meta(ctx)` runs once per (page × locale) at build time. The full per-locale SSG flow — `[locale]/…` mirror routes, the build-time `<I18nProvider>` wrap, and the dist layout — is documented in [Internationalization → URL strategies](/docs/i18n/url-strategies).
912
+
913
+ `loaderData` is the PAGE loader's result. Because `meta` runs server-side after the loader, the SSR'd `<head>` is already correct on first paint — no client-side title patching, no flash.
914
+
915
+ ## Examples
916
+
917
+ ### Authenticated dashboard with cookie-driven loader
918
+
919
+ ```tsx
920
+ // src/pages/dashboard/index.tsx
921
+ import { useLoaderData } from '@voltro/web'
922
+
923
+ export const renderMode = 'ssr' as const
924
+
925
+ export const loader = async ({ headers }) => {
926
+ const cookieHeader = headers.cookie ?? ''
927
+ const me = await fetch(`${INTERNAL_API}/auth/me`, { headers: { cookie: cookieHeader } })
928
+ if (!me.ok) throw new RedirectError('/login')
929
+ const user = await me.json()
930
+ return { user }
931
+ }
932
+
933
+ export default function Dashboard() {
934
+ const { user } = useLoaderData<{ user: User }>()
935
+ return <h1>Hi {user.name}</h1>
936
+ }
937
+ ```
938
+
939
+ `RedirectError` is the framework's way to issue a 303 from a loader. See [Navigation](/docs/routing/navigation) for client-side analog.
940
+
941
+ ### SSG with per-post meta
942
+
943
+ `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):
944
+
945
+ ```tsx
946
+ // src/pages/blog/[slug].tsx
947
+ import { listPostSlugs, type Post } from '../../content/posts'
948
+
949
+ export const renderMode = 'static' as const
950
+
951
+ export const getStaticPaths = async () => {
952
+ const slugs = await listPostSlugs() // your own content source — fs / CMS / API
953
+ return slugs.map((slug) => ({ params: { slug } }))
954
+ }
955
+
956
+ export const loader = async ({ params, query }) => {
957
+ // `query` is present only server-side (SSG build / SSR). Resolves the
958
+ // backend query's first snapshot.
959
+ return { post: await query!('posts.getBySlug', { slug: params.slug }) }
960
+ }
961
+
962
+ export const meta = ({ loaderData }: { loaderData: { post: Post } }): PageMeta => ({
963
+ title: `${loaderData.post.title} — Blog`,
964
+ description: loaderData.post.excerpt,
965
+ })
966
+ ```
967
+
968
+ ## Loaders are NOT React hooks
969
+
970
+ They're plain async functions. They can't call `useSubscription`, `useState`, etc. — they run server-side.
971
+
972
+ If you need a reactive query (live updates), use `useSubscription` in the component AFTER hydration; for the initial render's data, use the loader.
973
+
974
+ ## Anti-patterns
975
+
976
+ - **Calling `ctx.ai.generate(...)` in a loader without timeouts.** Loaders shouldn't take >2s. For slow data, render a Suspense fallback + `useSubscription` after hydration.
977
+ - **Loaders that mutate state.** Loaders are reads — they're cached, retried, run at build time. Use mutations for writes.
978
+ - **Hardcoding env-only secrets in `meta` tags.** `meta` ships to the client. Public meta only.
979
+
980
+ ## Where to read next
981
+
982
+ - [Navigation](/docs/routing/navigation) — client-side routing, prefetch, Link
983
+ - [Render modes](/docs/routing/render-modes) — which mode means what for loaders
984
+
985
+
986
+
987
+ ---
988
+
989
+ <!-- source: en/routing/navigation.md -->
990
+ ## Navigation
991
+
992
+ _Link, useNavigate, prefetch on hover, programmatic redirects, and the external/hash escape hatches._
993
+
994
+ Voltro's router is client-side after first paint. Links update the URL via `history.pushState` + re-render the matching page, without a full reload. Loader data prefetches on hover so the next page is ready by the time the user clicks.
995
+
996
+ ## Typed URLs — the `routes` builder
997
+
998
+ `<Link to=…>` does not take a bare string. Its `to` prop is a **branded
999
+ `VoltroUrl`**, minted only by the app's generated `routes` builder or by
1000
+ `externalUrl()`. This makes a typo or a link to a route that doesn't exist
1001
+ a compile error instead of a dead link at runtime.
1002
+
1003
+ The codegen writes a `routes` builder from your `src/pages/**` tree. Call
1004
+ the entry for a pattern with its params to get a typed URL:
1005
+
1006
+ ```tsx
1007
+ import { Link } from '@voltro/web'
1008
+ import { routes } from './.framework/routes' // generated by `voltro dev`
1009
+
1010
+ <Link to={routes['/notes/[id]']({ id: '42' })}>Open note 42</Link>
1011
+ ```
1012
+
1013
+ - `routes['/pattern'](params)` → `VoltroRouteUrl`. Missing/extra params are a type error.
1014
+ - `withQuery(url, { env: 'prod' })` — append a query string, keeps the brand.
1015
+ - `withHash(url, 'section-3')` — append a `#hash`, keeps the brand.
1016
+ - `externalUrl('https://example.com')` — the escape hatch for anything the
1017
+ codegen can't model: cross-origin, `mailto:`, `tel:`, hash-only, or a
1018
+ sibling-app route. A deliberate no-op wrapper so any raw string still has
1019
+ to be opted in at the call site.
1020
+
1021
+ ```tsx
1022
+ import { withQuery, withHash, externalUrl } from '@voltro/web'
1023
+
1024
+ <Link to={withQuery(routes['/notes/[id]']({ id: '42' }), { tab: 'comments' })}>Comments</Link>
1025
+ <Link to={withHash(routes['/docs/[[...slug]]']({ slug: ['routing'] }), 'priority')}>Priority</Link>
1026
+ <Link to={externalUrl('mailto:hi@x.com')}>Email us</Link>
1027
+ ```
1028
+
1029
+ ## `<Link>`
1030
+
1031
+ ```tsx
1032
+ import { Link } from '@voltro/web'
1033
+ import { routes } from './.framework/routes'
1034
+
1035
+ <Link to={routes['/notes/[id]']({ id: '42' })}>Open note 42</Link>
1036
+ ```
1037
+
1038
+ What it does:
1039
+
1040
+ - Renders an `<a href="/notes/42">` so the link is a real anchor (SEO, right-click → "Open in new tab", screen readers, etc. all just work).
1041
+ - Intercepts plain left-clicks → `history.pushState` + matches the new URL.
1042
+ - Modifier keys + middle-click + external URLs pass through to the browser's native behaviour.
1043
+
1044
+ ## Prefetch on hover
1045
+
1046
+ ```tsx
1047
+ <Link to={routes['/notes/[id]']({ id: '42' })} prefetch>Open note 42</Link>
1048
+ ```
1049
+
1050
+ With `prefetch`, hovering / focusing the link fires the destination's loader in the background. By the time the user actually clicks, `useLoaderData()` resolves immediately on the new page.
1051
+
1052
+ Behaviour:
1053
+
1054
+ - Idempotent — multiple hovers fire one loader call, results are cached.
1055
+ - Cached until used or invalidated — a prefetched result stays in the loader cache and is consumed on the next navigation to that route; it isn't discarded on a timer. It's dropped when the route is invalidated (e.g. an error-boundary reset or a mutation that invalidates the loader's data).
1056
+ - No effect for `static` pages without loaders (nothing to prefetch).
1057
+
1058
+ For "everything on the page is prefetchable", apps usually wire `prefetch` on every internal link by default. Not much downside — loaders are cheap; the wasted ones are typically empty.
1059
+
1060
+ ## `useNavigate`
1061
+
1062
+ For programmatic navigation:
1063
+
1064
+ ```tsx
1065
+ import { useNavigate } from '@voltro/web'
1066
+
1067
+ const SignOutButton = () => {
1068
+ const navigate = useNavigate()
1069
+ const onSignOut = async () => {
1070
+ await fetch('/auth/signout', { method: 'POST' })
1071
+ navigate('/login')
1072
+ }
1073
+ return <button onClick={onSignOut}>Sign out</button>
1074
+ }
1075
+ ```
1076
+
1077
+ Pass a path string. The router updates `window.location.pathname` + renders the new page.
1078
+
1079
+ ## External + hash URLs
1080
+
1081
+ Wrap anything the codegen can't model in `externalUrl()`. `<Link>` detects URLs starting with a scheme (`http://`, `https://`, `mailto:`, `tel:`, `#anchor`) at runtime and falls back to plain browser navigation; a route URL from the `routes` builder does SPA navigation.
1082
+
1083
+ ```tsx
1084
+ <Link to={externalUrl('https://example.com')}>External</Link> {/* opens normally */}
1085
+ <Link to={externalUrl('mailto:hi@x.com')}>Email</Link> {/* mailto: handler */}
1086
+ <Link to={externalUrl('#section')}>Anchor</Link> {/* in-page scroll */}
1087
+ <Link to={routes['/dashboard']({})}>Internal</Link> {/* SPA nav */}
1088
+ ```
1089
+
1090
+ ## Active link styling
1091
+
1092
+ ```tsx
1093
+ import { Link, useLocation } from '@voltro/web'
1094
+
1095
+ const Nav = () => {
1096
+ const pathname = useLocation()
1097
+ return (
1098
+ <ul>
1099
+ <li><Link to={routes['/']({})} className={pathname === '/' ? 'active' : ''}>Home</Link></li>
1100
+ <li><Link to={routes['/about']({})} className={pathname === '/about' ? 'active' : ''}>About</Link></li>
1101
+ </ul>
1102
+ )
1103
+ }
1104
+ ```
1105
+
1106
+ For "active if URL starts with prefix" (parent nav highlighting):
1107
+
1108
+ ```tsx
1109
+ className={pathname.startsWith('/dashboard') ? 'active' : ''}
1110
+ ```
1111
+
1112
+ Compose this into your own `NavLink` wrapper with `useLocation()` + `<Link>` when you reuse the pattern across many links.
1113
+
1114
+ ## Redirects from a loader
1115
+
1116
+ When the loader detects "user should be elsewhere":
1117
+
1118
+ ```ts
1119
+ import { RedirectError } from '@voltro/web'
1120
+
1121
+ export const loader = async ({ headers }) => {
1122
+ if (!signedIn(headers)) throw new RedirectError('/login?from=/dashboard')
1123
+ return { /* … */ }
1124
+ }
1125
+ ```
1126
+
1127
+ The framework catches it + emits a 303 with `location: /login?from=/dashboard` on SSR; on a client navigation it runs `navigate(..., { replace: true })` so Back doesn't bounce onto the page that redirected. The default status is 303 (a redirect always lands the browser on a GET of the target); pass `{ status: 307 | 308 }` for a method-preserving redirect. The `redirect()` helper is throwing sugar — `if (!signedIn(headers)) redirect('/login')`.
1128
+
1129
+ For client-side redirects (e.g. after a button click):
1130
+
1131
+ ```tsx
1132
+ const onSubmit = async () => {
1133
+ await mutate.run({ /* … */ })
1134
+ navigate('/success')
1135
+ }
1136
+ ```
1137
+
1138
+ ## Scroll behaviour
1139
+
1140
+ By default, the router scrolls to the top on every push navigation. Override per-link:
1141
+
1142
+ ```tsx
1143
+ <Link to={withHash(routes['/long-page']({}), 'section-3')}>Jump to section 3</Link>
1144
+ ```
1145
+
1146
+ Hash links scroll to the matching `id`. Setting `<Link to={routes['/foo']({})} replace>` replaces the history entry (no back-button entry).
1147
+
1148
+ ### Back/forward scroll restoration
1149
+
1150
+ The router restores the previous scroll position on **back/forward** navigations. It sets `history.scrollRestoration = 'manual'` and owns restoration itself, saving each entry's scroll offset before you leave it and re-applying it (after the target route paints) when you pop back. This is automatic — no setup. Because the router restores after the loader-gated target paints, the offset lands on the right content even for a page that's still fetching when you click Back.
1151
+
1152
+ Push/replace navigations still scroll to top (or to the hash target); only back/forward restores.
1153
+
1154
+ ## Blocking navigation (unsaved changes)
1155
+
1156
+ `useBlocker` holds a pending navigation so you can prompt before the user leaves — the unsaved-changes guard.
1157
+
1158
+ ```tsx
1159
+ import { useBlocker } from '@voltro/web'
1160
+
1161
+ function EditForm() {
1162
+ const [dirty, setDirty] = useState(false)
1163
+ const blocker = useBlocker(dirty) // block while the form has unsaved edits
1164
+
1165
+ return (
1166
+ <form onChange={() => setDirty(true)}>
1167
+ {/* …fields… */}
1168
+ {blocker.blocked && (
1169
+ <div role="dialog">
1170
+ Discard unsaved changes?
1171
+ <button onClick={blocker.retry}>Discard &amp; leave</button>
1172
+ <button onClick={blocker.reset}>Stay</button>
1173
+ </div>
1174
+ )}
1175
+ </form>
1176
+ )
1177
+ }
1178
+ ```
1179
+
1180
+ When `useBlocker`'s argument is `true` (or a predicate returning `true`) and the user tries to leave — a `<Link>` click, an intercepted `<a>`, or an imperative `navigate` — the navigation is **held** and the hook returns `{ blocked: true, to, retry, reset }`:
1181
+
1182
+ - `retry()` — proceed with the held-back navigation.
1183
+ - `reset()` — cancel it and stay on the page.
1184
+ - `to` — where the user was trying to go (render it in the prompt if you like).
1185
+
1186
+ A full-page unload (tab close, reload, typed URL) additionally triggers the browser's native leave prompt while any blocker is active.
1187
+
1188
+ Pass a **predicate** to allow some destinations:
1189
+
1190
+ ```tsx
1191
+ // Block everything except an explicit sign-out.
1192
+ const blocker = useBlocker(({ to }) => dirty && to !== '/logout')
1193
+ ```
1194
+
1195
+ ## Route announcer (accessibility)
1196
+
1197
+ On a full page load a screen reader announces the new page. A client-side SPA navigation swaps the DOM without that announcement — so the router ships a built-in **route announcer**: a visually-hidden `aria-live` region that speaks the new page's title (from the route's `meta`, falling back to the pathname) on every navigation. This is automatic — no setup, nothing to render. Give each route a `meta.title` and the announcement is meaningful:
1198
+
1199
+ ```tsx
1200
+ export const meta = () => ({ title: 'Team · Acme' })
1201
+ ```
1202
+
1203
+ ## History APIs
1204
+
1205
+ `navigate` takes a path string only — `(to: string, opts?: { replace?: boolean })`. There is no numeric history overload:
1206
+
1207
+ ```ts
1208
+ const navigate = useNavigate()
1209
+ navigate('/foo') // push
1210
+ navigate('/foo', { replace: true }) // replace the current entry
1211
+ ```
1212
+
1213
+ For history traversal, reach for the browser API directly:
1214
+
1215
+ ```ts
1216
+ window.history.back() // back
1217
+ window.history.forward() // forward
1218
+ ```
1219
+
1220
+ ## Reading + writing search params
1221
+
1222
+ Read the query string with `useSearchParams()` — a plain `URLSearchParams`. It's SSR-aware (on the server it reads the request URL; on the client, `window.location.search`):
1223
+
1224
+ ```tsx
1225
+ import { useSearchParams } from '@voltro/web'
1226
+
1227
+ const tab = useSearchParams().get('tab') ?? 'overview'
1228
+ ```
1229
+
1230
+ Write it with `useSetSearchParams()` — the setter updates the query string on the current pathname (via `navigate`), so the URL changes **and** every reader re-renders immediately:
1231
+
1232
+ ```tsx
1233
+ import { useSearchParams, useSetSearchParams } from '@voltro/web'
1234
+
1235
+ function Tabs() {
1236
+ const tab = useSearchParams().get('tab') ?? 'overview'
1237
+ const setParams = useSetSearchParams()
1238
+ return (
1239
+ <nav>
1240
+ <button onClick={() => setParams({ tab: 'overview' })}>Overview</button>
1241
+ <button onClick={() => setParams({ tab: 'members' })}>Members</button>
1242
+ </nav>
1243
+ )
1244
+ }
1245
+ ```
1246
+
1247
+ The setter takes either an object / `URLSearchParams`, or an updater that receives the current params:
1248
+
1249
+ ```ts
1250
+ const setParams = useSetSearchParams()
1251
+ setParams({ tab: 'members' }) // set the whole query
1252
+ setParams((p) => { p.set('page', '2'); return p }) // patch one param
1253
+ setParams({}) // clear the query string
1254
+ ```
1255
+
1256
+ Search-param writes default to a history **replace** (a filter/tab tweak shouldn't stack a Back entry per keystroke). Pass `{ push: true }` for a distinct history entry, or `{ scroll: false }` to keep the scroll position:
1257
+
1258
+ ```ts
1259
+ setParams({ page: '2' }, { push: true })
1260
+ ```
1261
+
1262
+ During SSR there is no history to write — read `useSearchParams()` off the request URL for the first paint and call `useSetSearchParams()` on the client after hydration.
1263
+
1264
+ ## Prefetching programmatically
1265
+
1266
+ ```tsx
1267
+ import { usePrefetch } from '@voltro/web'
1268
+
1269
+ const Card = ({ id }) => {
1270
+ const prefetch = usePrefetch()
1271
+ return (
1272
+ <article onMouseEnter={() => prefetch(`/notes/${id}`)}>
1273
+ {/* …card body, no Link inside */}
1274
+ </article>
1275
+ )
1276
+ }
1277
+ ```
1278
+
1279
+ Useful when the prefetch trigger isn't a `<Link>` (e.g. an entire card area, where the inner link is buried).
1280
+
1281
+ ## Anti-patterns
1282
+
1283
+ - **`<a href>` for internal queries.** Falls through the SPA — full reload. Use `<Link>` instead.
1284
+ - **`window.location.href = '/foo'`.** Same — full reload. Use `useNavigate()`.
1285
+ - **`prefetch` on every link blindly.** For cookie-gated loaders that hit DB, hovering 50 nav items can pile up 50 DB queries. Use `prefetch` for high-confidence destinations only.
1286
+
1287
+ ## Where to read next
1288
+
1289
+ - [Loaders & meta](/docs/routing/loaders-and-meta) — what prefetch warms up
1290
+ - [Islands](/docs/routing/islands) — for pages where most of the JS is stripped
1291
+
1292
+
1293
+
1294
+ ---
1295
+
1296
+ <!-- source: en/routing/islands.md -->
1297
+ ## Islands
1298
+
1299
+ _interactive: 'islands' — ship pure HTML with selectively-hydrated interactive components._
1300
+
1301
+ The **islands** model: serve the page as pure HTML, then hydrate only the bits that need interactivity. The rest of the page is just text + CSS — no React runtime, no JS bundle, no hydration cost.
1302
+
1303
+ Voltro implements islands per-page via the `interactive` export:
1304
+
1305
+ ```tsx
1306
+ export const renderMode = 'static' as const
1307
+ export const interactive = 'islands' as const
1308
+ ```
1309
+
1310
+ With `interactive: 'islands'`, the framework strips the page's React runtime from the HTML, but loads `*.island.tsx` files as separate chunks + hydrates them in place.
1311
+
1312
+ ## When to use islands
1313
+
1314
+ - **Marketing pages** with one interactive widget (a pricing toggle, a code playground).
1315
+ - **Docs** that are mostly text but have a search modal + theme toggle.
1316
+ - **Blog posts** with an embedded poll or comment widget.
1317
+
1318
+ The split lets you ship near-zero JS for the body + JS only for the islands. Lighthouse scores stay high; React's overhead applies only to the interactive parts.
1319
+
1320
+ ## Writing an island
1321
+
1322
+ Wrap a component with `island(Component, { name, hydrate })` and default-export the result. The plain component is NOT enough — without the `island()` call the component is never registered, and at hydration time the runtime logs `island "…" not registered`.
1323
+
1324
+ ```tsx
1325
+ // src/components/LikeButton.island.tsx
1326
+ import { island } from '@voltro/web'
1327
+ import { useState } from 'react'
1328
+
1329
+ const LikeButton = ({ initial }: { initial: number }) => {
1330
+ const [count, setCount] = useState(initial)
1331
+ return (
1332
+ <button onClick={() => setCount((n) => n + 1)}>
1333
+ ❤ {count}
1334
+ </button>
1335
+ )
1336
+ }
1337
+
1338
+ export default island(LikeButton, { name: 'LikeButton', hydrate: 'visible' })
1339
+ ```
1340
+
1341
+ - **`name`** — the stable id under which the component is registered. Must be unique within the app. Both the SSR and the client bundle import the file, so the same `island()` call runs on both sides and registers the component in each.
1342
+ - **`hydrate`** — when the client runtime should hydrate this island (defaults to `'visible'`). The five strategies are in the table below.
1343
+
1344
+ Use it in a page:
1345
+
1346
+ ```tsx
1347
+ // src/pages/blog/[slug].tsx
1348
+ import LikeButton from '../../components/LikeButton.island'
1349
+
1350
+ export const renderMode = 'static' as const
1351
+ export const interactive = 'islands' as const
1352
+
1353
+ export default function Post() {
1354
+ return (
1355
+ <article>
1356
+ <h1>Post title</h1>
1357
+ <p>…body content…</p>
1358
+ <LikeButton initial={42} />
1359
+ </article>
1360
+ )
1361
+ }
1362
+ ```
1363
+
1364
+ What happens at build:
1365
+
1366
+ 1. The page is server-rendered to HTML. The `island()` wrapper emits a marker `<div>` carrying the name, props, and hydrate strategy:
1367
+ ```html
1368
+ <div data-voltro-island data-island-name="LikeButton"
1369
+ data-island-hydrate="visible" data-island-props='{"initial":42}'>
1370
+ <button>❤ 42</button>
1371
+ </div>
1372
+ ```
1373
+ 2. The island bundles into the client chunk.
1374
+ 3. The client runtime scans for `[data-voltro-island]` markers, looks each name up in its registry, and hydrates that `<div>` per its `data-island-hydrate` strategy.
1375
+
1376
+ The rest of the page stays as inert HTML.
1377
+
1378
+ ## Hydrate strategies
1379
+
1380
+ Each island declares WHEN it hydrates via the `hydrate` option (default `'visible'`):
1381
+
1382
+ | Strategy | When the island hydrates | Use for |
1383
+ |---|---|---|
1384
+ | `load` | Immediately, as soon as the client runtime mounts | Above-the-fold widgets users touch within the first second — search box, primary CTA. |
1385
+ | `idle` | When the browser is idle (`requestIdleCallback`, `setTimeout` fallback) | Important widgets that don't need instant interactivity — analytics, secondary nav. |
1386
+ | `visible` (default) | When the element scrolls into the viewport (IntersectionObserver) | Anything below the fold — comment box, related-articles carousel. |
1387
+ | `interaction` | On the first pointer / keyboard event on the element | Heavy widgets users *might* touch — embedded playground, deep tree viewer. Defers cost until commitment. |
1388
+ | `never` | Never — the server-rendered HTML stays inert | Server-only displays that never change after SSR (a build-time status badge). |
1389
+
1390
+ Mix freely inside one page: a `load` search box, a `visible` comment widget, and a `never` build banner can all coexist.
1391
+
1392
+ ## Bundle savings
1393
+
1394
+ For a typical Voltro page:
1395
+
1396
+ | Mode | JS shipped |
1397
+ |---|---|
1398
+ | `interactive: 'full'` | Full React + page bundle (~80-150 KB gzipped) |
1399
+ | `interactive: 'islands'` | Island boot loader + island chunks (typically 10-30 KB gzipped) |
1400
+ | `interactive: 'none'` | Zero |
1401
+
1402
+ For pages where the body never moves, the savings are dramatic.
1403
+
1404
+ ## Island boundaries
1405
+
1406
+ The island component owns its sub-tree's interactivity. Inside an island, you can:
1407
+
1408
+ - `useState`, `useEffect`, every React hook
1409
+ - Import + use any other component
1410
+ - Render JSX freely
1411
+
1412
+ What you CAN'T do:
1413
+
1414
+ - Make the *parent page* interactive from inside. The island can't trigger a page-level re-render.
1415
+ - Read from React Context defined in the page. Each island has its own React root.
1416
+ - Share state across islands directly. Use the URL, `localStorage`, or a custom message channel.
1417
+
1418
+ Each island is independent — there is no shared React root across islands. To coordinate, use the URL, `localStorage`, or a custom message channel.
1419
+
1420
+ ## Props serialisation
1421
+
1422
+ Props passed to an island must be **JSON-serialisable**. The framework serialises them into the marker's `data-island-props` attribute + hydrates with the same values.
1423
+
1424
+ OK:
1425
+
1426
+ ```tsx
1427
+ <LikeButton initial={42} kind="heart" tags={['blog']} />
1428
+ ```
1429
+
1430
+ NOT OK:
1431
+
1432
+ ```tsx
1433
+ <LikeButton onClick={() => …} /> // functions can't serialise
1434
+ <LikeButton date={new Date()} /> // Date → string; use ISO + parse inside
1435
+ <LikeButton ref={someRef} /> // refs are component-local
1436
+ ```
1437
+
1438
+ If you need to pass a function reference, define it INSIDE the island.
1439
+
1440
+ ## When NOT to use islands
1441
+
1442
+ - **Whole page is interactive.** Use `interactive: 'full'` — you'd just be adding the island boot overhead for no benefit.
1443
+ - **Islands that share state.** Each island is its own root — two islands talking is painful. Coordinate via the URL, `localStorage`, or a message channel.
1444
+ - **Islands that hydrate immediately and dominate the page weight.** If the island is the whole page minus a header, just go `interactive: 'full'`.
1445
+
1446
+ ## Combining with render modes
1447
+
1448
+ | `renderMode` × `interactive` | Use case |
1449
+ |---|---|
1450
+ | `static` + `islands` | Marketing landing with a pricing toggle |
1451
+ | `static` + `none` | Pure-content blog posts |
1452
+ | `static` + `full` | SPA-like docs sites |
1453
+ | `ssr` + `islands` | Personalised pages with a few interactive widgets |
1454
+ | `isr` + `islands` | High-traffic listings with a "like" button |
1455
+
1456
+ ## Inspecting
1457
+
1458
+ The build pipeline reports which chunks are islands:
1459
+
1460
+ ```
1461
+ [voltro build] vite build…
1462
+ chunks emitted:
1463
+ main.abc.js 145 KB ← page runtime (full pages only)
1464
+ island-LikeButton.def.js 3.4 KB ← per-island bundle
1465
+ island-Search.ghi.js 8.1 KB
1466
+ ```
1467
+
1468
+ Inspect dashboard's "Bundles" panel shows the per-island size + hydration timings.
1469
+
1470
+ ## Anti-patterns
1471
+
1472
+ - **Wrapping everything in one big island.** Defeats the purpose — you've just rebuilt full hydration with extra steps.
1473
+ - **Passing 100 KB of JSON as island props.** The serialised payload ends up in the HTML — pretty quickly an island's "props" cost dwarfs the saved bundle.
1474
+ - **Calling `useLoaderData` inside an island.** Loaders run for the PAGE, not islands. Islands receive props from the page; the page reads loader data.
1475
+
1476
+ ## Where to read next
1477
+
1478
+ - [Render modes](/docs/routing/render-modes) — pairs with `interactive`
1479
+ - [Navigation](/docs/routing/navigation) — Link + prefetch work the same on islands pages
1480
+
1481
+
1482
+
1483
+ ---
1484
+
1485
+ <!-- source: en/routing/styling.md -->
1486
+ ## Styling (Tailwind v4)
1487
+
1488
+ _Tailwind v4 is auto-loaded in every web app's Vite pipeline. The mandatory @source glob, @theme design tokens, and the two silent-failure gotchas (the framework root is .framework/, not src/; a second @theme after @layer is dropped)._
1489
+
1490
+ Every Voltro web app's Vite pipeline auto-loads `@tailwindcss/vite`. Apps that
1491
+ don't use Tailwind pay no runtime cost (the plugin emits nothing when no
1492
+ `@import "tailwindcss"` appears in any CSS file).
1493
+
1494
+ To use it:
1495
+
1496
+ ```css
1497
+ /* src/globals.css */
1498
+ @import "tailwindcss";
1499
+
1500
+ /* IMPORTANT: the framework's Vite root is `<app>/.framework/`, not
1501
+ `<app>/src/`. Without @source, Tailwind's content scanner misses every .tsx
1502
+ file under src/ — you get an empty `@layer utilities` and zero applied
1503
+ styles. The path is relative to THIS css file. */
1504
+ @source "./**/*.{tsx,ts,jsx,js}";
1505
+
1506
+ @theme {
1507
+ --color-background: #0a0a0a;
1508
+ --color-foreground: #fafafa;
1509
+ --radius: 0.5rem;
1510
+ }
1511
+ ```
1512
+
1513
+ ```tsx
1514
+ // src/pages/layout.tsx
1515
+ import '../globals.css'
1516
+ ```
1517
+
1518
+ Tailwind v4's `@theme` block doubles as the framework's design-tokens surface —
1519
+ define your colours / radii / fonts there once and they become available as
1520
+ `bg-background`, `text-foreground`, etc. The shadcn convention works directly on
1521
+ top: copy a component's source, the classes resolve.
1522
+
1523
+ > **Gotcha:** keep ALL `@theme` blocks ABOVE any `@layer` rules in the same
1524
+ > stylesheet. A second `@theme` block placed AFTER `@layer base` is silently
1525
+ > dropped by Tailwind v4 — none of its tokens are emitted.
1526
+
1527
+ ## Using `@voltro/ui-shadcn` — the kit `@source` is mandatory
1528
+
1529
+ If your app imports `@voltro/ui-shadcn/tokens.css`, you MUST declare a SECOND
1530
+ `@source` pointing at the kit's source — otherwise every class that exists ONLY
1531
+ inside a kit component (animations like `motion-safe:animate-mesh-drift-a`,
1532
+ `motion-safe:animate-twinkle`, kit-internal prose variants) is silently dropped
1533
+ from the generated CSS. The components mount and the keyframes register, but the
1534
+ `animate-*` utility classes never resolve — no warning is emitted.
1535
+
1536
+ ```css
1537
+ /* src/globals.css — app uses kit compositions */
1538
+ @import "@voltro/ui-shadcn/tokens.css";
1539
+
1540
+ @source "./**/*.{tsx,ts,jsx,js}";
1541
+ @source "../node_modules/@voltro/ui-shadcn/src/**/*.{tsx,ts,jsx,js}";
1542
+ ```
1543
+
1544
+ Why: the kit's `tokens.css` ships its own `@source "./**/*"`, but `@source` paths
1545
+ resolve relative to the IMPORTING css file — so once your `globals.css` imports
1546
+ the kit, the kit's `./**/*` glob expands to YOUR app's src, not the kit's. The
1547
+ kit's own components are then never scanned. Declaring the explicit kit path
1548
+ closes the gap — the published package ships its `src/` precisely so this glob
1549
+ matches (workspace link and npm install alike).
1550
+
1551
+ How to verify: `curl -sS http://localhost:<port>/@fs/<abs>/src/globals.css | grep
1552
+ animate-` should list every kit animation utility you use. If a class is missing,
1553
+ the `@source` is misconfigured.