Routing
Catmint uses file-based routing to map your directory structure to URL paths. Every directory inside app/ that contains a page.tsx file becomes a route. This guide covers static routes, dynamic segments, catch-all routes, route groups, priority scoring, and programmatic navigation.
Static Routes
The simplest routes are static. The directory path directly maps to the URL:
app/page.tsx -> /
app/about/page.tsx -> /about
app/blog/page.tsx -> /blog
app/contact/us/page.tsx -> /contact/us
Each page.tsx file must export a default React component. The component receives no special props by default -- use hooks like useParams() and useSearch() from catmint/hooks to access route data.
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>This page is served at /about.</p>
</div>
)
}
Dynamic Segments
Wrap a directory name in square brackets to create a dynamic segment. The matched value is available via useParams():
app/blog/[slug]/page.tsx -> /blog/:slug
app/users/[id]/page.tsx -> /users/:id
app/shop/[category]/[item]/page.tsx -> /shop/:category/:item
// app/blog/[slug]/page.tsx
import { useParams } from 'catmint/hooks'
export default function BlogPostPage() {
const { slug } = useParams()
return (
<div>
<h1>Blog Post</h1>
<p>Viewing post: {slug}</p>
</div>
)
}
Dynamic segments match exactly one URL segment. Visiting /blog/hello-world sets slug to "hello-world". You can chain multiple dynamic segments in a single path.
Catch-All Segments
Use the spread syntax inside brackets to match any number of path segments. The parameter value is an array of strings:
app/docs/[...path]/page.tsx -> /docs/*
// app/docs/[...path]/page.tsx
import { useParams } from 'catmint/hooks'
export default function DocsPage() {
const { path } = useParams()
// /docs/guides/routing -> path = ["guides", "routing"]
return (
<div>
<h1>Documentation</h1>
<p>Path: {path.join(' / ')}</p>
</div>
)
}
Catch-all segments require at least one segment to match. /docs alone will not match a catch-all route at app/docs/[...path]/page.tsx.
Optional Catch-All Segments
Double brackets make the catch-all optional, meaning the route also matches the parent path with no additional segments:
app/docs/[[...path]]/page.tsx -> /docs and /docs/*
// app/docs/[[...path]]/page.tsx
import { useParams } from 'catmint/hooks'
export default function DocsPage() {
const { path } = useParams()
// /docs -> path = []
// /docs/intro -> path = ["intro"]
// /docs/a/b/c -> path = ["a", "b", "c"]
if (path.length === 0) {
return <h1>Documentation Index</h1>
}
return <h1>Docs: {path.join(' / ')}</h1>
}
When accessed at /docs with no trailing segments, path is an empty array.
Route Groups
Directories wrapped in parentheses create route groups. They organize files and allow different layouts to be applied without adding a segment to the URL:
app/
(marketing)/
layout.tsx # Marketing layout
page.tsx # Renders at /
pricing/
page.tsx # Renders at /pricing
(dashboard)/
layout.tsx # Dashboard layout
settings/
page.tsx # Renders at /settings
profile/
page.tsx # Renders at /profile
The group name (e.g., (marketing)) is stripped from the URL entirely. Route groups are useful for applying different layouts or middleware to different sections of your application while keeping URL paths clean.
Route groups can contain their own
layout.tsxandmiddleware.tsfiles. These apply only to pages within that group.
Route Priority Scoring
When multiple routes could match the same URL, Catmint uses a priority scoring system to determine the best match. Each segment type contributes a different weight:
| Segment Type | Weight | Example |
|---|---|---|
| Static | 4x | about |
| Dynamic | 3x | [slug] |
| Catch-all | 2x | [...path] |
| Optional catch-all | 1x | [[...path]] |
The total score for a route is the sum of weights across all its segments. Higher scores win. For example, given these routes:
app/blog/latest/page.tsx # Score: 4 + 4 = 8 (static + static)
app/blog/[slug]/page.tsx # Score: 4 + 3 = 7 (static + dynamic)
app/blog/[...path]/page.tsx # Score: 4 + 2 = 6 (static + catch-all)
app/[[...path]]/page.tsx # Score: 1 (optional catch-all)
A request to /blog/latest matches the first route (score 8) because static segments are always preferred over dynamic ones.
Trailing Slash Handling
Catmint normalizes trailing slashes by default. Both /about and /about/ resolve to the same route. You can configure trailing slash behavior in catmint.config.ts:
// catmint.config.ts
import { defineConfig } from 'catmint/config'
export default defineConfig({
mode: 'fullstack',
routing: {
trailingSlash: 'ignore', // 'ignore' | 'always' | 'never'
},
})
| Option | Behavior |
|---|---|
"ignore" | Both forms match the same route (default) |
"always" | Redirects to the trailing slash version |
"never" | Redirects to the non-trailing-slash version |
Explicit Routing with defineRoutes
For cases where file-based routing is insufficient, you can define routes explicitly using defineRoutes() from catmint/routing. This is useful for aliasing paths, programmatically generating routes, or integrating with external data sources:
// catmint.config.ts
import { defineConfig } from 'catmint/config'
import { defineRoutes } from 'catmint/routing'
export default defineConfig({
mode: 'fullstack',
routes: defineRoutes((route) => {
route('/legacy-about', './app/about/page.tsx')
route('/old-blog/:slug', './app/blog/[slug]/page.tsx')
route('/products/:category/:id', './app/shop/[category]/[item]/page.tsx')
}),
})
Routes defined with defineRoutes() are merged with the file-based routes. If a conflict occurs, explicitly defined routes take precedence.
The Link Component
Use the Link component from catmint/link for client-side navigation. It renders an anchor tag and intercepts clicks to perform navigation without a full page reload:
// app/components/Nav.client.tsx
import { Link } from 'catmint/link'
export function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
<Link href="/blog/hello-world">A Post</Link>
</nav>
)
}
The Link component accepts all standard anchor attributes in addition to these:
| Prop | Type | Description |
|---|---|---|
href | string | The destination URL path |
prefetch | boolean | Prefetch the route on hover (default: true) |
replace | boolean | Replace current history entry instead of pushing |
Programmatic Navigation with redirect
Use the redirect() function from catmint/routing to perform server-side redirects. It works in server components, middleware, and server functions:
// app/old-page/page.tsx
import { redirect } from 'catmint/routing'
export default function OldPage() {
redirect('/new-page')
}
// With a custom status code
export default function MovedPage() {
redirect('/new-location', 301)
}
The redirect() function throws internally to halt rendering and send the redirect response. It accepts an optional second argument for the HTTP status code (defaults to 302).
Query Parameters
Access query string parameters with the useSearch() hook from catmint/hooks:
// app/search/page.tsx
import { useSearch } from 'catmint/hooks'
export default function SearchPage() {
const { q, page } = useSearch()
// /search?q=catmint&page=2 -> q = "catmint", page = "2"
return (
<div>
<h1>Search results for: {q}</h1>
<p>Page {page ?? 1}</p>
</div>
)
}
Next Steps
- Layouts -- nested layout chains and layout persistence
- Middleware -- intercept requests with the onion execution model
- API Endpoints -- define HTTP handlers alongside your pages
