Routing

Routing

Claim a URL in VitNode - plugin manifest routes for reusable packages, and host app routes for site-specific pages.

VitNode uses TanStack Start for routing. Routes are divided into three tiers:

TierDeclarationLocationPurpose
Plugin Routesroutes/manifest.tsplugins/*/src/routes/manifest.tsRecommended. Reusable across any VitNode install.
Application RoutesFile-based routesapps/web/src/routes/**Site-specific pages owned directly by your app.
Core RoutesCode-based routesBuilt into @vitnode/coreSystem routes (/login, /admin/*, /search).

Plugins declare routes as serializable data. The build system mounts them into the route tree with SSR and automatic code splitting.

Declare the Route in routes/manifest.ts

plugins/blog/src/routes/manifest.ts
import type { PluginRouteDefinition } from "@vitnode/core/routing"

export const routes: PluginRouteDefinition[] = [
  {
    entry: "routes/blog-page",
    id: "blog",
    path: "/blog",
  },
]

Create the Page Component

plugins/blog/src/routes/blog-page.tsx
const BlogPage = () => (
  <div className="container mx-auto p-4">
    <h1 className="text-3xl font-bold">Blog Overview</h1>
  </div>
)

export default BlogPage

View the Page

bun dev
pnpm dev
npm run dev

Open http://localhost:3000/blog.


2. Create a Page in Your Application

When creating a page that belongs only to your host site, add a file in apps/web/src/routes/:

Choose a Pathless Shell

  • _main/ for public pages (with header, navigation, and footer).
  • _admin/ for administrative screens.

Create the Route File

apps/web/src/routes/_main/about.tsx
import { createFileRoute } from "@tanstack/react-router"
import { pageHead } from "#/lib/page-head"

export const Route = createFileRoute("/_main/about")({
  head: () =>
    pageHead({
      title: "About Us",
      description: "Learn more about our team and mission.",
    }),
  component: AboutPage,
})

function AboutPage() {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold">About Us</h1>
    </div>
  )
}

Dynamic Segments & Parameters

Dynamic parameters differ slightly between manifests and route files:

ContextSyntaxExampleAccess Parameter
Plugin Manifest:parampath: "/blog/:slug"params.slug in load / props
App Route File$param_main/blog.$slug.tsxRoute.useParams().slug

VitNode compiles :slug into TanStack Router's $slug syntax automatically.


Plugin Route Lifecycle (definePluginRoute)

To load data, define metadata, or customize breadcrumbs in a plugin route module, export route:

plugins/blog/src/routes/post-page.tsx
import type { PluginRoutePageProps } from "@vitnode/core/routing"
import { definePluginRoute } from "@vitnode/core/routing"

interface Post {
  title: string
  body: string
}

export const route = definePluginRoute({
  load: async ({ params }) => {
    return await fetchPostBySlug(params.slug)
  },
  head: ({ loaderData }) => ({
    title: loaderData?.title,
  }),
})

const PostPage = ({ loaderData }: PluginRoutePageProps<Post>) => (
  <article className="container mx-auto p-4">
    <h1 className="text-3xl font-bold">{loaderData.title}</h1>
    <p>{loaderData.body}</p>
  </article>
)

export default PostPage

Declare load above head

TypeScript infers loaderData in head and the page component from what load returns. Always declare load before head.

Learn More