@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
@@ -1,6 +1,6 @@
1
1
  // Unit test with `@voltro/testing` — no database, no running server. The base
2
2
  // `documents.create` mutation is a plain tenant()-scoped insert, so it's fully
3
- // unit-runnable. The row-HISTORY / as-of reads are populated by the versioning
3
+ // unit-runnable. The row-HISTORY / as-of reads are populated by the row-history
4
4
  // plugin's post-commit tap (not wired into the unit store), so those are
5
5
  // integration-tested elsewhere; here we cover the write + tenant isolation that
6
6
  // every version-1 snapshot is built from. Run with `voltro test` (vitest).
@@ -45,6 +45,7 @@ export default {
45
45
  // on the zero-infra memory store. To actually STORE + query events, add a
46
46
  // sink AND a SQL store (the postgres-lite sink needs a SqlClient):
47
47
  //
48
+ // pnpm add @voltro/plugin-analytics-postgres
48
49
  // import { postgresAnalytics } from '@voltro/plugin-analytics-postgres'
49
50
  // store: 'postgres' as const, // or 'sqlite' (file:./.voltro/db.sqlite)
50
51
  // analytics: postgresAnalytics(), // events → _voltro_events; aggregate/timeseries/topN
@@ -13,20 +13,19 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/database": "0.51.0",
18
- "@voltro/plugin-analytics-postgres": "0.51.0",
19
- "@voltro/plugin-billing": "0.51.0",
20
- "@voltro/plugin-multitenancy": "0.51.0",
21
- "@voltro/plugin-notifications": "0.51.0",
22
- "@voltro/plugin-presence": "0.51.0",
23
- "@voltro/protocol": "0.51.0",
24
- "@voltro/runtime": "0.51.0",
25
- "@voltro/sql-postgres": "0.51.0",
16
+ "@voltro/cli": "0.53.0",
17
+ "@voltro/database": "0.53.0",
18
+ "@voltro/plugin-billing": "0.53.0",
19
+ "@voltro/plugin-multitenancy": "0.53.0",
20
+ "@voltro/plugin-notifications": "0.53.0",
21
+ "@voltro/plugin-presence": "0.53.0",
22
+ "@voltro/protocol": "0.53.0",
23
+ "@voltro/runtime": "0.53.0",
24
+ "@voltro/sql-postgres": "0.53.0",
26
25
  "effect": "^3.22.0"
27
26
  },
