Multiple Layouts
Use different layouts for different sections of your application
Harpy.js supports multiple layouts, allowing you to create different visual structures for different parts of your application. This is perfect for having a marketing layout for your homepage and a sidebar layout for your documentation or admin dashboard.
Why Use Multiple Layouts?
Landing Pages
Full-width marketing pages with hero sections, testimonials, and call-to-action buttons.
Documentation
Sidebar navigation layout for docs, guides, and knowledge base articles.
Authentication
Centered, minimal layout for login, register, and password reset pages.
Admin Dashboard
Complex layout with sidebar, top navigation, and content area for admin panels.
1. Setting the Default Layout
In main.ts, configure your default layout that will be used for all routes unless explicitly overridden:
1// src/main.ts2import { NestFactory } from '@nestjs/core';3import { FastifyAdapter } from '@nestjs/platform-fastify';4import { AppModule } from './app.module';5import { setupHarpyApp } from '@harpy-js/core';6import DefaultLayout from './layouts/layout';78async function bootstrap() {9 const app = await NestFactory.create(10 AppModule,11 new FastifyAdapter(),12 );1314 // Set the default layout for the entire application15 await setupHarpyApp(app, {16 layout: DefaultLayout, // This is the default layout17 distDir: 'dist',18 publicDir: 'public',19 });2021 await app.listen({22 port: 3000,23 host: '0.0.0.0',24 });25}2627bootstrap();💡 Tip: The default layout is typically a simple, full-width layout used for landing pages and marketing content.
Example: Default Layout
1// src/layouts/layout.tsx2import { Link, type JsxLayoutProps } from '@harpy-js/core';34export default function DefaultLayout({5 children,6 meta,7 lang,8}: JsxLayoutProps & { lang: string }) {9 return (10 <html lang={lang || 'en'}>11 <head>12 <title>{meta?.title ?? 'My App'}</title>13 <meta charSet="utf-8" />14 <meta name="viewport" content="width=device-width, initial-scale=1" />15 <link rel="stylesheet" href="/styles/styles.css" />16 </head>17 <body>18 <header>19 <nav>20 <Link href="/">Home</Link>21 <Link href="/about">About</Link>22 </nav>23 </header>24 <main>{children}</main>25 <footer>26 <p>© 2025 My App</p>27 </footer>28 </body>29 </html>30 );31}2. Creating Additional Layouts
Create additional layouts in your src/layouts/ folder. Here's an example of a dashboard layout with a sidebar:
1// src/layouts/dashboard-layout.tsx2import { JsxLayoutProps } from '@harpy-js/core';3import Sidebar from './components/Sidebar';45export default function DashboardLayout({6 children,7 meta,8 hydrationScripts,9 sections = [],10}: JsxLayoutProps & {11 hydrationScripts?: Array<{ componentName: string; path: string }>;12 sections?: NavSection[];13}) {14 return (15 <html lang="en">16 <head>17 <title>{meta?.title ?? 'Dashboard'}</title>18 <meta charSet="utf-8" />19 <link rel="stylesheet" href="/styles/styles.css" />20 </head>21 <body>22 <div className="flex h-screen">23 {/* Sidebar navigation */}24 <Sidebar sections={sections} />25 26 {/* Main content */}27 <main className="flex-1 overflow-auto">28 {children}29 </main>30 </div>31 32 {/* Hydration scripts */}33 {hydrationScripts?.map((script) => (34 <script35 key={script.componentName}36 type="module"37 src={script.path}38 defer39 />40 ))}41 </body>42 </html>43 );44}Example: Authentication Layout
A minimal, centered layout for authentication pages:
1// src/layouts/auth-layout.tsx2import { JsxLayoutProps } from '@harpy-js/core';34export default function AuthLayout({5 children,6 meta,7}: JsxLayoutProps) {8 return (9 <html lang="en">10 <head>11 <title>{meta?.title ?? 'Sign In'}</title>12 <meta charSet="utf-8" />13 <link rel="stylesheet" href="/styles/styles.css" />14 </head>15 <body className="bg-gray-100">16 <div className="min-h-screen flex items-center justify-center">17 <div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">18 {children}19 </div>20 </div>21 </body>22 </html>23 );24}3. Using Layouts in Controllers
Override the default layout for specific routes by passing the layout option to the @JsxRender decorator:
Using the Default Layout
1// src/features/home/home.controller.ts2import { Controller, Get } from '@nestjs/common';3import { JsxRender } from '@harpy-js/core';4import HomePage from './views/home-page';56@Controller()7export class HomeController {8 @Get()9 @JsxRender(HomePage, {10 // No layout specified - uses DefaultLayout from main.ts11 meta: {12 title: 'Welcome to My App',13 description: 'Home page description',14 },15 })16 async index() {17 return {18 message: 'Welcome to the home page!',19 };20 }21}Overriding with Dashboard Layout
1// src/features/dashboard/dashboard.controller.ts2import { Controller, Get } from '@nestjs/common';3import { JsxRender } from '@harpy-js/core';4import DashboardPage from './views/dashboard-page';5import DashboardLayout from '../../layouts/dashboard-layout';67@Controller('dashboard')8export class DashboardController {9 @Get()10 @JsxRender(DashboardPage, {11 layout: DashboardLayout, // Override with DashboardLayout12 meta: {13 title: 'Dashboard',14 description: 'Application dashboard',15 },16 })17 async index() {18 return {19 stats: { users: 1250, revenue: 45600 },20 };21 }22}Passing Data to Layouts
Layouts can receive data from controllers, such as navigation sections:
1// src/features/docs/docs.controller.ts2import { Controller, Get, Req } from '@nestjs/common';3import type { FastifyRequest } from 'fastify';4import { JsxRender, NavigationService } from '@harpy-js/core';5import DocsPage from './views/docs-page';6import DashboardLayout from '../../layouts/dashboard-layout';78@Controller('docs')9export class DocsController {10 constructor(private readonly navigationService: NavigationService) {}1112 @Get()13 @JsxRender(DocsPage, {14 layout: DashboardLayout,15 meta: {16 title: 'Documentation',17 },18 })19 async docs(@Req() req: FastifyRequest) {20 const currentPath = req.url ?? '/';21 const sections = this.navigationService.getSectionsForRoute(currentPath);2223 return {24 sections, // Pass navigation sections to the layout25 };26 }27}Real-World Example: This Documentation Site
This very documentation site uses multiple layouts! Here's how it's structured:
Project Structure
src/├── layouts/│ ├── layout.tsx # Default layout (landing pages)│ ├── dashboard-layout.tsx # Dashboard layout (docs, admin)│ ├── auth-layout.tsx # Authentication layout (login, register)│ └── components/│ ├── Sidebar.tsx│ └── MobileMenu.tsx├── features/│ ├── home/│ │ └── home.controller.ts # Uses DefaultLayout│ ├── docs/│ │ └── docs.controller.ts # Uses DashboardLayout│ └── auth/│ └── auth.controller.ts # Uses AuthLayout└── main.ts # Configures default layout🏠 Homepage (/)
Uses DefaultLayout
- • Full-width hero section
- • Feature showcase
- • Call-to-action buttons
- • Footer with links
📚 Docs (/docs/*)
Uses DashboardLayout
- • Sidebar navigation
- • Active section highlighting
- • Mobile-responsive menu
- • Table of contents
Layout Props Reference
All layouts receive these props from the JsxLayoutProps interface:
| Prop | Type | Description |
|---|---|---|
| children | React.ReactNode | The page content to render |
| meta | MetaOptions | SEO metadata (title, description, etc.) |
| hydrationScripts | Array | Client-side hydration scripts |
| lang | string | Current language code (for i18n) |
⚠️ Note: You can extend JsxLayoutProps with custom props by using TypeScript intersection types, as shown in the DashboardLayout example above.
Best Practices
✅ Keep Layouts Focused
Each layout should serve a specific purpose. Don't create too many layouts - typically 2-4 layouts are sufficient for most applications.
💡 Share Common Components
Extract shared header, footer, and navigation components into reusable modules that can be used across different layouts.
🎨 Consistent Styling
Maintain consistent branding, colors, and typography across all layouts even if the structure differs.
📱 Mobile-First
Design your layouts to be responsive. Complex layouts like dashboards should collapse appropriately on mobile devices.