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: ``` -- **` + + +``` + +```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] + + + + +``` + +```svelte [Svelte] + + + +

Post {params.id}

+``` + +```tsx [Solid] +// pages/posts/[id].tsx +import { useParams } from '@void/solid'; + +export default function PostPage() { + const params = useParams<'/posts/:id'>(); // { id: string } + return

Post {params.id}

; +} +``` + +::: + 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] + + + +``` + +```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] + + + +``` + +```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.