Pages & dynamic segments
Filesystem → URL mapping, dynamic [id] segments, catch-all [...slug] queries, query params, and private files.
A page is any *.tsx file under src/pages/ that's not a special file (layout.tsx, error.tsx, loading.tsx, not-found.tsx) and doesn't start with _. Its default export is the page component; the URL comes from the file path.
A static page
// src/pages/about.tsx → /about
import type { ReactNode } from 'react'
export default function About(): ReactNode {
return (
<div className="max-w-2xl mx-auto py-12 px-6">
<h1 className="text-3xl font-bold">About us</h1>
<p>Voltro Cloud is a framework for shipping multi-tenant SaaS.</p>
</div>
)
}That's it. Save the file, the CLI's discovery sees it on next save, the page is live at /about.
index files
index.tsx maps to the directory's URL:
src/pages/index.tsx → /
src/pages/users/index.tsx → /users
src/pages/admin/index.tsx → /adminDynamic segments
Brackets in the filename are dynamic. The captured value comes through useParams<T>():
// src/pages/users/[id].tsx → /users/:id
import { useParams } from '@voltro/web'
export default function User() {
const { id } = useParams<{ id: string }>()
return <h1>User {id}</h1>
}Multiple dynamic segments in one path:
src/pages/orgs/[orgId]/projects/[projectId].tsx
// → /orgs/:orgId/projects/:projectId
const { orgId, projectId } = useParams<{ orgId: string; projectId: string }>()Catch-all queries
[...name] captures one OR more URL segments as a single param value (joined by /):
// src/pages/docs/[...slug].tsx → /docs/<anything>
const { slug } = useParams<{ slug: string }>()
// /docs/intro/getting-started → slug = "intro/getting-started"Optional catch-all (matches the base URL too):
// src/pages/docs/[[...slug]].tsx
// /docs → slug = ""
// /docs/foo → slug = "foo"
// /docs/foo/bar → slug = "foo/bar"Priority
When multiple files could match (static, dynamic, catch-all), priority is:
- Static segments win over dynamic.
- Dynamic single (
[id]) wins over catch-all ([...slug]). - Optional catch-all (
[[...slug]]) wins over required catch-all ([...slug]) — the optional form scores as more specific, so it matches first.
src/pages/users/index.tsx # /users → wins for /users
src/pages/users/[id].tsx # /users/:id → wins for /users/42
src/pages/users/new.tsx # /users/new → wins (static beats dynamic)
src/pages/[...rest].tsx # everything elseQuery strings
Voltro doesn't bake query params into the query — they're orthogonal to the URL pattern:
import { useLocation } from '@voltro/web'
const Page = () => {
const pathname = useLocation()
// …
// For the search string, parse it from the request URL via useServerRequest()
// (SSR) or window.location.search (client after hydration).
}For SSR pages that need server-side query parsing:
import { useServerRequest } from '@voltro/web'
export const renderMode = 'ssr' as const
export default function SearchPage() {
const req = useServerRequest()
const q = req
? new URL(req.url, 'http://x').searchParams.get('q') ?? ''
: new URLSearchParams(window.location.search).get('q') ?? ''
// …
}Parse the query string explicitly via useServerRequest() on the server and window.location.search on the client, as shown above.
Private files
Files starting with _ are skipped by discovery — they're helpers next to pages:
src/pages/
├── _components/
│ └── UserCard.tsx # NOT a route — import from siblings
├── users/
│ ├── _helpers.ts # NOT a route
│ └── [id].tsx # → /users/:idYou can also use the colocation pattern: keep page-specific components in a directory named with _ prefix.
Trailing slashes
The canonical form is no trailing slash — always link with <Link to="/about">, not <Link to="/about/">.
The framework does NOT emit a trailing-slash redirect on its own. If you need /about/ → /about normalisation (for SEO), configure a 301 redirect at your reverse proxy.
What pages CAN'T do
- Live outside
src/pages/. Discovery walks one root. Helpers + components go elsewhere; pages go here. - Have multiple default exports. One page per file.
- Be
.tsfiles. Pages must be.tsx— React components only. - Be discovered via dynamic import. The CLI generates the import statements at boot; runtime adds need a re-discover (which
voltro devdoes on save).
Where to read next
- Layouts & route groups — wrap pages in shared chrome
- Loaders & meta — server-side data +
<head>tags - Render modes —
static/ssr/isr