Add Page#
This project uses TanStack Start file-based routing: any .tsx under src/routes/ becomes a route, and the file path determines the URL.
Create a Page#
Add a file under src/routes/_app/:
tsx// src/routes/_app/todos.tsx
import { useEffect, useState } from "react"
import { createFileRoute } from "@tanstack/react-router"
import { http } from "@/lib/http"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
interface Todo { id: string; title: string }
function TodosPage() {
const [todos, setTodos] = useState<Todo[]>([])
const [title, setTitle] = useState("")
const fetchTodos = async () => {
const res = await http.get<Todo[]>("/v1/todos/list")
if (res.success && res.data) setTodos(res.data)
}
const addTodo = async () => {
if (!title) return
const res = await http.post("/v1/todos/create", { title })
if (res.success) {
setTitle("")
void fetchTodos()
}
}
useEffect(() => { void fetchTodos() }, [])
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">Todos</h1>
<div className="flex gap-2 max-w-md">
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="New todo" />
<Button onClick={addTodo}>Add</Button>
</div>
<div className="space-y-2">
{todos.map((t) => (
<div key={t.id} className="p-3 bg-card rounded border">{t.title}</div>
))}
</div>
</div>
)
}
export const Route = createFileRoute("/_app/todos")({ component: TodosPage })
Vite regenerates the route tree (routeTree.gen.ts) on save — no manual registration.
⚠️ The path in
createFileRoute("/_app/todos")must match the file path; otherwise the build fails.
Route Groups#
| Directory | URL | Purpose |
|---|---|---|
_app/... | /dashboard, /profile, ... | Authenticated dashboard with sidebar |
_app/owner/... | /owner/users, ... | Owner-only admin pages |
en/..., zh/... | /en/about, /zh/docs, ... | Public marketing site |
v1/... | /v1/users/me, ... | API Server Routes |
Top-level signin.tsx, signup.tsx | /signin, /signup | Public pages |
Auth Protection#
src/routes/_app/route.tsx wraps the whole group in <RequireAuth>, so any page under _app/ is automatically auth-gated — no per-page boilerplate. Unauthenticated users are redirected to /signin.
tsx// src/routes/_app/route.tsx (excerpt)
<RequireAuth>
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<Topbar />
<Outlet /> {/* ← child pages render here */}
</SidebarInset>
</SidebarProvider>
</RequireAuth>
Admin Pages#
Place admin pages under src/routes/_app/owner/ (e.g. createFileRoute("/_app/owner/users")). The sidebar filters menu items by user.role === "owner" so regular users don't see the Owner entries.