Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/guide/pages-routing/actions-and-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ For dynamic routes, pass `params` in the options:

```ts
// pages/users/[id].server.ts has an action
const form = useForm('/users/:id', { name: '' }, { params: { id: '42' } });
const form = useForm('/users/[id]', { name: '' }, { params: { id: '42' } });
<Form form={form} method="put">{/* submits to /users/42 */}</Form>
```

Expand Down Expand Up @@ -209,7 +209,7 @@ Actions and forms keep the current scroll position when updating the page in pla
Pass `preserveScroll: false` to reset scroll after an in-place update, or `true` to keep it even after a redirect:

```ts
await action('/users/:id?update', {
await action('/users/[id]?update', {
params: { id: '42' },
data: { name: 'Alice' },
preserveScroll: false,
Expand All @@ -233,15 +233,15 @@ export const actions = {
update: defineHandler.withValidator({
body: updateUserSchema,
})(async (c, { body }) => {
const { id } = c.req.param() as PageParams<'/users/:id'>;
const { id } = c.req.param() as PageParams<'/users/[id]'>;
await db
.update(users)
.set(body)
.where(eq(users.id, Number(id)));
}),

delete: defineHandler(async (c) => {
const { id } = c.req.param() as PageParams<'/users/:id'>;
const { id } = c.req.param() as PageParams<'/users/[id]'>;
await db.delete(users).where(eq(users.id, Number(id)));
}),
};
Expand Down Expand Up @@ -269,7 +269,7 @@ export const actions = {
When a page exports named actions, append `?actionName` to the URL:

```ts
const form = useForm('/users/:id?update', { name: '' }, { params: { id } });
const form = useForm('/users/[id]?update', { name: '' }, { params: { id } });
<Form form={form} method="put">{/* submits to /users/42?update */}</Form>
```

Expand All @@ -282,7 +282,7 @@ Use `action()` for a button or other mutation that does not need form state. It
```ts
import { action } from '@void/react'; // or "@void/vue", "@void/svelte", "@void/solid"

