---
title: "Eco Namespace"
description: "Reference for the eco namespace API used to define pages, components, and data."
order: 1
---

# `eco` Namespace

A unified API for defining components, pages, and page data in EcoPages.

## Overview

The `eco` namespace provides a consistent, type-safe interface for:

1. **`eco.component()`** - Factory for defining reusable components with dependencies and optional lazy-loading
2. **`eco.html()`** - Semantic alias for the document shell component (owns `<html>`, `<head>`, `<body>`)
3. **`eco.layout()`** - Semantic alias for route layout components (page-level wrappers)
4. **`eco.page()`** - Factory for defining page components with optional inline `staticPaths`, `staticProps`, and `metadata`
5. **`eco.embed()`** - Explicit render helper for foreign or same-integration components during mixed rendering
6. **`eco.metadata()`** - Type-safe wrapper for page metadata (legacy pattern)
7. **`eco.staticPaths()`** - Type-safe wrapper for dynamic route generation (legacy pattern)
8. **`eco.staticProps()`** - Type-safe wrapper for static data fetching (legacy pattern)


## Dependency discovery

Discovery is enabled by default for modules declaring `eco.component()`, `eco.layout()`, `eco.html()`, or `eco.page()`, as well as MDX documents compiled through `@ecopages/mdx/core`.

```tsx
import { eco } from '@ecopages/core';
import { Counter } from './counter';
import './page.css';

export default eco.page({
    render: () => <Counter />,
});
```

Direct static named/default imports of local Eco Components contribute their Dependencies transitively. Configured TypeScript path aliases and relative imports are supported for both Components and CSS stylesheets (e.g., `import '@/components/button'` or `import '@/styles/main.css'`). Side-effect CSS imports become stylesheet Dependencies; the shared transform removes the imports so the existing asset pipeline owns delivery in server and browser builds. In MDX documents, top-level component and CSS imports are discovered while markdown code blocks and dynamic imports within functions are safely ignored.

Discovery is conservative and module-scoped: every Eco declaration in a file shares its imported Components and styles, even when a render condition omits a Component. It does not infer which Components actually render. **Best practice:** author **one `eco.component()` per file**. Co-locating multiple components in a single file will cause all declared components in that file to share discovered dependencies.

Imported Layouts contribute assets; only the explicit `layout` option controls Layout composition.

Explicit `dependencies` remain supported. Explicit Components come first, followed by discovered Components in import order, with duplicate Components removed. Explicit stylesheet declarations take precedence over discovered references to the same resolved file, preserving their attributes and order. Discovered stylesheets are **not** written into `config.dependencies.stylesheets`; they stay on Component identity until collection. Inferred styles follow explicit styles and are emitted once per collection. Circular dependency traversal is guarded by Component config identity, so two Components in one file remain distinct.

When a Page combines its own relative assets with a content entry, use `mergePageDependencies()`. Do not object-spread the two bags: spread copies `ownerFile` onto the other side's relative paths.

```ts
import { eco, mergePageDependencies } from '@ecopages/core';
import { getEntryDependencies } from 'ecopages:content/posts/server';

export default eco.page({
	dependencies: async ({ props }) =>
		mergePageDependencies({ stylesheets: ['./post.css'] }, await getEntryDependencies(props.slug)),
	render: () => <article />,
});
```

Browser scripts remain explicit:

```ts
scripts: [{
    src: './counter.script.ts',
    ssr: true,
    lazy: { 'on:visible': true },
}]
```

For Lit and Ecopages JSX (Radiant hosts), `ssr: true` imports the script on the server before rendering so `customElements.define` runs without a separate value import in the component file. Only those integrations run SSR preload today; other renderers ignore `ssr: true` until they adopt the same preloader. `lazy` controls browser delivery only; it does not suppress server import. Lazy entries with `ssr: true` do not emit a duplicate eager browser script—browser delivery follows `lazy`, or loads eagerly when `lazy` is omitted. String-form `scripts: ['./file.ts']` stays browser-only unless you opt in with `{ src, ssr: true }`.

