@voltro/cli 0.51.0 → 0.53.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 (233) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/THIRD-PARTY-NOTICES.md +8311 -3318
  3. package/dist/{agentsMd-D6yD7IQv.js → agentsMd-0l980yhL.js} +4 -1
  4. package/dist/agentsMd-SDDSkyl4.js +2 -0
  5. package/dist/{apiBuild-CPDHXF72.js → apiBuild-CaPfoWku.js} +11 -5
  6. package/dist/apiBuild-DHtLXYx9.js +2 -0
  7. package/dist/bin.js +1 -1
  8. package/dist/build-D-OnvNMf.js +843 -0
  9. package/dist/{checkCommand-DNuPiWMc.js → checkCommand-C5elt0tW.js} +92 -46
  10. package/dist/checkCommand-D2ZduVlh.js +2 -0
  11. package/dist/{cloudCmd-F4YJeqM3.js → cloudCmd-QUXh-b5w.js} +1 -1
  12. package/dist/codegen-BWpt3VgF.js +2 -0
  13. package/dist/{codegen-CrMXs4hb.js → codegen-FEk8AZHb.js} +2 -2
  14. package/dist/{codegenCommand-BNBHcNNj.js → codegenCommand-BOiWQ5hz.js} +12 -12
  15. package/dist/{codemodRunner-BDVixlSw.js → codemodRunner-BjtB2lq6.js} +691 -545
  16. package/dist/{commands-B1OiS9bX.js → commands-DyxAmhP0.js} +36 -36
  17. package/dist/{dashboardCommand-C-vvPY1B.js → dashboardCommand-BdKTyT13.js} +3 -3
  18. package/dist/{dataCommand-C1GxXW5q.js → dataCommand-Bab9X7s8.js} +27 -27
  19. package/dist/{dbCommand-If4Y1xQ-.js → dbCommand-06O2finM.js} +277 -236
  20. package/dist/dbCommand-B1EXBC6f.js +2 -0
  21. package/dist/{dev-kdAg9Q7l.js → dev-C6LGF4iY.js} +2998 -2379
  22. package/dist/dev-GjJWAYo2.js +3 -0
  23. package/dist/doctorCommand-B0hX0tdz.js +2 -0
  24. package/dist/{doctorCommand-nKmeW78u.js → doctorCommand-etMkflRc.js} +332 -220
  25. package/dist/{dormancyCommand-CY3wa_SW.js → dormancyCommand-UwZ1AZzB.js} +1 -1
  26. package/dist/{embeddingsCommand-BDLIgje_.js → embeddingsCommand-C70zWHwo.js} +1 -1
  27. package/dist/{envCommand-C6V_xVlT.js → envCommand-dSyKvRkM.js} +15 -15
  28. package/dist/{evolveCommand-D3c4DSfN.js → evolveCommand-CG0_ebO5.js} +2 -2
  29. package/dist/fileConventions-DASGEmj-.js +35 -0
  30. package/dist/{fileTaxonomy-CJfgOllU.js → fileTaxonomy-B7uxipWS.js} +55 -55
  31. package/dist/fontPipeline-LxIHa1vo.js +2 -0
  32. package/dist/fontPipeline-Tsh8kZfA.js +152 -0
  33. package/dist/frameworkTableAssembly-C_7Z-rMs.js +2 -0
  34. package/dist/{frameworkTableAssembly-BwJVEKLr.js → frameworkTableAssembly-DKx3ba3S.js} +5 -5
  35. package/dist/imagePipeline-B_GVJgm6.js +2 -0
  36. package/dist/imagePipeline-CBZmjT4i.js +127 -0
  37. package/dist/index.js +1 -1
  38. package/dist/{infoCommand-BnRFEF1o.js → infoCommand-_53iOc_j.js} +1 -1
  39. package/dist/{inspect-CtL_xTbu.js → inspect-Bd8-9wsi.js} +1 -1
  40. package/dist/inspect-CuoDInfZ.js +2 -0
  41. package/dist/{inspectGateHint-BjnFubmH.js → inspectGateHint-4LxkNtrz.js} +1 -1
  42. package/dist/inspectMetrics-CGF94puw.js +143 -0
  43. package/dist/manifestBuild-C4-J1-m_.js +2 -0
  44. package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
  45. package/dist/{metaCommands-CfRLra0s.js → metaCommands-Cn2oboG4.js} +9 -3
  46. package/dist/{migrate-DehuBakM.js → migrate-Cko9rswM.js} +2 -2
  47. package/dist/{pageConvention-cEiRxdab.js → pageConvention-C938S8oC.js} +1 -1
  48. package/dist/{privacyCommand-XejDMvmu.js → privacyCommand-DWTQMC6R.js} +2 -2
  49. package/dist/{probeCommand-C9gazU0H.js → probeCommand-DkGGLknv.js} +83 -24
  50. package/dist/{projectScaffold-mIX_DpSe.js → projectScaffold-B4dmTlwT.js} +1 -1
  51. package/dist/{projectScaffold-BIl97_E6.js → projectScaffold-EzlErR4E.js} +1 -1
  52. package/dist/{renderModeScan-D7J1B7Kw.js → renderModeScan-CUbOeOAg.js} +28 -11
  53. package/dist/{renderProfile-1OWWAAtx.js → renderProfile-CskIgAfn.js} +2 -2
  54. package/dist/{runtimeTrace-ZsBU7Tkx.js → runtimeTrace-c0APJz7E.js} +1 -1
  55. package/dist/{sdkgen-O4XqWOjM.js → sdkgen-BiQCgIEr.js} +1 -1
  56. package/dist/serveCommand-CueKQgzl.js +2443 -0
  57. package/dist/serveCommand-DsnrVN3U.js +2 -0
  58. package/dist/serveEntry.js +1 -1
  59. package/dist/start-BJzZLbt8.js +3 -0
  60. package/dist/start-ekPan8BT.js +1510 -0
  61. package/dist/startEntry.js +1 -1
  62. package/dist/{staticCommand-Dr2M6tpU.js → staticCommand-xlSL-IWk.js} +1 -1
  63. package/dist/{test-rXFq4S76.js → test-BWPQcRoB.js} +1 -1
  64. package/dist/updateCommand-Bqql_rsQ.js +2 -0
  65. package/dist/{updateCommand-Bs322Q78.js → updateCommand-C_8I8Rzo.js} +139 -115
  66. package/dist/webDev-C7jWJ5dX.js +2 -0
  67. package/dist/{webDev-B-ubQEMX.js → webDev-oczpugbx.js} +1767 -913
  68. package/dist/{webhooksCommand-FLYY9IXh.js → webhooksCommand-4SVPDjKg.js} +1 -1
  69. package/package.json +72 -18
  70. package/templates/AGENTS.core.md +11 -0
  71. package/templates/AGENTS.md +19 -6
  72. package/templates/agent-docs/_index.md +8 -6
  73. package/templates/agent-docs/_manifest.json +31 -15
  74. package/templates/agent-docs/ai.md +2 -2
  75. package/templates/agent-docs/authentication.md +1 -1
  76. package/templates/agent-docs/cli.md +97 -15
  77. package/templates/agent-docs/configuration.md +17 -0
  78. package/templates/agent-docs/data.md +680 -33
  79. package/templates/agent-docs/database/advancedqueries.md +7 -7
  80. package/templates/agent-docs/database/columntypes.md +2 -2
  81. package/templates/agent-docs/database/querying.md +1 -1
  82. package/templates/agent-docs/database/schema.md +2 -2
  83. package/templates/agent-docs/database/seedsdialects.md +2 -2
  84. package/templates/agent-docs/database/transactions.md +3 -3
  85. package/templates/agent-docs/deployment.md +30 -3
  86. package/templates/agent-docs/internationalization.md +2 -2
  87. package/templates/agent-docs/introduction.md +52 -0
  88. package/templates/agent-docs/local-first-mobile.md +132 -7
  89. package/templates/agent-docs/observability.md +2 -0
  90. package/templates/agent-docs/plugins/ai-flows.md +1 -1
  91. package/templates/agent-docs/plugins/audit.md +5 -5
  92. package/templates/agent-docs/plugins/auth.md +1 -1
  93. package/templates/agent-docs/plugins/cdc-out.md +2 -2
  94. package/templates/agent-docs/plugins/comments.md +142 -0
  95. package/templates/agent-docs/plugins/notifications.md +47 -4
  96. package/templates/agent-docs/plugins/presence.md +16 -3
  97. package/templates/agent-docs/plugins/prometheus.md +1 -1
  98. package/templates/agent-docs/plugins/queue.md +129 -0
  99. package/templates/agent-docs/plugins/{versioning.md → row-history.md} +22 -22
  100. package/templates/agent-docs/plugins/storage.md +2 -2
  101. package/templates/agent-docs/plugins.md +38 -12
  102. package/templates/agent-docs/reference.md +54 -5
  103. package/templates/agent-docs/routing.md +868 -50
  104. package/templates/agent-docs/schema-driven-ui.md +292 -5
  105. package/templates/agent-docs/security.md +125 -8
  106. package/templates/agent-docs/templates/apibackends.md +14 -14
  107. package/templates/agent-docs/templates/overview.md +1 -1
  108. package/templates/agent-docs/whats-new.md +171 -54
  109. package/templates/apps/api-ai/package.json +6 -7
  110. package/templates/apps/api-ai/tests/summarize.test.ts +1 -1
  111. package/templates/apps/api-auth/package.json +8 -8
  112. package/templates/apps/api-backend/package.json +7 -7
  113. package/templates/apps/api-backend-deactivation/package.json +7 -7
  114. package/templates/apps/api-backend-mail/package.json +8 -8
  115. package/templates/apps/api-backend-mariadb/package.json +9 -9
  116. package/templates/apps/api-backend-sqlite/package.json +8 -8
  117. package/templates/apps/api-backend-storage/package.json +8 -8
  118. package/templates/apps/api-cms/package.json +9 -10
  119. package/templates/apps/api-collab/package.json +8 -8
  120. package/templates/apps/api-data-advanced/package.json +8 -8
  121. package/templates/apps/api-durable/package.json +8 -8
  122. package/templates/apps/api-feature-flags/package.json +9 -9
  123. package/templates/apps/api-governance/package.json +8 -8
  124. package/templates/apps/api-kv/package.json +8 -8
  125. package/templates/apps/api-moderation/package.json +8 -8
  126. package/templates/apps/api-observability/package.json +8 -8
  127. package/templates/apps/api-ratelimit/package.json +8 -8
  128. package/templates/apps/api-rbac/package.json +8 -8
  129. package/templates/apps/api-rest/package.json +7 -7
  130. package/templates/apps/{api-versioning → api-row-history}/README.md +3 -3
  131. package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.server.ts +1 -1
  132. package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.server.ts +1 -1
  133. package/templates/apps/{api-versioning → api-row-history}/app.config.ts +3 -3
  134. package/templates/apps/{api-versioning → api-row-history}/database/schema.ts +1 -1
  135. package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.server.ts +1 -1
  136. package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.ts +1 -1
  137. package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.ts +1 -1
  138. package/templates/apps/{api-versioning → api-row-history}/package.json +8 -8
  139. package/templates/apps/api-row-history/template.json +6 -0
  140. package/templates/apps/{api-versioning → api-row-history}/tests/documents.create.test.ts +1 -1
  141. package/templates/apps/api-saas/app.config.ts +1 -0
  142. package/templates/apps/api-saas/package.json +10 -11
  143. package/templates/apps/api-saas-starter/package.json +10 -10
  144. package/templates/apps/api-search/package.json +8 -8
  145. package/templates/apps/api-status/package.json +8 -8
  146. package/templates/apps/api-webhooks/package.json +9 -9
  147. package/templates/apps/changelog/app.config.ts +26 -2
  148. package/templates/apps/changelog/content/releases/{0.1.0.mdx → v0-1-0.mdx} +0 -1
  149. package/templates/apps/changelog/content/releases/{0.2.0.mdx → v0-2-0.mdx} +0 -1
  150. package/templates/apps/changelog/package.json +8 -8
  151. package/templates/apps/changelog/src/collections/releases.collection.ts +48 -0
  152. package/templates/apps/changelog/src/globals.d.ts +1 -1
  153. package/templates/apps/changelog/src/locales/de.ts +1 -1
  154. package/templates/apps/changelog/src/locales/en.ts +1 -1
  155. package/templates/apps/changelog/src/pages/[locale]/[slug]/page.tsx +7 -4
  156. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +13 -41
  157. package/templates/apps/changelog/src/pages/[slug]/page.test.tsx +26 -51
  158. package/templates/apps/changelog/src/pages/[slug]/page.tsx +21 -19
  159. package/templates/apps/changelog/src/pages/page.test.tsx +14 -37
  160. package/templates/apps/changelog/src/pages/page.tsx +18 -12
  161. package/templates/apps/edge-functions/package.json +2 -2
  162. package/templates/apps/frontend-admin/package.json +8 -8
  163. package/templates/apps/frontend-app/package.json +9 -9
  164. package/templates/apps/frontend-auth/package.json +8 -8
  165. package/templates/apps/frontend-blank/package.json +7 -7
  166. package/templates/apps/frontend-cms/package.json +9 -9
  167. package/templates/apps/frontend-collab/package.json +10 -10
  168. package/templates/apps/frontend-collab/src/pages/page.test.tsx +10 -9
  169. package/templates/apps/frontend-collab/src/pages/page.tsx +42 -65
  170. package/templates/apps/frontend-contact/package.json +7 -7
  171. package/templates/apps/frontend-dashboard/package.json +7 -7
  172. package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
  173. package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
  174. package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
  175. package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
  176. package/templates/apps/frontend-docs/package.json +8 -7
  177. package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
  178. package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
  179. package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
  180. package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
  181. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
  182. package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
  183. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
  184. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
  185. package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
  186. package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
  187. package/templates/apps/frontend-i18n/package.json +6 -6
  188. package/templates/apps/frontend-landing/package.json +6 -7
  189. package/templates/apps/frontend-portal/package.json +8 -8
  190. package/templates/apps/frontend-saas/package.json +8 -8
  191. package/templates/apps/frontend-spa/package.json +7 -7
  192. package/templates/apps/frontend-ssr/package.json +7 -7
  193. package/templates/apps/frontend-ssr-api/package.json +8 -8
  194. package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
  195. package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
  196. package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
  197. package/templates/apps/frontend-static-blog/package.json +8 -6
  198. package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
  199. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
  200. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
  201. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
  202. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
  203. package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
  204. package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
  205. package/templates/apps/frontend-status/package.json +8 -8
  206. package/templates/apps/mobile-app/package.json +4 -4
  207. package/dist/agentsMd-Bu_XQgVf.js +0 -2
  208. package/dist/apiBuild-GDKuGOMV.js +0 -2
  209. package/dist/build-DETLZAFt.js +0 -752
  210. package/dist/checkCommand-CWcnDArJ.js +0 -2
  211. package/dist/codegen-DiMn2KkZ.js +0 -2
  212. package/dist/dbCommand-C27HIsGE.js +0 -2
  213. package/dist/dev-CK522MV5.js +0 -3
  214. package/dist/doctorCommand-BK4l18eG.js +0 -2
  215. package/dist/fileConventions-Cof68_BL.js +0 -33
  216. package/dist/frameworkTableAssembly-CVDB2hCq.js +0 -2
  217. package/dist/inspect-CuGDYES0.js +0 -2
  218. package/dist/inspectMetrics-CfdKLh6t.js +0 -72
  219. package/dist/manifestBuild-CPjhvM62.js +0 -2
  220. package/dist/serveCommand-BRnPCxVd.js +0 -2
  221. package/dist/serveCommand-DdiYNBBu.js +0 -2362
  222. package/dist/start-BLNmWkLa.js +0 -1154
  223. package/dist/start-Dzicuyw8.js +0 -3
  224. package/dist/updateCommand-eXB35SEv.js +0 -2
  225. package/dist/webDev-DposiF3j.js +0 -2
  226. package/templates/apps/api-versioning/template.json +0 -6
  227. package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
  228. package/templates/apps/changelog/src/lib/releases.ts +0 -21
  229. package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
  230. /package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.ts +0 -0
  231. /package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.ts +0 -0
  232. /package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.server.ts +0 -0
  233. /package/templates/apps/{api-versioning → api-row-history}/tsconfig.json +0 -0
