Skip to content
Merged
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Deploy hosted production before publishing the npm packages: the CLI checks the
## Best Practices That Must Be Followed

- **Do not use Python for repository scripts or automation**, including inline CI scripts and ad-hoc maintenance commands. Use Node.js with TypeScript/JavaScript, or shell/PowerShell where appropriate.
- **Keep user-facing documentation focused on using Void.** Explain public APIs, configuration, observable behavior, and actions users need to take. Do not add implementation details, internal workarounds, defensive checks, normalization rules, or bug-fix history unless they help users make a decision or resolve an actionable problem. Maintainer-only knowledge belongs in source comments, repository/package `AGENTS.md`, or contributor guides. This applies to app guides, API/CLI references, consumer-facing READMEs, and shipped app-development skills.
- **Keep user-facing documentation focused on using Void.** Explain public APIs, configuration, observable behavior, and actions users need to take. Do not add implementation details, internal workarounds, defensive checks, normalization rules, or bug-fix history unless they help users make a decision or resolve an actionable problem. Maintainer-only knowledge belongs in source comments, repository/package `AGENTS.md`, or contributor guides. This applies to app guides, API/CLI references, consumer-facing READMEs, and shipped app-development skills. Code examples should type-check. When an API does not infer a route, param, or path type, annotate the example with the public type that checks it, such as `PageParams`, `RouteUrl`, or `routeUrl()`.
- **Keep Pages examples framework-agnostic.** Void Pages supports React, Vue, Svelte, and Solid. When a docs example uses an adapter API and the syntax differs, show all four in a `::: code-group` with those tab labels. When the code is the same, write it once; if only the import path differs, name the other packages in a comment (`@void/react`, `@void/vue`, `@void/svelte`, `@void/solid`). Do not use one framework's component syntax, import, or file extension as the only example. Keep a single framework when the section is that framework's own API, a labeled custom SSR renderer, a JSX route handler, or an integration guide. Do not wrap a one-snippet example in a code group.
- **Keep native Cloudflare deploys Workers Free-compatible by default.** New public Void SDK/runtime features must work with `void deploy --platform cloudflare` on Workers Free unless the underlying Cloudflare product has no Free offering. Paid-only functionality must be explicitly opt-in, inferred and emitted only when the app uses it, and checked before provisioning or building. Its documentation and deploy error must name the required Cloudflare product/plan, link to enable it, and explain any API-token permissions. Platform-internal paid services must never become requirements of a single-app native Cloudflare deploy. Add regression coverage proving that an app which does not use the paid feature emits no paid binding and performs no entitlement probe.
- Every implementation plan should include the following:
Expand Down
6 changes: 4 additions & 2 deletions docs/guide/edge/revalidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,15 @@ Purge the destination pathname with `revalidate({ paths })`. Purging `/en/docs/f
You can purge ISR cache entries programmatically from a route handler:

```ts
import { defineHandler } from 'void';
import { revalidate } from 'void/isr';
import type { RouteUrl } from 'void/routes';