Named barrel re-exports (`export { Counter } from './counter'`) are followed for the imported binding only. Discovery does not follow `export * from './components'`, dynamic imports, namespace imports (`import * as`), package Components/CSS, CSS Modules, or custom import attributes. `export *` would pull an entire kit into the page asset graph; keep that explicit with `dependencies.components`. Plain-function modules retain their existing behavior. Ordinary utility imports do not become browser script entries. Missing supported imports report the owner file and import specifier.

## Component Patterns

EcoPages provides a unified API for creating components and pages. While there are a few ways to define components, **`eco.component()` is the standard and recommended approach** for most use cases, as it ensures proper dependency management, lazy loading, and type safety.

## Component Creation

### `eco.component()` (Standard)

This is the primary way to create components in EcoPages. It handles:
- **Dependency Management:** Automatically injects stylesheets and scripts.
- **Lazy Loading:** Supports interaction, visibility, and idle triggers.
- **Type Safety:** Infers props for both internal usage and custom elements.


For components that need dependencies, scripts, or lazy loading:

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

export const Counter = eco.component({
	dependencies: {
		stylesheets: ['./counter.css'],
		scripts: [{ src: './counter.script.ts', ssr: true, lazy: { 'on:interaction': 'mouseenter,focusin' } }],
	},
	render: ({ count }) => <my-counter count={count}></my-counter>,
});
```

**When to use:**

- Components with client-side interactivity
- Components requiring scripts or stylesheets
- Lazy-loaded components with hydration strategies
- Web components or custom elements

## Optimizations & Specific Integrations

While `eco.component()` is the default, there are specific scenarios where lighter-weight alternatives are preferred for optimization or specific framework integrations.

### Simple JSX Functions (Optimization)

For purely presentational components (static UI) that **do not require** client-side interactivity, scripts, or dedicated stylesheets, you can use plain JSX functions. This is a performance optimization that avoids the overhead of a custom element wrapper and works especially well with utility-first CSS where styles are expressed inline through class names.

```tsx
import type { PropsWithChildren } from '@/types';
import { cn } from 'your-library';

export function Card({ children, className }: PropsWithChildren<{ className?: string }>) {
	return <div class={cn('p-6 rounded-2xl border border-white/10 bg-zinc-900/30', className)}>{children}</div>;
}
```

**When to use:**

- Static/presentational UI (cards, alerts, badges, layout primitives)
- Projects using utility-first CSS (Tailwind, UnoCSS, etc.)
- Components that only render HTML without scripts or dedicated stylesheets
- Zero runtime cost - just functions returning markup

> **Note:** If your component requires a dedicated CSS file, use `eco.component()` instead to manage the stylesheet dependency.

### Plain React Components (Integration)

You can use standard React components directly if they only rely on hooks (`useState`, `useEffect`) and local styling patterns such as Tailwind CSS. This is useful for migrating existing React code or when you need complex state management that doesn't require the full `eco` lifecycle.

Powered by [`@ecopages/react`](https://github.com/ecopages/react).

> **Warning:** These components **cannot** declare external dependencies (scripts/stylesheets) or use EcoPages' lazy loading strategies directly. For those features, wrap them in `eco.component()`.
>
> **Important:** Plain React functions are **not** valid as `eco.page({ layout })` values or as `dependencies.components` entries. The renderer validates that dependency graph nodes are **declared** components (`eco.component()`, `eco.layout()`, or `eco.html()`) with bound `config.identity`. Use `eco.layout()` for route wrappers instead of ad-hoc functions.

```tsx
import { useState } from 'react';

