Pages routing
Put UI components in pages/ to make them routes. A page's server loader returns data, and the component gets it as props. The pattern is inspired by Inertia.js and works with React, Vue, Svelte and Solid.
On this pageSee how files map to URLs · Pair a page with a loader · Choose a UI framework adapter
Pages mode starts when a pages/ directory exists. It can sit beside routes/. Use pages for UI and routes for APIs. Page components normally run on the server for the first render and in the browser for navigation and interaction.
You can also use your own client-side router and call the SDK's APIs with typed fetch. Pages mode is optional.
Setup
In an empty project, void init can set up Pages. Choose Vite+ or plain Vite. Then pick React, Vue, Svelte or Solid, and a database or static starter. The generated app includes the matching Vite config and page files.
This step adds Pages by hand. Install a framework adapter beside void.
npm install @void/reactnpm install @void/vuenpm install @void/sveltenpm install @void/solidExample output: npm reports the package added.
This step adds both plugins to your Vite config.
import { voidReact } from '@void/react/plugin';
// vite.config.ts
import { defineConfig } from 'vite';
import { voidPlugin } from 'void';
export default defineConfig({
plugins: [voidPlugin(), voidReact()],
});import { voidVue } from '@void/vue/plugin';
// vite.config.ts
import { defineConfig } from 'vite';
import { voidPlugin } from 'void';
export default defineConfig({
plugins: [voidPlugin(), voidVue()],
});import { voidSvelte } from '@void/svelte/plugin';
// vite.config.ts
import { defineConfig } from 'vite';
import { voidPlugin } from 'void';
export default defineConfig({
plugins: [voidPlugin(), voidSvelte()],
});import { voidSolid } from '@void/solid/plugin';
// vite.config.ts
import { defineConfig } from 'vite';
import { voidPlugin } from 'void';
export default defineConfig({
plugins: [voidPlugin(), voidSolid()],
});Each adapter includes its framework's Vite plugin. Pass framework plugin options via voidReact({ react: { ... } }), voidVue({ vue: { ... } }), voidSvelte({ svelte: { ... } }), or voidSolid({ solid: { ... } }) if needed.
Directory structure
Pages can be flat files such as about.vue, or directories such as about/index.vue. Both map to the same route:
- pages/
- layout.tsxRoot layout (wraps all pages)
- index.tsx→ /
- index.server.tsServer-side loader & action for /
- about.tsx→ /about
- users/
- layout.tsxNested layout for /users/*
- [id].tsx→ /users/:id
- [id].server.tsServer-side loader & action for /users/:id
- blog/
- hello.md→ /blog/hello (Markdown page)
Each page can have a .server.ts file that runs only on the server. It can export:
- A loader, which runs on
GETand returns the data that becomes the page component's props - Actions, which handle mutations from forms and programmatic calls. Export a single
actionor multiple named actions when a page has several mutations ssr = falseto opt a route out of server-rendered component HTML while keeping server loaders and client-side routing
File-based routing rules are the same as server routing: [param] for dynamic segments, [...param] for catch-all, (group)/ for route groups.
How navigation works
The first request receives rendered HTML. Later navigations run the loader and update the page without reloading the document. Form submissions run an action, then refresh page props or redirect.
This step turns off server-rendered HTML for one route. Export ssr = false from its .server.ts file.
// pages/dashboard.server.ts
import { defineHandler } from 'void';
export const ssr = false;
export const loader = defineHandler(async () => {
return { title: 'Dashboard' };
});The loader still runs on the first request. Its props are embedded in the HTML shell. The component mounts in the browser instead of hydrating.
Render and prerender flags combine like this:
| Page exports | Behavior |
|---|---|
ssr unset or true | Server-render component HTML on request. |
ssr = false | Return a client-mounted shell on request. |
ssr = false + prerender = true | Prerender a client-mounted shell with embedded loader data. |
ssr = false + prerender = false | Return the client-mounted shell only on request; never prerender it. |
Island page + ssr = false | Invalid. Island pages already use the island renderer. |
output: "static" + prerender unset | Auto-prerender pages that have known paths, including client-only shells. |
This step adds SPA navigation with Link. It renders an <a> that navigates without a full reload.
import { Link } from "@void/react";
<Link href="/users">Users</Link>
<Link href={`/users/${id}`}>View</Link><script setup lang="ts">
import { Link } from '@void/vue';
</script>
<template>
<Link href="/users">Users</Link>
<Link :href="`/users/${id}`">View</Link>
</template><script>
import { Link } from "@void/svelte";
</script>
<Link href="/users">Users</Link>
<Link href={`/users/${id}`}>View</Link>import { Link } from "@void/solid";
<Link href="/users">Users</Link>
<Link href={`/users/${id}`}>View</Link>Link also supports query data, history replacement, full-page navigation, and cancellation:
<Link href="/users" data={{ page: 2, tag: ['active', 'new'] }}>
Filtered users
</Link>
<Link href="/users" replace>
Users
</Link>
<Link href="/logout" reloadDocument>
Sign out
</Link>
<Link
href="/settings"
onNavigate={(event) => {
if (!confirm('Leave this page?')) {
event.preventDefault();
}
}}
>
Settings
</Link>prefetch and reloadDocument work only with GET links. GET data becomes query parameters. Use scalars or arrays.
This step navigates in code with useRouter().
import { useRouter } from '@void/vue'; // or "@void/react", "@void/svelte", "@void/solid"
const router = useRouter();
router.visit('/users');
router.refresh(); // re-fetch current page props
router.visit('/logout', { method: 'POST' }); // non-GET navigationrouter.visit() updates the page in place for same-origin URLs. http(s) URLs on another origin get a full page load. Other schemes such as javascript: are rejected.
This step reads dynamic params with useParams() from the same adapter.
import { useParams } from '@void/react';
export default function PostPage() {
const { id } = useParams<{ id: string }>();
return <h1>Post {id}</h1>;
}Scroll restoration
The SDK manages scroll position for Pages automatically:
- Back/forward navigation and reloads restore the saved scroll position.
- New navigation scrolls to the top of the page.
- Hash links (
/docs#api) scroll to the target element. Same-page hash links (#section) skip the server fetch entirely.
This step keeps the scroll position with preserveScroll: true.
router.visit('/users', { preserveScroll: true });Link also accepts preserveScroll:
<Link href="/users" preserveScroll>
Users
</Link><Link href="/users" preserve-scroll>Users</Link><Link href="/users" preserveScroll>Users</Link><Link href="/users" preserveScroll>
Users
</Link>