Documentation Index Fetch the complete documentation index at: https://mintlify.com/cloudflare/vinext/llms.txt
Use this file to discover all available pages before exploring further.
vinext implements Next.js file-system routing conventions for both the Pages Router and App Router. Routes are discovered by scanning the pages/ and app/ directories at startup and hot-reloaded when files change.
Pages Router
The Pages Router follows the original Next.js routing conventions:
Basic Routes
File Route pages/index.tsx/pages/about.tsx/aboutpages/blog/index.tsx/blogpages/blog/first-post.tsx/blog/first-post
Dynamic Routes
Dynamic segments are defined using square brackets:
File Route Pattern Example Match pages/posts/[id].tsx/posts/:id/posts/123pages/blog/[year]/[month].tsx/blog/:year/:month/blog/2024/03
import { useRouter } from 'next/router' ;
export default function Post () {
const router = useRouter ();
const { id } = router . query ;
return < div > Post ID: { id } </ div > ;
}
Catch-All Routes
Catch-all routes capture multiple segments:
File Route Pattern Example Match pages/docs/[...slug].tsx/docs/:slug+/docs/api/referencepages/[[...slug]].tsx/:slug*/ or /any/path
pages/docs/[...slug].tsx
pages/[[...slug]].tsx
// Required catch-all — must have at least one segment
export default function Docs () {
const router = useRouter ();
const { slug } = router . query ; // ['api', 'reference']
return < div > Path: { slug . join ( '/' ) } </ div > ;
}
API Routes
API routes are defined in pages/api/:
import type { NextApiRequest , NextApiResponse } from 'next' ;
export default function handler ( req : NextApiRequest , res : NextApiResponse ) {
res . status ( 200 ). json ({ message: 'Hello World' });
}
Route Precedence
vinext matches routes following Next.js specificity rules:
Static routes (most specific)
Dynamic routes (by position — earlier is more specific)
Catch-all routes
Optional catch-all (least specific)
/blog/latest ← Static — highest priority
/blog/:id ← Dynamic
/blog/:category/:id ← Dynamic (more segments = more specific)
/blog/:slug+ ← Catch-all
/:slug* ← Optional catch-all — lowest priority
App Router
The App Router introduces a more powerful routing system with layouts, loading states, and parallel routes.
File Conventions
File Purpose page.tsxPage component (defines a route) layout.tsxLayout wrapping children template.tsxRe-mounting layout loading.tsxLoading UI (Suspense fallback) error.tsxError boundary not-found.tsx404 page forbidden.tsx403 page unauthorized.tsx401 page route.tsAPI route handler
Basic Routes
app/
page.tsx → /
about/
page.tsx → /about
blog/
page.tsx → /blog
[slug]/
page.tsx → /blog/:slug
Layouts
Layouts wrap multiple pages and persist across navigation:
app/layout.tsx
app/dashboard/layout.tsx
// Root layout — wraps entire app
export default function RootLayout ({ children }) {
return (
< html >
< body >
< nav > Global Navigation </ nav >
{ children }
</ body >
</ html >
);
}
When navigating from /dashboard/analytics to /dashboard/settings, the dashboard layout persists — only the page component re-renders.
Route Groups
Route groups organize files without affecting the URL structure:
app/
(marketing)/
page.tsx → /
about/
page.tsx → /about
layout.tsx ← Shared layout for marketing pages
(app)/
dashboard/
page.tsx → /dashboard
layout.tsx ← Shared layout for app pages
Route groups (denoted by parentheses) are invisible in URLs but allow multiple layouts at the same level.
Dynamic Routes
Dynamic routes work the same as Pages Router:
export default async function Post ({ params }) {
// In Next.js 15+, params is a Promise
const { id } = await params ;
return < div > Post { id } </ div > ;
}
vinext supports both await params (Next.js 15+) and direct property access params.id (pre-15) via thenable objects.
Parallel Routes
Parallel routes allow rendering multiple pages in the same layout:
app/
dashboard/
@analytics/
page.tsx ← Named slot
@team/
page.tsx ← Named slot
page.tsx ← Default slot (children)
layout.tsx
export default function Layout ({ children , analytics , team }) {
return (
< div >
< div > { children } </ div >
< div > { analytics } </ div >
< div > { team } </ div >
</ div >
);
}
Slots are passed as props to the layout at their directory level. If a slot doesn’t have a matching page for a route, default.tsx is rendered:
app/dashboard/@analytics/default.tsx
export default function AnalyticsDefault () {
return < div > Select a dashboard to view analytics </ div > ;
}
Intercepting Routes
Intercepting routes allow showing a different page when navigating from within the app:
app/
feed/
@modal/
(..)photos/
[id]/
page.tsx ← Intercepts /photos/:id when navigating from /feed
page.tsx
photos/
[id]/
page.tsx ← Regular route (direct navigation or refresh)
Interception conventions:
Convention Meaning (.)Same level (..)One level up (..)(..)Two levels up (...)App root
app/feed/@modal/(.)photos/[id]/page.tsx
// This renders in a modal when navigating from /feed to /photos/123
export default async function PhotoModal ({ params }) {
const { id } = await params ;
return < Modal >< Photo id = { id } /></ Modal > ;
}
Refreshing the page or direct navigation to /photos/123 renders the regular route instead.
Route Matching Implementation
vinext converts Next.js file conventions to internal URL patterns:
packages/vinext/src/routing/pages-router.ts
function fileToRoute ( file : string , pagesDir : string ) : Route | null {
// Remove extension
const withoutExt = file . replace ( / \. ( tsx ? | jsx ? ) $ / , "" );
const segments = withoutExt . split ( path . sep );
// Handle index files: pages/index.tsx -> /
if ( segments [ segments . length - 1 ] === "index" ) {
segments . pop ();
}
const params : string [] = [];
let isDynamic = false ;
const urlSegments = segments . map (( segment ) => {
// Catch-all: [...slug] -> :slug+
const catchAllMatch = segment . match ( / ^ \[\.\.\. ( \w + ) \] $ / );
if ( catchAllMatch ) {
isDynamic = true ;
params . push ( catchAllMatch [ 1 ]);
return `: ${ catchAllMatch [ 1 ] } +` ;
}
// Optional catch-all: [[...slug]] -> :slug*
const optionalCatchAllMatch = segment . match ( / ^ \[\[\.\.\. ( \w + ) \]\] $ / );
if ( optionalCatchAllMatch ) {
isDynamic = true ;
params . push ( optionalCatchAllMatch [ 1 ]);
return `: ${ optionalCatchAllMatch [ 1 ] } *` ;
}
// Dynamic segment: [id] -> :id
const dynamicMatch = segment . match ( / ^ \[ ( \w + ) \] $ / );
if ( dynamicMatch ) {
isDynamic = true ;
params . push ( dynamicMatch [ 1 ]);
return `: ${ dynamicMatch [ 1 ] } ` ;
}
return segment ;
});
const pattern = "/" + urlSegments . join ( "/" );
return {
pattern: pattern === "/" ? "/" : pattern ,
filePath: path . join ( pagesDir , file ),
isDynamic ,
params ,
};
}
Precedence Scoring
Routes are sorted by specificity using a scoring algorithm:
packages/vinext/src/routing/pages-router.ts
function routePrecedence ( pattern : string ) : number {
const parts = pattern . split ( "/" ). filter ( Boolean );
let score = 0 ;
for ( let i = 0 ; i < parts . length ; i ++ ) {
const p = parts [ i ];
if ( p . endsWith ( "+" )) {
score += 10000 + i ; // catch-all: high penalty
} else if ( p . endsWith ( "*" )) {
score += 20000 + i ; // optional catch-all: highest penalty
} else if ( p . startsWith ( ":" )) {
score += 100 + i ; // dynamic: moderate penalty by position
}
// static segments contribute nothing (better specificity)
}
return score ;
}
Lower scores = higher priority. Static segments don’t add to the score, making them most specific.
Route Discovery
Routes are discovered at startup and cached:
packages/vinext/src/routing/pages-router.ts
const routeCache = new Map < string , { routes : Route []; promise : Promise < Route []> }>();
export async function pagesRouter ( pagesDir : string ) : Promise < Route []> {
const cacheKey = `pages: ${ pagesDir } ` ;
const cached = routeCache . get ( cacheKey );
if ( cached ) return cached . promise ;
const promise = scanPageRoutes ( pagesDir );
routeCache . set ( cacheKey , { routes: [], promise });
const routes = await promise ;
routeCache . set ( cacheKey , { routes , promise });
return routes ;
}
The cache is invalidated when files are added or removed:
export function invalidateRouteCache ( pagesDir : string ) : void {
routeCache . delete ( `pages: ${ pagesDir } ` );
routeCache . delete ( `api: ${ pagesDir } ` );
}
The Vite plugin sets up a file watcher that calls invalidateRouteCache() when the directory structure changes.
i18n Routing
vinext supports internationalized routing for Pages Router:
module . exports = {
i18n: {
locales: [ 'en' , 'fr' , 'de' ],
defaultLocale: 'en' ,
},
};
This enables locale prefixes automatically:
/ → default locale (en)
/fr → French
/fr/about → French about page
The useRouter() hook exposes the current locale:
import { useRouter } from 'next/router' ;
export default function Page () {
const router = useRouter ();
return < div > Locale: { router . locale } </ div > ;
}
Domain-based i18n routing is not supported. Only path-based locale prefixes work.
basePath
Deploy your app under a URL prefix:
module . exports = {
basePath: '/docs' ,
};
All routes are automatically prefixed:
pages/index.tsx → /docs
pages/getting-started.tsx → /docs/getting-started
Links and navigation respect the basePath automatically
trailingSlash
Force URLs to end with or without a trailing slash:
module . exports = {
trailingSlash: true ,
};
vinext issues 308 redirects to the canonical form:
/about → /about/ (with trailingSlash: true)
/about/ → /about (with trailingSlash: false)
Next Steps
Server Components Learn how RSC integration works in App Router
Caching & ISR Understand incremental static regeneration
Architecture Deep dive into vinext’s architecture
API Routes Build API endpoints