@@ -12,17 +12,17 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/client": "0.51.0",
17
- "@voltro/env": "0.51.0",
18
- "@voltro/i18n": "0.51.0",
19
- "@voltro/ui-shadcn": "0.51.0",
20
- "@voltro/web": "0.51.0",
15
+ "@voltro/cli": "0.53.0",
16
+ "@voltro/client": "0.53.0",
17
+ "@voltro/env": "0.53.0",
18
+ "@voltro/i18n": "0.53.0",
19
+ "@voltro/ui-shadcn": "0.53.0",
20
+ "@voltro/web": "0.53.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.51.0",
25
+ "@voltro/devtools": "0.53.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "jsdom": "^30.0.0",
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/client": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/i18n": "0.51.0",
20
- "@voltro/ui-shadcn": "0.51.0",
21
- "@voltro/web": "0.51.0",
15
+ "@{{projectName}}/api": "0.53.0",
16
+ "@voltro/cli": "0.53.0",
17
+ "@voltro/client": "0.53.0",
18
+ "@voltro/env": "0.53.0",
19
+ "@voltro/i18n": "0.53.0",
20
+ "@voltro/ui-shadcn": "0.53.0",
21
+ "@voltro/web": "0.53.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.51.0",
26
+ "@voltro/devtools": "0.53.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "jsdom": "^30.0.0",
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: From CMS to static pages with getStaticPaths
3
+ date: 2025-02-03
4
+ excerpt: Enumerate your content once; the build emits one pre-rendered page per entry.
5
+ readingMinutes: 3
6
+ ---
7
+
8
+ A dynamic route like `src/pages/blog/[slug].tsx` matches infinitely many URLs. To pre-render it statically, the build needs to know WHICH slugs exist — that is what `getStaticPaths` answers.
9
+
10
+ In this template, `getStaticPaths` maps over the `posts` array in `src/content/posts.ts`. Point it at a CMS API, a database query, or a folder of markdown files instead, and the build will pre-render exactly those pages — the Next.js "static generation from a content source" pattern, with no extra config.
11
+
12
+ URLs you do not enumerate simply are not built. If a page ALSO sets `renderMode = 'ssr' | 'isr'`, `voltro start` renders the un-enumerated ones on demand; a pure `'static'` page 404s for unknown slugs, which is exactly what you want for a fixed content set.
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: Why this blog ships zero JavaScript
3
+ date: 2025-01-15
4
+ excerpt: Every page is pre-rendered at build time, so the browser downloads HTML and nothing else.
5
+ readingMinutes: 3
6
+ ---
7
+
8
+ This page was rendered ONCE, when you ran `voltro build`. The result is a flat `dist/blog/hello-static/index.html` that any CDN can serve in single-digit milliseconds.
9
+
10
+ There is no server rendering per request, no React hydration on the list page, no rpc client on the wire. The list page even declares `interactive = 'none'`, which strips every `<script>` the framework would otherwise emit.
11
+
12
+ When you DO need a sprinkle of interactivity — a reading-progress bar, a copy button, a comment box — you reach for an island instead of hydrating the whole tree. Read the next post for how that works.
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: Islands: hydrate the 5% that moves
3
+ date: 2025-01-22
4
+ excerpt: A static page with one interactive widget should ship one widget of JavaScript — not the whole page.
5
+ readingMinutes: 4
6
+ ---
7
+
8
+ Look at the top of this page: a thin progress bar tracks how far you have scrolled. That bar is an ISLAND — a single component the framework hydrates inside otherwise-static HTML.
9
+
10
+ This page declares `interactive = 'islands'`, so the framework renders the article once and only attaches React to the `island()`-wrapped component. The surrounding HTML has zero React lifecycle running.
11
+
12
+ Each island picks WHEN it wakes up via a hydrate strategy: `load` (immediately), `idle`, `visible` (on scroll-into-view), or `interaction` (on first click). The progress bar uses `load` because it must track scrolling from the very first frame.
13
+
14
+ The lesson: don't default to `islands` because it sounds fast. Use it when most of the page is static and only a few widgets move. A dashboard where everything is interactive wants `full` hydration instead.
@@ -12,16 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/client": "0.51.0",
17
- "@voltro/env": "0.51.0",
18
- "@voltro/i18n": "0.51.0",
19
- "@voltro/web": "0.51.0",
15
+ "@voltro/content": "0.53.0",
16
+ "effect": "^3.22.0",
17
+ "@voltro/cli": "0.53.0",
18
+ "@voltro/client": "0.53.0",
19
+ "@voltro/env": "0.53.0",
20
+ "@voltro/i18n": "0.53.0",
21
+ "@voltro/web": "0.53.0",
20
22
  "react": "^19.0.0",
21
23
  "react-dom": "^19.0.0"
22
24
  },
23
25
  "devDependencies": {
24
- "@voltro/devtools": "0.51.0",
26
+ "@voltro/devtools": "0.53.0",
25
27
  "@types/react": "^19.0.0",
26
28
  "@types/react-dom": "^19.0.0",
27
29
  "jsdom": "^30.0.0",
@@ -0,0 +1,27 @@
1
+ // The blog's content collection — content/posts/**/*.md through the
2
+ // framework's content pipeline (@voltro/content). Frontmatter is
3
+ // schema-checked at build time (a violation FAILS the build naming the file),
4
+ // bodies render to HTML with syntax-highlighted code fences, and no markdown
5
+ // engine ships to the browser: SSG reads the filesystem; SPA navigations
6
+ // fetch the build-emitted JSON artifacts under /assets/content/posts/.
7
+ //
8
+ // Point a CMS/DB-backed site at @voltro/cms instead — files are versioned
9
+ // DEVELOPER content, a CMS is editorial content.
10
+
11
+ import { Schema } from 'effect'
12
+ import { defineCollection } from '@voltro/content'
13
+
14
+ export const posts = defineCollection({
15
+ name: 'posts',
16
+ directory: 'content/posts',
17
+ schema: Schema.Struct({
18
+ title: Schema.String,
19
+ /** ISO date, shown + used for ordering. */
20
+ date: Schema.String,
21
+ /** One-line summary — feeds the list + `<meta name="description">`. */
22
+ excerpt: Schema.String,
23
+ readingMinutes: Schema.Union(Schema.NumberFromString, Schema.Number),
24
+ }),
25
+ })
26
+
27
+ export type Post = Schema.Schema.Type<typeof posts.schema>
@@ -7,14 +7,17 @@
7
7
  // once per non-default locale). `voltro build` produces one HTML file per
8
8
  // pair, e.g. /de/blog/hello-static.
9
9
  import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../../../../lib/locale'
10
- import { posts } from '../../../../content/posts'
10
+ import { getCollection } from '@voltro/content'
11
+ import { posts } from '../../../../collections/posts.collection'
11
12
 
12
13
  export { default } from '../../../blog/[slug]/page'
13
14
  export { renderMode, interactive, loader, meta } from '../../../blog/[slug]/page'
14
15
 
15
16
  export const getStaticPaths = async (): Promise<
16
17
  Array<{ params: { locale: string; slug: string } }>
17
- > =>
18
- SUPPORTED_LOCALES
18
+ > => {
19
+ const entries = await getCollection(posts.name)
20
+ return SUPPORTED_LOCALES
19
21
  .filter((l) => l !== DEFAULT_LOCALE)
20
- .flatMap((locale) => posts.map((post) => ({ params: { locale, slug: post.slug } })))
22
+ .flatMap((locale) => entries.map((post) => ({ params: { locale, slug: post.slug } })))
23
+ }
@@ -8,7 +8,13 @@
8
8
 
9
9
  import { describe, expect, test, vi } from 'vitest'
10
10
  import type { ComponentType } from 'react'
11
- import { posts } from '../../content/posts'
11
+ import { dirname, join } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { setContentRoot, getCollection } from '@voltro/content'
14
+ import { posts as postsCollection } from '../../collections/posts.collection'
15
+
16
+ setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..'))
17
+ const posts = await getCollection(postsCollection.name)
12
18
 
13
19
  // The mirrors transitively import the base pages, which import @voltro/web
14
20
  // (island / loader / notFound / useLocation via lib/locale) and @voltro/i18n.
@@ -10,10 +10,16 @@
10
10
  import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
11
11
  import { act, createElement, type ComponentType, type ReactNode } from 'react'
12
12
  import { createRoot, type Root } from 'react-dom/client'
13
- import { posts, type Post } from '../../../content/posts'
13
+ import { dirname, join } from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { setContentRoot, getCollection, type ContentEntry } from '@voltro/content'
16
+ import { posts as postsCollection, type Post } from '../../../collections/posts.collection'
14
17
  import en from '../../../locales/en'
15
18
 
16
- const loaderData = vi.fn<() => Post>()
19
+ // Real content: the collection reads this app's content/posts/*.md.
20
+ setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..'))
21
+
22
+ const loaderData = vi.fn<() => ContentEntry<Post>>()
17
23
 
18
24
  const fill = (msg: string, values?: Record<string, unknown>): string =>
19
25
  values ? msg.replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? `{${k}}`)) : msg
