07 Pages
In this section, we'll implement automatic route generation by scanning the pages/ directory, removing hardcoded page imports from the router. The full code is available at 7-pages.
The Problem
In the previous implementations, we hardcoded page imports in the router:
import Hello from '../../../../../playground/pages/hello.vue'
import World from '../../../../../playground/pages/world.vue'
const routes = [
{ path: '/hello', component: Hello },
{ path: '/world', component: World },
]This is problematic because:
- Users can't add new pages without modifying framework code
- The paths are hardcoded to the playground location
- It doesn't follow Nuxt's file-based routing convention
How Does Nuxt Framework Solve This?
The real Nuxt framework uses a sophisticated page scanning and route generation system:
1. Page Scanning
The resolvePagesRoutes function in packages/nuxt/src/pages/utils.ts scans page directories:
export async function resolvePagesRoutes (pattern: string | string[], nuxt = useNuxt()): Promise<NuxtPage[]> {
const pagesDirs = getLayerDirectories(nuxt).map(d => d.appPages)
const scannedFiles: ScannedFile[] = []
for (const dir of pagesDirs) {
const files = await resolveFiles(dir, pattern)
scannedFiles.push(...files.map(file => ({ relativePath: relative(dir, file), absolutePath: file })))
}
const allRoutes = generateRoutesFromFiles(uniqueBy(scannedFiles, 'relativePath'), {
shouldUseServerComponents: !!nuxt.options.experimental.componentIslands,
})
return uniqueBy(allRoutes, 'path')
}2. Route Generation from Files
The generateRoutesFromFiles function parses file paths into route segments, handling:
- Dynamic segments:
[param].vue→:param - Optional segments:
[[param]].vue→:param? - Catch-all:
[...param].vue→:param(.*)* - Route groups:
(groupName)/page.vue
3. Template Generation
The routes template is defined in packages/nuxt/src/pages/module.ts:
addTemplate({
filename: 'routes.mjs',
getContents ({ app }) {
if (!app.pages) { return 'export default []' }
const { routes, imports } = normalizeRoutes(app.pages, new Set(), {
serverComponentRuntime,
clientComponentRuntime,
overrideMeta: !!nuxt.options.experimental.scanPageMeta,
})
return [...imports, `export default ${routes}`].join('\n')
},
})4. Route Normalization
The normalizeRoutes function converts NuxtPage objects to route definitions with:
- Dynamic imports for page components
- Server/client component mode switching
- Static and dynamic metadata merging
5. Router Usage
The router imports from #build/routes:
import routes from '#build/routes'
const router = createRouter({
history: import.meta.server ? createMemoryHistory() : createWebHistory(),
routes,
})We'll implement a simplified version of this pattern.
Page Scanner
Create the scanner
Create a module to scan the pages directory and generate route definitions.
import { readdir } from 'node:fs/promises'
import { join, parse } from 'node:path'
export interface NuxtPage {
name: string
path: string
file: string
}
export async function scanPages(pagesDir: string): Promise<NuxtPage[]> {
const files = await readdir(pagesDir)
const pages: NuxtPage[] = []
for (const file of files) {
const { name, ext } = parse(file)
if (ext !== '.vue') continue
const routePath = name === 'index' ? '/' : `/${name}`
pages.push({
name,
path: routePath,
file: join(pagesDir, file),
})
}
return pages
}
export function generateRoutesCode(pages: NuxtPage[]): string {
const imports = pages
.map((page, i) => `import Page${i} from '${page.file}'`)
.join('\n')
const routes = pages
.map((page, i) => ` { name: '${page.name}', path: '${page.path}', component: Page${i} }`)
.join(',\n')
return `${imports}
export default [
${routes}
]`
}Key points:
scanPages()reads the pages directory and returns page metadataindex.vuemaps to/, other files map to/{filename}generateRoutesCode()creates JavaScript code that exports the routes array- The generated code includes imports for each page component
Example output
For a pages directory with index.vue, hello.vue, and world.vue:
import Page0 from '/path/to/pages/index.vue'
import Page1 from '/path/to/pages/hello.vue'
import Page2 from '/path/to/pages/world.vue'
export default [
{ name: 'index', path: '/', component: Page0 },
{ name: 'hello', path: '/hello', component: Page1 },
{ name: 'world', path: '/world', component: Page2 }
]Update Vite Builder
Add routesCode to BuildOptions
Update the vite builder to accept generated routes code and register the #routes virtual module.
import { build as _build, mergeConfig, type InlineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { virtual } from './plugins/virtual'
export interface BuildOptions {
buildDir: string
clientEntry: string
serverEntry: string
appComponent: string
routesCode: string
}
export const bundle = async (options: BuildOptions) => {
const { buildDir, clientEntry, serverEntry, appComponent, routesCode } = options
// Virtual file system for #app and #routes modules
const vfs: Record<string, string> = {
'#app': `export { default } from '${appComponent}'`,
'#routes': routesCode,
}
const defaultConfig = {
plugins: [vue(), virtual(vfs)],
// ... rest remains the same
}
// ...
}Key changes:
- Added
routesCodetoBuildOptions - Register
#routesvirtual module with the generated routes code
Update Router
Import from virtual module
Update the router to import routes from the #routes virtual module.
import {
createRouter as _createRouter,
createMemoryHistory,
createWebHistory,
} from 'vue-router'
import routes from '#routes'
export const createRouter = () => {
const history = import.meta.server
? createMemoryHistory()
: createWebHistory()
const router = _createRouter({
history,
routes,
})
return router
}Key changes:
- Removed hardcoded page imports
- Import
routesfrom#routesvirtual module - The routes are now dynamically generated based on the pages directory
Update loadNuxt
Scan pages and generate routes
Update loadNuxt to scan pages and pass the generated code to the vite builder.
import { join, resolve } from 'node:path'
import { createDevServer, createNitro } from 'nitro'
import { bundle } from '@nuxt/vite-builder'
import { scanPages, generateRoutesCode } from '../pages/scan'
export const buildDir = resolve(process.cwd(), '.nuxt')
export const loadNuxt = async () => {
const appComponent = resolve(process.cwd(), 'App.vue')
const pagesDir = resolve(process.cwd(), 'pages')
// Scan pages and generate routes code
const pages = await scanPages(pagesDir)
const routesCode = generateRoutesCode(pages)
await bundle({
clientEntry: join(import.meta.dirname, '../app/entry.client.ts'),
serverEntry: join(import.meta.dirname, '../app/entry.server.ts'),
buildDir,
appComponent,
routesCode,
})
const nitro = await createNitro({
renderer: resolve(import.meta.dirname, './runtime/nitro/renderer.ts'),
})
const server = await createDevServer(nitro)
return { server }
}Key changes:
- Import
scanPagesandgenerateRoutesCodefrom the pages module - Resolve
pagesDirfrom the current working directory - Scan pages and generate routes code before bundling
- Pass
routesCodeto the bundle function
How It Works
When the build runs:
loadNuxt()callsscanPages()to discover pages inpages/directorygenerateRoutesCode()creates JavaScript code with imports and route definitions- The generated code is passed to
bundle()asroutesCode - The virtual plugin registers
#routes→ generated routes code - When Vite encounters
import routes from '#routes', it serves the generated code - The router uses the dynamically generated routes
Testing
Create pages in the playground:
playground/
├── App.vue
└── pages/
├── index.vue
├── hello.vue
└── world.vueRun the server:
npx nuxiAll routes will be automatically available:
/→index.vue/hello→hello.vue/world→world.vue
Summary
By implementing automatic page scanning:
- File-based routing: Pages are discovered from the filesystem
- No hardcoded imports: Routes are generated at build time
- User-friendly: Adding a new page is as simple as creating a
.vuefile - Similar to Nuxt: This pattern mirrors how real Nuxt handles page routing
Current Limitations
This implementation is simplified compared to real Nuxt:
- No nested routes (subdirectories)
- No dynamic routes (
[id].vue) - No route metadata or middleware
These features would be added in a more complete implementation using the same pattern - scan the filesystem, generate code, serve through virtual modules.
