diff --git a/AGENTS.md b/AGENTS.md
index 760c468ae..61025dc69 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -19,6 +19,7 @@ Deploy hosted production before publishing the npm packages: the CLI checks the
- **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 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:
- During plan generation: ask clarifying questions when necessary, especially around developer-experience defining design decisions.
diff --git a/docs/guide/pages-routing/markdown.md b/docs/guide/pages-routing/markdown.md
index ed4b4c04f..e53b25bb4 100644
--- a/docs/guide/pages-routing/markdown.md
+++ b/docs/guide/pages-routing/markdown.md
@@ -136,13 +136,13 @@ Welcome to Void. Here's an interactive demo:
```
-- **`
+
+
+ Filtered users
+ Users
+ Sign out
+ Settings
+
+```
+
+```svelte [Svelte]
+
+
+Filtered users
+
+Users
+
+Sign out
+
+ {
+ if (!confirm('Leave this page?')) {
+ event.preventDefault();
+ }
+ }}
+>
+ Settings
+
+```
+
+```tsx [Solid]
+import { Link } from '@void/solid';
+
Filtered users
@@ -228,6 +301,8 @@ With [`routing.typedRoutes`](../type-safety.md#checked-url-strings), which apps
```
+:::
+
`prefetch` and `reloadDocument` work only with GET links. GET `data` becomes query parameters; use scalar values or arrays.
For programmatic navigation, use `useRouter()`. Pass a typed destination object to `visit()`, `prefetch()`, or `flush()`:
@@ -256,7 +331,9 @@ You can also pass URL strings or `URL` instances to these router methods. See [T
For dynamic route params, use `useParams()` from the same adapter package. Pass the page's URL pattern as the type argument to type the result:
-```tsx
+::: code-group
+
+```tsx [React]
// pages/posts/[id].tsx
import { useParams } from '@void/react';
@@ -266,6 +343,42 @@ export default function PostPage() {
}
```
+```vue [Vue]
+
+
+
+
+
;
+}
+```
+
+:::
+
See [Page params](../type-safety.md#page-params) for how patterns are checked.
### Active links
diff --git a/docs/guide/type-safety.md b/docs/guide/type-safety.md
index 261491c0c..c9d44a8a2 100644
--- a/docs/guide/type-safety.md
+++ b/docs/guide/type-safety.md
@@ -61,14 +61,46 @@ export const loader = defineHandler(async (c) => {
});
```
-```vue
+::: code-group
+
+```tsx [React]
+// pages/users/index.tsx
+import type { Props } from './index.server';
+
+export default function UsersPage({ users }: Props) {
+ // users: { id: number; name: string; ... }[]
+}
+```
+
+```vue [Vue]
+```
+
+```svelte [Svelte]
+
+
```
+```tsx [Solid]
+// pages/users/index.tsx
+import type { Props } from './index.server';
+
+export default function UsersPage(props: Props) {
+ // props.users: { id: number; name: string; ... }[]
+}
+```
+
+:::
+
No manual interface is needed. Props stay in sync with whatever the loader returns.
## Action → useForm
@@ -104,7 +136,7 @@ The `action()` helper gets the same type checking. See [Actions & Forms](./pages
## 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`, 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';
@@ -134,7 +166,7 @@ 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`, 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' } });
@@ -143,9 +175,11 @@ routeUrl('/files/:path{.+}', { params: { path: 'docs/get started' } });
### Page params
-Pass a page's URL pattern as the type argument of `useParams()` to type the params it reads. For `pages/users/[id].tsx`, 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
+```tsx [React]
import { useParams } from '@void/react';
const { id } = useParams<'/users/:id'>(); // id: string
@@ -153,7 +187,40 @@ useParams<'/users/:id'>().idd; // Type error: no such param
useParams<'/userz/:id'>(); // Type error: no page matches
```
-The pattern must be one of your pages; HTTP and WebSocket routes are not accepted. A catch-all page such as `pages/files/[...path].tsx` gives `{ path: string }`. An unnamed catch-all (`[...].tsx`) has no params.
+```vue [Vue]
+
+```
+
+```svelte [Svelte]
+
+```
+
+```tsx [Solid]
+import { useParams } from '@void/solid';
+
+const params = useParams<'/users/:id'>();
+params.id; // string
+params.idd; // Type error: no such param
+useParams<'/userz/:id'>(); // Type error: no page matches
+```
+
+:::
+
+The pattern must be one of your pages; HTTP and WebSocket routes are not accepted. A catch-all page such as `pages/files/[...path].tsx`, `.vue`, or `.svelte` gives `{ path: string }`. An unnamed catch-all (`[...].tsx`, `[...].vue`, or `[...].svelte`) has no params.
The pattern is checked by TypeScript only. Pass the pattern of the page the component renders in; without a pattern, `useParams()` returns `Record`.
@@ -187,14 +254,59 @@ export default defineConfig({
A string must then match one of your routes, optionally followed by a query string or hash:
-```tsx
+::: code-group
+
+```tsx [React]
+import { Link } from '@void/react';
+import { routeUrl } from 'void/routes';
+
+About
+Profile
+Profile
+About // Type error: no route matches
+```
+
+```vue [Vue]
+
+
+
+ About
+ Profile
+ Profile
+
+ About
+
+```
+
+```svelte [Svelte]
+
+
+About
+Profile
+Profile
+
+About
+```
+
+```tsx [Solid]
+import { Link } from '@void/solid';
+import { routeUrl } from 'void/routes';
+
About
Profile
Profile
About // 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`, 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.
`void/routes` exports two types for these strings:
@@ -203,13 +315,54 @@ Links can also point to the current page (`?page=2`, `#comments`) and to `http:/
Cast a string that TypeScript can't check, such as a path from your CMS or a file in `public/`:
-```tsx
+::: code-group
+
+```tsx [React]
+import { Link } from '@void/react';
import type { Href, RouteUrl } from 'void/routes';
{post.title}
Website
```
+```vue [Vue]
+
+
+
+ {{ post.title }}
+ Website
+
+```
+
+```svelte [Svelte]
+
+
+{post.title}
+Website
+```
+
+```tsx [Solid]
+import { Link } from '@void/solid';
+import type { Href, RouteUrl } from 'void/routes';
+
+{post.title}
+Website
+```
+
+:::
+
Restart the dev server or run `void prepare` after changing `routing.typedRoutes`.
Route types update with the dev server. For CI or standalone typechecking, run `void prepare` and follow [Setup](#setup). The [API reference](../reference/api/types.md#void-routes) lists the URL helper and route types.