Next.js Pages & Layouts: Complete Guide with Examples
The Building Blocks of Every Next.js App
If the App Router is the skeleton of your Next.js application, then pages and layouts are the muscles and skin. Every single route in your app is built from these two fundamental concepts.
Understanding how they work together — and where to draw the line between them — is what separates beginners from production-ready developers.
In this module, we'll build a course learning platform (Tech3Space-style) where users browse courses, view lessons, and navigate through nested topics. By the end, you'll know exactly how to architect any multi-page application with clean, reusable layouts.
What is page.tsx?
A page.tsx file is the UI that renders at a specific route. It is the only required file to make a route accessible.
app/
├── page.tsx ← Renders at: /
├── about/
│ └── page.tsx ← Renders at: /about
└── courses/
└── page.tsx ← Renders at: /courses
Key Rules:
- A
page.tsxmust export a default React component - It can be a Server Component (default) or a Client Component (
'use client') - It receives route parameters and search params as props
Basic Page Example
1// app/courses/page.tsx 2import Link from "next/link"; 3 4export default function CoursesPage() { 5 return ( 6 <main className="max-w-6xl mx-auto px-4 py-12"> 7 <h1 className="text-4xl font-bold mb-6">Explore Courses</h1> 8 <p className="text-lg text-gray-600 mb-8"> 9 Master modern web development with our hands-on curriculum. 10 </p> 11 <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> 12 <CourseCard title="Next.js Mastery" slug="nextjs" /> 13 <CourseCard title="Python Fundamentals" slug="python" /> 14 <CourseCard title="System Design" slug="system-design" /> 15 </div> 16 </main> 17 ); 18} 19 20function CourseCard({ title, slug }: { title: string; slug: string }) { 21 return ( 22 <Link 23 href={`/courses/${slug}`} 24 className="block p-6 border rounded-xl hover:shadow-lg transition" 25 > 26 <h2 className="text-xl font-semibold">{title}</h2> 27 <span className="text-blue-600 mt-2 inline-block">Start Learning →</span> 28 </Link> 29 ); 30}
Visit http://localhost:3000/courses and this component renders. Simple.
What is layout.tsx?
A layout.tsx file is a shared UI shell that wraps multiple pages. It persists across navigation and does not re-render when users move between pages that share the same layout.
app/
├── layout.tsx ← Root layout (wraps EVERYTHING)
├── page.tsx ← Home page
└── courses/
├── layout.tsx ← Courses layout (wraps /courses/*)
├── page.tsx ← /courses
└── [slug]/
└── page.tsx ← /courses/python
Key Rules:
- A layout must accept a
childrenprop - Layouts are nested — the root layout wraps all child layouts
- Layouts preserve state across navigation (they don't unmount)
Root Layout
The root layout is mandatory and lives at app/layout.tsx. It wraps your entire application.
1// app/layout.tsx 2import type { Metadata } from "next"; 3import { Inter } from "next/font/google"; 4import "./globals.css"; 5 6const inter = Inter({ subsets: ["latin"] }); 7 8export const metadata: Metadata = { 9 title: { 10 default: "Tech3Space — Learn Tech Skills", 11 template: "%s | Tech3Space", 12 }, 13 description: "Master modern development with project-based courses", 14}; 15 16export default function RootLayout({ 17 children, 18}: { 19 children: React.ReactNode; 20}) { 21 return ( 22 <html lang="en"> 23 <body className={`${inter.className} antialiased`}> 24 {children} 25 </body> 26 </html> 27 ); 28}
Notice:
- The
<html>and<body>tags live here and nowhere else metadataprovides SEO defaults for every page- The
templatemeans child pages can inject their title:"Python Course | Tech3Space"
Nested Layouts in Action
Here's where Next.js shines. Layouts can be nested to create persistent UI regions that stay mounted as users navigate.
Imagine our course platform:
app/
├── layout.tsx ← Root: HTML, fonts, global providers
├── page.tsx ← Landing page
└── courses/
├── layout.tsx ← Courses layout: sidebar + header
├── page.tsx ← Course catalog
└── [slug]/ ← /courses/python
├── layout.tsx ← Course layout: lesson sidebar
├── page.tsx ← Course overview
└── lessons/
└── [id]/
└── page.tsx ← Individual lesson
Root Layout
1// app/layout.tsx 2import { Navbar } from "@/components/layout/Navbar"; 3import { Footer } from "@/components/layout/Footer"; 4 5export default function RootLayout({ children }: { children: React.ReactNode }) { 6 return ( 7 <html lang="en"> 8 <body> 9 <Navbar /> 10 <div className="min-h-screen">{children}</div> 11 <Footer /> 12 </body> 13 </html> 14 ); 15}
What persists: Navbar and Footer on every single page.
Courses Layout
1// app/courses/layout.tsx 2import { CourseSidebar } from "@/components/layout/CourseSidebar"; 3import { CourseHeader } from "@/components/layout/CourseHeader"; 4 5export default function CoursesLayout({ 6 children, 7}: { 8 children: React.ReactNode; 9}) { 10 return ( 11 <div className="flex min-h-[calc(100vh-4rem)]"> 12 <CourseSidebar /> 13 <div className="flex-1 flex flex-col"> 14 <CourseHeader /> 15 <main className="flex-1 p-6 bg-gray-50">{children}</main> 16 </div> 17 </div> 18 ); 19}
What persists: Sidebar and header for /courses, /courses/python, /courses/python/functions, etc.
What changes: Only the children (the actual page content) swaps out.
Course Detail Layout
1// app/courses/[slug]/layout.tsx 2import { LessonNavigator } from "@/components/courses/LessonNavigator"; 3 4export default async function CourseDetailLayout({ 5 children, 6 params, 7}: { 8 children: React.ReactNode; 9 params: Promise<{ slug: string }>; 10}) { 11 const { slug } = await params; 12 13 return ( 14 <div className="flex gap-6"> 15 <aside className="w-64 shrink-0"> 16 <LessonNavigator courseSlug={slug} /> 17 </aside> 18 <article className="flex-1 max-w-3xl">{children}</article> 19 </div> 20 ); 21}
Visual result:
┌─────────────────────────────────────────┐
│ Navbar (root layout) │
├──────────┬──────────────────────────────┤
│ │ Course Header │
│ Sidebar ├──────────────────────────────┤
│ (courses │ │
│ layout) │ Lesson Navigator │
│ │ (course layout) │
│ │ │
│ │ [PAGE CONTENT — changes] │
│ │ │
├──────────┴──────────────────────────────┤
│ Footer (root layout) │
└─────────────────────────────────────────┘
When a user clicks from /courses/python/functions to /courses/python/classes:
- Navbar ✅ stays mounted
- Footer ✅ stays mounted
- Course sidebar ✅ stays mounted
- Lesson navigator ✅ stays mounted
- Only the article content 🔄 re-renders
This is incredibly powerful for performance and user experience.
Shared Navigation Components
Let's build the actual components that make up our layouts.
Header / Navbar
1// components/layout/Navbar.tsx 2import Link from "next/link"; 3 4export function Navbar() { 5 return ( 6 <header className="sticky top-0 z-50 bg-white border-b"> 7 <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between"> 8 <Link href="/" className="text-xl font-bold text-blue-600"> 9 Tech3Space 10 </Link> 11 <nav className="flex gap-6"> 12 <Link href="/courses" className="hover:text-blue-600">Courses</Link> 13 <Link href="/about" className="hover:text-blue-600">About</Link> 14 <Link href="/dashboard" className="hover:text-blue-600">My Learning</Link> 15 </nav> 16 </div> 17 </header> 18 ); 19}
Footer
1// components/layout/Footer.tsx 2export function Footer() { 3 return ( 4 <footer className="bg-gray-900 text-gray-300 py-12"> 5 <div className="max-w-7xl mx-auto px-4 grid grid-cols-1 md:grid-cols-4 gap-8"> 6 <div> 7 <h3 className="text-white font-semibold mb-4">Tech3Space</h3> 8 <p className="text-sm">Empowering developers with practical skills.</p> 9 </div> 10 <div> 11 <h4 className="text-white font-medium mb-3">Learn</h4> 12 <ul className="space-y-2 text-sm"> 13 <li><a href="/courses/nextjs">Next.js</a></li> 14 <li><a href="/courses/python">Python</a></li> 15 <li><a href="/courses/react">React</a></li> 16 </ul> 17 </div> 18 <div> 19 <h4 className="text-white font-medium mb-3">Company</h4> 20 <ul className="space-y-2 text-sm"> 21 <li><a href="/about">About</a></li> 22 <li><a href="/careers">Careers</a></li> 23 </ul> 24 </div> 25 </div> 26 </footer> 27 ); 28}
Sidebar
1// components/layout/CourseSidebar.tsx 2import Link from "next/link"; 3 4const categories = [ 5 { name: "All Courses", href: "/courses" }, 6 { name: "Web Development", href: "/courses?category=web" }, 7 { name: "Data Science", href: "/courses?category=data" }, 8 { name: "DevOps", href: "/courses?category=devops" }, 9]; 10 11export function CourseSidebar() { 12 return ( 13 <aside className="w-64 bg-white border-r p-4"> 14 <h2 className="font-semibold text-gray-900 mb-4">Categories</h2> 15 <nav className="space-y-1"> 16 {categories.map((cat) => ( 17 <Link 18 key={cat.name} 19 href={cat.href} 20 className="block px-3 py-2 rounded-md hover:bg-gray-100 text-sm" 21 > 22 {cat.name} 23 </Link> 24 ))} 25 </nav> 26 </aside> 27 ); 28}
Loading UI with loading.tsx
When a page fetches data, you don't want users staring at a blank screen. Next.js lets you define a loading.tsx file that shows instantly while the page component streams in.
app/
├── courses/
│ ├── loading.tsx ← Shows while /courses loads
│ ├── page.tsx
│ └── [slug]/
│ ├── loading.tsx ← Shows while /courses/python loads
│ └── page.tsx
Global Loading State
1// app/loading.tsx 2export default function GlobalLoading() { 3 return ( 4 <div className="min-h-screen flex items-center justify-center"> 5 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" /> 6 </div> 7 ); 8}
Course-Specific Skeleton
1// app/courses/loading.tsx 2export default function CoursesLoading() { 3 return ( 4 <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> 5 {Array.from({ length: 6 }).map((_, i) => ( 6 <div key={i} className="border rounded-xl p-4 animate-pulse"> 7 <div className="h-40 bg-gray-200 rounded-lg mb-4" /> 8 <div className="h-4 bg-gray-200 rounded w-3/4 mb-2" /> 9 <div className="h-4 bg-gray-200 rounded w-1/2" /> 10 </div> 11 ))} 12 </div> 13 ); 14}
How it works:
- User navigates to
/courses loading.tsxrenders immediatelypage.tsxstreams in when data is readyloading.tsxis automatically replaced
No isLoading state. No useEffect. Next.js handles it.
Error Handling with error.tsx
Errors happen. APIs fail. Databases timeout. Instead of crashing your entire app, Next.js lets you isolate errors with error.tsx.
app/
├── error.tsx ← Catches errors in root pages
└── courses/
├── error.tsx ← Catches errors in /courses/*
├── page.tsx
└── [slug]/
└── page.tsx
Error Boundary Example
1// app/courses/error.tsx 2"use client"; // Error boundaries must be client components 3 4import { useEffect } from "react"; 5 6export default function CoursesError({ 7 error, 8 reset, 9}: { 10 error: Error & { digest?: string }; 11 reset: () => void; 12}) { 13 useEffect(() => { 14 // Log to error reporting service (Sentry, LogRocket, etc.) 15 console.error("Courses page error:", error); 16 }, [error]); 17 18 return ( 19 <div className="min-h-[50vh] flex flex-col items-center justify-center text-center"> 20 <h2 className="text-2xl font-bold text-red-600 mb-4"> 21 Something went wrong! 22 </h2> 23 <p className="text-gray-600 mb-6 max-w-md"> 24 We couldn't load the courses. This might be a temporary issue. 25 </p> 26 <button 27 onClick={reset} 28 className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" 29 > 30 Try Again 31 </button> 32 </div> 33 ); 34}
What error.tsx does:
- Catches runtime errors in its segment and all child segments
- Prevents the entire app from crashing
- Provides a
reset()function to retry rendering - Can be nested — a child
error.tsxhandles errors closer to the source
Not Found Pages with not-found.tsx
When a resource doesn't exist — a course slug is invalid, a user profile is deleted — show a proper 404 page.
app/
├── not-found.tsx ← Global 404
└── courses/
├── page.tsx
└── [slug]/
├── not-found.tsx ← Course-specific 404
└── page.tsx
Triggering a 404 Programmatically
1// app/courses/[slug]/page.tsx 2import { notFound } from "next/navigation"; 3 4interface Props { 5 params: Promise<{ slug: string }>; 6} 7 8export default async function CoursePage({ params }: Props) { 9 const { slug } = await params; 10 const course = await getCourseBySlug(slug); 11 12 if (!course) { 13 notFound(); // ← Renders the nearest not-found.tsx 14 } 15 16 return ( 17 <div> 18 <h1>{course.title}</h1> 19 <p>{course.description}</p> 20 </div> 21 ); 22}
Custom Not Found UI
1// app/courses/[slug]/not-found.tsx 2import Link from "next/link"; 3 4export default function CourseNotFound() { 5 return ( 6 <div className="text-center py-20"> 7 <h1 className="text-6xl font-bold text-gray-200 mb-4">404</h1> 8 <h2 className="text-2xl font-semibold mb-4">Course Not Found</h2> 9 <p className="text-gray-600 mb-8"> 10 The course you're looking for doesn't exist or has been moved. 11 </p> 12 <Link 13 href="/courses" 14 className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700" 15 > 16 Browse All Courses 17 </Link> 18 </div> 19 ); 20}
Real-Life Example: Course Platform Architecture
Let's build the complete routing structure for a platform like Tech3Space:
app/
├── layout.tsx ← Root: Navbar + Footer
├── page.tsx ← Landing page
├── loading.tsx ← Global loading
├── error.tsx ← Global error
├── not-found.tsx ← Global 404
│
├── courses/
│ ├── layout.tsx ← Courses shell: sidebar + header
│ ├── loading.tsx ← Course grid skeleton
│ ├── error.tsx ← Course fetch error
│ ├── page.tsx ← /courses (catalog)
│ └── [slug]/ ← /courses/python
│ ├── layout.tsx ← Course detail: lesson sidebar
│ ├── loading.tsx ← Lesson list skeleton
│ ├── not-found.tsx ← Invalid course slug
│ ├── page.tsx ← Course overview
│ └── lessons/
│ └── [id]/
│ ├── page.tsx ← /courses/python/functions
│ └── loading.tsx ← Video player skeleton
│
├── dashboard/
│ ├── layout.tsx ← Dashboard shell
│ ├── page.tsx ← /dashboard
│ └── settings/
│ └── page.tsx ← /dashboard/settings
│
└── about/
└── page.tsx ← /about
The Nested Layout Flow
When a user visits /courses/python/functions:
- Root layout renders (Navbar + Footer)
- Courses layout renders inside root (Course sidebar + header)
- Course detail layout renders inside courses layout (Lesson navigator)
- Page renders inside course detail layout (The actual lesson content)
- If data is loading → loading.tsx at the closest level shows
- If an error occurs → error.tsx at the closest level catches it
- If course not found → not-found.tsx at the closest level shows
Complete Code: Course Lesson Page
1// app/courses/[slug]/lessons/[id]/page.tsx 2import { notFound } from "next/navigation"; 3import { VideoPlayer } from "@/components/courses/VideoPlayer"; 4import { LessonNotes } from "@/components/courses/LessonNotes"; 5import { getLesson } from "@/services/lessonService"; 6 7interface Props { 8 params: Promise<{ slug: string; id: string }>; 9} 10 11export async function generateMetadata({ params }: Props) { 12 const { slug, id } = await params; 13 const lesson = await getLesson(slug, id); 14 return { 15 title: lesson?.title ?? "Lesson", 16 }; 17} 18 19export default async function LessonPage({ params }: Props) { 20 const { slug, id } = await params; 21 const lesson = await getLesson(slug, id); 22 23 if (!lesson) { 24 notFound(); 25 } 26 27 return ( 28 <div className="space-y-6"> 29 <div className="aspect-video bg-black rounded-xl overflow-hidden"> 30 <VideoPlayer src={lesson.videoUrl} /> 31 </div> 32 <div> 33 <h1 className="text-2xl font-bold mb-2">{lesson.title}</h1> 34 <p className="text-gray-600">{lesson.description}</p> 35 </div> 36 <LessonNotes notes={lesson.notes} /> 37 </div> 38 ); 39}
1// app/courses/[slug]/lessons/[id]/loading.tsx 2export default function LessonLoading() { 3 return ( 4 <div className="space-y-6 animate-pulse"> 5 <div className="aspect-video bg-gray-200 rounded-xl" /> 6 <div className="h-8 bg-gray-200 rounded w-2/3" /> 7 <div className="h-4 bg-gray-200 rounded w-full" /> 8 <div className="h-4 bg-gray-200 rounded w-5/6" /> 9 </div> 10 ); 11}
Special Files Quick Reference
| File | Purpose | Scope |
|---|---|---|
page.tsx | Route UI | The specific route |
layout.tsx | Shared wrapper | Route + all children |
loading.tsx | Loading state | Route + all children |
error.tsx | Error boundary | Route + all children |
not-found.tsx | 404 UI | Route + all children |
template.tsx | Re-mounting layout | Route + all children (resets state) |
default.tsx | Parallel route fallback | Parallel slot |
Best Practices
- Keep layouts thin — Layouts should structure UI, not contain business logic
- Use nested layouts — Don't put everything in the root layout
- Create skeletons — Always provide
loading.tsxfor data-heavy routes - Handle errors gracefully — Every data-fetching route should have an
error.tsx - 404s should guide users — Always include a link back to safety in
not-found.tsx - Metadata per page — Use
generateMetadatafor dynamic SEO titles
Summary
| Concept | File | What It Does |
|---|---|---|
| Page | page.tsx | Renders the actual route content |
| Root Layout | app/layout.tsx | Wraps entire app (HTML, fonts, nav) |
| Nested Layout | app/.../layout.tsx | Wraps a section (sidebars, headers) |
| Loading | loading.tsx | Shows while page data loads |
| Error | error.tsx | Catches runtime errors |
| Not Found | not-found.tsx | Shows when resource doesn't exist |
What's Next?
In Module 5, we'll dive into Data Fetching — how Server Components fetch data, how caching works, and how to build the backend API routes that power these pages.
Your mission: Build the layout structure for your Tech3Space app. Create the root layout with Navbar and Footer, a courses layout with a sidebar, and a course detail layout with a lesson navigator. Add loading.tsx and error.tsx files to every segment. Test navigating between routes and watch how layouts persist while only pages change!