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 里直接获取数据。