@@ -48,6 +54,7 @@ vi.mock('@voltro/i18n', () => ({
48
54
  }))
49
55
 
50
56
  const { default: BlogPost, getStaticPaths, loader, meta } = await import('./page')
57
+ const posts = await getCollection<Post>(postsCollection.name)
51
58
 
52
59
  ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
53
60
 
@@ -76,6 +83,7 @@ afterEach(() => {
76
83
  describe('blog [slug] — getStaticPaths', () => {
77
84
  test('enumerates one path per post', async () => {
78
85
  const paths = await getStaticPaths()
86
+ expect(posts.length).toBeGreaterThan(0)
79
87
  expect(paths.map((p) => p.params.slug)).toEqual(posts.map((p) => p.slug))
80
88
  })
81
89
  })
@@ -84,7 +92,9 @@ describe('blog [slug] — loader', () => {
84
92
  test('returns the matching post for a known slug', async () => {
85
93
  const post = await loader({ params: { slug: 'hello-static' } } as never)
86
94
  expect(post.slug).toBe('hello-static')
87
- expect(post.title).toContain('zero JavaScript')
95
+ expect(post.data.title).toContain('zero JavaScript')
96
+ // The loader returns RENDERED html — the component never sees markdown.
97
+ expect(post.html).toContain('<p>')
88
98
  })
89
99
 
90
100
  test('throws notFound for an unknown slug', async () => {
@@ -98,8 +108,8 @@ describe('blog [slug] — meta', () => {
98
108
  test('builds a per-post title + description from the loader data', () => {
99
109
  const post = posts[0]!
100
110
  const m = meta({ loaderData: post })
101
- expect(m.title).toContain(post.title)
102
- expect(m.description).toBe(post.excerpt)
111
+ expect(m.title).toContain(post.data.title)
112
+ expect(m.description).toBe(post.data.excerpt)
103
113
  })
104
114
  })
105
115
 
@@ -108,9 +118,9 @@ describe('blog [slug] — render', () => {
108
118
  const post = posts[1]!
109
119
  loaderData.mockReturnValue(post)
110
120
  render(createElement(BlogPost))
111
- expect(container.querySelector('article h1')?.textContent).toBe(post.title)
112
- expect(container.textContent).toContain(`${post.readingMinutes} min read`)
113
- const paras = post.body.split('\n\n')
114
- expect(container.querySelectorAll('article p').length).toBeGreaterThanOrEqual(paras.length)
121
+ expect(container.querySelector('article h1')?.textContent).toBe(post.data.title)
122
+ expect(container.textContent).toContain(`${post.data.readingMinutes} min read`)
123
+ // The body arrives as rendered html paragraphs.
124
+ expect(container.querySelectorAll('article div p').length).toBeGreaterThanOrEqual(2)
115
125
  })
116
126
  })
@@ -1,9 +1,11 @@
1
1
  // A dynamic route — `/blog/<slug>` — pre-rendered STATICALLY, one HTML
2
2
  // file per post. Three exports make that work:
3
3
  //
4
- // • getStaticPaths → enumerate which slugs to build (from the content
5
- // source; point it at a CMS/DB in a real app).
6
- // • loader → fetch THIS post's data; runs at build per slug.
4
+ // • getStaticPaths → enumerate which slugs to build (from the `posts`
5
+ // content collection one .md file per post).
6
+ // • loader → fetch THIS post's entry; runs at build per slug and
7
+ // returns the RENDERED html (code fences highlighted
8
+ // server-side; nothing markdown-shaped in the bundle).
7
9
  // • meta(loaderData)→ per-post <title>/<description> baked into the HTML.
8
10
  //
9
11
  // `interactive: 'islands'` hydrates ONLY the <ReadingProgress> island; the
@@ -11,8 +13,9 @@
11
13
 
12
14
  import type { ReactNode } from 'react'
13
15
  import { useLoaderData, notFound, type LoaderFn, type PageMeta } from '@voltro/web'
16
+ import { getCollection, getEntry, type ContentEntry } from '@voltro/content'
14
17
  import { T, useLocale } from '@voltro/i18n'
15
- import { posts, type Post } from '../../../content/posts'
18
+ import { posts, type Post } from '../../../collections/posts.collection'
16
19
  import { withLocalePrefix } from '../../../lib/locale'
17
20
  import ReadingProgress from '../../../components/ReadingProgress.island'
18
21
 
@@ -22,14 +25,13 @@ export const interactive = 'islands' as const
22
25
  // Which concrete paths to pre-render. Every returned `params` becomes one
23
26
  // built HTML file; un-enumerated slugs are simply not built (→ 404).
24
27
  export const getStaticPaths = async (): Promise<Array<{ params: { slug: string } }>> =>
25
- posts.map((post) => ({ params: { slug: post.slug } }))
28
+ (await getCollection(posts.name)).map((post) => ({ params: { slug: post.slug } }))
26
29
 
27
30
  // Runs at build time for each enumerated slug. Returning `notFound()` skips
28
- // the artifact at build / 404s at runtime — defensive, though getStaticPaths
29
- // only ever feeds us slugs that exist.
30
- export const loader: LoaderFn<Post> = async ({ params }) => {
31
- const post = posts.find((p) => p.slug === params.slug)
32
- if (!post) return notFound(`post ${params.slug}`)
31
+ // the artifact at build / 404s at runtime.
32
+ export const loader: LoaderFn<ContentEntry<Post>> = async ({ params }) => {
33
+ const post = await getEntry<Post>(posts.name, params.slug)
34
+ if (post === null) return notFound(`post ${params.slug}`)
33
35
  return post
34
36
  }
35
37
 
@@ -37,15 +39,13 @@ export const loader: LoaderFn<Post> = async ({ params }) => {
37
39
  // pre-rendered HTML (great for SEO + social cards). The title/description are
38
40
  // POST CONTENT, not chrome, so they are the same on `/blog/x` and `/de/blog/x`
39
41
  // — no per-locale variant needed here.
40
- export const meta = ({ loaderData }: { loaderData: Post }): PageMeta => ({
41
- title: `${loaderData.title} — {{capProjectName}}`,
42
- description: loaderData.excerpt,
42
+ export const meta = ({ loaderData }: { loaderData: ContentEntry<Post> }): PageMeta => ({
43
+ title: `${loaderData.data.title} — {{capProjectName}}`,
44
+ description: loaderData.data.excerpt,
43
45
  })
44
46
 
45
47
  export default function BlogPost(): ReactNode {
46
- const post = useLoaderData<Post>()
47
- // The active locale (from the layout's URL provider) keeps the "all posts"
48
- // link on the current language.
48
+ const post = useLoaderData<ContentEntry<Post>>()
49
49
  const locale = useLocale()
50
50
  return (
51
51
  <main>
@@ -56,13 +56,11 @@ export default function BlogPost(): ReactNode {
56
56
  </a>
57
57
  </p>
58
58
  <article>
59
- <h1>{post.title}</h1>
59
+ <h1>{post.data.title}</h1>
60
60
  <p className="muted">
61
- {post.date} · <T id="post.readingTimeRead" values={{ minutes: post.readingMinutes }} />
61
+ {post.data.date} · <T id="post.readingTimeRead" values={{ minutes: post.data.readingMinutes }} />
62
62
  </p>
63
- {post.body.split('\n\n').map((para, i) => (
64
- <p key={i}>{para}</p>
65
- ))}
63
+ <div dangerouslySetInnerHTML={{ __html: post.html ?? '' }} />
66
64
  </article>
67
65
  </main>
68
66
  )
@@ -12,10 +12,16 @@
12
12
  import { afterEach, describe, expect, test, vi } from 'vitest'
13
13
  import { act, createElement, type ReactNode } from 'react'
14
14
  import { createRoot, type Root } from 'react-dom/client'
15
- import { posts } from '../content/posts'
15
+ import { dirname, join } from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+ import { setContentRoot } from '@voltro/content'
16
18
  import en from '../locales/en'
17
19
  import de from '../locales/de'
18
20
 
21
+ // The list now comes from the posts CONTENT COLLECTION — point the server
22
+ // half at this app's root and load the REAL content/posts/*.md files.
23
+ setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..'))
24
+
19
25
  const catalogs: Record<string, Record<string, string>> = { en, de }
20
26
  let currentLocale = 'en'
21
27
 
@@ -23,6 +29,10 @@ let currentLocale = 'en'
23
29
  const fill = (msg: string, values?: Record<string, unknown>): string =>
24
30
  values ? msg.replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? `{${k}}`)) : msg
25
31
 
32
+ vi.mock('@voltro/web', () => ({
33
+ useLoaderData: () => entries,
34
+ }))
35
+
26
36
  vi.mock('@voltro/i18n', () => ({
27
37
  // The locale catalogs call these at module top-level; identity mirrors the
28
38
  // real (compile-time-only) implementations so importing them doesn't throw.
@@ -38,7 +48,9 @@ vi.mock('@voltro/i18n', () => ({
38
48
  fill(catalogs[currentLocale]?.[id] ?? id, values),
39
49
  }))
40
50
 
41
- const { default: Index, meta, renderMode, interactive } = await import('./page')
51
+ const { default: Index, meta, renderMode, interactive, loader } = await import('./page')
52
+ // The real markdown entries, loaded through the real collection pipeline.
53
+ const entries = await loader()
42
54
 
43
55
  ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
44
56
 
@@ -83,11 +95,12 @@ describe('index — localized render', () => {
83
95
  currentLocale = 'en'
84
96
  render(createElement(Index))
85
97
  const links = Array.from(container.querySelectorAll('.post-list a'))
86
- expect(links).toHaveLength(posts.length)
98
+ expect(links).toHaveLength(entries.length)
99
+ expect(entries.length).toBeGreaterThan(0)
87
100
  // Newest-first: the 2025-02-03 post precedes the 2025-01-15 one.
88
101
  expect(links[0]!.getAttribute('href')).toBe('/blog/cms-to-ssg')
89
102
  expect(links[links.length - 1]!.getAttribute('href')).toBe('/blog/hello-static')
90
- expect(container.textContent).toContain(`${posts.length} posts`)
103
+ expect(container.textContent).toContain(`${entries.length} posts`)
91
104
  })
92
105
 
93
106
  test('prefixes post links with /de and renders German chrome (de)', () => {
@@ -95,6 +108,6 @@ describe('index — localized render', () => {
95
108
  render(createElement(Index))
96
109
  const links = Array.from(container.querySelectorAll('.post-list a'))
97
110
  expect(links[0]!.getAttribute('href')).toBe('/de/blog/cms-to-ssg')
98
- expect(container.textContent).toContain(`${posts.length} Beiträge`)
111
+ expect(container.textContent).toContain(`${entries.length} Beiträge`)
99
112
  })
100
113
  })
@@ -1,18 +1,22 @@
1
1
  // Blog index, mounted at `/` (default locale) and mirrored at `/de` by
2
- // src/pages/[locale]/index.tsx. The post LIST is pure content — no
2
+ // src/pages/[locale]/page.tsx. The post LIST is pure content — no
3
3
  // interactivity — so it ships zero JavaScript: `interactive = 'none'` strips
4
- // every framework <script> from the rendered HTML. The list is read from the
5
- // same content source the [slug] pages + getStaticPaths use.
4
+ // every framework <script> from the rendered HTML.
5
+ //
6
+ // The list comes from the `posts` CONTENT COLLECTION (content/posts/*.md):
7
+ // the loader runs at build time and reads the filesystem — markdown with
8
+ // schema-checked frontmatter, no markdown dependency in this app.
6
9
  //
7
10
  // `meta` is a FUNCTION of `{ locale }` — that's what localises each variant's
8
11
  // <title> / <description> in the pre-rendered HTML. Post titles/excerpts are
9
- // CONTENT (src/content/posts.ts) and are NOT translated; only the chrome
10
- // (heading, count line, reading-time label) comes from the catalogs.
12
+ // CONTENT and are NOT translated; only the chrome (heading, count line,
13
+ // reading-time label) comes from the catalogs.
11
14
 
12
15
  import type { ReactNode } from 'react'
13
- import type { PageMeta } from '@voltro/web'
16
+ import { useLoaderData, type PageMeta } from '@voltro/web'
17
+ import { getCollection, type ContentEntry } from '@voltro/content'
14
18
  import { T, useLocale } from '@voltro/i18n'
15
- import { posts } from '../content/posts'
19
+ import { posts, type Post } from '../collections/posts.collection'
16
20
  import { getCatalog, withLocalePrefix } from '../lib/locale'
17
21
 
18
22
  export const renderMode = 'static' as const
@@ -26,10 +30,13 @@ export const meta = ({ locale }: { locale: string }): PageMeta => {
26
30
  }
27
31
  }
28
32
 
29
- // Newest first sort a copy so the source array stays stable.
30
- const byDateDesc = [...posts].sort((a, b) => b.date.localeCompare(a.date))
33
+ // Newest first. Runs once at build time (this page is static).
34
+ export const loader = async (): Promise<ReadonlyArray<ContentEntry<Post>>> =>
35
+ [...await getCollection<Post>(posts.name)]
36
+ .sort((a, b) => b.data.date.localeCompare(a.data.date))
31
37
 
32
38
  export default function Index(): ReactNode {
39
+ const entries = useLoaderData<ReadonlyArray<ContentEntry<Post>>>()
33
40
  const locale = useLocale()
34
41
  return (
35
42
  <main>
@@ -37,18 +44,18 @@ export default function Index(): ReactNode {
37
44
  <T id="home.heading" />
38
45
  </h1>
39
46
  <p className="muted">
40
- <T id="home.postCount" values={{ count: posts.length }} />
47
+ <T id="home.postCount" values={{ count: entries.length }} />
41
48
  </p>
42
49
  <ul className="post-list">
43
- {byDateDesc.map((post) => (
50
+ {entries.map((post) => (
44
51
  <li key={post.slug}>
45
- <a href={withLocalePrefix(`/blog/${post.slug}`, locale)}>{post.title}</a>
52
+ <a href={withLocalePrefix(`/blog/${post.slug}`, locale)}>{post.data.title}</a>
46
53
  <p>
47
54
  <span className="muted">
48
- {post.date} · <T id="post.readingTime" values={{ minutes: post.readingMinutes }} />
55
+ {post.data.date} · <T id="post.readingTime" values={{ minutes: post.data.readingMinutes }} />
49
56
  </span>
50
57
  {' — '}
51
- {post.excerpt}
58
+ {post.data.excerpt}
52
59
  </p>
53
60
  </li>
54
61
  ))}
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/client": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/i18n": "0.51.0",
20
- "@voltro/ui-shadcn": "0.51.0",
21
- "@voltro/web": "0.51.0",
15
+ "@{{projectName}}/api": "0.53.0",
16
+ "@voltro/cli": "0.53.0",
17
+ "@voltro/client": "0.53.0",
18
+ "@voltro/env": "0.53.0",
19
+ "@voltro/i18n": "0.53.0",
20
+ "@voltro/ui-shadcn": "0.53.0",
21
+ "@voltro/web": "0.53.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.51.0",
26
+ "@voltro/devtools": "0.53.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "jsdom": "^30.0.0",
@@ -15,9 +15,9 @@
15
15
  "dependencies": {
16
16
  "@react-native-async-storage/async-storage": "1.23.1",
17
17
  "@react-native-community/netinfo": "11.4.1",
18
- "@voltro/client": "0.51.0",
19
- "@voltro/react-native": "0.51.0",
20
- "@{{projectName}}/api": "0.51.0",
18
+ "@voltro/client": "0.53.0",
19
+ "@voltro/react-native": "0.53.0",
20
+ "@{{projectName}}/api": "0.53.0",
21
21
  "expo": "^52.0.0",
22
22
  "expo-constants": "~17.0.0",
23
23
  "expo-linking": "~7.0.0",
@@ -32,7 +32,7 @@
32
32
  "@types/node": "^22.0.0",
33
33
  "@types/react": "^19.0.0",
34
34
  "@vitest/coverage-v8": "^4.1.10",
35
- "@voltro/cli": "0.51.0",
35
+ "@voltro/cli": "0.53.0",
36
36
  "typescript": "^6.0.3",
37
37
  "vitest": "^4.1.10"
38
38
  }
@@ -1,2 +0,0 @@
1
- import { a as e } from "./agentsMd-D6yD7IQv.js";
2
- export { e as runAgentsMd };
@@ -1,2 +0,0 @@
1
- import { c as e, l as t } from "./apiBuild-CPDHXF72.js";
2
- export { e as runApiBuild, t as runServeBundleBuild };