Skip to main content

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

FileRoute
pages/index.tsx/
pages/about.tsx/about
pages/blog/index.tsx/blog
pages/blog/first-post.tsx/blog/first-post

Dynamic Routes

Dynamic segments are defined using square brackets:
FileRoute PatternExample Match
pages/posts/[id].tsx/posts/:id/posts/123
pages/blog/[year]/[month].tsx/blog/:year/:month/blog/2024/03
pages/posts/[id].tsx
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:
FileRoute PatternExample Match
pages/docs/[...slug].tsx/docs/:slug+/docs/api/reference
pages/[[...slug]].tsx/:slug*/ or /any/path
// 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/:
pages/api/hello.ts
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:
  1. Static routes (most specific)
  2. Dynamic routes (by position — earlier is more specific)
  3. Catch-all routes
  4. 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

FilePurpose
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:
// 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:
app/posts/[id]/page.tsx
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
app/dashboard/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:
ConventionMeaning
(.)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:
next.config.js
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:
next.config.js
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:
next.config.js
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

Build docs developers (and LLMs) love