AiTechWorlds
AiTechWorlds
Layouts are one of Next.js's most useful features. They let you define a UI shell — navigation, sidebar, footer — that wraps specific routes without re-rendering when the user navigates between those routes. Understanding the layout system unlocks clean, performant app structure.
A layout.tsx file wraps all pages at its level and below. The layout itself doesn't unmount when the user navigates to a child route — only the {children} content changes:
// src/app/layout.tsx — wraps EVERY page in the app
import type { Metadata } from "next";
export const metadata: Metadata = {
title: { template: "%s | AiTechWorlds", default: "AiTechWorlds" },
description: "Learn React, Next.js, Python, and AI.",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<TopNav />
{children}
<Footer />
</body>
</html>
);
}
Every page gets <TopNav> and <Footer> automatically. No need to include them in each page component.
Create a layout.tsx inside a folder to apply it only to routes in that folder:
src/app/
├── layout.tsx → Root layout: TopNav + Footer (all routes)
├── page.tsx → /
├── about/
│ └── page.tsx → /about (uses root layout only)
└── dashboard/
├── layout.tsx → Dashboard layout: Sidebar + main area
├── page.tsx → /dashboard
├── settings/
│ └── page.tsx → /dashboard/settings
└── analytics/
└── page.tsx → /dashboard/analytics
Dashboard routes get the root layout (TopNav, Footer) AND the dashboard layout (Sidebar):
// src/app/dashboard/layout.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const user = await getSession();
if (!user) redirect("/login"); // Protect all dashboard routes
return (
<div className="flex min-h-screen">
<DashboardSidebar user={user} />
<main className="flex-1 bg-gray-50">
{children}
</main>
</div>
);
}
Now /dashboard, /dashboard/settings, and /dashboard/analytics all have the sidebar, and they're all protected by the auth check — without any code in the page components.
Layouts run on the server by default. This means you can fetch data in layouts, check authentication, read cookies, and do database queries:
// src/app/courses/[courseSlug]/layout.tsx
import { db } from "@/lib/db";
import { notFound } from "next/navigation";
import CourseSidebar from "@/components/CourseSidebar";
interface Props {
children: React.ReactNode;
params: { courseSlug: string };
}
export default async function CourseLayout({ children, params }: Props) {
const course = await db.course.findUnique({
where: { slug: params.courseSlug },
include: {
lessons: { orderBy: { order: "asc" } },
},
});
if (!course) notFound();
return (
<div className="flex">
<CourseSidebar course={course} />
<div className="flex-1 p-8">
{children}
</div>
</div>
);
}
The course data is fetched once for all lesson pages under /courses/[courseSlug]/. Navigating between lessons doesn't re-fetch the course — the layout doesn't re-render.
Each layout and page can export a metadata object or generateMetadata function. Next.js merges them from parent to child:
// src/app/layout.tsx
export const metadata: Metadata = {
title: { template: "%s | AiTechWorlds", default: "AiTechWorlds" },
description: "Learn React, Next.js, Python, and AI.",
icons: { icon: "/favicon.ico" },
};
// src/app/courses/page.tsx
export const metadata: Metadata = {
title: "Browse Courses", // Becomes: "Browse Courses | AiTechWorlds"
description: "Practical courses for modern developers.",
};
// src/app/courses/[slug]/page.tsx — dynamic metadata
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const course = await getCourse(params.slug);
return {
title: course.title, // Becomes: "React Complete Course | AiTechWorlds"
description: course.description,
openGraph: {
title: course.title,
description: course.description,
images: [{ url: course.coverImage, width: 1200, height: 630 }],
},
};
}
The %s in the template becomes the page's title. Without a title in the page, the default is used.
Group routes with a (name) folder that doesn't appear in the URL. This lets you apply different layouts to different route sections:
src/app/
├── (marketing)/
│ ├── layout.tsx → Marketing layout (clean, no sidebar)
│ ├── page.tsx → /
│ ├── about/
│ │ └── page.tsx → /about
│ └── pricing/
│ └── page.tsx → /pricing
└── (app)/
├── layout.tsx → App layout (sidebar, auth required)
├── dashboard/
│ └── page.tsx → /dashboard
└── settings/
└── page.tsx → /settings
The (marketing) and (app) folders are invisible in the URL. /about and /dashboard both exist at the top level, but they use different layouts.
Show two different pages in the same layout at the same time using @slot naming:
src/app/
├── layout.tsx
├── page.tsx
├── @modal/
│ └── (.)photos/[id]/
│ └── page.tsx → Renders as a modal overlay
└── photos/
├── page.tsx → /photos
└── [id]/
└── page.tsx → /photos/123 (full page)
// src/app/layout.tsx
export default function Layout({
children,
modal, // The @modal slot
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
{children}
{modal} {/* The modal renders here, on top */}
</>
);
}
Clicking a photo shows it in a modal overlay. Refreshing the URL shows it as a full page. This is Instagram-style routing — and it's all in the file system.
template.tsx is like layout.tsx but it re-mounts when the user navigates between child routes. Use it when you need fresh state on every navigation:
// src/app/dashboard/template.tsx
export default function DashboardTemplate({ children }: { children: React.ReactNode }) {
// This unmounts and remounts on every navigation
// Use case: page entry animations, resetting scroll position
return <>{children}</>;
}
Most of the time you want layout.tsx (persists state). Use template.tsx when re-mounting is intentional.
Next lesson: Loading UI and Suspense — showing skeletons while data streams in.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises