Ecopages0.2.0-rc.4

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:

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:

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 config.__eco metadata (eco.component(), eco.layout(), or eco.html()). Undeclared values throw UndeclaredComponentDependencyError.

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.

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:

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:

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

props receives LayoutPropsContext (params, query, locals). See 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, persistLayouts is enabled by default. Each layout tier is cached by config.__eco.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 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:

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

Every entry in dependencies.components must be a declared eco component, same as on pages.

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