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   → /admin

Dynamic 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:

  1. Static segments win over dynamic.
  2. Dynamic single ([id]) wins over catch-all ([...slug]).
  3. 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 else

Query 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/:id

You 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 .ts files. 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 dev does on save).