---
# System prepended metadata

title: Next.js 關鍵特性解析，專案與環境搭建及核心功能說明

---

# Next.js 關鍵特性解析，專案與環境搭建及核心功能說明

## 第一章：Next.js 概述

Next.js 由 Vercel 開發，是一個基於 React 的框架，提供：

1. **伺服器端渲染（SSR）**：在伺服器先產出 HTML，改善 SEO 與首屏速度。
2. **靜態生成（SSG）**：建置時產出靜態頁面，速度快、安全性高。
3. **自動路由**：`pages/` 下的檔案即是路由。
4. **API 路由**：`pages/api/` 直接寫後端端點，快速做全端應用。
5. **增量靜態再生（ISR）**：靜態頁可在背景重生，兼顧即時性與效能。
6. **內建樣式支援**：CSS、Sass、CSS Modules 皆可用。

> 總結：Next.js 比起純 React，多了 **SSR/SSG/路由/API** 等能力，能更快做出高效能、SEO 友善的網站。

<br/>

## 第二章：建立 Next.js 專案（Pages Router）

### 建立專案

```bash
npx create-next-app@latest
```

依互動提示（示例）：

```
✔ What is your project named? … next-project
✔ Would you like to use TypeScript? …  Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … No
✔ Would you like to use `src/` directory? …  Yes
✔ Would you like to use App Router? (recommended) … No   # 本教材走 Pages Router
✔ Would you like to customize the default import alias (@/*)? …  Yes
✔ What import alias would you like configured? … @/*
```

**參考範例專案**：[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect)

### 專案結構重點

* `pages/`
  * `index.tsx`：首頁，對應 `/`。
  * `_app.tsx`：自訂 App 根組件（全域樣式、狀態、版型包裹）。
  * `_document.tsx`：自訂 HTML 結構（`<html>`, `<body>`, `<Head>` 等）。
* `public/`：靜態資源（favicon、圖片…）。
* `styles/`：全域或 CSS Modules 樣式。
* `components/`：可重用的 React 元件。
* **`node_modules`資料夾**：
   - 這個資料夾包含了專案的所有依賴包，由 npm 或 yarn 安裝。
* **`package.json`**：
   - 這個檔案定義了專案的基本資訊和依賴包。
* **`next.config.js`**：
   - 這個檔案用來設定 Next.js 應用程式的設定。
* **`tsconfig.json`**：
   - 這個檔案用來設定 TypeScript 的設定。

**_app.tsx 範例**

```tsx
// pages/_app.tsx
import type { AppProps } from 'next/app'
import '@/styles/globals.css'

export default function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}
```

**_document.tsx 範例**

```tsx
// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}
```

<br/>

## 第三章：頁面與路由（Pages Router）

### 基本路由

* `pages/index.tsx` → `/`
* `pages/about.tsx` → `/about`

```tsx
// pages/index.tsx
export default function Home() {
  return <h1>Welcome to the Home Page</h1>
}
```

```tsx
// pages/about.tsx
export default function About() {
  return <h1>About Us</h1>
}
```

### 動態路由

`pages/posts/[id].tsx` → `/posts/:id`

```tsx
// pages/posts/[id].tsx
import { useRouter } from 'next/router'

export default function Post() {
  const { query } = useRouter()
  const { id } = query
  return <h1>Post ID: {id}</h1>
}
```

### 連結導覽

```tsx
// pages/index.tsx
import Link from 'next/link'
export default function Home() {
  const postId = 123
  return (
    <div>
      <h1>Welcome</h1>
      <Link href="/about">Go to About</Link>
      <br />
      <Link href={`/posts/${postId}`}>Go to Post {postId}</Link>
    </div>
  )
}
```

### 練習：列表頁 + 動態詳情（假資料）

**fixtures/fakeData.ts**

```ts
export const postData = [
  { id: 1, title: 'Hello World', content: 'This is my first post' },
  { id: 2, title: 'Hello Again', content: 'This is my second post' },
  { id: 3, title: 'Hello Again', content: 'This is my third post' },
  { id: 4, title: 'Hello Again', content: 'This is my fourth post' },
]
```

