Creating a page
Nuxt's file-based routing turns a file under
app/pages/ into a route, and the path below app/pages/ becomes the URL. So
app/pages/hello.vue serves /hello, and app/pages/notes/index.vue serves /notes.
The package the file lives in is not part of the URL. A page at
packages/layer-notes/app/pages/notes/index.vue still serves /notes in any application that
extends that layer.
Decide which package owns it
Answer this before you create the file, because moving a page later means moving its components, server routes, and navigation with it:
| The page is | Put it in |
|---|---|
| Product-only, behind sign-in | apps/web/app/pages/ |
| A public website page | packages/layer-site/app/pages/ |
| A blog post | The blog layer's content directory |
| A documentation page | The docs layer's content directory |
| Part of a feature you'd reuse | A new packages/layer-* |
The rule behind the table is in Technical details: application code stays with its application, and reusable code stays in the layer that owns it.
The last two rows are not Vue files. @app/layer-blog and @app/layer-docs each register a Nuxt
Content collection in their own content.config.ts and one catch-all route that renders it, so a
post or a documentation page is a Markdown file under that layer's content/ directory and its path
below that directory becomes the URL. A .vue file dropped into either layer's app/pages/ becomes
an unrelated route and never appears in the blog or the documentation.
Add a product page
Create the file:
<template>
<div>
<h1>Hello</h1>
<p>Welcome to your new page.</p>
</div>
</template>
Run the product application and open http://localhost:3000/hello:
pnpm dev:web
A public website page works the same way. Put it in the layer that owns that surface — such as
packages/layer-site/app/pages/hello.vue — and open it on port 3100 with pnpm dev:site.
Add a page in a new layer
When the page belongs to a feature rather than to one application, give the feature its own layer. Its pages, components, composables, server routes, and configuration then travel together, and another project can extend it as it stands.
Create the layer
Give it a package name in the workspace scope and declare what it extends:
{
"name": "@app/layer-notes",
"dependencies": {
"@app/layer-base": "workspace:*"
}
}
export default defineNuxtConfig({
extends: ['@app/layer-base'],
})
Add the page
<template>
<div>
<h1>Notes</h1>
</div>
</template>
Depend on the layer
{
"dependencies": {
"@app/layer-notes": "workspace:*"
}
}
Extend it
export default defineNuxtConfig({
extends: ['@app/layer-auth', '@app/layer-payments', '@app/layer-notes'],
})
Install and run
pnpm install
pnpm dev:web
The page is now at http://localhost:3000/notes. The layer's name adds no prefix — only the
directories under app/pages/ decide the route.
runtimeConfig, or a hook, rather than assuming the host defines a particular key. A missing option
then fails by name instead of surfacing as undefined somewhere else.Add it to the navigation
Navigation belongs to whichever package renders it. For a product page, add an entry to the product's aside navigation:
import type { NavigationMenuItem } from '@nuxt/ui'
export const navigationItems: NavigationMenuItem[] = [
// existing items
{
label: 'Notes',
to: '/notes',
icon: 'i-lucide-notebook',
},
]
The website, blog, and docs each own their own navigation in their own layer. Add the entry next to the page rather than in an application that doesn't own the route.
Next steps
- Customization — colors, logo, fonts, and page head
- Technical details — what each package owns
- First deploy — ship what you built
