React Router v7 virtual* file routes on Next.js.
*Not really π€ͺ
- Programatically generate Next.js App Router files.
- Mix and match with file-based routing.
- Reusable file templates.
- Fully typesafe.
npm install next-virtual-routes
Then, add the following to your next.config.ts
file:
// next.config.ts
import { withRoutes } from "next-virtual-routes"
export default withRoutes({
routes: [
/* Routes config */
],
/* Next.js config */
})
The routes
property accepts the following values:
- An array of
route()
calls. - A function that returns an array of
route()
calls. - An async function that returns an array of
route()
calls.
For advanced configuration, you can also pass an object:
// next.config.ts
import { withRoutes } from "next-virtual-routes"
export default withRoutes({
routes: {
formatter: "prettier",
strict: true,
config: [
/* Routes config */
],
},
/* Next.js config */
})
A lower level function is also exposed for cases where you need more control.
// next.config.ts
import { generateRoutes } from "next-virtual-routes"
export default async () => {
await generateRoutes([
/* Routes config */
])
return {
/* Next.js config */
}
}
Call route
in your routes configuration to programatically create a route file.
Pass the file and a path template relative to your next.config.ts
.
// next.config.ts
import { route, withRoutes } from "next-virtual-routes"
export default withRoutes({
routes: [route("blog/page.tsx", "src/templates/page.tsx")],
})
Then, create the template.
// src/templates/page.tsx
export function Page() {
return "Hello world"
}
This generates the /src/app/blog/page.tsx
file with the following content:
// src/app/blog/page.tsx
export function Page() {
return "Hello world"
}
Warning
Always import files inside templates using path aliases to prevent errors.
You can optionally pass a serializable context
object as a third parameter.
// next.config.ts
export default withRoutes({
routes: [
route("home/page.tsx", "src/templates/page.tsx", {
static: true,
}),
route("blog/page.tsx", "src/templates/page.tsx", {
static: false,
}),
],
})
If you are using TypeScript, you can use declaration merging to add a type
to the context
object.
// next.config.ts
declare module "next-virtual-routes" {
interface Context {
static: boolean
}
}
export default withRoutes({
routes: [
route("home/page.tsx", "src/templates/page.tsx", {
static: true,
}),
route("blog/page.tsx", "src/templates/page.tsx", {
static: false,
}),
],
})
You can then access this data in your templates using the context
global object.
// src/templates/page.tsx
export const dynamic = context.static ? "force-static" : "force-dynamic"
export function Page() {
return context.static ? "Static rendering" : "Dynamic rendering"
}
Named exports with statically analyzable expressions are evaluated when applying the template. The previous template generates the following content:
// src/app/home/page.tsx
const context = {
static: true,
}
export const dynamic = "force-static"
export function Page() {
return context.static ? "Static rendering" : "Dynamic rendering"
}
// src/app/about/page.tsx
const context = {
static: false,
}
export const dynamic = "force-dynamic"
export function Page() {
return context.static ? "Static rendering" : "Dynamic rendering"
}
This enables programmatic control of Route Segment configuration, Middleware matchers and more.
Programatically generates a route.
Function | Type |
---|---|
route |
(path: RouteFilePath, template: string, context?: Context or undefined) => Route |
Examples:
export default withRoutes({
routes: [route("blog/page.tsx", "src/templates/page.tsx")],
})
Use declaration merging to add a type to the context
object.
declare module "next-virtual-routes" {
interface Context {
static: boolean
}
}
export default withRoutes({
routes: [
route("home/page.tsx", "src/templates/page.tsx", {
static: true,
}),
route("blog/page.tsx", "src/templates/page.tsx", {
static: false,
}),
],
})
Adds a path prefix to a set of routes.
Function | Type |
---|---|
prefix |
(prefix: string, children: Route[]) => Route[] |
Examples:
const routes = [
...prefix("blog", [
route("page.tsx", "src/templates/page.tsx"),
route("[...slug]/page.tsx", "src/templates/page.tsx"),
])
]
Adds context to a set of routes. Nested context is deeply merged.
Function | Type |
---|---|
context |
(context: Context, children: Route[]) => Route[] |
Examples:
declare module "next-virtual-routes" { interface Context { render: "static" | "dynamic" } }
const routes = [
...context({ render: "static" }, [
route("page.tsx", "src/templates/page.tsx"),
route("page.tsx", "src/templates/page.tsx"),
])
]
TODO: document
Function | Type |
---|---|
generateRoutes |
(config: RoutesDefinition or RoutesPluginConfig) => Promise<void> |
TODO: document
Function | Type |
---|---|
withRoutes |
({ routes, ...nextConfig }: NextConfigWithRoutesPlugin) => Promise<NextConfig> |
TODO: document
Property | Type | Description |
---|
TODO: document
Type | Type |
---|---|
Route |
{ path: string template: string context?: Context } |
TODO: document
Type | Type |
---|---|
RoutesDefinition |
Route[] or (() => Route[] or Promise<Route[]>) |
TODO: document
Type | Type |
---|---|
RoutesPluginConfig |
{ config: RoutesDefinition banner?: string[] footer?: string[] cwd?: string log?: boolean cache?: boolean watch?: boolean cacheFile?: string clearAppDir?: boolean formatter?: "prettier" formatterConfigFile?: string } |