);
export default DocsLayout;
```
Rules:
- Must default-export a function component
- Must render `` where child routes should appear
- Must set `path` as a static property for config-based routing
- `metadata` on layouts applies to all child pages (merged with page metadata, page wins)
- Use `LayoutComponent` type from `rasengan` for TypeScript
## Metadata System
```ts
type Metadata = {
title?: string;
description?: string;
openGraph?: {
type?: string;
title?: string;
description?: string;
url: string;
image: string;
width?: string;
height?: string;
};
twitter?: {
card: 'summary_large_image' | 'summary';
image: string;
title: string;
description?: string;
creator?: string;
site?: string;
};
links?: Array<{ rel: string; type?: string; sizes?: string; href: string }>;
metaTags?: Array<{ name?: string; property?: string; content: string }>;
};
```
Rules:
- Page metadata takes priority over layout metadata (merged, page wins)
- Loader-returned `meta` overrides static metadata
- `openGraph.url` and `openGraph.image` are required when using Open Graph
- `twitter.card`, `twitter.image`, `twitter.title` are required when using Twitter cards
## MDX Pages
MDX files in `_routes/` automatically render as pages:
```mdx
---
metadata:
title: My MDX Page
description: Page description
toc: true
---
```
Custom MDX components configured at project root via `mdx-components.{js,ts,jsx,tsx}`:
```tsx
import { defineMDXConfig } from '@rasenganjs/mdx';
export default defineMDXConfig({
components: {
h1: ({ children }) =>
{children}
,
},
layout: ({ children, toc }) => (
{children}
),
});
```
The `@rasenganjs/mdx` Vite plugin must be configured in `rasengan.config.js`:
```js
import mdx from '@rasenganjs/mdx/plugin';
export default defineConfig({
vite: { plugins: [mdx()] },
});
```
## Error Boundaries & 404
- Each route is wrapped with an `ErrorBoundary` automatically
- In development: shows stack traces. In production: generic "Application Error"
- Custom error UI per route is not yet supported
Config-based 404:
```tsx
import { RouterComponent, defineRouter } from 'rasengan';
const NotFound = () => (
404
Page not found
);
class AppRouter extends RouterComponent {}
export default defineRouter({
pages: [Home],
notFoundComponent: NotFound,
})(AppRouter);
```
A catch-all `*` route is auto-added by `generateRoutes()`.
## Navigation
```tsx
import { Link, NavLink, useNavigate, ScrollRestoration } from 'rasengan';
About
Section
isActive ? 'active' : ''}>About
const navigate = useNavigate();
navigate('/about');
```
## Anti-patterns
| Don't ❌ | Do ✅ |
| ------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Forgetting to set `.path` on page/layout components | Always set `.path` as a static property for config-based routing |
| Using `React.FC` type for pages/layouts | Use `PageComponent` / `LayoutComponent` from `rasengan` |
| Putting metadata only in the component body | Use static `.metadata` property so the framework reads it at build time |
| Importing directly from `react-router` | Import routing APIs from `rasengan` |
| Rendering `` outside layouts | Only `` in layout components |
| Defining loaders as exported functions but not attaching them | Attach loader as static `.loader` property on component |
| Nesting pages too deep in `_routes/` | Keep route tree shallow — each directory adds a path segment |
| Forgetting `"type": "module"` in package.json | Rasengan is ESM-only |
| Using `navigate()` outside route components | Only use navigation hooks within router-rendered components |