export function Counter() {
	const [count, setCount] = useState(0);

	return (
		<button
			onClick={() => setCount(count + 1)}
			className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
		>
			Count: {count}
		</button>
	);
}
```

**When to use:**

- Migrating existing React codebases
- Complex state logic within a single component
- When you are already using the React integration

> **Note:** If you need to attach external assets or lazy-load the component, switch to `eco.component()`.

### Comparison

| Aspect | Simple JSX | Plain React | `eco.component()` |
| :--- | :--- | :--- | :--- |
| React hooks | No | Yes | Yes |
| Scripts/Stylesheets | No | No | Yes |
| Lazy loading | No | No | Yes |
| Hydration strategies | No | No | Yes |
| Runtime cost | Zero | Minimal | Minimal |
| Use case | Static UI | Interactive UI | Advanced UI |

All patterns can coexist in the same project. Use the right tool for the job.

## React Support

EcoPages fully supports React components. You can use `eco.component()` with React to managing dependencies and hydration while maintaining type safety.

Since React components return `ReactNode` or `JSX.Element` (not `EcoPagesElement`), you need to specify the return type generic:

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

type ButtonProps = {
	label: string;
	onClick?: () => void;
};

// Specify ReactNode as the second generic argument
export const Button = eco.component<ButtonProps, ReactNode>({
	dependencies: {
		stylesheets: ['./button.css'],
	},
	render: ({ label, onClick }) => (
		<button className="eco-button" onClick={onClick}>
			{label}
		</button>
	),
});
```

You can also use it for pages:

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

export default eco.page<Props, JSX.Element>({
	layout: BaseLayout,
	render: () => <h1>Hello React</h1>,
});
```

## Two Patterns for Pages

### Consolidated API (Recommended)

Define everything in one place:

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

export default eco.page<BlogPostProps>({
	layout: BaseLayout,
	staticPaths: async () => ({ paths: getAllSlugs() }),
	staticProps: async ({ pathname }) => ({
		props: { post: await getPost(pathname.params.slug) },
	}),
	metadata: ({ props: { post } }) => ({
		title: post.title,
		description: post.excerpt,
	}),
	render: ({ post }) => <article>{post.content}</article>,
});
```

### Separate Exports (Legacy)

The traditional pattern with named exports:

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

export const getStaticPaths = eco.staticPaths(async () => ({
	paths: getAllSlugs(),
}));

export const getStaticProps = eco.staticProps(async ({ pathname }) => ({
	props: { post: await getPost(pathname.params.slug) },
}));

export const getMetadata = eco.metadata<typeof getStaticProps>(({ props: { post } }) => ({
	title: post.title,
	description: post.excerpt,
}));

export default eco.page<typeof getStaticProps>({
	render: ({ post }) => <article>{post.content}</article>,
});
```

Both patterns work and can be mixed - the renderer checks for attached properties first, then falls back to named exports.

## API Reference

### `eco.html()`

Creates the document shell component — the outermost HTML wrapper rendered once per page. Semantically equivalent to `eco.component()` but signals to tooling and readers that this component owns the full document structure (`<html>`, `<head>`, `<body>`).

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

export const Document = eco.html({
	dependencies: {
		stylesheets: ['./document.css'],
	},
	render: ({ children, metadata }) => (
		<html lang="en">
			<head>
				<title>{metadata?.title ?? 'EcoPages'}</title>
			</head>
			<body>{children}</body>
		</html>
	),
});
```

### `eco.layout()`

Creates a route layout component — a wrapper rendered around page content. Semantically equivalent to `eco.component()` but clearly communicates that the component is intended to be used as a `layout` in `eco.page()`.

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

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

Use `eco.layout()` components as the `layout` option in `eco.page()`. EcoPages merges declared layouts into the page dependency graph automatically — you do not need to list the layout again in `dependencies.components` unless you also render it manually inside `render` (unusual).

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

