布局与页面

Next.js 基于文件系统路由:文件夹决定 URL 段,page.tsxlayout.tsx 决定每段对应的 UI。本章讲清楚二者如何协作,以及动态段和 searchParams 的用法。

创建页面

app 下放一个 page.tsx 并默认导出组件,这个路由就变得公开可访问:

// app/page.tsx → /
export default function Page() {
  return <h1>Hello Next.js!</h1>;
}

创建布局

布局是多个页面共享的 UI。在客户端导航时布局会保留状态、保持交互性,且不会重新渲染。

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-CN">
      <body>
        <main>{children}</main>
      </body>
    </html>
  );
}

位于 app/ 根部的叫 根布局,是必须的,而且只有它能写 <html><body>

嵌套路由与嵌套布局

嵌套文件夹 = 嵌套 URL 段。例如 /blog/[slug] 对应:

app/
├── layout.tsx         # 根布局
├── blog/
│   ├── layout.tsx     # 博客区布局(可选)
│   ├── page.tsx       # /blog
│   └── [slug]/
│       └── page.tsx   # /blog/hello-world

布局会自动嵌套包裹:根布局 → 博客布局 → 博客页。这样可以在各层按需复用导航、侧栏、面包屑等结构。

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <section className="blog">{children}</section>;
}

动态段

用方括号创建动态段:[slug][id]。Next.js 15+ 的 params 是一个 Promise,需要 await

// app/blog/[slug]/page.tsx
export default async function Post({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

配合 generateStaticParams 可以在构建时预生成所有动态路由,获得纯静态站点的性能。

searchParams

在服务端组件页面中通过 searchParams 读取查询参数:

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [k: string]: string | string[] | undefined }>;
}) {
  const { q } = await searchParams;
  return <p>搜索词:{q}</p>;
}

注意:使用 searchParams 会让页面自动变成 动态渲染,因为它依赖进来的请求。

客户端组件可以用 useSearchParams 这个 hook。

页面间导航

使用 next/link 实现客户端导航(带自动预加载):

import Link from 'next/link';

export default async function Posts() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map(p => (
        <li key={p.slug}>
          <Link href={`/blog/${p.slug}`}>{p.title}</Link>
        </li>
      ))}
    </ul>
  );
}

下一章我们会深入讲解 Link 的预取、流式渲染与客户端过渡。

佈局與頁面

Next.js 以檔案系統作為路由:資料夾決定 URL 段落,page.tsxlayout.tsx 則決定每段對應的 UI。本章講清楚兩者如何協作,以及動態段與 searchParams 的用法。

建立頁面

app 之下放一個 page.tsx 並預設匯出元件,該路由便會公開可存取:

// app/page.tsx → /
export default function Page() {
  return <h1>Hello Next.js!</h1>;
}

建立佈局

佈局是多個頁面共用的 UI。在客戶端導航時佈局會保留狀態、保持互動性,且不會重新渲染。

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-HK">
      <body>
        <main>{children}</main>
      </body>
    </html>
  );
}

位於 app/ 根部的叫 根佈局,它是必要的,而且只有它能寫 <html><body>

巢狀路由與巢狀佈局

巢狀資料夾 = 巢狀 URL 段。例如 /blog/[slug] 對應:

app/
├── layout.tsx         # 根佈局
├── blog/
│   ├── layout.tsx     # 網誌區佈局(可選)
│   ├── page.tsx       # /blog
│   └── [slug]/
│       └── page.tsx   # /blog/hello-world

佈局會自動巢狀包裹:根佈局 → 網誌佈局 → 網誌頁。可在各層依需求複用導航、側欄、麵包屑等結構。

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <section className="blog">{children}</section>;
}

動態段

用方括號建立動態段:[slug][id]。Next.js 15+ 的 params 是一個 Promise,需要 await

// app/blog/[slug]/page.tsx
export default async function Post({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

配合 generateStaticParams 可以在建置時預生所有動態路由,獲得純靜態站台的效能。

searchParams

在伺服器元件頁面中透過 searchParams 讀取查詢參數:

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [k: string]: string | string[] | undefined }>;
}) {
  const { q } = await searchParams;
  return <p>搜尋詞:{q}</p>;
}

注意:使用 searchParams 會令頁面自動變成 動態渲染,因為它依賴進入的請求。

客戶端元件可改用 useSearchParams 這個 hook。

頁面間導航

使用 next/link 實作客戶端導航(帶自動預載):

import Link from 'next/link';

export default async function Posts() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map(p => (
        <li key={p.slug}>
          <Link href={`/blog/${p.slug}`}>{p.title}</Link>
        </li>
      ))}
    </ul>
  );
}

下一章會深入講解 Link 的預取、串流渲染與客戶端過渡。

Layouts and Pages

Next.js uses file-system routing: folders define URL segments while page.tsx and layout.tsx define the UI for each segment. This chapter covers how they compose, plus dynamic segments and searchParams.

Creating a page

Add a page.tsx anywhere under app/ and default-export a React component to make that route public:

// app/page.tsx → /
export default function Page() {
  return <h1>Hello Next.js!</h1>;
}

Creating a layout

A layout is UI shared across multiple pages. On client-side navigation, layouts preserve state, stay interactive, and do not rerender.

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <main>{children}</main>
      </body>
    </html>
  );
}

The layout at the root of app/ is the root layout — it's required, and only it may include the <html> and <body> tags.

Nested routes and layouts

Nested folders map to nested URL segments. /blog/[slug] corresponds to:

app/
├── layout.tsx         # root layout
├── blog/
│   ├── layout.tsx     # blog-section layout (optional)
│   ├── page.tsx       # /blog
│   └── [slug]/
│       └── page.tsx   # /blog/hello-world

Layouts nest automatically: root layout wraps the blog layout which wraps the individual post page. Use this to share navigation, sidebars, or breadcrumbs at the appropriate level.

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <section className="blog">{children}</section>;
}

Dynamic segments

Brackets create dynamic segments: [slug], [id]. In Next.js 15+, params is a Promise and must be awaited:

// app/blog/[slug]/page.tsx
export default async function Post({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Pair dynamic segments with generateStaticParams to prerender every permutation at build time for a fully static site.

searchParams

In a Server Component page, read the query string via the searchParams prop:

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [k: string]: string | string[] | undefined }>;
}) {
  const { q } = await searchParams;
  return <p>Search term: {q}</p>;
}

Heads up: accessing searchParams opts the page into dynamic rendering, because it depends on the incoming request.

Client Components should use the useSearchParams hook instead.

Linking between pages

Use next/link for fast client-side navigation with built-in prefetching:

import Link from 'next/link';

export default async function Posts() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map(p => (
        <li key={p.slug}>
          <Link href={`/blog/${p.slug}`}>{p.title}</Link>
        </li>
      ))}
    </ul>
  );
}

The next chapter dives deeper into prefetching, streaming, and client-side transitions.