# Shell Patterns for Vue Router Router-specific additions to [Shell Patterns (Fundamentals)](shell-patterns.md). Read the fundamentals guide first; this document only covers the parts that depend on vue-router — route shape, zones and route data via `meta`, the auth guard, and the two ways a shell can hand the runtime its router. If you're starting from scratch, work through [Getting started with Vue Router](getting-started-vue-router.md) first; this doc picks up where that leaves off. ## Two integration modes vue-router registers routes at runtime (`router.addRoute()`), so — unlike the React adapters, which compose a route _tree_ — the Vue runtime always grafts routes onto a router the shell already created. There are two shapes for that, and they differ only in **who owns the app root**: | Mode | Entry | Who owns the root component | Providers | Lazy modules | | ------------------ | ------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------ | ---------------------------------------- | | **Router-owning** | `registry.resolve(options)` / `createModularApp(registry, options)` | Your component rendering `` | Installed app-wide via `app.use(manifest)` | Fully wired (`resolve()` has the router) | | **Framework mode** | `registry.resolveManifest(options)` | You wrap `` in `manifest.Providers` | A `Providers` component you mount | Eager routes only | The **router-owning** path is the one the getting-started guide uses — the manifest is itself a Vue plugin, so `app.use(manifest)` wires every modular context in one line, and you pass the router (and the auth guard) straight to `resolve()`: ```ts const router = createRouter({ history: createWebHistory(), routes: shellRoutes }); const manifest = createModularApp(registry, { router, parentRouteName: "root", authGuard }); const app = createApp(App); app.use(router); app.use(manifest); // provides navigation / modules / slots / shared deps app-wide app.mount("#app"); ``` **Framework mode** suits shells that want to keep every provider inside the Vue tree (so app-level providers like i18n or a query client can wrap them). You spread the eager module routes into your own `createRouter`, and wrap `` in the `Providers` component the manifest hands back: ```ts import { createApp, defineComponent, h } from "vue"; import { createRouter, createWebHistory, RouterView } from "vue-router"; const manifest = registry.resolveManifest(); const router = createRouter({ history: createWebHistory(), routes: [{ path: "/", component: Layout }, ...manifest.routes], }); const Root = defineComponent({ name: "Root", setup: () => () => h(manifest.Providers, null, () => h(RouterView)), }); createApp(Root).use(router).mount("#app"); ``` Framework mode is also where the journeys and compositions plugins thread their own providers automatically (`manifest.Providers` includes them), which is why the journey and composition examples use it. See [Journeys](../packages/journeys/README.md) and [Compositions](../packages/compositions/README.md). ## Module routes A module's `createRoutes` returns a vue-router `RouteRecordRaw` (or an array of them). The runtime grafts it onto the live router — under a named parent route when you pass `parentRouteName`, otherwise at the top level. ```ts import { defineModule } from "@modular-vue/core"; import type { RouteRecordRaw } from "vue-router"; export default defineModule()({ id: "billing", version: "1.0.0", createRoutes: (): RouteRecordRaw => ({ path: "billing", component: () => import("./pages/BillingRoot.vue"), // lazy, code-split children: [ { path: "", component: () => import("./pages/Dashboard.vue") }, { path: "invoices/:invoiceId", component: () => import("./pages/InvoiceDetail.vue") }, ], }), }); ``` - Use `component: () => import("./Page.vue")` for a lazy, code-split route — vue-router resolves the async component on first visit. - A child with `path: ""` is the index route (vue-router's analog of React Router's `index: true`). - `createRoutes` is optional; a **headless module** (stores/commands/zones only) omits it. ## Route zones Zones are components a module contributes into named layout regions the shell owns — a detail panel, header actions. vue-router carries arbitrary per-route data on the route's `meta` field (the analog of React Router's `handle`), and the runtime reads zones from there. ### Declaring zones on a route Put the component on the route's `meta`: ```ts createRoutes: (): RouteRecordRaw => ({ path: "users/:userId", component: UserDetailPage, meta: { detailPanel: UserDetailSidebar, headerActions: UserDetailActions, }, }), ``` The shell reads them with `useZones`, passing its zone-shape alias: ```vue ``` Zones merge across the matched route chain **deepest-wins** per key — a deeper route overrides a shallower one for the same zone. An `undefined` value at a deeper level doesn't clobber an ancestor's value; set a key to `null` to explicitly clear an inherited one. The runtime logs a deduped `console.warn` in dev when a deeper route overrides a zone. ### Type-safe `meta` Declare the zone shape once in `app-shared` and type the whole `meta` channel through vue-router's global `RouteMeta` augmentation: ```ts // app-shared/src/app-types.ts import type { UiComponent } from "@modular-frontend/core"; export interface AppZones { detailPanel?: UiComponent; headerActions?: UiComponent; } export interface AppRouteData { headerVariant?: "portal" | "project" | "setup"; pageTitle?: string; } ``` > **Why `UiComponent`, not vue's `Component`?** `useZones()` constrains > each zone value to the framework-neutral `UiComponent` > (`(props) => any | new (props) => any`) so the merge logic stays shared across > bindings. A ` ``` Both `useZones` and `useRouteData` return a **`ComputedRef`** (not a plain object): they derive from the reactive `useRoute()`, so they recompute on navigation. In `