export default eco.page({
	layout: BaseLayout,
	render: () => <h1>Hello</h1>,
});
```

#### Nested layouts (outer → inner)

Pass a single layout or an **array** of layouts. Array order is **outermost first**, innermost last.

```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>,
});
```

At factory time, EcoPages normalizes this stack onto `config.layouts` (components) and `config.layoutEntries` (components plus optional per-tier `props` factories).

Per-tier prop factories are supported when a tier needs route-scoped props:

```tsx
layout: [
	AppShell,
	{
		component: DocsSection,
		props: ({ params }) => ({ activeSection: params.slug }),
	},
],
```

On React sites with [`@ecopages/react-router`](/docs/ecosystem/react-router), each tier is cached independently during SPA navigation. Routes that share an outer layout — for example `[AppShell, Docs]` and `[AppShell, Settings]` — reuse the same mounted outer instance while inner tiers swap.

### `eco.component()`

Define a reusable component with dependencies. Entries in `dependencies.components` must themselves be declared eco components (`eco.component()`, `eco.layout()`, or `eco.html()`).

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

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

**With lazy-loaded scripts:**

```tsx
export const Counter = eco.component({
	dependencies: {
		stylesheets: ['./counter.css'], // loaded immediately
		scripts: [
			{ src: './counter.script.ts', ssr: true, lazy: { 'on:interaction': 'mouseenter,focusin' } },
		],
	},
	render: ({ count }) => <my-counter count={count}></my-counter>,
});
```

#### Lazy Loading Options

Lazy loading is configured per dependency entry in `dependencies.scripts`:

```tsx
// Custom element (Lit or Radiant): server registration + lazy browser load
scripts: [{ src: './counter.script.ts', ssr: true, lazy: { 'on:interaction': 'mouseenter,focusin' } }]

// Browser-only: no server import (string form or object without ssr: true)
scripts: ['./analytics.ts']
scripts: [{ src: './analytics.ts', lazy: { 'on:idle': true } }]
```

Custom-element entries can mix triggers; each object entry can define its own `lazy` rule:

```tsx
scripts: [{ src: './component.script.ts', ssr: true, lazy: { 'on:visible': true } }]
```

Each entry can define its own trigger, so mixed strategies in one component are supported.

These map directly to [`scripts-injector`](https://github.com/ecopages/scripts-injector) attributes.

### `eco.embed()`

Renders a declared eco component explicitly and optionally injects `children` into the props bag before invocation. During mixed rendering, `eco.embed()` routes through the active foreign-child runtime so the owning integration can queue and resolve cross-integration subtrees.

Integration-owned JSX wrappers expose the same contract:

| Integration   | Import                          |
| :------------ | :------------------------------ |
| Ecopages JSX  | `@ecopages/ecopages-jsx/eco-embed` |
| React         | `@ecopages/react/eco-embed`     |
| KitaJS        | `@ecopages/kitajs/eco-embed`      |

```tsx
import { eco } from '@ecopages/core';
import { KitaShell } from '@/components/kita-shell';

// Imperative handoff
const html = eco.embed(KitaShell, { id: 'shell' }, '<span>Leaf</span>');
```

```tsx
/** @jsxImportSource @ecopages/jsx */
import { EcoEmbed } from '@ecopages/ecopages-jsx/eco-embed';
import { KitaShell } from '@/components/kita-shell';

// JSX wrapper (preferred in .tsx / .kita.tsx / React files)
<EcoEmbed component={KitaShell} props={{ id: 'shell' }}>
	<span>Leaf</span>
</EcoEmbed>;
```

**Children at foreign-subtree queue boundaries**

Core rejects plain opaque objects before they enter the foreign-subtree queue. Values that would stringify to `[object Object]` throw a `TypeError` with guidance to use `EcoEmbed` or pass already-serialized HTML.

Accepted without throwing: HTML strings, template results (`strings` / `values`), markup nodes with `outerHTML`, arrays, and framework element markers (`$$typeof`).

Use `EcoEmbed` (or pre-serialized HTML) for cross-integration shell stacks. See [Ecopages JSX — Mixed Rendering](/docs/integrations/ecopages-jsx#mixed-rendering) for the full boundary contract.

### `eco.page()`

Define a page component with optional inline static functions.

**Consolidated API (everything in one place):**

```tsx
type BlogPostProps = { slug: string; title: string; text: string };