**pages/posts/index.tsx**

```tsx
import { postData } from '@/fixtures/fakeData'
import Link from 'next/link'

export default function PostsPage() {
  return (
    <div>
      {postData.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
          <Link href={`/posts/${post.id}`}>Read more</Link>
        </div>
      ))}
    </div>
  )
}
```

**pages/posts/[id].tsx**

```tsx
import { useRouter } from 'next/router'
import { postData } from '@/fixtures/fakeData'
import Link from 'next/link'

export default function Post() {
  const { id } = useRouter().query
  const current = postData.find((p) => String(p.id) === String(id))
  return (
    <div>
      <h1>Post ID: {id}</h1>
      <h2>{current?.title}</h2>
      <p>{current?.content}</p>
      <Link href="/posts">返回列表</Link>
    </div>
  )
}
```

參考專案：[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-router](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-router)


<br/>


## 第四章：SSR 與 SSG（Pages Router）

### SSR：`getServerSideProps`

每次請求都在伺服器抓資料並渲染。

```tsx
// pages/time.tsx
export default function TimePage({ currentTime }: { currentTime: string }) {
  return (
    <div>
      <h1>Current Server Time</h1>
      <p>{currentTime}</p>
    </div>
  )
}

export async function getServerSideProps() {
  return { props: { currentTime: new Date().toLocaleString() } }
}
```

### SSG：`getStaticProps` + `getStaticPaths`

建置時產靜態頁。

```ts
// fixtures/fakeData.ts
export const postData = [
  { id: 1, title: 'Hello World', content: 'This is my first post' },
  { id: 2, title: 'Hello Again', content: 'This is my second post' },
  { id: 3, title: 'Hello Again', content: 'This is my third post' },
  { id: 4, title: 'Hello Again', content: 'This is my fourth post' },
]
```

```tsx
// pages/posts/[id].tsx
import { postData } from '@/fixtures/fakeData'
import type { GetStaticPropsContext } from 'next'
import Link from 'next/link'

type Post = { id: number; title: string; content: string }

export default function BlogPost({ post }: { post: Post | null }) {
  if (!post) return <div>Post not found</div>
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <Link href="/posts">Back to posts</Link>
    </div>
  )
}

export async function getStaticProps({ params }: GetStaticPropsContext) {
  const post = postData.find((p) => String(p.id) === String(params?.id)) || null
  return { props: { post } }
}

export async function getStaticPaths() {
  const paths = postData.map((p) => ({ params: { id: String(p.id) } }))
  return { paths, fallback: false }
}
```

參考專案：[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-SSR-SSG](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-SSR-SSG)

<br/>

## 第五章：API 路由（Pages Router）

### 基本 API 範例

```ts
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ message: 'Hello, Next.js API!' })
}
```

前端呼叫：

```tsx
import { useEffect, useState } from 'react'

export default function Home() {
  const [data, setData] = useState<null | { message: string }>(null)

  useEffect(() => {
    (async () => {
      const r = await fetch('/api/hello')
      const json = await r.json()
      setTimeout(() => setData(json), 1000)
    })()
  }, [])

  if (!data) return <h1>Loading...</h1>
  return <h1>{data.message}</h1>
}
```

### 表單 POST / 驗證

```ts
// pages/api/submitForm.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    const { name, email } = req.body
    if (!name || !email) return res.status(400).json({ error: 'Name and email are required' })
    return res.status(200).json({ message: `Form submitted successfully, ${name}!` })
  }
  res.setHeader('Allow', ['POST'])
  res.status(405).end(`Method ${req.method} Not Allowed`)
}
```

```tsx
// pages/form.tsx
export default function FormPage() {
  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const fd = new FormData(e.currentTarget)
    const name = String(fd.get('name') || '')
    const email = String(fd.get('email') || '')
    const r = await fetch('/api/submitForm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email }) })
    console.log(await r.json())
  }
  return (
    <form onSubmit={onSubmit}>
      <label>Name: <input type="text" name="name" /></label>
      <br />
      <label>Email: <input type="email" name="email" /></label>
      <button type="submit">Submit</button>
    </form>
  )
}
```

