Getting Started

Creating a page

Add a page to the product or the public website, and decide which package owns it.

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 isPut it in
Product-only, behind sign-inapps/web/app/pages/
A public website pagepackages/layer-site/app/pages/
A blog postThe blog layer's content directory
A documentation pageThe docs layer's content directory
Part of a feature you'd reuseA 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:

apps/web/app/pages/hello.vue
<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:

packages/layer-notes/package.json
{
  "name": "@app/layer-notes",
  "dependencies": {
    "@app/layer-base": "workspace:*"
  }
}
packages/layer-notes/nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@app/layer-base'],
})

Add the page

packages/layer-notes/app/pages/notes/index.vue
<template>
  <div>
    <h1>Notes</h1>
  </div>
</template>

Depend on the layer

apps/web/package.json
{
  "dependencies": {
    "@app/layer-notes": "workspace:*"
  }
}

Extend it

apps/web/nuxt.config.ts
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.

A layer that needs something from its host should ask for it through a module option, 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:

apps/web/app/constants/asideNavigation.ts
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

Copyright © 2026