export default eco.page<BlogPostProps>({
	layout: BaseLayout,

	// Generate paths for dynamic routes
	staticPaths: async () => ({
		paths: posts.map((p) => ({ params: { slug: p.slug } })),
	}),

	// Fetch data at build time
	staticProps: async ({ pathname }) => ({
		props: {
			slug: pathname.params.slug as string,
			title: post.title,
			text: post.text,
		},
	}),

	// Generate page metadata
	metadata: ({ props: { title, slug } }) => ({
		title: `${title} | My Blog`,
		description: `Read about ${slug}`,
	}),

	// Render the page
	render: ({ title, text }) => (
		<article>
			<h1>{title}</h1>
			<p>{text}</p>
		</article>
	),
});
```

**Simple page without data fetching:**

```tsx
export default eco.page({
	layout: BaseLayout,
	metadata: () => ({
		title: 'Home',
		description: 'Welcome to EcoPages',
	}),
	render: () => <h1>Welcome</h1>,
});
```

**With layout (single or nested):**

```tsx
export default eco.page({
	layout: BaseLayout, // or layout: [AppShell, SectionLayout]
	render: () => <h1>Content</h1>,
});
```

**With nested layouts:**

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



## Complete Examples

### Dynamic Blog Post (Consolidated API)

```tsx
// pages/blog/[slug].tsx
import { eco } from '@ecopages/core';
import { BaseLayout } from '@/layouts/base-layout';
import { getBlogPost, getAllBlogPostSlugs, getAuthor } from '@/mocks/data';

type BlogPostProps = {
	slug: string;
	title: string;
	text: string;
	authorId: string;
	authorName: string;
};

export default eco.page<BlogPostProps>({
	layout: BaseLayout,

	staticPaths: async () => {
		return { paths: getAllBlogPostSlugs() };
	},

	staticProps: async ({ pathname }) => {
		const slug = pathname.params.slug as string;
		const post = getBlogPost(slug);
		if (!post) throw new Error(`Post not found: ${slug}`);
		const author = getAuthor(post.authorId);
		return {
			props: {
				slug,
				title: post.title,
				text: post.text,
				authorId: post.authorId,
				authorName: author?.name ?? 'Unknown',
			},
		};
	},

	metadata: ({ props: { title, slug } }) => ({
		title: `${title} | My Blog`,
		description: `Read the blog post: ${slug}`,
	}),

	render: ({ title, text, authorId, authorName }) => (
		<article>
			<h1>{title}</h1>
			<p>
				By <a href={`/blog/author/${authorId}`}>{authorName}</a>
			</p>
			<div>{text}</div>
		</article>
	),
});
```

### Lazy-Loaded Component

```tsx
// components/counter/counter.tsx
import { eco } from '@ecopages/core';

export const Counter = eco.component({
	dependencies: {
		stylesheets: ['./counter.css'],
		scripts: [{ src: './counter.script.ts', ssr: true, lazy: { 'on:interaction': 'mouseenter,focusin' } }],
	},
	render: ({ count }) => <my-counter count={count}></my-counter>,
});
```

**HTML Output:**

```html
<scripts-injector>
	<script type="ecopages/injector-map">
		{
			"on:interaction": {
				"value": "mouseenter,focusin",
				"scripts": ["/_assets/components/counter/counter.script.js"]
			}
		}
	</script>
	<my-counter count="5">
		<!-- SSR content -->
	</my-counter>
</scripts-injector>
```

### Page with Lazy Component

```tsx
// pages/index.tsx
import { eco } from '@ecopages/core';
import { BaseLayout } from '@/layouts/base-layout';
import { Counter } from '@/components/counter';

export default eco.page({
	layout: BaseLayout,
	metadata: () => ({
		title: 'Home',
		description: 'Welcome to EcoPages',
	}),
	render: () => (
		<>
			<h1>Welcome</h1>
			<Counter count={5} />
		</>
	),
});
```

The static `Counter` import is discovered automatically. You do not list it again in `dependencies.components`. Lazy scripts stay on the Component.

## Type Definitions

```typescript
type LazyTrigger = { 'on:idle': true } | { 'on:interaction': string } | { 'on:visible': true | string };

