Layouts: shared UI across routes
Part 1's root layout.tsx has been the bare minimum — <html> and <body>, nothing else. This part adds a real header and footer to it, then a second layout scoped to just the posts section, to show how layouts nest.
Building out the root layout
// app/layout.tsx
import Link from "next/link";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<header>
<Link href="/">devnotes</Link>
<nav>
<Link href="/posts">Posts</Link>
</nav>
</header>
<main>{children}</main>
<footer>© 2026 devnotes</footer>
</body>
</html>
);
}{children} is where the actual matched page renders — everything else in this file (the header, the nav, the footer) now appears on every single route in the project, written exactly once. Navigating from / to /posts re-renders {children}; the header and footer around it don't remount.
A second layout, scoped to one section
// app/posts/layout.tsx
export default function PostsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="posts-section">
<aside>Browse by tag (coming in a later part)</aside>
{children}
</div>
);
}Adding layout.tsx inside app/posts/ wraps every route under /posts — the list page, and every individual post page once part 6 adds them — with this sidebar, without touching the root layout at all. It doesn't replace the root layout; both apply, nested: RootLayout wraps PostsLayout, which wraps whichever page.tsx actually matched.
Layouts don't get route params by default
// app/posts/layout.tsx — this does NOT receive a specific post's data
export default function PostsLayout({ children }: { children: React.ReactNode }) {
// no access to which specific post, if any, is currently being viewed
return <div className="posts-section">{children}</div>;
}A layout wraps every route beneath it, including ones with different dynamic segments — PostsLayout renders identically whether the current page is the post list or one specific post's detail page (part 6). This is deliberate: a layout is for structure shared across an entire section, not per-page content, which is exactly why it doesn't automatically know which specific post is currently open.
Putting page-specific content — a specific post's title, meant only for that one page — inside a layout instead of the page itself. A layout persists across every route it wraps; content that should change per-route belongs in that route's own page.tsx.
FAQ
What's the difference between layout.tsx and template.tsx?
A layout.tsx persists across navigations within the routes it wraps — state inside it survives moving between pages, as described above. A template.tsx (rarer, not used in this series) creates a fresh instance on every navigation instead, useful specifically when you need an effect or animation to re-run on each page change rather than staying mounted.
Do loading.tsx and error.tsx work the same way as layout.tsx?
They follow the same file-based nesting convention — placed alongside a page.tsx, they apply to that route and everything beneath it — but they serve different purposes: loading.tsx shows automatically while a route's data is being fetched, and error.tsx catches a rendering error in that route segment, neither of which layout.tsx does.
Can a layout fetch its own data?
Yes — a layout is just an async Server Component like any page.tsx, so it can await a fetch directly inside it. That data becomes available to the layout's own markup, but still isn't passed down to children — the parent/child relationship in the props is one-directional.
Next: actual data — every page so far has rendered hardcoded text. Time to fetch real posts.