@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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +20236 -0
- package/bin/voltro.mjs +43 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +9 -0
- package/dist/commands-CfPH2Wf4.js +18061 -0
- package/dist/frameworkInspectState-CX2250XB.js +86 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +5 -0
- package/dist/inspectState.d.ts +116 -0
- package/dist/inspectState.js +2 -0
- package/dist/startup.d.ts +26 -0
- package/dist/startup.js +2 -0
- package/dist/startupRunner-CRhuUl91.js +71 -0
- package/package.json +88 -0
- package/templates/AGENTS.core.md +258 -0
- package/templates/AGENTS.md +351 -0
- package/templates/agent-docs/_index.md +93 -0
- package/templates/agent-docs/_manifest.json +655 -0
- package/templates/agent-docs/ai.md +1845 -0
- package/templates/agent-docs/authentication.md +1788 -0
- package/templates/agent-docs/caching.md +624 -0
- package/templates/agent-docs/cli.md +1650 -0
- package/templates/agent-docs/configuration.md +295 -0
- package/templates/agent-docs/data.md +2172 -0
- package/templates/agent-docs/database/advancedqueries.md +1583 -0
- package/templates/agent-docs/database/columntypes.md +1200 -0
- package/templates/agent-docs/database/hosting.md +881 -0
- package/templates/agent-docs/database/migrations.md +2938 -0
- package/templates/agent-docs/database/misc.md +270 -0
- package/templates/agent-docs/database/overview.md +108 -0
- package/templates/agent-docs/database/querying.md +1622 -0
- package/templates/agent-docs/database/scaling.md +331 -0
- package/templates/agent-docs/database/schema.md +1458 -0
- package/templates/agent-docs/database/seedsdialects.md +1235 -0
- package/templates/agent-docs/database/transactions.md +285 -0
- package/templates/agent-docs/deployment.md +999 -0
- package/templates/agent-docs/internationalization.md +359 -0
- package/templates/agent-docs/introduction.md +438 -0
- package/templates/agent-docs/multi-tenancy.md +610 -0
- package/templates/agent-docs/observability.md +350 -0
- package/templates/agent-docs/plugins.md +1273 -0
- package/templates/agent-docs/reference.md +978 -0
- package/templates/agent-docs/routing.md +1553 -0
- package/templates/agent-docs/scheduling.md +666 -0
- package/templates/agent-docs/schema-driven-ui.md +607 -0
- package/templates/agent-docs/security.md +42 -0
- package/templates/agent-docs/templates/apibackends.md +3214 -0
- package/templates/agent-docs/templates/appshells.md +2062 -0
- package/templates/agent-docs/templates/custom.md +128 -0
- package/templates/agent-docs/templates/overview.md +122 -0
- package/templates/agent-docs/templates/serverless.md +315 -0
- package/templates/agent-docs/testing.md +376 -0
- package/templates/agent-docs/workflows.md +1351 -0
- package/templates/apps/api-ai/README.md +105 -0
- package/templates/apps/api-ai/actions/summarize.action.server.tsx +32 -0
- package/templates/apps/api-ai/actions/summarize.action.ts +33 -0
- package/templates/apps/api-ai/agents/support.agent.server.tsx +31 -0
- package/templates/apps/api-ai/agents/support.agent.tsx +27 -0
- package/templates/apps/api-ai/app.config.ts +57 -0
- package/templates/apps/api-ai/database/schema.ts +67 -0
- package/templates/apps/api-ai/package.json +27 -0
- package/templates/apps/api-ai/seeds/docs.seed.ts +67 -0
- package/templates/apps/api-ai/template.json +6 -0
- package/templates/apps/api-ai/tests/summarize.test.ts +50 -0
- package/templates/apps/api-ai/tools/searchDocs.tool.tsx +60 -0
- package/templates/apps/api-ai/tsconfig.json +5 -0
- package/templates/apps/api-auth/.env +17 -0
- package/templates/apps/api-auth/README.md +109 -0
- package/templates/apps/api-auth/actions/me.action.server.ts +20 -0
- package/templates/apps/api-auth/actions/me.action.ts +19 -0
- package/templates/apps/api-auth/app.config.ts +59 -0
- package/templates/apps/api-auth/database/schema.ts +26 -0
- package/templates/apps/api-auth/package.json +28 -0
- package/templates/apps/api-auth/template.json +6 -0
- package/templates/apps/api-auth/tests/me.test.ts +37 -0
- package/templates/apps/api-auth/tsconfig.json +5 -0
- package/templates/apps/api-backend/README.md +25 -0
- package/templates/apps/api-backend/app.config.ts +39 -0
- package/templates/apps/api-backend/database/schema.ts +57 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend/package.json +28 -0
- package/templates/apps/api-backend/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend/template.json +6 -0
- package/templates/apps/api-backend/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend/tsconfig.json +5 -0
- package/templates/apps/api-backend-deactivation/README.md +49 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.server.ts +17 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +18 -0
- package/templates/apps/api-backend-deactivation/app.config.ts +20 -0
- package/templates/apps/api-backend-deactivation/database/schema.ts +41 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.server.ts +6 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +16 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.server.ts +10 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +19 -0
- package/templates/apps/api-backend-deactivation/package.json +28 -0
- package/templates/apps/api-backend-deactivation/template.json +6 -0
- package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +47 -0
- package/templates/apps/api-backend-deactivation/tsconfig.json +5 -0
- package/templates/apps/api-backend-mail/README.md +39 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.server.ts +15 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +21 -0
- package/templates/apps/api-backend-mail/app.config.ts +34 -0
- package/templates/apps/api-backend-mail/database/schema.ts +57 -0
- package/templates/apps/api-backend-mail/emails/welcome.email.tsx +106 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mail/package.json +30 -0
- package/templates/apps/api-backend-mail/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mail/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mail/template.json +6 -0
- package/templates/apps/api-backend-mail/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mail/tsconfig.json +5 -0
- package/templates/apps/api-backend-mariadb/.env.example +35 -0
- package/templates/apps/api-backend-mariadb/README.md +31 -0
- package/templates/apps/api-backend-mariadb/app.config.ts +68 -0
- package/templates/apps/api-backend-mariadb/database/schema.ts +57 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mariadb/package.json +30 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mariadb/template.json +6 -0
- package/templates/apps/api-backend-mariadb/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mariadb/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/README.md +86 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +20 -0
- package/templates/apps/api-backend-storage/app.config.ts +38 -0
- package/templates/apps/api-backend-storage/database/schema.ts +57 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-storage/package.json +27 -0
- package/templates/apps/api-backend-storage/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-storage/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-storage/template.json +6 -0
- package/templates/apps/api-backend-storage/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-storage/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/.env +17 -0
- package/templates/apps/api-data-advanced/README.md +88 -0
- package/templates/apps/api-data-advanced/app.config.ts +56 -0
- package/templates/apps/api-data-advanced/database/actors.entity.ts +14 -0
- package/templates/apps/api-data-advanced/database/authors.entity.ts +30 -0
- package/templates/apps/api-data-advanced/database/authors.relations.ts +15 -0
- package/templates/apps/api-data-advanced/database/books.entity.ts +59 -0
- package/templates/apps/api-data-advanced/database/books.relations.ts +10 -0
- package/templates/apps/api-data-advanced/database/index.ts +23 -0
- package/templates/apps/api-data-advanced/database/tenants.entity.ts +11 -0
- package/templates/apps/api-data-advanced/package.json +28 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.server.ts +18 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +40 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.server.ts +17 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +39 -0
- package/templates/apps/api-data-advanced/seeds/catalog.seed.ts +101 -0
- package/templates/apps/api-data-advanced/template.json +6 -0
- package/templates/apps/api-data-advanced/tests/queries.test.ts +124 -0
- package/templates/apps/api-data-advanced/tsconfig.json +5 -0
- package/templates/apps/api-durable/README.md +81 -0
- package/templates/apps/api-durable/aggregates/orderStats.aggregate.ts +49 -0
- package/templates/apps/api-durable/app.config.ts +41 -0
- package/templates/apps/api-durable/database/schema.ts +65 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.server.ts +43 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +23 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +45 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +40 -0
- package/templates/apps/api-durable/package.json +28 -0
- package/templates/apps/api-durable/schedules/nightlyReport.cron.tsx +37 -0
- package/templates/apps/api-durable/startup/warm.startup.tsx +33 -0
- package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +27 -0
- package/templates/apps/api-durable/template.json +6 -0
- package/templates/apps/api-durable/tests/orders.place.test.ts +91 -0
- package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +21 -0
- package/templates/apps/api-durable/tsconfig.json +5 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.server.tsx +103 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.tsx +25 -0
- package/templates/apps/api-feature-flags/README.md +63 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.server.ts +11 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +18 -0
- package/templates/apps/api-feature-flags/app.config.ts +41 -0
- package/templates/apps/api-feature-flags/database/schema.ts +37 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.server.ts +29 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +28 -0
- package/templates/apps/api-feature-flags/package.json +29 -0
- package/templates/apps/api-feature-flags/template.json +6 -0
- package/templates/apps/api-feature-flags/tests/notes.create.test.ts +67 -0
- package/templates/apps/api-feature-flags/tsconfig.json +5 -0
- package/templates/apps/api-governance/.env +4 -0
- package/templates/apps/api-governance/README.md +61 -0
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -0
- package/templates/apps/api-governance/actions/profiles.get.action.ts +17 -0
- package/templates/apps/api-governance/app.config.ts +35 -0
- package/templates/apps/api-governance/database/schema.ts +41 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.server.ts +17 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +21 -0
- package/templates/apps/api-governance/package.json +29 -0
- package/templates/apps/api-governance/template.json +6 -0
- package/templates/apps/api-governance/tests/profiles.create.test.ts +59 -0
- package/templates/apps/api-governance/tsconfig.json +5 -0
- package/templates/apps/api-kv/README.md +100 -0
- package/templates/apps/api-kv/actions/sync.pull.action.server.ts +75 -0
- package/templates/apps/api-kv/actions/sync.pull.action.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.server.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +22 -0
- package/templates/apps/api-kv/actions/sync.status.action.server.ts +28 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +17 -0
- package/templates/apps/api-kv/app.config.ts +48 -0
- package/templates/apps/api-kv/database/schema.ts +62 -0
- package/templates/apps/api-kv/package.json +28 -0
- package/templates/apps/api-kv/queries/events.list.query.server.ts +14 -0
- package/templates/apps/api-kv/queries/events.list.query.ts +23 -0
- package/templates/apps/api-kv/template.json +6 -0
- package/templates/apps/api-kv/tests/sync.test.ts +103 -0
- package/templates/apps/api-kv/tsconfig.json +5 -0
- package/templates/apps/api-moderation/README.md +46 -0
- package/templates/apps/api-moderation/app.config.ts +33 -0
- package/templates/apps/api-moderation/database/schema.ts +45 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.server.ts +8 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +20 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.server.ts +9 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +25 -0
- package/templates/apps/api-moderation/package.json +29 -0
- package/templates/apps/api-moderation/template.json +6 -0
- package/templates/apps/api-moderation/tests/posts.create.test.ts +79 -0
- package/templates/apps/api-moderation/tsconfig.json +5 -0
- package/templates/apps/api-observability/README.md +106 -0
- package/templates/apps/api-observability/app.config.ts +32 -0
- package/templates/apps/api-observability/database/schema.ts +38 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.server.ts +33 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +15 -0
- package/templates/apps/api-observability/package.json +29 -0
- package/templates/apps/api-observability/queries/notes.list.query.server.ts +7 -0
- package/templates/apps/api-observability/queries/notes.list.query.ts +14 -0
- package/templates/apps/api-observability/template.json +6 -0
- package/templates/apps/api-observability/tests/notes.create.test.ts +36 -0
- package/templates/apps/api-observability/tsconfig.json +5 -0
- package/templates/apps/api-ratelimit/README.md +49 -0
- package/templates/apps/api-ratelimit/app.config.ts +42 -0
- package/templates/apps/api-ratelimit/database/schema.ts +37 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.server.ts +16 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +34 -0
- package/templates/apps/api-ratelimit/package.json +29 -0
- package/templates/apps/api-ratelimit/template.json +6 -0
- package/templates/apps/api-ratelimit/tests/notes.create.test.ts +69 -0
- package/templates/apps/api-ratelimit/tsconfig.json +5 -0
- package/templates/apps/api-rbac/README.md +59 -0
- package/templates/apps/api-rbac/app.config.ts +48 -0
- package/templates/apps/api-rbac/database/schema.ts +37 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +22 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +26 -0
- package/templates/apps/api-rbac/package.json +29 -0
- package/templates/apps/api-rbac/template.json +6 -0
- package/templates/apps/api-rbac/tests/notes.create.test.ts +80 -0
- package/templates/apps/api-rbac/tsconfig.json +5 -0
- package/templates/apps/api-rest/README.md +85 -0
- package/templates/apps/api-rest/app.config.ts +70 -0
- package/templates/apps/api-rest/database/schema.ts +52 -0
- package/templates/apps/api-rest/lib/product.ts +30 -0
- package/templates/apps/api-rest/package.json +27 -0
- package/templates/apps/api-rest/routes/v1/products.create.route.tsx +45 -0
- package/templates/apps/api-rest/routes/v1/products.delete.route.tsx +25 -0
- package/templates/apps/api-rest/routes/v1/products.get.route.tsx +29 -0
- package/templates/apps/api-rest/routes/v1/products.list.route.tsx +43 -0
- package/templates/apps/api-rest/template.json +6 -0
- package/templates/apps/api-rest/tests/products.create.test.ts +53 -0
- package/templates/apps/api-rest/tsconfig.json +5 -0
- package/templates/apps/api-saas/README.md +106 -0
- package/templates/apps/api-saas/app.config.ts +51 -0
- package/templates/apps/api-saas/database/schema.ts +50 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.server.ts +52 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas/package.json +31 -0
- package/templates/apps/api-saas/queries/projects.list.query.server.ts +7 -0
- package/templates/apps/api-saas/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas/template.json +6 -0
- package/templates/apps/api-saas/tests/projects.create.test.ts +47 -0
- package/templates/apps/api-saas/tsconfig.json +5 -0
- package/templates/apps/api-search/README.md +68 -0
- package/templates/apps/api-search/app.config.ts +31 -0
- package/templates/apps/api-search/database/schema.ts +47 -0
- package/templates/apps/api-search/lib/search.ts +27 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.server.ts +19 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +35 -0
- package/templates/apps/api-search/package.json +29 -0
- package/templates/apps/api-search/queries/articles.list.query.server.ts +14 -0
- package/templates/apps/api-search/queries/articles.list.query.ts +19 -0
- package/templates/apps/api-search/seeds/articles.seed.ts +41 -0
- package/templates/apps/api-search/startup/searchBackfill.startup.tsx +24 -0
- package/templates/apps/api-search/template.json +6 -0
- package/templates/apps/api-search/tests/articles.create.test.ts +69 -0
- package/templates/apps/api-search/tsconfig.json +5 -0
- package/templates/apps/api-versioning/README.md +51 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.server.ts +13 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +14 -0
- package/templates/apps/api-versioning/actions/documents.history.action.server.ts +15 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +17 -0
- package/templates/apps/api-versioning/app.config.ts +24 -0
- package/templates/apps/api-versioning/database/schema.ts +38 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.server.ts +8 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +21 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +21 -0
- package/templates/apps/api-versioning/package.json +29 -0
- package/templates/apps/api-versioning/template.json +6 -0
- package/templates/apps/api-versioning/tests/documents.create.test.ts +37 -0
- package/templates/apps/api-versioning/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/.env +6 -0
- package/templates/apps/api-webhooks/README.md +106 -0
- package/templates/apps/api-webhooks/app.config.ts +16 -0
- package/templates/apps/api-webhooks/database/schema.ts +49 -0
- package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +22 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.server.ts +37 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +15 -0
- package/templates/apps/api-webhooks/package.json +28 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.server.ts +7 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +15 -0
- package/templates/apps/api-webhooks/template.json +6 -0
- package/templates/apps/api-webhooks/tests/orders.fulfill.test.ts +51 -0
- package/templates/apps/api-webhooks/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/webhooks/orders.webhook.tsx +35 -0
- package/templates/apps/changelog/README.md +77 -0
- package/templates/apps/changelog/app.config.ts +27 -0
- package/templates/apps/changelog/content/releases/0.1.0.mdx +19 -0
- package/templates/apps/changelog/content/releases/0.2.0.mdx +28 -0
- package/templates/apps/changelog/package.json +30 -0
- package/templates/apps/changelog/scripts/generate-rss.mjs +38 -0
- package/templates/apps/changelog/src/globals.css +66 -0
- package/templates/apps/changelog/src/globals.d.ts +16 -0
- package/templates/apps/changelog/src/lib/locale.test.ts +72 -0
- package/templates/apps/changelog/src/lib/locale.ts +55 -0
- package/templates/apps/changelog/src/lib/releases.ts +21 -0
- package/templates/apps/changelog/src/locales/de.ts +22 -0
- package/templates/apps/changelog/src/locales/en.ts +30 -0
- package/templates/apps/changelog/src/pages/[locale]/[slug].tsx +24 -0
- package/templates/apps/changelog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +95 -0
- package/templates/apps/changelog/src/pages/[slug].test.tsx +127 -0
- package/templates/apps/changelog/src/pages/[slug].tsx +52 -0
- package/templates/apps/changelog/src/pages/index.test.tsx +112 -0
- package/templates/apps/changelog/src/pages/index.tsx +55 -0
- package/templates/apps/changelog/src/pages/layout.tsx +84 -0
- package/templates/apps/changelog/template.json +6 -0
- package/templates/apps/changelog/tsconfig.json +11 -0
- package/templates/apps/edge-functions/README.md +55 -0
- package/templates/apps/edge-functions/functions/aiComplete.serverless.ts +58 -0
- package/templates/apps/edge-functions/functions/currencyConvert.serverless.ts +47 -0
- package/templates/apps/edge-functions/functions/geoGreeting.serverless.ts +44 -0
- package/templates/apps/edge-functions/functions/health.serverless.ts +29 -0
- package/templates/apps/edge-functions/functions/resolveLink.serverless.ts +32 -0
- package/templates/apps/edge-functions/functions/shareLink.serverless.ts +30 -0
- package/templates/apps/edge-functions/functions/slackNotify.serverless.ts +40 -0
- package/templates/apps/edge-functions/functions/verifySignature.serverless.ts +52 -0
- package/templates/apps/edge-functions/package.json +21 -0
- package/templates/apps/edge-functions/template.json +6 -0
- package/templates/apps/edge-functions/tsconfig.json +10 -0
- package/templates/apps/frontend-admin/README.md +40 -0
- package/templates/apps/frontend-admin/app.config.ts +37 -0
- package/templates/apps/frontend-admin/package.json +30 -0
- package/templates/apps/frontend-admin/src/config.ts +8 -0
- package/templates/apps/frontend-admin/src/globals.css +76 -0
- package/templates/apps/frontend-admin/src/globals.d.ts +6 -0
- package/templates/apps/frontend-admin/src/lib/admin.ts +16 -0
- package/templates/apps/frontend-admin/src/lib/auth.ts +24 -0
- package/templates/apps/frontend-admin/src/locales/de.ts +67 -0
- package/templates/apps/frontend-admin/src/locales/en.ts +79 -0
- package/templates/apps/frontend-admin/src/locales/index.ts +15 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.tsx +32 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +29 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.test.tsx +73 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.tsx +35 -0
- package/templates/apps/frontend-admin/src/pages/admin/[entity].tsx +121 -0
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +119 -0
- package/templates/apps/frontend-admin/src/pages/admin/error.tsx +20 -0
- package/templates/apps/frontend-admin/src/pages/admin/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.test.tsx +88 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.tsx +65 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.test.tsx +114 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +97 -0
- package/templates/apps/frontend-admin/src/pages/admin/loading.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/admin/not-found.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-admin/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-admin/template.json +6 -0
- package/templates/apps/frontend-admin/tsconfig.json +11 -0
- package/templates/apps/frontend-app/README.md +98 -0
- package/templates/apps/frontend-app/app.config.ts +43 -0
- package/templates/apps/frontend-app/package.json +30 -0
- package/templates/apps/frontend-app/src/locales/de.ts +36 -0
- package/templates/apps/frontend-app/src/locales/en.ts +43 -0
- package/templates/apps/frontend-app/src/locales/index.ts +15 -0
- package/templates/apps/frontend-app/src/pages/index.test.tsx +167 -0
- package/templates/apps/frontend-app/src/pages/index.tsx +136 -0
- package/templates/apps/frontend-app/src/pages/layout.tsx +41 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.test.tsx +99 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.tsx +74 -0
- package/templates/apps/frontend-app/template.json +6 -0
- package/templates/apps/frontend-app/tsconfig.json +11 -0
- package/templates/apps/frontend-blank/README.md +18 -0
- package/templates/apps/frontend-blank/app.config.ts +29 -0
- package/templates/apps/frontend-blank/package.json +29 -0
- package/templates/apps/frontend-blank/src/locales/de.ts +15 -0
- package/templates/apps/frontend-blank/src/locales/en.ts +22 -0
- package/templates/apps/frontend-blank/src/locales/index.ts +15 -0
- package/templates/apps/frontend-blank/src/pages/index.test.tsx +55 -0
- package/templates/apps/frontend-blank/src/pages/index.tsx +27 -0
- package/templates/apps/frontend-blank/src/pages/layout.test.tsx +54 -0
- package/templates/apps/frontend-blank/src/pages/layout.tsx +35 -0
- package/templates/apps/frontend-blank/template.json +6 -0
- package/templates/apps/frontend-blank/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/README.md +65 -0
- package/templates/apps/frontend-contact/app.config.ts +25 -0
- package/templates/apps/frontend-contact/functions/sendMessage.serverless.ts +69 -0
- package/templates/apps/frontend-contact/package.json +33 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.test.tsx +142 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +104 -0
- package/templates/apps/frontend-contact/src/config.ts +12 -0
- package/templates/apps/frontend-contact/src/globals.css +84 -0
- package/templates/apps/frontend-contact/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-contact/src/locales/de.ts +26 -0
- package/templates/apps/frontend-contact/src/locales/en.ts +29 -0
- package/templates/apps/frontend-contact/src/pages/[locale]/index.tsx +14 -0
- package/templates/apps/frontend-contact/src/pages/index.test.tsx +70 -0
- package/templates/apps/frontend-contact/src/pages/index.tsx +63 -0
- package/templates/apps/frontend-contact/src/pages/layout.tsx +65 -0
- package/templates/apps/frontend-contact/template.json +6 -0
- package/templates/apps/frontend-contact/tsconfig.json +11 -0
- package/templates/apps/frontend-dashboard/README.md +54 -0
- package/templates/apps/frontend-dashboard/app.config.ts +39 -0
- package/templates/apps/frontend-dashboard/package.json +29 -0
- package/templates/apps/frontend-dashboard/src/config.ts +8 -0
- package/templates/apps/frontend-dashboard/src/globals.css +72 -0
- package/templates/apps/frontend-dashboard/src/globals.d.ts +6 -0
- package/templates/apps/frontend-dashboard/src/lib/auth.ts +27 -0
- package/templates/apps/frontend-dashboard/src/locales/de.ts +49 -0
- package/templates/apps/frontend-dashboard/src/locales/en.ts +60 -0
- package/templates/apps/frontend-dashboard/src/locales/index.ts +15 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.tsx +34 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +32 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.test.tsx +74 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.tsx +40 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/error.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.tsx +38 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.test.tsx +88 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/loading.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/not-found.tsx +19 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.test.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.tsx +35 -0
- package/templates/apps/frontend-dashboard/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-dashboard/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-dashboard/template.json +6 -0
- package/templates/apps/frontend-dashboard/tsconfig.json +11 -0
- package/templates/apps/frontend-docs/README.md +19 -0
- package/templates/apps/frontend-docs/app.config.ts +25 -0
- package/templates/apps/frontend-docs/package.json +29 -0
- package/templates/apps/frontend-docs/src/globals.css +32 -0
- package/templates/apps/frontend-docs/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-docs/src/lib/locale.ts +56 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +24 -0
- package/templates/apps/frontend-docs/src/locales/en.ts +28 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug].tsx +18 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +67 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].test.tsx +115 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].tsx +82 -0
- package/templates/apps/frontend-docs/src/pages/index.test.tsx +90 -0
- package/templates/apps/frontend-docs/src/pages/index.tsx +45 -0
- package/templates/apps/frontend-docs/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-docs/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-docs/template.json +6 -0
- package/templates/apps/frontend-docs/tsconfig.json +11 -0
- package/templates/apps/frontend-i18n/README.md +61 -0
- package/templates/apps/frontend-i18n/app.config.ts +33 -0
- package/templates/apps/frontend-i18n/package.json +28 -0
- package/templates/apps/frontend-i18n/src/globals.css +46 -0
- package/templates/apps/frontend-i18n/src/globals.d.ts +6 -0
- package/templates/apps/frontend-i18n/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-i18n/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-i18n/src/locales/de.ts +24 -0
- package/templates/apps/frontend-i18n/src/locales/en.ts +25 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/about.tsx +11 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +50 -0
- package/templates/apps/frontend-i18n/src/pages/about.test.tsx +75 -0
- package/templates/apps/frontend-i18n/src/pages/about.tsx +31 -0
- package/templates/apps/frontend-i18n/src/pages/index.test.tsx +102 -0
- package/templates/apps/frontend-i18n/src/pages/index.tsx +43 -0
- package/templates/apps/frontend-i18n/src/pages/layout.test.tsx +86 -0
- package/templates/apps/frontend-i18n/src/pages/layout.tsx +70 -0
- package/templates/apps/frontend-i18n/template.json +6 -0
- package/templates/apps/frontend-i18n/tsconfig.json +11 -0
- package/templates/apps/frontend-landing/README.md +17 -0
- package/templates/apps/frontend-landing/app.config.ts +25 -0
- package/templates/apps/frontend-landing/package.json +29 -0
- package/templates/apps/frontend-landing/src/globals.css +23 -0
- package/templates/apps/frontend-landing/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-landing/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +24 -0
- package/templates/apps/frontend-landing/src/locales/en.ts +26 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +37 -0
- package/templates/apps/frontend-landing/src/pages/index.test.tsx +110 -0
- package/templates/apps/frontend-landing/src/pages/index.tsx +75 -0
- package/templates/apps/frontend-landing/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-landing/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-landing/template.json +6 -0
- package/templates/apps/frontend-landing/tsconfig.json +11 -0
- package/templates/apps/frontend-spa/README.md +45 -0
- package/templates/apps/frontend-spa/app.config.ts +27 -0
- package/templates/apps/frontend-spa/package.json +29 -0
- package/templates/apps/frontend-spa/src/globals.css +84 -0
- package/templates/apps/frontend-spa/src/locales/de.ts +22 -0
- package/templates/apps/frontend-spa/src/locales/en.ts +29 -0
- package/templates/apps/frontend-spa/src/locales/index.ts +15 -0
- package/templates/apps/frontend-spa/src/pages/index.test.tsx +137 -0
- package/templates/apps/frontend-spa/src/pages/index.tsx +123 -0
- package/templates/apps/frontend-spa/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-spa/template.json +6 -0
- package/templates/apps/frontend-spa/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr/README.md +68 -0
- package/templates/apps/frontend-ssr/app.config.ts +32 -0
- package/templates/apps/frontend-ssr/package.json +29 -0
- package/templates/apps/frontend-ssr/src/globals.css +67 -0
- package/templates/apps/frontend-ssr/src/locales/de.ts +41 -0
- package/templates/apps/frontend-ssr/src/locales/en.ts +54 -0
- package/templates/apps/frontend-ssr/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.test.tsx +69 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.tsx +54 -0
- package/templates/apps/frontend-ssr/src/pages/feed.test.tsx +73 -0
- package/templates/apps/frontend-ssr/src/pages/feed.tsx +64 -0
- package/templates/apps/frontend-ssr/src/pages/index.test.tsx +89 -0
- package/templates/apps/frontend-ssr/src/pages/index.tsx +72 -0
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +37 -0
- package/templates/apps/frontend-ssr/template.json +6 -0
- package/templates/apps/frontend-ssr/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr-api/README.md +50 -0
- package/templates/apps/frontend-ssr-api/app.config.ts +43 -0
- package/templates/apps/frontend-ssr-api/package.json +30 -0
- package/templates/apps/frontend-ssr-api/src/globals.css +38 -0
- package/templates/apps/frontend-ssr-api/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr-api/src/locales/de.ts +20 -0
- package/templates/apps/frontend-ssr-api/src/locales/en.ts +31 -0
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.test.tsx +105 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.tsx +83 -0
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +28 -0
- package/templates/apps/frontend-ssr-api/template.json +6 -0
- package/templates/apps/frontend-ssr-api/tsconfig.json +11 -0
- package/templates/apps/frontend-static-blog/README.md +49 -0
- package/templates/apps/frontend-static-blog/app.config.ts +34 -0
- package/templates/apps/frontend-static-blog/package.json +28 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.test.tsx +65 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +35 -0
- package/templates/apps/frontend-static-blog/src/content/posts.ts +64 -0
- package/templates/apps/frontend-static-blog/src/globals.css +75 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-static-blog/src/locales/de.ts +19 -0
- package/templates/apps/frontend-static-blog/src/locales/en.ts +26 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug].tsx +20 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +62 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].test.tsx +116 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].tsx +69 -0
- package/templates/apps/frontend-static-blog/src/pages/index.test.tsx +100 -0
- package/templates/apps/frontend-static-blog/src/pages/index.tsx +58 -0
- package/templates/apps/frontend-static-blog/src/pages/layout.tsx +63 -0
- package/templates/apps/frontend-static-blog/template.json +6 -0
- package/templates/apps/frontend-static-blog/tsconfig.json +11 -0
- package/templates/baselines/bare/.env.example +37 -0
- package/templates/baselines/bare/README.md +47 -0
- package/templates/baselines/bare/baseline.json +31 -0
- package/templates/baselines/bare/deploy/README.md +43 -0
- package/templates/baselines/bare/deploy/voltro.service.example +36 -0
- package/templates/baselines/compose/.env.example +46 -0
- package/templates/baselines/compose/README.md +69 -0
- package/templates/baselines/compose/baseline.json +51 -0
- package/templates/baselines/compose/docker/.dockerignore +38 -0
- package/templates/baselines/compose/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose/docker-compose.dev.yml +89 -0
- package/templates/baselines/compose/docker-compose.prod.yml +87 -0
- package/templates/baselines/compose/docker-compose.yml +41 -0
- package/templates/baselines/compose-mariadb/.env.example +57 -0
- package/templates/baselines/compose-mariadb/README.md +78 -0
- package/templates/baselines/compose-mariadb/baseline.json +51 -0
- package/templates/baselines/compose-mariadb/docker/.dockerignore +38 -0
- package/templates/baselines/compose-mariadb/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose-mariadb/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose-mariadb/docker/mariadb-init.sql +6 -0
- package/templates/baselines/compose-mariadb/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose-mariadb/docker-compose.dev.yml +117 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +114 -0
- package/templates/baselines/compose-mariadb/docker-compose.yml +79 -0
- package/templates/baselines/helm/.env.example +39 -0
- package/templates/baselines/helm/README.md +98 -0
- package/templates/baselines/helm/baseline.json +53 -0
- package/templates/baselines/helm/charts/voltro-app/.helmignore +10 -0
- package/templates/baselines/helm/charts/voltro-app/Chart.yaml +10 -0
- package/templates/baselines/helm/charts/voltro-app/templates/_helpers.tpl +36 -0
- package/templates/baselines/helm/charts/voltro-app/templates/configmap.yaml +13 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-api.yaml +120 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-web.yaml +45 -0
- package/templates/baselines/helm/charts/voltro-app/templates/ingress.yaml +37 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-service.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-statefulset.yaml +73 -0
- package/templates/baselines/helm/charts/voltro-app/templates/secret.yaml +33 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-api.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-web.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/values-dev.yaml +21 -0
- package/templates/baselines/helm/charts/voltro-app/values-prod.yaml +58 -0
- package/templates/baselines/helm/charts/voltro-app/values-staging.yaml +25 -0
- package/templates/baselines/helm/charts/voltro-app/values.yaml +109 -0
- package/templates/baselines/helm/deploy/README.md +94 -0
- package/templates/patches/@effect__cluster@0.59.0.patch +262 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// German catalog. Mirrors every key in `en.ts` — `defineLocale<typeof en>()`
|
|
2
|
+
// fails the build if a key is missing or misspelled. Translate the prose only;
|
|
3
|
+
// keep ICU placeholders (`{count}`, `{queryCall}`) and rich tags (`<code>`)
|
|
4
|
+
// identical to `en.ts`.
|
|
5
|
+
|
|
6
|
+
import { defineLocale } from '@voltro/i18n'
|
|
7
|
+
import en from './en'
|
|
8
|
+
|
|
9
|
+
export default defineLocale<typeof en>()({
|
|
10
|
+
'meta.home.title': '{count} Notizen — {{capProjectName}}',
|
|
11
|
+
'meta.home.description': 'Bei jeder Anfrage serverseitig aus der API gerendert.',
|
|
12
|
+
|
|
13
|
+
'lang.label': 'Sprache',
|
|
14
|
+
|
|
15
|
+
'home.badge': 'renderMode: ssr · aus der API gespeist',
|
|
16
|
+
'home.heading': 'Notizen',
|
|
17
|
+
'home.lead': 'Diese Zeilen stehen im serverseitig gerenderten HTML — der Loader hat sie mit {queryCall} aus der API geholt, bevor die Seite gesendet wurde. Danach übernimmt <code>useSubscription</code> für Live-Updates.',
|
|
18
|
+
'home.empty': 'Noch keine Notizen. Erstelle eine über die API (ihre <code>notes.create</code>-Mutation), aktualisiere, und sie steht im SSR-HTML.',
|
|
19
|
+
'home.done': '✓ erledigt',
|
|
20
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Base catalog — the source of truth for this app's UI strings. Every key here
|
|
2
|
+
// MUST be mirrored in `de.ts`; `defineLocale<typeof en>()` enforces that parity
|
|
3
|
+
// at build time, so a missing translation fails typecheck rather than silently
|
|
4
|
+
// falling back.
|
|
5
|
+
//
|
|
6
|
+
// Messages use ICU syntax: `{name}` placeholders and rich tags like
|
|
7
|
+
// `<code>…</code>` (rendered by passing a matching function to
|
|
8
|
+
// `<T values={{ code: (c) => <code>{c}</code> }} />`). A code snippet that
|
|
9
|
+
// itself contains `{`/`}` (e.g. `query('notes.list', {})`) is passed as a
|
|
10
|
+
// ReactNode value (`{queryCall}`) — NOT written into the message — so ICU never
|
|
11
|
+
// tries to parse its braces.
|
|
12
|
+
|
|
13
|
+
import { defineCatalog } from '@voltro/i18n'
|
|
14
|
+
|
|
15
|
+
export default defineCatalog({
|
|
16
|
+
// Tab title + description. `meta.home.title` is a plain lookup with a
|
|
17
|
+
// `{count}` token substituted in `meta({ loaderData })` (NOT ICU-formatted),
|
|
18
|
+
// and `{{capProjectName}}` is a scaffold token — both are fine here.
|
|
19
|
+
'meta.home.title': '{count} notes — {{capProjectName}}',
|
|
20
|
+
'meta.home.description': 'Server-rendered from the api on every request.',
|
|
21
|
+
|
|
22
|
+
// ---- Layout shell (pages/layout.tsx) ----
|
|
23
|
+
'lang.label': 'Language',
|
|
24
|
+
|
|
25
|
+
// ---- Home page (pages/index.tsx) ----
|
|
26
|
+
'home.badge': 'renderMode: ssr · fed by the api',
|
|
27
|
+
'home.heading': 'Notes',
|
|
28
|
+
'home.lead': 'These rows are in the server-rendered HTML — the loader fetched them from the api with {queryCall} before the page was sent. Then <code>useSubscription</code> takes over for live updates.',
|
|
29
|
+
'home.empty': 'No notes yet. Create one against the api (its <code>notes.create</code> mutation), refresh, and it’s in the SSR HTML.',
|
|
30
|
+
'home.done': '✓ done',
|
|
31
|
+
} as const)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Catalog lookup OUTSIDE React — for the page's `meta({ locale, loaderData })`
|
|
2
|
+
// locale-aware <title> / <meta description>. This app is cookie-i18n (no
|
|
3
|
+
// `/<locale>` URL prefix), so @voltro/web hands `meta` the active locale from
|
|
4
|
+
// the `voltro:lang` cookie. Inside React, use `<T>` / `useT()` via the
|
|
5
|
+
// framework's auto-wired <I18nProvider> instead.
|
|
6
|
+
|
|
7
|
+
import en from './en'
|
|
8
|
+
import de from './de'
|
|
9
|
+
|
|
10
|
+
// Both catalogs share `en`'s keys (defineLocale enforces the mirror), so a
|
|
11
|
+
// known-key lookup returns `string` — not `string | undefined` — which is what
|
|
12
|
+
// `PageMeta.title` requires under exactOptionalPropertyTypes.
|
|
13
|
+
export type Messages = Record<keyof typeof en, string>
|
|
14
|
+
|
|
15
|
+
export const getCatalog = (locale?: string): Messages =>
|
|
16
|
+
(locale === 'de' ? de : en) as Messages
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// `/` SSR-fed-by-the-api page. The loader pulls `notes.list` from the api's
|
|
4
|
+
// rpc via the server-only `query` fn; `meta` is a function of the loader data
|
|
5
|
+
// so the <title> reflects the real row count. The component renders the SSR'd
|
|
6
|
+
// snapshot, then upgrades to `useSubscription` on the client. We mock
|
|
7
|
+
// `@voltro/web` (loader hook) + `@voltro/client` (subscription).
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
10
|
+
import { act, createElement, type ReactNode } from 'react'
|
|
11
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
12
|
+
import { I18nProvider } from '@voltro/i18n'
|
|
13
|
+
import enCatalog from '../locales/en'
|
|
14
|
+
|
|
15
|
+
const loaderData = vi.fn<() => unknown>()
|
|
16
|
+
const useSubscription = vi.fn<() => { data: unknown }>()
|
|
17
|
+
vi.mock('@voltro/web', () => ({ useLoaderData: () => loaderData() }))
|
|
18
|
+
vi.mock('@voltro/client', () => ({ useSubscription: () => useSubscription() }))
|
|
19
|
+
|
|
20
|
+
const { default: Home, meta, loader, renderMode } = await import('./index')
|
|
21
|
+
|
|
22
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
23
|
+
|
|
24
|
+
interface Note { id: string; title: string; body: string; done: boolean }
|
|
25
|
+
|
|
26
|
+
let container: HTMLDivElement
|
|
27
|
+
let root: Root
|
|
28
|
+
|
|
29
|
+
// The page uses <T>, so it needs an <I18nProvider> (real English catalog).
|
|
30
|
+
const render = (node: ReactNode): void => {
|
|
31
|
+
container = document.createElement('div')
|
|
32
|
+
document.body.appendChild(container)
|
|
33
|
+
act(() => {
|
|
34
|
+
root = createRoot(container)
|
|
35
|
+
root.render(
|
|
36
|
+
createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
loaderData.mockReset()
|
|
43
|
+
useSubscription.mockReset()
|
|
44
|
+
useSubscription.mockReturnValue({ data: undefined })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
if (root) act(() => root.unmount())
|
|
49
|
+
container?.remove()
|
|
50
|
+
document.body.innerHTML = ''
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const sample: ReadonlyArray<Note> = [
|
|
54
|
+
{ id: 'n_1', title: 'First note', body: 'hello', done: false },
|
|
55
|
+
{ id: 'n_2', title: 'Second note', body: '', done: true },
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
describe('index — loader', () => {
|
|
59
|
+
test('drains `notes.list` from the api via the server-side query fn', async () => {
|
|
60
|
+
const query = vi.fn(async () => sample)
|
|
61
|
+
const data = await loader({ query } as never)
|
|
62
|
+
expect(query).toHaveBeenCalledWith('notes.list', {})
|
|
63
|
+
expect(data.notes).toEqual(sample)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('returns an empty list when no server-side query fn is present (client nav)', async () => {
|
|
67
|
+
const data = await loader({ query: undefined } as never)
|
|
68
|
+
expect(data.notes).toEqual([])
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('index — meta', () => {
|
|
73
|
+
test('bakes the row count into the <title>', () => {
|
|
74
|
+
expect(renderMode).toBe('ssr')
|
|
75
|
+
expect(meta({ locale: 'en', loaderData: { notes: sample } }).title).toContain('2 notes')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('index — render', () => {
|
|
80
|
+
test('renders the SSR snapshot rows when the subscription has no data yet', () => {
|
|
81
|
+
loaderData.mockReturnValue({ notes: sample })
|
|
82
|
+
render(createElement(Home))
|
|
83
|
+
const items = Array.from(container.querySelectorAll('li'))
|
|
84
|
+
expect(items).toHaveLength(2)
|
|
85
|
+
expect(container.textContent).toContain('First note')
|
|
86
|
+
expect(container.textContent).toContain('✓ done')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('the live subscription data overrides the SSR snapshot once connected', () => {
|
|
90
|
+
loaderData.mockReturnValue({ notes: sample })
|
|
91
|
+
useSubscription.mockReturnValue({
|
|
92
|
+
data: [{ id: 'n_9', title: 'Live note', body: '', done: false }],
|
|
93
|
+
})
|
|
94
|
+
render(createElement(Home))
|
|
95
|
+
const items = Array.from(container.querySelectorAll('li'))
|
|
96
|
+
expect(items).toHaveLength(1)
|
|
97
|
+
expect(container.textContent).toContain('Live note')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('shows the empty state when there are no notes', () => {
|
|
101
|
+
loaderData.mockReturnValue({ notes: [] })
|
|
102
|
+
render(createElement(Home))
|
|
103
|
+
expect(container.textContent).toContain('No notes yet')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// `/` — `renderMode: 'ssr'`, fed by the sibling api. This is the SSR-from-a-
|
|
2
|
+
// real-backend pattern: the loader runs server-side per request and pulls
|
|
3
|
+
// rows from the api BEFORE the HTML is sent, so the first paint + the
|
|
4
|
+
// <title> reflect real data (crawlable, no client round-trip). The component
|
|
5
|
+
// then upgrades the same data to live with `useSubscription`.
|
|
6
|
+
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import { useLoaderData, type LoaderFn, type PageMeta } from '@voltro/web'
|
|
9
|
+
import { useSubscription } from '@voltro/client'
|
|
10
|
+
import { T } from '@voltro/i18n'
|
|
11
|
+
import { getCatalog } from '../locales'
|
|
12
|
+
|
|
13
|
+
// The row shape `notes.list` streams. Kept local so the page is
|
|
14
|
+
// self-contained — you can also `import type { Note }` from the api's
|
|
15
|
+
// generated types (a type-only cross-app import is browser-safe).
|
|
16
|
+
interface Note {
|
|
17
|
+
readonly id: string
|
|
18
|
+
readonly title: string
|
|
19
|
+
readonly body: string
|
|
20
|
+
readonly done: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface HomeData {
|
|
24
|
+
readonly notes: ReadonlyArray<Note>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const renderMode = 'ssr' as const
|
|
28
|
+
|
|
29
|
+
// `query` is present ONLY server-side (ssr/isr). It invokes the api's rpc
|
|
30
|
+
// directly over POST /rpc, forwarding the request's session cookie so the
|
|
31
|
+
// SAME Subject + tenant resolve as the WS path. A streaming query is drained
|
|
32
|
+
// to its FIRST snapshot — here, the current notes for this tenant. Pass the
|
|
33
|
+
// row type so the result is typed without importing the api.
|
|
34
|
+
export const loader: LoaderFn<HomeData> = async ({ query }) => ({
|
|
35
|
+
notes: query ? await query<ReadonlyArray<Note>>('notes.list', {}) : [],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// `meta` as a function of the loader data AND the active locale → the <title>
|
|
39
|
+
// reflects real api data (SEO + social cards) in the visitor's language. It's a
|
|
40
|
+
// plain catalog lookup (not ICU-formatted), so the row count is substituted
|
|
41
|
+
// into the `{count}` token by hand.
|
|
42
|
+
export const meta = ({ locale, loaderData }: { readonly locale: string; readonly loaderData: HomeData }): PageMeta => ({
|
|
43
|
+
title: getCatalog(locale)['meta.home.title'].replace('{count}', String(loaderData.notes.length)),
|
|
44
|
+
description: getCatalog(locale)['meta.home.description'],
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export default function Home(): ReactNode {
|
|
48
|
+
// The SSR'd snapshot from the loader — correct on first paint.
|
|
49
|
+
const { notes: initial } = useLoaderData<HomeData>()
|
|
50
|
+
// Live data once the WebSocket connects. `data` is `undefined` during SSR,
|
|
51
|
+
// so the server renders `initial` and the client upgrades to `live` on connect.
|
|
52
|
+
const { data: live } = useSubscription<ReadonlyArray<Note>>('app', 'notes.list')
|
|
53
|
+
const notes = live ?? initial
|
|
54
|
+
const code = (c: ReactNode) => <code>{c}</code>
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<section>
|
|
58
|
+
<span className="badge"><T id="home.badge" /></span>
|
|
59
|
+
<h1><T id="home.heading" /></h1>
|
|
60
|
+
<p className="lead">
|
|
61
|
+
<T
|
|
62
|
+
id="home.lead"
|
|
63
|
+
values={{ code, queryCall: <code>{"query('notes.list', {})"}</code> }}
|
|
64
|
+
/>
|
|
65
|
+
</p>
|
|
66
|
+
{notes.length === 0 ? (
|
|
67
|
+
<p className="empty">
|
|
68
|
+
<T id="home.empty" values={{ code }} />
|
|
69
|
+
</p>
|
|
70
|
+
) : (
|
|
71
|
+
<ul className="notes">
|
|
72
|
+
{notes.map((note) => (
|
|
73
|
+
<li key={note.id}>
|
|
74
|
+
<span className="title">{note.title}</span>{' '}
|
|
75
|
+
{note.done ? <span className="done"><T id="home.done" /></span> : null}
|
|
76
|
+
{note.body ? <p className="body">{note.body}</p> : null}
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
)}
|
|
81
|
+
</section>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Root layout. Loads the stylesheet; the page provides the content.
|
|
2
|
+
// Don't render <html>/<head>/<body> here — the framework shell owns them.
|
|
3
|
+
//
|
|
4
|
+
// The app is bilingual (see `locales` in app.config.ts): <LocaleSwitcher>
|
|
5
|
+
// writes the `voltro:lang` cookie + reloads so the framework re-renders in the
|
|
6
|
+
// chosen language.
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import { useLocale, useT } from '@voltro/i18n'
|
|
9
|
+
import { LocaleSwitcher } from '@voltro/ui-shadcn'
|
|
10
|
+
import '../globals.css'
|
|
11
|
+
|
|
12
|
+
const LOCALES = [
|
|
13
|
+
{ code: 'en', label: 'English' },
|
|
14
|
+
{ code: 'de', label: 'Deutsch' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export default function Layout({ children }: { readonly children: ReactNode }): ReactNode {
|
|
18
|
+
const locale = useLocale()
|
|
19
|
+
const langLabel = useT('lang.label')
|
|
20
|
+
return (
|
|
21
|
+
<div className="shell">
|
|
22
|
+
<header style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '1rem' }}>
|
|
23
|
+
<LocaleSwitcher locales={LOCALES} current={locale} ariaLabel={langLabel} />
|
|
24
|
+
</header>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "frontend-ssr-api",
|
|
3
|
+
"kind": "web",
|
|
4
|
+
"summary": "Server-rendered pages fed by a sibling api — an `ssr` loader calls `ctx.query` over the api's POST /rpc so real rows are in the first paint and the <title>, then `useSubscription` upgrades the same data to live. Pairs with api-backend.",
|
|
5
|
+
"tags": ["web", "frontend", "ssr", "rpc", "fullstack", "seo"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "Bundler"
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*"],
|
|
10
|
+
"exclude": ["node_modules", "dist", ".framework"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Bilingual (en/de) static SSG blog (template: **frontend-static-blog**).
|
|
4
|
+
Every page is pre-rendered at build time — no server, no per-request
|
|
5
|
+
rendering — once PER LOCALE: English at the bare path, German under `/de`.
|
|
6
|
+
|
|
7
|
+
## Boot
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install
|
|
11
|
+
pnpm --filter @{{projectName}}/{{appName}} dev # dev server with HMR
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What this template demonstrates
|
|
15
|
+
|
|
16
|
+
| Concept | Where |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `renderMode = 'static'` (pre-render at build) | every page |
|
|
19
|
+
| `interactive = 'none'` (strip ALL JS) | `src/pages/index.tsx` |
|
|
20
|
+
| `interactive = 'islands'` (hydrate one widget) | `src/pages/blog/[slug].tsx` |
|
|
21
|
+
| `island()` + `hydrate: 'load'` strategy | `src/components/ReadingProgress.island.tsx` |
|
|
22
|
+
| `getStaticPaths` (enumerate dynamic routes) | `src/pages/blog/[slug].tsx` |
|
|
23
|
+
| `loader` + `useLoaderData<T>()` | `src/pages/blog/[slug].tsx` |
|
|
24
|
+
| `meta` as a function of loader data (per-post SEO) | `src/pages/blog/[slug].tsx` |
|
|
25
|
+
| A swappable content source (the "CMS") | `src/content/posts.ts` |
|
|
26
|
+
| Bilingual URL-prefix i18n (en + `/de`) | `src/lib/locale.ts`, `src/locales/{en,de}.ts` |
|
|
27
|
+
| `[locale]` mirror pages (per-locale SSG) | `src/pages/[locale]/**` |
|
|
28
|
+
| Cross-product `getStaticPaths` (locale × slug) | `src/pages/[locale]/blog/[slug].tsx` |
|
|
29
|
+
|
|
30
|
+
## Build + serve
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm --filter @{{projectName}}/{{appName}} build # → dist/ (one HTML file per post)
|
|
34
|
+
pnpm --filter @{{projectName}}/{{appName}} start # serve dist/ locally
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Because the output is pure static files, you can also push `dist/` to a
|
|
38
|
+
cheap CDN instead of running a server:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
voltro static deploy --host cloudflare-pages --project-name {{appName}}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Adding posts
|
|
45
|
+
|
|
46
|
+
Append to the array in `src/content/posts.ts`. `getStaticPaths` reads
|
|
47
|
+
the same array, so a new entry is pre-rendered on the next build — no
|
|
48
|
+
route config. In a real app, replace that array with a CMS / database /
|
|
49
|
+
filesystem read; nothing else changes.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Web app for the {{projectName}} project — a BILINGUAL (en/de) STATIC blog.
|
|
2
|
+
//
|
|
3
|
+
// Every page declares `renderMode = 'static'`, so `voltro build`
|
|
4
|
+
// pre-renders flat HTML at build time and `voltro start` (or any CDN
|
|
5
|
+
// via `voltro static deploy`) serves it with zero per-request work.
|
|
6
|
+
// `port` is allocated by the scaffolder from the project's portRange.
|
|
7
|
+
//
|
|
8
|
+
// Strategy B (URL-prefix i18n): the default locale lives at the bare path
|
|
9
|
+
// (`/`, `/blog/…`) and every other locale is prefixed (`/de`, `/de/blog/…`).
|
|
10
|
+
// `locales` is load-bearing — it tells `voltro build` to pre-render a
|
|
11
|
+
// per-locale HTML variant for every `[locale]/…` mirror page, each wrapped
|
|
12
|
+
// in the right catalog's <I18nProvider>. Cookie i18n can't translate a
|
|
13
|
+
// zero-hydration static page, so the URL strategy is the only one that works
|
|
14
|
+
// here. Every code listed MUST have a matching `src/locales/<code>.ts`. Keep
|
|
15
|
+
// this in sync with SUPPORTED_LOCALES / DEFAULT_LOCALE in src/lib/locale.ts.
|
|
16
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
17
|
+
|
|
18
|
+
// Public, build-time env (baked into the bundle; read via `publicEnv`).
|
|
19
|
+
// Never a secret here — a static site has no server to hide one.
|
|
20
|
+
export const env = defineEnv({
|
|
21
|
+
VOLTRO_PUBLIC_SITE_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
type: 'web' as const,
|
|
26
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
27
|
+
port: {{port}},
|
|
28
|
+
// Follow the visitor's OS theme, set before first paint (no flash).
|
|
29
|
+
theme: 'system' as const,
|
|
30
|
+
// The shipped catalogs. `defaultLocale` MUST be one of `locales`.
|
|
31
|
+
locales: ['en', 'de'] as const,
|
|
32
|
+
defaultLocale: 'en' as const,
|
|
33
|
+
env,
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"build": "voltro build .",
|
|
9
|
+
"start": "voltro start .",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"test": "voltro test"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@voltro/cli": "0.1.0",
|
|
15
|
+
"@voltro/client": "0.1.0",
|
|
16
|
+
"@voltro/env": "0.1.0",
|
|
17
|
+
"@voltro/i18n": "0.1.0",
|
|
18
|
+
"@voltro/web": "0.1.0",
|
|
19
|
+
"react": "^19.0.0",
|
|
20
|
+
"react-dom": "^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/react": "^19.0.0",
|
|
24
|
+
"@types/react-dom": "^19.0.0",
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"vitest": "^3.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The <ReadingProgress> island — a scroll-driven progress bar. We mock
|
|
4
|
+
// `@voltro/web`'s `island()` to the identity so the default export IS the raw
|
|
5
|
+
// component, then assert it renders the progress-bar element and recomputes its
|
|
6
|
+
// width on scroll.
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
9
|
+
import { act, createElement, type ComponentType, type ReactNode } from 'react'
|
|
10
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
11
|
+
|
|
12
|
+
vi.mock('@voltro/web', () => ({
|
|
13
|
+
island: (Component: ComponentType<unknown>) => Component,
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const { default: ReadingProgress } = await import('./ReadingProgress.island')
|
|
17
|
+
|
|
18
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
19
|
+
|
|
20
|
+
let container: HTMLDivElement
|
|
21
|
+
let root: Root
|
|
22
|
+
|
|
23
|
+
const render = (node: ReactNode): void => {
|
|
24
|
+
container = document.createElement('div')
|
|
25
|
+
document.body.appendChild(container)
|
|
26
|
+
act(() => {
|
|
27
|
+
root = createRoot(container)
|
|
28
|
+
root.render(node)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
if (root) act(() => root.unmount())
|
|
34
|
+
container?.remove()
|
|
35
|
+
document.body.innerHTML = ''
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('ReadingProgress island', () => {
|
|
39
|
+
test('renders the progress-bar element, hidden from assistive tech', () => {
|
|
40
|
+
render(createElement(ReadingProgress))
|
|
41
|
+
const bar = container.querySelector('.reading-progress') as HTMLElement
|
|
42
|
+
expect(bar).not.toBeNull()
|
|
43
|
+
expect(bar.getAttribute('aria-hidden')).toBe('true')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('starts at 0% when the document is not scrollable', () => {
|
|
47
|
+
render(createElement(ReadingProgress))
|
|
48
|
+
const bar = container.querySelector('.reading-progress') as HTMLElement
|
|
49
|
+
// jsdom reports scrollHeight === clientHeight (0), so max <= 0 → 0%.
|
|
50
|
+
expect(bar.style.width).toBe('0%')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('recomputes width to 100% at the bottom of a scrollable document', () => {
|
|
54
|
+
render(createElement(ReadingProgress))
|
|
55
|
+
const el = document.documentElement
|
|
56
|
+
Object.defineProperty(el, 'scrollHeight', { value: 2000, configurable: true })
|
|
57
|
+
Object.defineProperty(el, 'clientHeight', { value: 1000, configurable: true })
|
|
58
|
+
Object.defineProperty(el, 'scrollTop', { value: 1000, configurable: true })
|
|
59
|
+
act(() => {
|
|
60
|
+
window.dispatchEvent(new Event('scroll'))
|
|
61
|
+
})
|
|
62
|
+
const bar = container.querySelector('.reading-progress') as HTMLElement
|
|
63
|
+
expect(bar.style.width).toBe('100%')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// An ISLAND: a single interactive widget hydrated inside an otherwise-static
|
|
2
|
+
// page. The blog post page is `interactive: 'islands'`, so the framework
|
|
3
|
+
// renders the article once and attaches React ONLY to this component — the
|
|
4
|
+
// surrounding HTML never runs a React lifecycle.
|
|
5
|
+
//
|
|
6
|
+
// `hydrate: 'load'` wakes it immediately (it must track scrolling from the
|
|
7
|
+
// first frame). Other strategies: 'idle', 'visible' (on scroll-into-view),
|
|
8
|
+
// 'interaction' (on first pointer/key), 'never' (inert HTML forever).
|
|
9
|
+
|
|
10
|
+
import type { ReactNode } from 'react'
|
|
11
|
+
import { useEffect, useState } from 'react'
|
|
12
|
+
import { island } from '@voltro/web'
|
|
13
|
+
|
|
14
|
+
function ReadingProgress(): ReactNode {
|
|
15
|
+
const [pct, setPct] = useState(0)
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const onScroll = (): void => {
|
|
19
|
+
const el = document.documentElement
|
|
20
|
+
const max = el.scrollHeight - el.clientHeight
|
|
21
|
+
setPct(max > 0 ? Math.min(100, (el.scrollTop / max) * 100) : 0)
|
|
22
|
+
}
|
|
23
|
+
onScroll()
|
|
24
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
25
|
+
window.addEventListener('resize', onScroll)
|
|
26
|
+
return () => {
|
|
27
|
+
window.removeEventListener('scroll', onScroll)
|
|
28
|
+
window.removeEventListener('resize', onScroll)
|
|
29
|
+
}
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
return <div className="reading-progress" style={{ width: `${pct}%` }} aria-hidden="true" />
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default island(ReadingProgress, { name: 'ReadingProgress', hydrate: 'load' })
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The content source for the blog.
|
|
2
|
+
//
|
|
3
|
+
// This is a plain in-repo array — the simplest stand-in for a CMS. In a
|
|
4
|
+
// real app, swap the body of `getStaticPaths` + the page loader to read
|
|
5
|
+
// from your CMS / database / filesystem instead. NOTHING else changes:
|
|
6
|
+
// the build still pre-renders one HTML file per entry.
|
|
7
|
+
//
|
|
8
|
+
// Because this module is imported by both the index page (the list) and
|
|
9
|
+
// the [slug] page (the detail) AND by `getStaticPaths`, it is the single
|
|
10
|
+
// source of truth for "which posts exist".
|
|
11
|
+
|
|
12
|
+
export interface Post {
|
|
13
|
+
/** URL segment — `/blog/<slug>`. Must be unique + DNS-safe. */
|
|
14
|
+
readonly slug: string
|
|
15
|
+
readonly title: string
|
|
16
|
+
/** ISO date, shown + used for ordering. */
|
|
17
|
+
readonly date: string
|
|
18
|
+
/** One-line summary — feeds the list + the `<meta name="description">`. */
|
|
19
|
+
readonly excerpt: string
|
|
20
|
+
readonly readingMinutes: number
|
|
21
|
+
/** Body paragraphs, separated by a blank line. */
|
|
22
|
+
readonly body: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const posts: ReadonlyArray<Post> = [
|
|
26
|
+
{
|
|
27
|
+
slug: 'hello-static',
|
|
28
|
+
title: 'Why this blog ships zero JavaScript',
|
|
29
|
+
date: '2025-01-15',
|
|
30
|
+
excerpt: 'Every page is pre-rendered at build time, so the browser downloads HTML and nothing else.',
|
|
31
|
+
readingMinutes: 3,
|
|
32
|
+
body: `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.
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
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.`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
slug: 'islands-not-hydration',
|
|
40
|
+
title: 'Islands: hydrate the 5% that moves',
|
|
41
|
+
date: '2025-01-22',
|
|
42
|
+
excerpt: 'A static page with one interactive widget should ship one widget of JavaScript — not the whole page.',
|
|
43
|
+
readingMinutes: 4,
|
|
44
|
+
body: `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.
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
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.`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
slug: 'cms-to-ssg',
|
|
54
|
+
title: 'From CMS to static pages with getStaticPaths',
|
|
55
|
+
date: '2025-02-03',
|
|
56
|
+
excerpt: 'Enumerate your content once; the build emits one pre-rendered page per entry.',
|
|
57
|
+
readingMinutes: 3,
|
|
58
|
+
body: `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.
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
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.`,
|
|
63
|
+
},
|
|
64
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* Self-contained styles — plain CSS, no Tailwind / no kit dependency, so
|
|
2
|
+
* this template builds with nothing but @voltro/web. Swap in
|
|
3
|
+
* @voltro/ui-shadcn/tokens.css if you want the design system. */
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--bg: #ffffff;
|
|
7
|
+
--fg: #18181b;
|
|
8
|
+
--muted: #71717a;
|
|
9
|
+
--accent: #7c3aed;
|
|
10
|
+
--border: #e4e4e7;
|
|
11
|
+
--maxw: 42rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:root.dark {
|
|
15
|
+
--bg: #0a0a0a;
|
|
16
|
+
--fg: #fafafa;
|
|
17
|
+
--muted: #a1a1aa;
|
|
18
|
+
--accent: #a78bfa;
|
|
19
|
+
--border: #27272a;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
* { box-sizing: border-box; }
|
|
23
|
+
|
|
24
|
+
html, body { margin: 0; min-height: 100vh; }
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
background: var(--bg);
|
|
28
|
+
color: var(--fg);
|
|
29
|
+
font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main {
|
|
33
|
+
max-width: var(--maxw);
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
padding: 3rem 1.25rem 6rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
a { color: var(--accent); text-decoration: none; }
|
|
39
|
+
a:hover { text-decoration: underline; }
|
|
40
|
+
|
|
41
|
+
h1 { font-size: 2rem; line-height: 1.2; margin: 0 0 0.5rem; }
|
|
42
|
+
h2 { font-size: 1.1rem; margin: 0; }
|
|
43
|
+
|
|
44
|
+
.site-header {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
border-bottom: 1px solid var(--border);
|
|
48
|
+
padding: 1rem 1.25rem;
|
|
49
|
+
}
|
|
50
|
+
.site-header .brand { color: var(--fg); font-weight: 600; }
|
|
51
|
+
.site-header .spacer { flex: 1; }
|
|
52
|
+
.site-header .lang { display: flex; gap: 0.5rem; align-items: center; font-size: 0.85rem; }
|
|
53
|
+
.site-header .lang .label { color: var(--muted); }
|
|
54
|
+
.site-header .lang a { color: var(--fg); font-weight: 400; }
|
|
55
|
+
.site-header .lang a[aria-current='true'] { color: var(--accent); font-weight: 600; }
|
|
56
|
+
|
|
57
|
+
.muted { color: var(--muted); font-size: 0.9rem; }
|
|
58
|
+
|
|
59
|
+
.post-list { list-style: none; padding: 0; margin: 2rem 0 0; }
|
|
60
|
+
.post-list li { padding: 1.25rem 0; border-bottom: 1px solid var(--border); }
|
|
61
|
+
.post-list a { font-size: 1.25rem; font-weight: 600; color: var(--fg); }
|
|
62
|
+
.post-list p { margin: 0.35rem 0 0; color: var(--muted); }
|
|
63
|
+
|
|
64
|
+
article p { margin: 1.1rem 0; }
|
|
65
|
+
|
|
66
|
+
/* The reading-progress island — a fixed bar pinned to the top. */
|
|
67
|
+
.reading-progress {
|
|
68
|
+
position: fixed;
|
|
69
|
+
top: 0;
|
|
70
|
+
left: 0;
|
|
71
|
+
height: 3px;
|
|
72
|
+
background: var(--accent);
|
|
73
|
+
transition: width 80ms linear;
|
|
74
|
+
z-index: 50;
|
|
75
|
+
}
|