參考專案：[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-api](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-pojrect-api)

<br/>

## 第六章：綜合實作 A —— Todo List（API 路由 + TS）

### 前端頁面

```tsx
// pages/todos.tsx
import { useEffect, useState } from 'react'

type Todo = { id: number; title: string; completed: boolean }

export default function TodoPage() {
  const [todos, setTodos] = useState<Todo[]>([])
  const [newTodo, setNewTodo] = useState('')

  useEffect(() => {
    (async () => {
      const r = await fetch('/api/todos')
      setTodos(await r.json())
    })()
  }, [])

  const addTodo = async (): Promise<void> => {
    const r = await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title: newTodo }),
    })
    const newItem: Todo = await r.json()
    setTodos((t) => [...t, newItem])
    setNewTodo('')
  }

  const delTodo = async (id: number): Promise<void> => {
    await fetch(`/api/todos?id=${id}`, { method: 'DELETE' })
    setTodos((t) => t.filter((x) => x.id !== id))
  }

  const toggle = async (id: number) => {
    const cur = todos.find((t) => t.id === id)
    if (!cur) return
    const r = await fetch(`/api/todos?id=${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: cur.title, completed: !cur.completed }) })
    const updated: Todo = await r.json()
    setTodos((t) => t.map((x) => (x.id === id ? updated : x)))
  }

  return (
    <div>
      <h1>Todo List</h1>
      <input value={newTodo} onChange={(e) => setNewTodo(e.target.value)} placeholder="Add a new todo" />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} onClick={() => toggle(todo.id)}>
              {todo.title}
            </span>
            <button onClick={() => delTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  )
}
```

### API 路由

```ts
// pages/api/todos.ts
// pages/api/todos.ts
import type { NextApiRequest, NextApiResponse } from 'next'

type Todo = { id: number; title: string; completed: boolean }
let todos: Todo[] = [
  { id: 1, title: 'Learn Next.js', completed: false },
  { id: 2, title: 'Build a Next.js app', completed: false },
]

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  switch (req.method) {
    case 'GET':
      return res.status(200).json(todos)
    case 'POST': {
      const { title } = req.body
      const newTodo: Todo = { id: todos.length + 1, title, completed: false }
      todos.push(newTodo)
      return res.status(201).json(newTodo)
    }
    case 'PUT': {
      const id = Number(req.query.id)
      const { title, completed } = req.body
      const i = todos.findIndex((t) => t.id === id)
      if (i === -1) return res.status(404).json({ message: 'Todo not found' })
      todos[i] = { ...todos[i], title, completed }
      return res.status(200).json(todos[i])
    }
    case 'DELETE': {
      const id = Number(req.query.id)
      todos = todos.filter((t) => t.id !== id)
      return res.status(200).json({ message: 'Todo deleted' })
    }
    default:
      res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE'])
      return res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}
```

> 教學用記憶體陣列僅供示範；正式環境請改用資料庫或外部 API。

<br/>


## 第七章：樣式與版型（CSS Modules + Layout）

**components/Navbar/index.tsx**

```tsx
import styles from './Navbar.module.css'
export default function Navbar() {
  return <div className={styles.navbar}>Navbar</div>
}
```

**components/Navbar/Navbar.module.css**

```css
.navbar { background-color: blue; }
```

**pages/_app.tsx（版型包裹）**

```tsx
import type { AppProps } from 'next/app'
import Layout from '@/components/Layout'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}
```

**components/Layout/index.tsx**

```tsx
export default function Layout({ children }: { children?: React.ReactNode }) {
  return (
    <>
      <nav>I am nav</nav>
      {children}
    </>
  )
}
```

**pages/about.tsx**

```tsx
export default function AboutPage() {
  return <div>I AM AboutPage page</div>
}
```

VS Code 自動整理匯入（選用）：

```json
{
  "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }
}
```

**範例**：[https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-project-demo](https://github.com/IffyArt/front-website-course/tree/feature/next-typescript-project-demo)

<br/>
、