---
title: "Layouts"
description: "Wrap pages with shared chrome using eco.layout and nested layout composition."
order: 8
---

# Creating Layouts in Ecopages

Layouts wrap page content with shared chrome — navigation, sidebars, providers, and other structure that repeats across routes. This guide covers document includes, declared layout components, single and nested layout stacks, and how layouts interact with SPA routing.

## Includes Files

Includes files are mandatory in Ecopages and define the outer document shell. They live under `src/includes` (for example `html.tsx` and `head.tsx` with Ecopages JSX).

### html.tsx

Defines the overall HTML envelope:

```tsx
import { eco } from '@ecopages/core';
import type { HtmlTemplateProps } from '@ecopages/core';
import type { JsxRenderable } from '@ecopages/jsx';

export const Html = eco.html<HtmlTemplateProps<JsxRenderable>, JsxRenderable>({
	dependencies: {
		stylesheets: ['./html.css'],
	},
	render: ({ children }) => <html lang="en">{children}</html>,
});
```

### head.tsx

Defines `<head>` content — title, meta, icons:

```tsx
import { eco, type PageHeadProps } from '@ecopages/core';
import type { JsxRenderable } from '@ecopages/jsx';

export const Head = eco.component<PageHeadProps<JsxRenderable>, JsxRenderable>({
	render: ({ metadata, children }) => (
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>{metadata.title}</title>
		<meta name="description" content={metadata.description} />
		<link rel="icon" href="/favicon.ico" />
		{children}
	</head>
	),
});
```

Includes are applied automatically. Pages and route layouts render **inside** this shell — they do not replace it.

## Declared layout components

Route layouts must be created with **`eco.layout()`** (or `eco.component()` when you need the broader factory). Plain functions — including standard React components without eco metadata — are **not** valid layout values.

**Why:** `eco.page({ layout })` merges layouts into `dependencies.components`. At render time, EcoPages validates that every dependency entry is a **declared** component with bound `config.identity` (`eco.component()`, `eco.layout()`, or `eco.html()`). Undeclared values throw `UndeclaredComponentDependencyError`.

```tsx
import { eco } from '@ecopages/core';
import type { ReactNode } from 'react';

export const BaseLayout = eco.layout<{ children: ReactNode }>({
	dependencies: {
		stylesheets: ['./base-layout.css'],
	},
	render: ({ children }) => <main>{children}</main>,
});
```

Use `eco.layout()` for wrappers around page content. Reserve `eco.html()` for the document shell in `src/includes`.

## Single layout on a page

Assign the layout on `eco.page()`. EcoPages wraps `render` output automatically — do not manually nest the layout inside `render` unless you have an exceptional one-off case.

```tsx
import { eco } from '@ecopages/core';
import { BaseLayout } from '@/layouts/base-layout';

export default eco.page({
	layout: BaseLayout,
	metadata: () => ({
		title: 'Home',
		description: 'Welcome',
	}),
	render: () => (
		<>
			<h1>Welcome to Ecopages</h1>
			<p>This is the homepage.</p>
		</>
	),
});
```

## Nested layouts (outer → inner)

Pages can declare **multiple** layout tiers. Pass an array in **outer → inner** order:

```tsx
import { eco } from '@ecopages/core';
import { AppShell } from '@/layouts/app-shell';
import { DocsSection } from '@/layouts/docs-section';

export default eco.page({
	layout: [AppShell, DocsSection],
	render: () => <h1>Documentation</h1>,
});
```

EcoPages normalizes the stack to:

- `config.layouts` — component references per tier
- `config.layoutEntries` — components plus optional per-tier `props` factories

### Per-tier layout props

When a tier needs route context, use a layout entry object:

```tsx
layout: [
	AppShell,
	{
		component: DocsSection,
		props: ({ params, locals }) => ({
			activeSlug: params.slug,
			viewer: locals.user?.name,
		}),
	},
],
```

`props` receives `LayoutPropsContext` (`params`, `query`, `locals`). See [Request Locals](/docs/core/request-locals) for how `locals` reach layout tiers during SSR and hydration.

### Shared outer layouts and SPA navigation

On React sites with [`@ecopages/react-router`](/docs/ecosystem/react-router), `persistLayouts` is enabled by default. Each layout tier is cached by `config.identity.file` (or `id`). Two routes such as `[AppShell, Docs]` and `[AppShell, Settings]` share one mounted **AppShell** during client navigation; only the inner tier swaps. React state in the outer layout survives SPA transitions.

Provider layouts that mount shared client state (query clients, MobX stores, audio engines) should also set `runtimeProvider: true` on `eco.layout()` so Ecopages vendors one shared copy of those npm packages across page chunks. See [Shared runtime vendors](/docs/integrations/react#shared-runtime-vendors) in the React integration guide.

**Note:** Full reloads, HMR, and bootstrap paths that refresh persisted layouts may replace a cached tier when the imported function reference changes.

## Layout dependencies

Declare stylesheets, scripts, and child components on the layout factory. Direct local component imports are discovered automatically:

```tsx
import { SiteHeader } from '@/components/site-header';
import { eco } from '@ecopages/core';

export const BaseLayout = eco.layout({
	dependencies: {
		stylesheets: ['./base-layout.css'],
		scripts: ['./base-layout.script.ts'],
	},
	render: ({ children }) => (
		<>
			<SiteHeader />
			<main>{children}</main>
		</>
	),
});
```

Every entry in `dependencies.components` must be a declared eco component, same as on pages. Use explicit `dependencies.components` only when discovery cannot follow the import (`export *` barrels, packages).

## How layouts are assigned

Assign layouts on `eco.page({ layout })` — one component or an outer→inner array. Import layout modules from `src/layouts/` (or elsewhere) and pass the declared component to `layout`; EcoPages does not auto-wrap pages based on layout file paths or route folders.

## Related guides

- [`eco` namespace reference](/docs/reference/eco-namespace) — full API for `eco.layout()` and `eco.page()`
- [Creating Pages](/docs/core/pages) — page structure and `dependencies`
- [React Router](/docs/ecosystem/react-router) — layout persistence during SPA navigation