export const POST = defineHandler(async (c) => {
// ... update content in database ...

// Purge specific pages
await revalidate({ paths: ['/', '/blog/hello-world'] });
// Purge specific pages. `satisfies RouteUrl[]` checks the paths when routing.typedRoutes is on.
await revalidate({ paths: ['/', '/blog/hello-world'] satisfies RouteUrl[] });

// Or purge all cached pages
await revalidate({ all: true });
Expand Down
15 changes: 9 additions & 6 deletions docs/guide/pages-routing/actions-and-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export const action = defineHandler.withValidator({
});
```

| Action returns | Behavior |
| --------------------- | ----------------------------------------------------- |
| Nothing (void) | Re-runs the loader, page re-renders with fresh props. |
| `c.redirect('/path')` | Navigates to another page. |
| Action returns | Behavior |
| ----------------- | ------------------------------------------------------------------------------------------------------------ |
| Nothing (void) | Re-runs the loader, page re-renders with fresh props. |
| `c.redirect(url)` | Navigates to another page. Build `url` with `routeUrl()`, or annotate a full path with `satisfies RouteUrl`. |

Middleware does not run again after the action. To change [shared data](./layouts.md#shared-data), update it in the action or redirect.

Expand Down Expand Up @@ -227,19 +227,22 @@ When a page needs multiple mutations, such as updating and deleting a user on th
import { users, updateUserSchema } from '@schema';
import { defineHandler } from 'void';
import { db, eq } from 'void/db';
import type { PageParams } from 'void/routes';

export const actions = {
update: defineHandler.withValidator({
body: updateUserSchema,
})(async (c, { body }) => {
const { id } = c.req.param() as PageParams<'/users/:id'>;
await db
.update(users)
.set(body)
.where(eq(users.id, Number(c.req.param('id'))));
.where(eq(users.id, Number(id)));
}),

delete: defineHandler(async (c) => {
await db.delete(users).where(eq(users.id, Number(c.req.param('id'))));
const { id } = c.req.param() as PageParams<'/users/:id'>;
await db.delete(users).where(eq(users.id, Number(id)));
}),
};
```
Expand Down
4 changes: 3 additions & 1 deletion docs/guide/pages-routing/head.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ Export a `head()` function from your `.server.ts` file. It receives the Hono con
```ts
// pages/posts/[slug].server.ts
import { defineHandler, defineHead } from 'void';
import type { PageParams } from 'void/routes';

export interface Props {
post: { title: string; excerpt: string; slug: string };
}

export const loader = defineHandler<Props>(async (c) => {
const post = await getPost(c.req.param('slug'));
const { slug } = c.req.param() as PageParams<'/posts/:slug'>;
const post = await getPost(slug);
return { post };
});

Expand Down
31 changes: 30 additions & 1 deletion docs/guide/type-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ The pattern must be one of your pages; HTTP and WebSocket routes are not accepte

The pattern is checked by TypeScript only. Pass the pattern of the page the component renders in; without a pattern, `useParams()` returns `Record<string, string>`.

On the server, `c.req.param('slug')` accepts any name. Read the params object through `PageParams` so a missing or misspelled name is a type error:

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

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

### Prerender paths

On a dynamic page, annotate `getPrerenderPaths()` with `PageParams` for that page's URL pattern. Each returned object must then include the page's params, and a missing or misspelled key is a type error. Void does not infer this type from the filename, so the check applies only when the annotation is present:
Expand All @@ -239,6 +247,14 @@ export async function getPrerenderPaths(): Promise<PageParams<'/blog/:slug'>[]>

In custom SSR mode the function returns full path strings, so annotate it as `Promise<string[]>`. See [Edge Prerendering](./edge/prerendering.md#dynamic-pages-with-params) and [Static Site Generation](./ssg.md#per-page-overrides).

`revalidate({ paths })` and `routing.prerender` are lists of plain strings. Annotate the array with `satisfies RouteUrl[]` so each path has to match a route. The check applies when [`routing.typedRoutes`](../reference/config.md#routing-typedroutes) is on; otherwise `RouteUrl` is `string`:

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

await revalidate({ paths: ['/', '/blog/hello-world'] satisfies RouteUrl[] });
```

### Checked URL strings

[`routing.typedRoutes`](../reference/config.md#routing-typedroutes) checks plain URL strings in `Link`'s `href`, `router.visit()`, `router.prefetch()`, `router.flush()`, and `c.rewrite()`. Apps created with `void init` have it on. To turn it on in an existing app:
Expand Down Expand Up @@ -308,9 +324,19 @@ import { routeUrl } from 'void/routes';

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.

`c.redirect()` is not part of that check. It still accepts any string. Build an internal path with `routeUrl()`, or annotate a full path with `satisfies RouteUrl`. With `routing.typedRoutes` on, a path that matches no route is a type error. An external URL stays a plain string:

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

c.redirect(routeUrl('/users/:id', { params: { id } }));
c.redirect('/users/42' satisfies RouteUrl);
c.redirect('https://example.com/login');
```

`void/routes` exports two types for these strings:

- `RouteUrl`: the URL of one of your routes. `routeUrl()` returns it, and `c.rewrite()` accepts it.
- `RouteUrl`: the URL of one of your routes. `routeUrl()` returns it, and `c.rewrite()` accepts it. Annotate a `c.redirect()` path or a prerender or revalidate path list with it.
- `Href`: anything a link can point to: a `RouteUrl`, a same-page query or hash, or an external URL.

Cast a string that TypeScript can't check, such as a path from your CMS or a file in `public/`:
Expand Down Expand Up @@ -376,8 +402,11 @@ Route types update with the dev server. For CI or standalone typechecking, run `
| Fetch client | Route paths, HTTP methods, `body`/`query`/`params`, response type |
| Navigation | Route patterns and required parameter names and values in `routeUrl()` and destination objects |
| Links | With `routing.typedRoutes`, URL strings in `Link`, router methods, and `c.rewrite()` |
| `c.redirect` | An internal path built with `routeUrl()`, or a full path annotated with `satisfies RouteUrl` |
| `useParams` | Page patterns and the names of the params they return |
| `c.req.param` | Page param names, when the result is annotated with `PageParams` |
| `getPrerenderPaths` | Page param names, when the return type is annotated with `PageParams` |
| `revalidate` | `paths`, and `routing.prerender`, when the array is annotated with `satisfies RouteUrl[]` |
| `useForm` | Action URLs, `data` fields, `errors` keys, `reset()` args (including `{ initial: true }`), `params`, `resetOnSuccess` |
| `action()` | Action URLs, `data` payload, `params` |

Expand Down
4 changes: 3 additions & 1 deletion docs/reference/api/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,13 @@ Type-safe wrapper for the page `head()` export in `.server.ts` files. Provides [
```ts
import { defineHandler, defineHead } from 'void';
import type { InferProps } from 'void';
import type { PageParams } from 'void/routes';

export type Props = InferProps<typeof loader>;

export const loader = defineHandler(async (c) => {
const post = await getPost(c.req.param('slug'));
const { slug } = c.req.param() as PageParams<'/posts/:slug'>;
const post = await getPost(slug);
return { post };
});

Expand Down
5 changes: 4 additions & 1 deletion docs/reference/api/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,14 @@ Revalidate ISR-cached pages on demand. In local development this is a no-op beca

```ts
import { revalidate } from 'void/isr';
import type { RouteUrl } from 'void/routes';

await revalidate({ paths: ['/', '/blog/hello'] });
await revalidate({ paths: ['/', '/blog/hello'] satisfies RouteUrl[] });
await revalidate({ all: true });
```

`paths` is `string[]`. Annotate the array with `satisfies RouteUrl[]` so TypeScript checks each path when [`routing.typedRoutes`](../config.md#routing-typedroutes) is on.

**Signature:**

```ts
Expand Down
6 changes: 4 additions & 2 deletions docs/reference/api/rewrites.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ return c.rewrite({

With [`routing.typedRoutes`](../config.md#routing-typedroutes), a string destination must be a [`RouteUrl`](./types.md#route-types): a URL that matches one of your routes. Cast a path built at runtime, such as ``c.rewrite(`/${locale}${path}` as RouteUrl)``.

For a typed redirect, build the URL with [`routeUrl()`](./types.md#routeurl-path-options):
For a typed redirect, build the URL with [`routeUrl()`](./types.md#routeurl-path-options). `c.redirect()` still accepts any string, so a full path is checked only when you annotate it with [`RouteUrl`](./types.md#route-types). That check applies when [`routing.typedRoutes`](../config.md#routing-typedroutes) is on. An external URL stays a plain string:

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

return c.redirect(routeUrl('/users/:id', { params: { id: 42 } }));
return c.redirect('/users/42' satisfies RouteUrl);
return c.redirect('https://example.com/login');
```

`c.redirect()` sends the location it is given. The request query is not added to an internal path, a protocol-relative URL, or an absolute URL, so `c.redirect('/notes')` from `/sign-in?redirectTo=/notes` goes to `/notes`. [`routing.redirects`](../../guide/edge/redirects.md) still merge the request query into the destination.
Expand Down
10 changes: 9 additions & 1 deletion docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ routing: {

#### `routing.typedRoutes`

Type-check URL strings against your app's routes in `Link`'s `href`, `router.visit()`, `router.prefetch()`, `router.flush()`, and `c.rewrite()`. Omitted or `false` means off; `void init` turns it on for new apps. It changes types only, not navigation at runtime. Strings may also be same-page queries or hashes, or `http://`, `https://`, `mailto:`, and `tel:` URLs. See [Checked URL strings](../guide/type-safety#checked-url-strings).
Type-check URL strings against your app's routes in `Link`'s `href`, `router.visit()`, `router.prefetch()`, `router.flush()`, and `c.rewrite()`. Omitted or `false` means off; `void init` turns it on for new apps. It changes types only, not navigation at runtime. Strings may also be same-page queries or hashes, or `http://`, `https://`, `mailto:`, and `tel:` URLs. `c.redirect()`, `revalidate({ paths })`, and `routing.prerender` stay plain strings until you annotate them. See [Checked URL strings](../guide/type-safety#checked-url-strings).

```ts
routing: {
Expand Down Expand Up @@ -505,6 +505,14 @@ Paths to prerender as static HTML at deploy time. Each path must start with `/`.
}
```

In `void.config.ts`, annotate the array with [`RouteUrl`](./api/types.md#route-types) so TypeScript checks the paths when [`routing.typedRoutes`](#routing-typedroutes) is on:

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

prerender: ['/', '/about', '/pricing'] satisfies RouteUrl[],
```

#### `routing.notFound`

Override how the asset layer answers a request that matched no asset and no worker route. One of `"single-page-application"`, `"404-page"`, or `"none"`. If omitted, Void infers it — see [Static Assets](../guide/edge/static-assets.md#navigation-and-middleware).
Expand Down
Loading