layout.tsx:共享外壳
src/app/
├── layout.tsx ← 根布局,所有页面共享
├── page.tsx
├── about/
│ ├── layout.tsx ← /about/* 共享
│ └── page.tsx
└── blog/
├── layout.tsx ← /blog/* 共享
└── page.tsx
每个 layout 包裹下面所有 page。
根 layout(必须)
src/app/layout.tsx:
import './globals.css';
import Link from 'next/link';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
<header className="border-b">
<nav className="max-w-4xl mx-auto p-4 flex gap-4">
<Link href="/" className="font-bold">WadeLy</Link>
<Link href="/blog">Blog</Link>
<Link href="/about">About</Link>
</nav>
</header>
<main className="max-w-4xl mx-auto p-8">
{children}
</main>
<footer className="border-t mt-12 p-4 text-center text-gray-500">
© 2026 WadeLy
</footer>
</body>
</html>
);
}
⚠ 根 layout 必须有 <html> 和 <body> 标签——只能在根 layout 里写。
嵌套 layout
// src/app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex gap-8">
<aside className="w-64">
<h2 className="font-bold mb-4">Categories</h2>
<ul>
<li><Link href="/blog/tech">Tech</Link></li>
<li><Link href="/blog/life">Life</Link></li>
</ul>
</aside>
<article className="flex-1">{children}</article>
</div>
);
}
访问 /blog 或 /blog/xxx → 都有左侧 aside。
metadata:每页 SEO
每个 layout / page 可以 export metadata:
// src/app/about/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'About · WadeLy',
description: 'About me, WadeLy.',
};
export default function AboutPage() {
return <h1>About</h1>;
}
Next.js 自动生成对应的 <title> <meta description> 等。详见 15-seo。
loading.tsx:加载状态
src/app/blog/
├── layout.tsx
├── page.tsx
└── loading.tsx ← 数据加载期间显示这个
// src/app/blog/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded mb-4" />
<div className="h-4 bg-gray-200 rounded mb-2" />
<div className="h-4 bg-gray-200 rounded w-3/4" />
</div>
);
}
数据加载(Suspense 边界)期间显示这个骨架屏。
error.tsx:错误边界
// src/app/blog/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div className="p-8 text-center">
<h2 className="text-2xl">出错了</h2>
<p className="text-gray-600">{error.message}</p>
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">
重试
</button>
</div>
);
}
某段路由出错 → 自动展示 error 组件 + 隔离其他部分(不让整页崩)。
template.tsx vs layout.tsx
| layout | template | |
|---|---|---|
| 状态保留 | ✓(切换不卸载) | ✗(切换重新挂载) |
| 用途 | 共享 UI 静态部分 | 需要每次切都"重置"的部分 |
99% 用 layout。template 极少用。
globals.css:全局样式
/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--font-sans: 'Inter', sans-serif;
}
body {
font-family: var(--font-sans);
}
globals.css 只能在根 layout 里 import 一次——其他地方导入会报错。
自定义字体(Next.js 优化)
// src/app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }) {
return (
<html lang="zh-CN" className={inter.className}>
<body>{children}</body>
</html>
);
}
Next 自动下载字体并自托管——比从 Google 加载快 + 不暴露用户 IP。
完整骨架(个人站)
// src/app/layout.tsx
import './globals.css';
import { Inter } from 'next/font/google';
import Link from 'next/link';
import type { Metadata } from 'next';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: { default: 'WadeLy', template: '%s · WadeLy' },
description: 'WadeLy 的个人主页和笔记。',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN" className={inter.className}>
<body className="min-h-screen bg-white text-gray-900">
<header className="sticky top-0 bg-white border-b">
<nav className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center">
<Link href="/" className="text-xl font-bold">WadeLy</Link>
<div className="flex gap-6">
<Link href="/blog" className="hover:text-blue-600">Blog</Link>
<Link href="/about" className="hover:text-blue-600">About</Link>
</div>
</nav>
</header>
<main className="max-w-4xl mx-auto px-4 py-8">
{children}
</main>
<footer className="border-t mt-16 py-8 text-center text-gray-500">
© 2026 WadeLy ·
<a href="https://github.com/me" className="ml-2 hover:text-gray-700">GitHub</a>
</footer>
</body>
</html>
);
}
下一篇:在 Server Component 里直接获取数据。