type DependencyEntry = {
	src?: string;
	content?: string;
	lazy?: LazyTrigger;
	ssr?: boolean;
	attributes?: Record<string, string>;
};

interface EcoComponentDependencies {
	scripts?: Array<string | DependencyEntry>;
	stylesheets?: Array<string | DependencyEntry>;
	modules?: string[];
	/** Declared eco components only — see `EcoDeclaredComponent`. */
	components?: EcoDeclaredComponent[];
}

type FileOwnedDependencyContribution = EcoComponentDependencies & {
	ownerFile?: string;
};

type PageDependenciesResult = FileOwnedDependencyContribution & {
	contributions?: FileOwnedDependencyContribution[];
};

type GetPageDependencies<T> = (context: {
	props: PagePropsFor<T>;
	params?: Record<string, string>;
	query?: Record<string, string>;
}) => PageDependenciesResult | undefined | Promise<PageDependenciesResult | undefined>;

// Shared base option shape used by component(), html(), and layout()
interface ComponentOptions<P, E = EcoPagesElement> {
	componentDir?: string;
	dependencies?: EcoComponentDependencies;
	render: (props: P) => E;
}

// html() and layout() accept the same options as component() but return
// narrower types to signal intent (EcoHtmlComponent / EcoLayoutComponent).
type HtmlOptions<E = EcoPagesElement> = ComponentOptions<Record<string, unknown>, E>;
type LayoutOptions<E = EcoPagesElement> = ComponentOptions<{ children: E }, E>;

type EcoPageLayoutSpec<E = EcoPagesElement> =
	| EcoDeclaredComponent<any, E>
	| { component: EcoDeclaredComponent<any, E>; props?: (context: LayoutPropsContext) => Record<string, unknown> };

/** Outer → inner. Normalized to `config.layouts` / `config.layoutEntries`. */
type EcoPageLayouts<E = EcoPagesElement> = EcoPageLayoutSpec<E> | EcoPageLayoutSpec<E>[];

interface PageOptions<T, E = EcoPagesElement> {
	componentDir?: string;
	dependencies?: EcoComponentDependencies | GetPageDependencies<T>;
	layout?: EcoPageLayouts<E>;
	staticPaths?: GetStaticPaths;
	staticProps?: GetStaticProps<T>;
	metadata?: GetMetadata<T>;
	render: (props: PagePropsFor<T>) => E;
}

type EcoPageComponent<T> = EcoComponent<PagePropsFor<T>> & {
	staticPaths?: GetStaticPaths;
	staticProps?: GetStaticProps<T>;
	metadata?: GetMetadata<T>;
};

type PagePropsFor<T> =
	T extends GetStaticProps<infer P>
		? P & { params?: Record<string, string>; query?: Record<string, string> }
		: T & { params?: Record<string, string>; query?: Record<string, string> };
```

## Benefits

### 1. Discoverability

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

eco. // IDE shows: component, html, layout, page, metadata, staticPaths, staticProps
```

### 2. Type Safety

Props flow through the system automatically:

```tsx
export default eco.page<{ title: string }>({
	staticProps: async () => ({ props: { title: 'Hello' } }),
	render: ({ title }) => <h1>{title}</h1>, // ✓ TypeScript knows `title` is a string
});
```

### 3. Single Source of Truth (Consolidated API)

All page configuration in one place - no hunting for separate exports:

```tsx
export default eco.page<Props>({
  layout: BaseLayout,
  staticPaths: async () => ...,
  staticProps: async () => ...,
  metadata: () => ...,
  render: () => ...,
});
```

### 4. Backwards Compatible

Both patterns work side by side - choose what works best for your use case.

## Implementation Notes

The renderer checks for attached properties first:

```typescript
// Check for attached static functions (consolidated API) or named exports (legacy)
const getStaticProps = Page.staticProps ?? module.getStaticProps;
const getMetadata = Page.metadata ?? module.getMetadata;
```

This ensures both patterns work seamlessly.
