# Navigation and composition

Connect the shell's URL and navigation intents to application mounting, unmounting, and state.

<span id="el-shell-es-dueño-de-la-navegación"></span>

<span class="section-anchor" id="section-1"></span>

## The shell owns navigation

The shell decides which application corresponds to each route and uses mounting, unmounting, events, and state to compose it. Astro can resolve pages; a React/Vue router can manage an application's internal routes. WU core does not include `wu.router` or `wu.navigate()`: connect your shell's navigation mechanism to its lifecycle.

`metadata.routes` in `wu.json` is descriptive information. The parser validates strings, duplicates, and limits but does not install History API handlers. Nonempty `wu.routes` is rejected in Framework 4 and must not be used as a routing recipe.

<span id="dos-niveles-de-rutas"></span>

<span class="section-anchor" id="section-2"></span>

## Two routing levels

| Level | Example | Owner |
| --- | --- | --- |
| Portal page | `/docs/framework/state` | Astro file router |
| Dynamic shell section | `/workspace/catalog` | Shell controller/router |
| Internal app view | `/products/:id` within catalog | That app's router |

The download base URL, such as `/apps/catalog`, is a technical resource and need not match the navigation route. Define the bundle base, internal router base, and user-visible route separately.

<span id="conectar-una-intención-de-navegación"></span>

<span class="section-anchor" id="section-3"></span>

## Connect a navigation intent

This example implements a shell convention; `shell:navigate` does not activate a built-in router:

```js
const routeTable = new Map([
  ['/workspace/catalog', 'catalog'],
  ['/workspace/reports', 'reports'],
]);

let activeApp = null;
let transition = Promise.resolve();

function showRoute(path) {
  transition = transition.catch(() => {}).then(async () => {
    const nextApp = routeTable.get(path);
    if (!nextApp || nextApp === activeApp) return;
    if (activeApp) await wu.unmount(activeApp, { force: true });
    activeApp = null;
    await wu.mount(nextApp, '#workspace-slot');
    activeApp = nextApp;
  });
  return transition;
}

const stop = wu.on('shell:navigate', ({ data }) => {
  if (!routeTable.has(data?.path)) return;
  history.pushState(null, '', data.path);
  showRoute(data.path).catch(console.error);
});

const onPopState = () => showRoute(location.pathname).catch(console.error);
window.addEventListener('popstate', onPopState);
await showRoute(location.pathname);

// Al retirar el controlador del shell:
// stop();
// window.removeEventListener('popstate', onPopState);
```

The table limits accepted routes and apps. The queue preserves order when the user navigates several times before loading finishes. The core also coordinates container ownership; maintaining clear shell state prevents an old promise from updating navigation controls for a newer route.

<span id="elementos-dentro-de-shadow-dom"></span>

<span class="section-anchor" id="section-4"></span>

## Elements inside Shadow DOM

`wu.mount(name, selector)` resolves the selector with `document.querySelector`. That selector does not cross ShadowRoots, and the current signature expects a string, not an HTMLElement. To let the shell locate the slot, place it in its light DOM or use a composition that exposes a selector-accessible node.

Inside a lifecycle, use the supplied `container` to manipulate the local root; do not search for it globally by ID again. The repository's Vue adapter illustrates why a stable root should be a sibling of the loading indicator, not its shell-managed container.

<span id="keep-alive-y-cambio-de-ruta"></span>

<span class="section-anchor" id="section-5"></span>

## Keep-alive and route changes

`hide` preserves state and listeners; it works when you retain the app's container. Switching many apps on one slot requires an explicit decision: unmount the previous owner or keep separate containers for each app that remains alive. Do not equate keep-alive with caching modules; they are different resources.

If Astro replaces pages through client navigation, bootstrapping must follow that lifecycle: release listeners and mounts from the previous page and connect the new slots. A deduplicated script import does not mean all future nodes are mounted automatically.

<span id="roles-de-ui"></span>

<span class="section-anchor" id="section-6"></span>

## UI roles

`wu.setPrincipal({ id: 'u1', roles: ['catalog-reader'] })` feeds `wu.can('catalog')`. Mounting checks the roles required by the manifest and may reject with `WU_ACCESS_DENIED`. This helps keep navigation and views consistent; data APIs must authorize every request on the server.

<span id="referencia-de-implementación"></span>

<span class="section-anchor" id="section-7"></span>

## Implementation references

Descriptive route validation: `src/core/wu-manifest.js`; access: `src/core/wu-core.js` and `src/core/wu-core.js`; selector: `src/core/wu-core.js`; container transition: `src/core/wu-core.js`; convention used by MCP: `src/core/wu-mcp-bridge.js`. Tests: `tests/unit/shared-container-navigation.test.js`, `tests/unit/sandbox-container-ownership.test.js`, `tests/unit/rbac.test.js`.