Routing
URL parameters
Read dynamic segments, catch-all paths and typed search parameters in VitNode plugin routes, and update the query string with the page's navigate prop.
A plugin route reads three parts of the URL: dynamic segments such as :id, a catch-all tail with *, and the query string. All three reach the loader and the page as props, typed where you declare a type.
Dynamic segments
Write :name for a segment that varies. The value arrives as a string in params, in load, head, breadcrumb and the page:
page('/site-notes/notes/:id', {
component: lazy(() => import('./pages/note-page')),
}),import type { PluginRoutePageProps } from '@vitnode/core/routing'
const NotePage = ({ params }: PluginRoutePageProps) => <h1>Note {params.id}</h1>
export default NotePageparams.id is always a string. Convert and validate it before you use it, and throw notFound() from @tanstack/react-router for a value that cannot match a record. Add a route shows the full loader.
Catch-all paths
A trailing * matches every remaining segment, so one page can own a whole subtree. It must be the last segment, and only a page() may end in one:
page('/site-notes/files/*', {
component: lazy(() => import('./pages/files-page')),
}),The matched tail arrives as params._splat. For /site-notes/files/2026/october, it is 2026/october:
import type { PluginRoutePageProps } from '@vitnode/core/routing'
const FilesPage = ({ params }: PluginRoutePageProps) => {
const segments = (params._splat ?? '').split('/').filter(Boolean)
return <p className="leading-relaxed">{segments.join(' / ')}</p>
}
export default FilesPageSearch parameters
Declare search on the route in routes.ts to parse the query string. The router runs it while it matches the URL, before the page chunk loads, and its return type becomes the page's search prop:
page('/site-notes/notes', {
component: lazy(() => import('./pages/notes-page')),
search: (input: Record<string, unknown>) => ({
page: Number(input.page) || 1,
}),
}),The page reads search and changes it with navigate, which stays on the same path:
import type { PluginRoutePageProps } from '@vitnode/core/routing'
import { Button } from '@vitnode/core/components/ui/button'
interface NotesSearch {
page: number
}
const NotesPage = ({ navigate, search }: PluginRoutePageProps<undefined, NotesSearch>) => (
<Button
onClick={() => void navigate({ search: { page: search.page + 1 } })}
type="button"
>
Page {search.page + 1}
</Button>
)
export default NotesPagePass resetScroll: false to navigate to keep the scroll position, for example when a filter changes. The loader also receives search, so it can fetch the right page. Key the query on the search values and use placeholderData: keepPreviousData so the current page stays on screen while the next loads; see Loading states.
A layout cannot declare search, because it has no URL of its own.
parseSearch in the page module
A page module's route can declare parseSearch instead. It runs inside the loader, after the page chunk has loaded, and only when routes.ts declares no search. Prefer search in routes.ts, which parses the URL before the page chunk downloads.
Related
- Navigation: link to a page with parameters and a query string.
- Route module: every argument
loadandheadreceive.