28
27
  "devDependencies": {
29
- "@voltro/testing": "0.51.0",
28
+ "@voltro/testing": "0.53.0",
30
29
  "typescript": "^6.0.3",
31
30
  "@vitest/coverage-v8": "^4.1.10",
32
31
  "vitest": "^4.1.10"
@@ -16,19 +16,19 @@
16
16
  "dependencies": {
17
17
  "@effect/platform": "^0.97.0",
18
18
  "@effect/rpc": "^0.76.0",
19
- "@voltro/cli": "0.51.0",
20
- "@voltro/database": "0.51.0",
21
- "@voltro/env": "0.51.0",
22
- "@voltro/plugin-auth": "0.51.0",
23
- "@voltro/plugin-billing": "0.51.0",
24
- "@voltro/plugin-multitenancy": "0.51.0",
25
- "@voltro/protocol": "0.51.0",
26
- "@voltro/runtime": "0.51.0",
27
- "@voltro/sql-postgres": "0.51.0",
19
+ "@voltro/cli": "0.53.0",
20
+ "@voltro/database": "0.53.0",
21
+ "@voltro/env": "0.53.0",
22
+ "@voltro/plugin-auth": "0.53.0",
23
+ "@voltro/plugin-billing": "0.53.0",
24
+ "@voltro/plugin-multitenancy": "0.53.0",
25
+ "@voltro/protocol": "0.53.0",
26
+ "@voltro/runtime": "0.53.0",
27
+ "@voltro/sql-postgres": "0.53.0",
28
28
  "effect": "^3.22.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@voltro/testing": "0.51.0",
31
+ "@voltro/testing": "0.53.0",
32
32
  "typescript": "^6.0.3",
33
33
  "@vitest/coverage-v8": "^4.1.10",
34
34
  "vitest": "^4.1.10"
@@ -13,17 +13,17 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/database": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/plugin-multitenancy": "0.51.0",
20
- "@voltro/plugin-search": "0.51.0",
21
- "@voltro/protocol": "0.51.0",
22
- "@voltro/runtime": "0.51.0",
16
+ "@voltro/cli": "0.53.0",
17
+ "@voltro/database": "0.53.0",
18
+ "@voltro/env": "0.53.0",
19
+ "@voltro/plugin-multitenancy": "0.53.0",
20
+ "@voltro/plugin-search": "0.53.0",
21
+ "@voltro/protocol": "0.53.0",
22
+ "@voltro/runtime": "0.53.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.51.0",
26
+ "@voltro/testing": "0.53.0",
27
27
  "typescript": "^6.0.3",
28
28
  "@vitest/coverage-v8": "^4.1.10",
29
29
  "vitest": "^4.1.10"
@@ -13,17 +13,17 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/database": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/plugin-rbac": "0.51.0",
20
- "@voltro/protocol": "0.51.0",
21
- "@voltro/runtime": "0.51.0",
22
- "@voltro/sql-postgres": "0.51.0",
16
+ "@voltro/cli": "0.53.0",
17
+ "@voltro/database": "0.53.0",
18
+ "@voltro/env": "0.53.0",
19
+ "@voltro/plugin-rbac": "0.53.0",
20
+ "@voltro/protocol": "0.53.0",
21
+ "@voltro/runtime": "0.53.0",
22
+ "@voltro/sql-postgres": "0.53.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.51.0",
26
+ "@voltro/testing": "0.53.0",
27
27
  "typescript": "^6.0.3",
28
28
  "@vitest/coverage-v8": "^4.1.10",
29
29
  "vitest": "^4.1.10"
@@ -13,18 +13,18 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/database": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/plugin-multitenancy": "0.51.0",
20
- "@voltro/plugin-webhooks": "0.51.0",
21
- "@voltro/protocol": "0.51.0",
22
- "@voltro/runtime": "0.51.0",
23
- "@voltro/sql-postgres": "0.51.0",
16
+ "@voltro/cli": "0.53.0",
17
+ "@voltro/database": "0.53.0",
18
+ "@voltro/env": "0.53.0",
19
+ "@voltro/plugin-multitenancy": "0.53.0",
20
+ "@voltro/plugin-webhooks": "0.53.0",
21
+ "@voltro/protocol": "0.53.0",
22
+ "@voltro/runtime": "0.53.0",
23
+ "@voltro/sql-postgres": "0.53.0",
24
24
  "effect": "^3.22.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.51.0",
27
+ "@voltro/testing": "0.53.0",
28
28
  "typescript": "^6.0.3",
29
29
  "@vitest/coverage-v8": "^4.1.10",
30
30
  "vitest": "^4.1.10"
@@ -1,12 +1,12 @@
1
1
  // `port` is allocated by `voltro create-project` / `voltro add-app` from
2
2
  // the project's portRange (in project.json).
3
3
  import { defineEnv, envVar } from '@voltro/env'
4
+ import { releases, tagsOf, visibleReleases, type ReleaseEntry } from './src/collections/releases.collection'
4
5
 
5
6
  // Typed PUBLIC environment. `VOLTRO_PUBLIC_SITE_NAME` titles the changelog
6
7
  // (header brand + RSS feed). Public vars are validated at boot and baked
7
8
  // into the browser bundle — NEVER put a secret here. Run `voltro env` for
8
- // the manifest. The RSS generator (scripts/generate-rss.mjs) reads the same
9
- // value from process.env at build time.
9
+ // the manifest.
10
10
  export const env = defineEnv({
11
11
  VOLTRO_PUBLIC_SITE_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
12
12
  })
@@ -24,4 +24,28 @@ export default {
24
24
  locales: ['en', 'de'] as const,
25
25
  defaultLocale: 'en' as const,
26
26
  env,
27
+ // The RSS feed is a BUILD ARTIFACT (dist/rss.xml, next to sitemap.xml) and
28
+ // a live dev route — built from the releases collection by the framework.
29
+ // Set SITE_URL in your deploy env; feed links must be absolute.
30
+ seo: {
31
+ siteUrl: process.env['SITE_URL'] ?? 'http://localhost:{{port}}',
32
+ },
33
+ feeds: [
34
+ {
35
+ path: '/rss.xml',
36
+ collection: releases.name,
37
+ title: '{{capProjectName}} Changelog',
38
+ description: 'Product updates and release notes.',
39
+ item: (entry: ReleaseEntry) =>
40
+ visibleReleases([entry]).length === 0
41
+ ? null
42
+ : {
43
+ title: entry.data.title,
44
+ link: `/${entry.slug}`,
45
+ date: entry.data.releasedAt,
46
+ description: entry.data.summary,
47
+ categories: tagsOf(entry.data),
48
+ },
49
+ },
50
+ ],
27
51
  }
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  version: 0.1.0
3
3
  releasedAt: 2026-01-15
4
- slug: v0-1-0
5
4
  title: First release
6
5
  summary: The initial cut — projects, file-based pages, and the reactive query layer.
7
6
  tags: [feature]
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  version: 0.2.0
3
3
  releasedAt: 2026-06-21
4
- slug: v0-2-0
5
4
  title: Typed env + faster boot
6
5
  summary: Declare your environment once; it's validated at startup with a hard public/secret boundary.
7
6
  tags: [feature, performance]
@@ -6,25 +6,25 @@
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
8
  "lint": "voltro doctor .",
9
- "build": "tsx scripts/generate-rss.mjs && voltro build .",
9
+ "build": "voltro build .",
10
10
  "start": "voltro start .",
11
11
  "typecheck": "tsc --noEmit",
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/changelog": "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/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",
21
22
  "react": "^19.0.0",
22
23
  "react-dom": "^19.0.0"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/react": "^19.0.0",
26
27
  "@types/react-dom": "^19.0.0",
27
- "tsx": "^4.22.4",
28
28
  "jsdom": "^30.0.0",
29
29
  "typescript": "^6.0.3",
30
30
  "@vitest/coverage-v8": "^4.1.10",
@@ -0,0 +1,48 @@
1
+ // The release notes as a CONTENT COLLECTION — content/releases/*.mdx through
2
+ // the 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 shiki-highlighted code fences on the server.
5
+ // The `.mdx` extension is a NAMING convention for release-note files — the
6
+ // body is plain markdown, not evaluated MDX.
7
+ //
8
+ // `draft: true` entries and future-dated `releasedAt` values are filtered by
9
+ // the pages + the RSS feed (see visibleReleases / app.config.ts feeds) — so
10
+ // an upcoming release can sit staged in content/releases/ without leaking.
11
+
12
+ import { Schema } from 'effect'
13
+ import { defineCollection, type ContentEntry } from '@voltro/content'
14
+
15
+ export const releases = defineCollection({
16
+ name: 'releases',
17
+ directory: 'content/releases',
18
+ schema: Schema.Struct({
19
+ version: Schema.String,
20
+ /** ISO date/datetime — ordering + the future-dated staging filter. */
21
+ releasedAt: Schema.String,
22
+ title: Schema.String,
23
+ summary: Schema.String,
24
+ /** `[feature, fix]`-style inline list in the frontmatter. */
25
+ tags: Schema.optional(Schema.Union(Schema.Array(Schema.String), Schema.String)),
26
+ /** `draft: true` keeps the entry out of the list + feed. */
27
+ draft: Schema.optional(Schema.String),
28
+ }),
29
+ })
30
+
31
+ export type ReleaseData = Schema.Schema.Type<typeof releases.schema>
32
+ export type ReleaseEntry = ContentEntry<ReleaseData>
33
+
34
+ /** The tags as a plain list (a single-tag frontmatter parses as a string). */
35
+ export const tagsOf = (r: ReleaseData): ReadonlyArray<string> =>
36
+ Array.isArray(r.tags) ? r.tags : typeof r.tags === 'string' && r.tags !== '' ? [r.tags] : []
37
+
38
+ /** Published releases, newest first — drafts and future-dated entries are
39
+ * excluded (staged-but-hidden until the date arrives). */
40
+ export const visibleReleases = (
41
+ entries: ReadonlyArray<ReleaseEntry>,
42
+ now: Date = new Date(),
43
+ ): ReadonlyArray<ReleaseEntry> => {
44
+ const nowIso = now.toISOString()
45
+ return [...entries]
46
+ .filter((e) => e.data.draft !== 'true' && e.data.releasedAt <= nowIso)
47
+ .sort((a, b) => b.data.releasedAt.localeCompare(a.data.releasedAt))
48
+ }
@@ -5,7 +5,7 @@
5
5
  // declared. Vite resolves it at build time; the declaration is type-only.
6
6
  declare module '*.css'
7
7
 
8
- // Minimal typing for Vite's `import.meta.glob` (used by src/lib/releases.ts)
8
+ // Minimal typing for Vite's `import.meta.glob`
9
9
  // so the app typechecks without depending on `vite/client` types being
10
10
  // resolvable. Vite provides the real implementation at build time.
11
11
  interface ImportMeta {
@@ -10,7 +10,7 @@ export default defineLocale<typeof en>()({
10
10
  'nav.rss': 'RSS',
11
11
  'nav.language': 'Sprache',
12
12
 
13
- 'footer.builtWith': 'Gebaut mit @voltro/changelog',
13
+ 'footer.builtWith': 'Gebaut mit Voltro Content Collections',
14
14
 
15
15
  'empty.body':
16
16
  'Noch keine Releases — lege eine <code>.mdx</code>-Datei unter <code>content/releases/</code> mit <code>version</code>- und <code>releasedAt</code>-Frontmatter an.',
@@ -18,7 +18,7 @@ export default defineCatalog({
18
18
  'nav.rss': 'RSS',
19
19
  'nav.language': 'Language',
20
20
 
21
- 'footer.builtWith': 'Built with @voltro/changelog',
21
+ 'footer.builtWith': 'Built with Voltro content collections',
22
22
 
23
23
  'empty.body':
24
24
  'No releases yet — add an <code>.mdx</code> file under <code>content/releases/</code> with <code>version</code> + <code>releasedAt</code> frontmatter.',
@@ -10,15 +10,18 @@
10
10
  // routes at the root. `voltro build` only emits the paths getStaticPaths
11
11
  // enumerates, so the static output is unambiguous — the bare `[slug]` never
12
12
  // enumerates a locale code, and only this mirror emits `/de/…`.
13
+ import { getCollection } from '@voltro/content'
13
14
  import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../../../lib/locale'
14
- import { releases } from '../../../lib/releases'
15
+ import { releases, visibleReleases, type ReleaseData } from '../../../collections/releases.collection'
15
16
 
16
17
  export { default } from '../../[slug]/page'
17
18
  export { renderMode, interactive, loader, meta } from '../../[slug]/page'
18
19
 
19
20
  export const getStaticPaths = async (): Promise<
20
21
  Array<{ params: { locale: string; slug: string } }>
21
- > =>
22
- SUPPORTED_LOCALES
22
+ > => {
23
+ const entries = visibleReleases(await getCollection<ReleaseData>(releases.name))
24
+ return SUPPORTED_LOCALES
23
25
  .filter((l) => l !== DEFAULT_LOCALE)
24
- .flatMap((locale) => releases.map((release) => ({ params: { locale, slug: release.slug } })))
26
+ .flatMap((locale) => entries.map((release) => ({ params: { locale, slug: release.slug } })))
27
+ }
@@ -7,48 +7,16 @@
7
7
  // locales × release slugs (here: de × every release).
8
8
 
9
9
  import { describe, expect, test, vi } from 'vitest'
10
+ import { dirname, join } from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+ import { setContentRoot } from '@voltro/content'
10
13
 
11
- interface Release {
12
- version: string
13
- releasedAt: string
14
- slug: string
15
- tags: ReadonlyArray<string>
16
- title: string
17
- summary: string
18
- html: string
19
- }
14
+ // The mirrors' getStaticPaths read the REAL `releases` content collection —
15
+ // this app's content/releases/*.mdx.
16
+ setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..'))
20
17
 
21
- const releases: ReadonlyArray<Release> = [
22
- {
23
- version: '0.2.0',
24
- releasedAt: '2026-06-21',
25
- slug: 'v0-2-0',
26
- tags: ['feature'],
27
- title: 'Typed env',
28
- summary: 'Declare your environment once.',
29
- html: '<p>body</p>',
30
- },
31
- {
32
- version: '0.1.0',
33
- releasedAt: '2026-01-15',
34
- slug: 'v0-1-0',
35
- tags: ['feature'],
36
- title: 'First release',
37
- summary: 'The initial cut.',
38
- html: '<p>body</p>',
39
- },
40
- ]
41
-
42
- // The real release source reads .mdx via `import.meta.glob` + node-gated shiki;
43
- // mock it (used by the [slug] mirror's cross-product getStaticPaths and by the
44
- // transitively-imported base pages).
45
- const releaseBySlug = (slug: string): Release | undefined => releases.find((r) => r.slug === slug)
46
- vi.mock('../../lib/releases', () => ({ releases, releaseBySlug }))
47
- vi.mock('@voltro/changelog', () => ({
48
- highlightRelease: async (r: Release) => r,
49
- }))
50
18
  vi.mock('@voltro/web', () => ({
51
- useLoaderData: () => ({ release: releases[0] }),
19
+ useLoaderData: () => null,
52
20
  notFound: (detail?: string) => {
53
21
  throw new Error(detail)
54
22
  },
@@ -81,9 +49,13 @@ describe('index mirror — getStaticPaths', () => {
81
49
  })
82
50
 
83
51
  describe('release mirror — getStaticPaths (locale × slug cross-product)', () => {
84
- test('emits one path per non-default locale × release slug', async () => {
52
+ test('emits one path per non-default locale × published release slug', async () => {
85
53
  const paths = await releaseMirror.getStaticPaths()
86
- expect(paths).toEqual(releases.map((r) => ({ params: { locale: 'de', slug: r.slug } })))
54
+ // Newest first, from the real collection.
55
+ expect(paths).toEqual([
56
+ { params: { locale: 'de', slug: 'v0-2-0' } },
57
+ { params: { locale: 'de', slug: 'v0-1-0' } },
58
+ ])
87
59
  })
88
60
 
89
61
  test('re-exports the base release page + its config exports', () => {
@@ -1,51 +1,22 @@
1
1
  // @vitest-environment jsdom
2
2
  //
3
- // `/[slug]` per-release page — `getStaticPaths` (one path per release),
4
- // `loader` (look up the release, shiki-highlight it, `notFound()` on a miss),
5
- // `meta(loaderData)`, and the render (which injects the highlighted HTML). We
6
- // mock `../lib/releases` (avoids the node-only glob) and `@voltro/changelog`
7
- // (`highlightRelease` pulls in shiki) and `@voltro/web` (`notFound`).
3
+ // `/[slug]` per-release page — `getStaticPaths` (one path per published
4
+ // release), `loader` (entry lookup through the REAL `releases` content
5
+ // collection, `notFound()` on a miss), `meta(loaderData)`, and the render
6
+ // (which injects the collection's server-rendered HTML). The collection reads
7
+ // this app's real content/releases/*.mdx no canned copies to drift.
8
8
 
9
9
  import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
10
10
  import { act, createElement, type ReactNode } from 'react'
11
11
  import { createRoot, type Root } from 'react-dom/client'
12
- // Type-only import erased at runtime, so it does NOT pull @voltro/changelog's
13
- // node-only glob (the value module stays mocked below). Using the real Release
14
- // type keeps the mock in lockstep with the framework's shape (tags: ReleaseTag[],
15
- // draft, body, …) so the test typechecks against what the page actually consumes.
16
- import type { Release } from '@voltro/changelog'
12
+ import { dirname, join } from 'node:path'
13
+ import { fileURLToPath } from 'node:url'
14
+ import { setContentRoot } from '@voltro/content'
15
+ import type { ReleaseEntry } from '../../collections/releases.collection'
17
16
 
18
- const releases: ReadonlyArray<Release> = [
19
- {
20
- version: '0.1.0',
21
- releasedAt: '2026-01-15',
22
- slug: 'v0-1-0',
23
- tags: ['feature'],
24
- title: 'First release',
25
- summary: 'The initial cut.',
26
- draft: false,
27
- body: 'The initial cut.',
28
- html: '<p>plain</p>',
29
- },
30
- ]
31
-
32
- const releaseBySlug = (slug: string): Release | undefined => releases.find((r) => r.slug === slug)
33
-
34
- // `../../lib/releases` — the path the PAGE imports. This read `../lib/releases`,
35
- // which from `src/pages/[slug]/` resolves to `src/pages/lib/releases`: a module
36
- // that does not exist, so the mock registered against nothing and the REAL
37
- // `src/lib/releases.ts` loaded. A wrong `vi.mock` specifier is silent in vitest
38
- // — it fails later and elsewhere, here as "No `loadReleases` export is defined
39
- // on the `@voltro/changelog` mock", because the real module calls it at import
40
- // time. The sibling `[locale]/mirrors.test.tsx`, at the same depth, had it right.
41
- vi.mock('../../lib/releases', () => ({ releases, releaseBySlug }))
42
- // `highlightRelease` runs shiki in node; the page only needs it to return a
43
- // (possibly re-rendered) release — the identity, tagged so we can assert it ran.
44
- vi.mock('@voltro/changelog', () => ({
45
- highlightRelease: async (r: Release) => ({ ...r, html: `<pre>${r.title}</pre>` }),
46
- }))
17
+ setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..'))
47
18
 
48
- const loaderData = vi.fn<() => { release: Release }>()
19
+ const loaderData = vi.fn<() => { release: ReleaseEntry }>()
49
20
  class NotFoundError extends Error {}
50
21
  vi.mock('@voltro/web', () => ({
51
22
  useLoaderData: () => loaderData(),
@@ -95,17 +66,19 @@ afterEach(() => {
95
66
  })
96
67
 
97
68
  describe('changelog [slug] — getStaticPaths', () => {
98
- test('enumerates one path per release slug', async () => {
69
+ test('enumerates one path per published release slug, newest first', async () => {
99
70
  const paths = await getStaticPaths()
100
- expect(paths.map((p) => p.params.slug)).toEqual(['v0-1-0'])
71
+ expect(paths.map((p) => p.params.slug)).toEqual(['v0-2-0', 'v0-1-0'])
101
72
  })
102
73
  })
103
74
 
104
75
  describe('changelog [slug] — loader', () => {
105
- test('resolves the release and runs it through highlightRelease', async () => {
76
+ test('resolves the release with server-rendered, highlighted html', async () => {
106
77
  const { release } = await loader({ params: { slug: 'v0-1-0' } } as never)
107
78
  expect(release.slug).toBe('v0-1-0')
108
- expect(release.html).toBe('<pre>First release</pre>')
79
+ expect(release.data.version).toBe('0.1.0')
80
+ // The collection renders code fences with shiki on the server.
81
+ expect(release.html).toContain('class="shiki')
109
82
  })
110
83
 
111
84
  test('throws notFound for an unknown slug', async () => {
@@ -116,18 +89,20 @@ describe('changelog [slug] — loader', () => {
116
89
  })
117
90
 
118
91
  describe('changelog [slug] — meta', () => {
119
- test('builds the per-release title + description from the loader data', () => {
120
- const m = meta({ loaderData: { release: releases[0]! } })
121
- expect(m.title).toContain('First release')
122
- expect(m.description).toBe('The initial cut.')
92
+ test('builds the per-release title + description from the loader data', async () => {
93
+ const { release } = await loader({ params: { slug: 'v0-1-0' } } as never)
94
+ const m = meta({ loaderData: { release } })
95
+ expect(m.title).toContain(release.data.title)
96
+ expect(m.description).toBe(release.data.summary)
123
97
  })
124
98
  })
125
99
 
126
100
  describe('changelog [slug] — render', () => {
127
- test('renders the title, version, and injects the highlighted HTML body', () => {
128
- loaderData.mockReturnValue({ release: { ...releases[0]!, html: '<p>the body html</p>' } })
101
+ test('renders the title, version, tags, and injects the HTML body', async () => {
102
+ const { release } = await loader({ params: { slug: 'v0-1-0' } } as never)
103
+ loaderData.mockReturnValue({ release: { ...release, html: '<p>the body html</p>' } })
129
104
  render(createElement(ReleasePage))
130
- expect(container.querySelector('h1')?.textContent).toBe('First release')
105
+ expect(container.querySelector('h1')?.textContent).toBe(release.data.title)
131
106
  expect(container.textContent).toContain('v0.1.0')
132
107
  expect(container.innerHTML).toContain('the body html')
133
108
  })
@@ -1,34 +1,36 @@
1
- // Per-release page. `getStaticPaths` enumerates every release slug so
2
- // `voltro build` pre-renders one HTML file per release. The loader runs at
3
- // build time (Node) where `highlightRelease` shiki-highlights the code
4
- // fences; in the browser it's a no-op and the plain marked HTML is used.
1
+ // Per-release page. `getStaticPaths` enumerates every published release slug
2
+ // so `voltro build` pre-renders one HTML file per release. The loader reads
3
+ // the entry from the `releases` collection the body arrives as RENDERED
4
+ // html with shiki-highlighted code fences (server-side; no markdown engine
5
+ // in the browser).
5
6
 
6
7
  import type { ReactNode } from 'react'
7
8
  import { useLoaderData, notFound, type LoaderFn } from '@voltro/web'
8
- import { highlightRelease } from '@voltro/changelog'
9
+ import { getCollection, getEntry } from '@voltro/content'
9
10
  import { T, useLocale } from '@voltro/i18n'
10
- import { releases, releaseBySlug, type Release } from '../../lib/releases'
11
+ import { releases, tagsOf, visibleReleases, type ReleaseData, type ReleaseEntry } from '../../collections/releases.collection'
11
12
  import { withLocalePrefix } from '../../lib/locale'
12
13
 
13
14
  export const renderMode = 'static' as const
14
15
  export const interactive = 'none' as const
15
16
 
16
17
  export const getStaticPaths = async (): Promise<Array<{ params: { slug: string } }>> =>
17
- releases.map((release) => ({ params: { slug: release.slug } }))
18
+ visibleReleases(await getCollection<ReleaseData>(releases.name))
19
+ .map((release) => ({ params: { slug: release.slug } }))
18
20
 
19
- export const loader: LoaderFn<{ release: Release }> = async ({ params }) => {
20
- const release = params.slug ? releaseBySlug(params.slug) : undefined
21
- if (!release) return notFound(`release ${params.slug ?? ''}`)
22
- return { release: await highlightRelease(release) }
21
+ export const loader: LoaderFn<{ release: ReleaseEntry }> = async ({ params }) => {
22
+ const release = params.slug ? await getEntry<ReleaseData>(releases.name, params.slug) : null
23
+ if (!release || release.data.draft === 'true') return notFound(`release ${params.slug ?? ''}`)
24
+ return { release }
23
25
  }
24
26
 
25
- export const meta = ({ loaderData }: { loaderData: { release: Release } }) => ({
26
- title: `${loaderData.release.title} — Changelog`,
27
- description: loaderData.release.summary,
27
+ export const meta = ({ loaderData }: { loaderData: { release: ReleaseEntry } }) => ({
28
+ title: `${loaderData.release.data.title} — Changelog`,
29
+ description: loaderData.release.data.summary,
28
30
  })
29
31
 
30
32
  export default function ReleasePage(): ReactNode {
31
- const { release } = useLoaderData<{ release: Release }>()
33
+ const { release } = useLoaderData<{ release: ReleaseEntry }>()
32
34
  // The active locale (from the layout's URL provider) keeps the back-link on
33
35
  // the current language.
34
36
  const locale = useLocale()
@@ -39,14 +41,14 @@ export default function ReleasePage(): ReactNode {
39
41
  <T id="post.backToList" />
40
42
  </a>
41
43
  </p>
42
- <h1>{release.title}</h1>
44
+ <h1>{release.data.title}</h1>
43
45
  <p className="changelog-meta">
44
- <time dateTime={release.releasedAt}>{release.releasedAt}</time> · v{release.version}
45
- {release.tags.map((tag) => (
46
+ <time dateTime={release.data.releasedAt}>{release.data.releasedAt}</time> · v{release.data.version}
47
+ {tagsOf(release.data).map((tag) => (
46
48
  <span key={tag} className="changelog-tag">{tag}</span>
47
49
  ))}
48
50
  </p>
49
- <div dangerouslySetInnerHTML={{ __html: release.html }} />
51
+ <div dangerouslySetInnerHTML={{ __html: release.html ?? '' }} />
50
52
  </article>
51
53
  )
52
54
  }