Nuxt & VueLesson
Nuxt layers
Several sites in one codebase usually means one of two pains: a separate app per site, with every shared fix made several times, or one tangled app where nobody can tell which code belongs to which site. A Nuxt layer is the middle way: each site keeps its own folder, and Nuxt merges them all into one app.
Know Nuxt layers already? Skip to In Terrarium.
Outside the box · General
The concept
A layer is a partial Nuxt app. It has the same shape as a full one: a nuxt.config.ts, and optionally app/pages/, app/components/, app/composables/, app/utils/, server/ and the rest. When an app uses a layer, Nuxt merges the layer's config into the app's and scans its folders as if they were the app's own, so its pages become routes and its components and composables are auto-imported everywhere.
There are two ways in. The extends key of nuxt.config.ts names layers from another folder, an npm package or a git repository. Any folder under the app's own layers/ directory is registered without being named anywhere; it only needs a nuxt.config.ts, which may be empty. Nuxt also gives each such folder an alias, #layers/<name>.
When two layers define the same page, component or composable, one wins. The app's own files beat every layer; among the folders in layers/, the later name in alphabetical order wins, which a numeric prefix such as 1.base/ can change.
Two things in a layer resolve against the app, not the layer: the ~ and @ aliases, and relative paths inside the layer's nuxt.config.ts. The docs suggest relative imports or the #layers/<name> alias in code, and a path built from the config file's own location in its config:
// layers/shop/nuxt.config.ts
import { fileURLToPath } from 'node:url'
export default defineNuxtConfig({
css: [fileURLToPath(new URL('./app/assets/shop.css', import.meta.url))],
})
Full referenceNuxt docs: layers/ directory
In Terrarium
Terrarium is one Nuxt app that serves every site, and each site is a . A Tenant is built as a layer: everything it owns sits in layers/<tenant>/, with its pages and components in app/, its own nuxt.config.ts, its manifest tenant.config.ts, and its content. The root of the repo is the Platform: shared code such as the catch-all page, the routing and the content config.
The root config, nuxt.config.ts, has no extends key and names no Tenant; Nuxt finds them in layers/ on its own.
A Tenant's layer config is small. Most only add the Tenant's stylesheet, with the path built from the config file's own location, as the Nuxt docs advise. Glass Box's is the whole file:
1import { fileURLToPath } from 'node:url'
2
3export default defineNuxtConfig({
4 css: [fileURLToPath(new URL('./app/assets/theme.css', import.meta.url))],
5})
Layers never need to override each other here. Each Tenant's pages live under its own app/pages/t/<tenant>/, so no two layers define the same page. A Tenant page replaces the Platform's catch-all for its addresses through routing, not layer priority: a path with the static segment journal is more specific than the catch-all's [tenant] parameter, so it wins.
Components are kept apart by name: each Tenant puts its components in a folder named after itself, such as layers/journal/app/components/journal/, and Nuxt names them from that folder, as in JournalStatTile. Components in app/components/content/, the ones Markdown can use, are the exception: Nuxt Content registers them by their bare file name, such as Sighting for the Atlas's Sighting.vue, so every layer's content components share one namespace and their names must not clash.
A layer brings presentation only. It never declares a content collection: the root content.config.ts builds every collection from the manifests, and shared/expand.ts finds those manifests by listing the same layers/ folder Nuxt scans.
Why it's built this way
- One app, one folder per site. Every Tenant runs in one app (Multi-tenant isolation says why). Layers keep sites from mixing: each Tenant's pages, styles and content sit in its own folder, which Nuxt merges at build. The cost: a few names, such as components Markdown uses, are shared and must not clash.
- Where Nuxt looks. Tenants once lived in
tenants/, unknown to Nuxt, so the root config listed each and the type check skipped their pages: the passed code it never read. Inlayers/, Nuxt finds and type-checks every Tenant, and adding one no longer edits the root config, a file. The cost: a folder name that isn't "Tenant". ADR-0018 (docs/adr/0018-tenant-layers-under-layers-directory.md). - Collections from manifests. Each is its own table, keeping Tenants apart, so a wrong name matters. Every Collection comes from tested code reading each Tenant's , never from a layer; Content collections says why a hand-written list lost. The cost: a layer only changes looks; new content means a manifest edit. ADR-0002 (docs/adr/0002-manifest-driven-config-generation.md).
What to watch for
- A layer is not a sandbox. Its config merges into the one app, so a stylesheet it lists loads on every page of every Tenant. Terrarium scopes each Tenant's CSS under one wrapper class, and keeps a layer's plugins and config flags inert outside its own pages. The short list of these traps is .claude/rules/tenant-layers.md.
- One global namespace. Components, composables and utils from every layer are auto-imported everywhere, and when two of them share a name, one silently replaces the other.
- A folder in
layers/is read twice. Nuxt treats it as a layer, and shared/expand.ts loads itstenant.config.tsand stops the build if the manifest's name differs from the folder's. A folder there is a Tenant, or the build fails. ~means the Platform. Inside a layer,~and@point at the root app. A layer imports its own files by relative path.
Try it in your project
Pick one section of your Nuxt app, such as an admin area. Move its pages and components into layers/admin/, add a layers/admin/nuxt.config.ts holding only export default defineNuxtConfig({}), and check that its routes and components still work with nothing added to the root config.
Where next
- LessonFile routing
- LessonContent collections
- LessonTesting
- LessonType checking
- JournalArchitecture & Deployment
Walkthroughs through this Lesson
- WalkthroughAdding a site
Last updated · Latest source on GitHub ↗