const result = await action('/users/:id?delete', {
const result = await action('/users/[id]?delete', {
params: { id: '42' },
method: 'DELETE',
});
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/pages-routing/head.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface Props {
}

export const loader = defineHandler<Props>(async (c) => {
const { slug } = c.req.param() as PageParams<'/posts/:slug'>;
const { slug } = c.req.param() as PageParams<'/posts/[slug]'>;
const post = await getPost(slug);
return { post };
});
Expand Down
18 changes: 9 additions & 9 deletions docs/guide/pages-routing/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ import { Link } from "@void/react";
import { routeUrl } from 'void/routes';

<Link href="/users">Users</Link>
<Link href={routeUrl('/users/:id', { params: { id } })}>View</Link>
<Link href={routeUrl('/users/[id]', { params: { id } })}>View</Link>
```

```vue [Vue]
Expand All @@ -175,7 +175,7 @@ import { routeUrl } from 'void/routes';

<template>
<Link href="/users">Users</Link>
<Link :href="routeUrl('/users/:id', { params: { id } })">View</Link>
<Link :href="routeUrl('/users/[id]', { params: { id } })">View</Link>
</template>
```

Expand All @@ -186,15 +186,15 @@ import { routeUrl } from 'void/routes';
</script>

<Link href="/users">Users</Link>
<Link href={routeUrl('/users/:id', { params: { id } })}>View</Link>
<Link href={routeUrl('/users/[id]', { params: { id } })}>View</Link>
```

```tsx [Solid]
import { Link } from "@void/solid";
import { routeUrl } from 'void/routes';

<Link href="/users">Users</Link>
<Link href={routeUrl('/users/:id', { params: { id } })}>View</Link>
<Link href={routeUrl('/users/[id]', { params: { id } })}>View</Link>
```

:::
Expand Down Expand Up @@ -313,7 +313,7 @@ import type { RouteDestination } from 'void/routes';

const router = useRouter();
const profile = {
pathname: '/users/:id',
pathname: '/users/[id]',
params: { id: 42 },
query: { tab: 'activity' },
} satisfies RouteDestination;
Expand All @@ -338,7 +338,7 @@ For dynamic route params, use `useParams()` from the same adapter package. Pass
import { useParams } from '@void/react';

export default function PostPage() {
const { id } = useParams<'/posts/:id'>(); // { id: string }
const { id } = useParams<'/posts/[id]'>(); // { id: string }
return <h1>Post {id}</h1>;
}
```
Expand All @@ -348,7 +348,7 @@ export default function PostPage() {
<script setup lang="ts">
import { useParams } from '@void/vue';

const params = useParams<'/posts/:id'>(); // { id: string }
const params = useParams<'/posts/[id]'>(); // { id: string }
</script>

<template>
Expand All @@ -361,7 +361,7 @@ const params = useParams<'/posts/:id'>(); // { id: string }
<script lang="ts">
import { useParams } from '@void/svelte';

const params = useParams<'/posts/:id'>(); // { id: string }
const params = useParams<'/posts/[id]'>(); // { id: string }
</script>

<h1>Post {params.id}</h1>
Expand All @@ -372,7 +372,7 @@ const params = useParams<'/posts/:id'>(); // { id: string }
import { useParams } from '@void/solid';

export default function PostPage() {
const params = useParams<'/posts/:id'>(); // { id: string }
const params = useParams<'/posts/[id]'>(); // { id: string }
return <h1>Post {params.id}</h1>;
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/ssg.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Dynamic pages with `getPrerenderPaths()` are prerendered for the returned param
// pages/blog/[slug].server.ts
import type { PageParams } from 'void/routes';

export async function getPrerenderPaths(): Promise<PageParams<'/blog/:slug'>[]> {
export async function getPrerenderPaths(): Promise<PageParams<'/blog/[slug]'>[]> {
return [{ slug: 'hello-world' }, { slug: 'getting-started' }];
}
```
Expand Down
61 changes: 35 additions & 26 deletions docs/guide/type-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,21 +127,21 @@ form.errors.foo; // ✗ TypeScript error
[Named actions](./pages-routing/actions-and-forms#named-actions) work the same way. The `?actionName` suffix selects the right types for that action:

```ts
const updateForm = useForm('/users/:id?update', { name: '' }, { params: { id } });
const deleteForm = useForm('/users/:id?delete', { id: '' }, { params: { id } });
const updateForm = useForm('/users/[id]?update', { name: '' }, { params: { id } });
const deleteForm = useForm('/users/[id]?delete', { id: '' }, { params: { id } });
// Each form's data and errors are typed from their respective validators
```

The `action()` helper gets the same type checking. See [Actions & Forms](./pages-routing/actions-and-forms) for the full API.

## Typed URLs and Navigation

Use `routeUrl()` from `void/routes` to build URLs from your app's page, HTTP, and WebSocket route patterns. For a page at `pages/users/[id].tsx`, `.vue`, or `.svelte`, the pattern is `/users/:id`:
Use `routeUrl()` from `void/routes` to build URLs from your app's page, HTTP, and WebSocket route patterns. For a page at `pages/users/[id].tsx`, `.vue`, or `.svelte`, the pattern is `/users/[id]`:

```ts
import { routeUrl } from 'void/routes';

const url = routeUrl('/users/:id', {
const url = routeUrl('/users/[id]', {
params: { id: 42 },
query: { tab: 'activity' },
hash: 'details',
Expand All @@ -151,13 +151,15 @@ const url = routeUrl('/users/:id', {

Your editor autocompletes route patterns and parameter names. TypeScript checks that the route exists and all required parameters are supplied. Parameter values accept strings or numbers; Void encodes them for the URL. Query keys are chosen by your app, and values can be strings, numbers, booleans, or arrays of those values. Arrays become repeated query parameters; `null` and `undefined` values are omitted.

Use bracket patterns with `routeUrl()`, typed `fetch()`, WebSocket `connect()`, page actions, and `useParams()`.

Pass the returned string to `Link`, a plain `<a>`, or `c.redirect()`. For programmatic navigation and `c.rewrite()`, you can pass a destination object directly:

```ts
import type { RouteDestination } from 'void/routes';

const profile = {
pathname: '/users/:id',
pathname: '/users/[id]',
params: { id: 42 },
query: { tab: 'activity' },
hash: 'details',
Expand All @@ -166,56 +168,63 @@ const profile = {

Destination objects use the same parameter checking and URL encoding as `routeUrl()`. See [Pages Routing](./pages-routing/overview.md#typed-navigation) for links and router methods, and [Rewrites](../reference/api/rewrites.md#c-rewrite-destination) for server navigation.

For a catch-all page such as `pages/files/[...path].tsx`, `.vue`, or `.svelte`, use `/files/:path{.+}` and pass the full path as one value:
For a catch-all page such as `pages/files/[...path].tsx`, `.vue`, or `.svelte`, use `/files/[...path]` and pass the full path as one value:

```ts
routeUrl('/files/[...path]', { params: { path: 'docs/get started' } });
// /files/docs/get%20started
```

For an unnamed catch-all (`[...]`), pass its value under the `'*'` key:

```ts
routeUrl('/files/:path{.+}', { params: { path: 'docs/get started' } });
routeUrl('/files/[...]', { params: { '*': 'docs/get started' } });
// /files/docs/get%20started
```

### Page params

Pass a page's URL pattern as the type argument of `useParams()` to type the params it reads. For a page at `pages/users/[id].tsx`, `.vue`, or `.svelte`, the pattern is `/users/:id`:
Pass a page's URL pattern as the type argument of `useParams()` to type the params it reads. For a page at `pages/users/[id].tsx`, `.vue`, or `.svelte`, the pattern is `/users/[id]`:

::: code-group

```tsx [React]
import { useParams } from '@void/react';

const { id } = useParams<'/users/:id'>(); // id: string
useParams<'/users/:id'>().idd; // Type error: no such param
useParams<'/userz/:id'>(); // Type error: no page matches
const { id } = useParams<'/users/[id]'>(); // id: string
useParams<'/users/[id]'>().idd; // Type error: no such param
useParams<'/userz/[id]'>(); // Type error: no page matches
```

```vue [Vue]
<script setup lang="ts">
import { useParams } from '@void/vue';

const params = useParams<'/users/:id'>();
const params = useParams<'/users/[id]'>();
params.id; // string
params.idd; // Type error: no such param
useParams<'/userz/:id'>(); // Type error: no page matches
useParams<'/userz/[id]'>(); // Type error: no page matches
</script>
```

```svelte [Svelte]
<script lang="ts">
import { useParams } from '@void/svelte';

const params = useParams<'/users/:id'>();
const params = useParams<'/users/[id]'>();
params.id; // string
params.idd; // Type error: no such param
useParams<'/userz/:id'>(); // Type error: no page matches
useParams<'/userz/[id]'>(); // Type error: no page matches
</script>
```

```tsx [Solid]
import { useParams } from '@void/solid';

const params = useParams<'/users/:id'>();
const params = useParams<'/users/[id]'>();
params.id; // string
params.idd; // Type error: no such param
useParams<'/userz/:id'>(); // Type error: no page matches
useParams<'/userz/[id]'>(); // Type error: no page matches
```

:::
Expand All @@ -229,7 +238,7 @@ On the server, `c.req.param('slug')` accepts any name. Read the params object th
```ts
import type { PageParams } from 'void/routes';

const { slug } = c.req.param() as PageParams<'/posts/:slug'>;
const { slug } = c.req.param() as PageParams<'/posts/[slug]'>;
```

### Prerender paths
Expand All @@ -240,7 +249,7 @@ On a dynamic page, annotate `getPrerenderPaths()` with `PageParams` for that pag
// pages/blog/[slug].server.ts
import type { PageParams } from 'void/routes';

export async function getPrerenderPaths(): Promise<PageParams<'/blog/:slug'>[]> {
export async function getPrerenderPaths(): Promise<PageParams<'/blog/[slug]'>[]> {
return [{ slug: 'hello-world' }];
}
```
Expand Down Expand Up @@ -282,7 +291,7 @@ import { routeUrl } from 'void/routes';

<Link href="/about">About</Link>
<Link href={`/users/${user.id}?tab=activity`}>Profile</Link>
<Link href={routeUrl('/users/:id', { params: { id } })}>Profile</Link>
<Link href={routeUrl('/users/[id]', { params: { id } })}>Profile</Link>
<Link href="/abuot">About</Link> // Type error: no route matches
```

Expand All @@ -295,7 +304,7 @@ import { routeUrl } from 'void/routes';
<template>
<Link href="/about">About</Link>
<Link :href="`/users/${user.id}?tab=activity`">Profile</Link>
<Link :href="routeUrl('/users/:id', { params: { id } })">Profile</Link>
<Link :href="routeUrl('/users/[id]', { params: { id } })">Profile</Link>
<!-- Type error: no route matches -->
<Link href="/abuot">About</Link>
</template>
Expand All @@ -309,7 +318,7 @@ import { routeUrl } from 'void/routes';

<Link href="/about">About</Link>
<Link href={`/users/${user.id}?tab=activity`}>Profile</Link>
<Link href={routeUrl('/users/:id', { params: { id } })}>Profile</Link>
<Link href={routeUrl('/users/[id]', { params: { id } })}>Profile</Link>
<!-- Type error: no route matches -->
<Link href="/abuot">About</Link>
```
Expand All @@ -320,13 +329,13 @@ import { routeUrl } from 'void/routes';

<Link href="/about">About</Link>
<Link href={`/users/${user.id}?tab=activity`}>Profile</Link>
<Link href={routeUrl('/users/:id', { params: { id } })}>Profile</Link>
<Link href={routeUrl('/users/[id]', { params: { id } })}>Profile</Link>
<Link href="/abuot">About</Link> // Type error: no route matches
```

:::

Links can also point to the current page (`?page=2`, `#comments`) and to `http://`, `https://`, `mailto:`, and `tel:` URLs. Relative paths such as `../settings` are rejected; use the absolute path. A fixed part with spaces or non-ASCII characters can be written as is (`/café menu`) or percent-encoded in uppercase, as `routeUrl()` returns it (`/caf%C3%A9%20menu`). A dynamic segment matches any text, including none, so `/users/:id` also accepts `/users/` and `/users/42/edit`. With a root dynamic page such as `pages/[slug].tsx`, `.vue`, or `.svelte`, a protocol-relative URL like `//example.com` passes too.
Links can also point to the current page (`?page=2`, `#comments`) and to `http://`, `https://`, `mailto:`, and `tel:` URLs. Relative paths such as `../settings` are rejected; use the absolute path. A fixed part with spaces or non-ASCII characters can be written as is (`/café menu`) or percent-encoded in uppercase, as `routeUrl()` returns it (`/caf%C3%A9%20menu`). A dynamic segment matches any text, including none, so `/users/[id]` also accepts `/users/` and `/users/42/edit`. With a root dynamic page such as `pages/[slug].tsx`, `.vue`, or `.svelte`, a protocol-relative URL like `//example.com` passes too.

The [SSE](./sse.md#client) and [live stream](./live.md#subscribe-from-the-browser) clients, `connectEventStream()` and `connectLiveStream()`, take one of your routes or an `http://` or `https://` URL for a stream on another origin. They reject same-page, `mailto:`, `tel:`, and `ws://` URLs. Cast a stream URL that TypeScript can't check with `as RouteUrl`; the cast turns off the check for that URL.

Expand All @@ -339,8 +348,8 @@ declare const id: string;
declare const dynamic: string;

c.redirect('/about');
c.redirect(routeUrl('/users/:id', { params: { id } }));
c.redirect({ pathname: '/users/:id', params: { id } });
c.redirect(routeUrl('/users/[id]', { params: { id } }));
c.redirect({ pathname: '/users/[id]', params: { id } });
c.redirect('https://example.com/login');
c.redirect(new URL('https://example.com/login'));
c.redirect(dynamic as Href);
Expand Down
8 changes: 5 additions & 3 deletions docs/guide/typed-fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ await fetch('/api/users', {
});

// Dynamic params interpolated from the route pattern
const user = await fetch('/api/users/:id', {
const user = await fetch('/api/users/[id]', {
params: { id: '42' },
});
```

Route patterns use the same brackets as your filenames.

## What Gets Type-Checked

The client constrains every part of the request:
Expand Down Expand Up @@ -64,7 +66,7 @@ Non-2xx responses throw a `FetchError`:
import { fetch, FetchError } from 'void/client';

try {
await fetch('/api/users/:id', { params: { id: '999' } });
await fetch('/api/users/[id]', { params: { id: '999' } });
} catch (e) {
if (e instanceof FetchError) {
console.log(e.status); // 404
Expand All @@ -81,7 +83,7 @@ try {
| `method` | `string` | HTTP method. Defaults to `"GET"`. |
| `body` | `object` | Request body (auto-serialized as JSON). |
| `query` | `Record<string, string>` | Query string parameters. |
| `params` | `Record<string, string>` | URL path parameters (`:id` segments). |
| `params` | `Record<string, string>` | URL path parameters (`[id]` segments). |
| `headers` | `HeadersInit` | Additional request headers. |
| `signal` | `AbortSignal` | Abort signal. |
| `baseURL` | `string` | Base URL prepended to the path. Useful for external calls. |
Expand Down
Loading
Loading