# 路由架構指南
## 🗺️ 路由結構總覽
DramaLing 使用 Next.js 14 App Router,採用檔案系統路由。
```
app/
├── (auth)/ # 認證相關頁面群組
│ ├── login/
│ ├── signup/
│ └── forgot-password/
├── (dashboard)/ # 需要登入的頁面群組
│ ├── layout.tsx # 共用 Dashboard Layout
│ ├── page.tsx # Dashboard 首頁
│ ├── flashcards/
│ ├── decks/
│ ├── progress/
│ └── settings/
├── api/ # API 路由
├── layout.tsx # 根 Layout
├── page.tsx # 首頁
└── not-found.tsx # 404 頁面
```
## 📁 詳細路由規劃
### 公開路由(無需登入)
| 路徑 | 頁面 | 說明 |
|------|------|------|
| `/` | 首頁 | Landing page,產品介紹 |
| `/features` | 功能介紹 | 詳細功能說明 |
| `/pricing` | 價格方案 | 訂閱方案(未來) |
| `/about` | 關於我們 | 團隊介紹 |
| `/privacy` | 隱私政策 | 法律文件 |
| `/terms` | 使用條款 | 法律文件 |
### 認證路由
| 路徑 | 頁面 | 說明 |
|------|------|------|
| `/login` | 登入 | Email/密碼或 Google 登入 |
| `/signup` | 註冊 | 新用戶註冊 |
| `/forgot-password` | 忘記密碼 | 密碼重設請求 |
| `/reset-password` | 重設密碼 | 實際重設密碼 |
| `/verify-email` | 驗證信箱 | Email 驗證頁面 |
### 受保護路由(需登入)
| 路徑 | 頁面 | 說明 |
|------|------|------|
| `/dashboard` | 儀表板 | 用戶主頁,學習統計 |
| `/flashcards` | 詞卡列表 | 所有詞卡總覽 |
| `/flashcards/new` | 新增詞卡 | AI 生成詞卡 |
| `/flashcards/[id]` | 詞卡詳情 | 單一詞卡檢視/編輯 |
| `/decks` | 卡組列表 | 詞卡分類管理 |
| `/decks/[id]` | 卡組詳情 | 特定卡組的詞卡 |
| `/learn/[deckId]` | 學習模式 | 間隔重複學習 |
| `/progress` | 學習進度 | 統計與成就 |
| `/settings` | 設定 | 個人資料與偏好 |
## 🔧 路由實作
### 1. 根 Layout
```typescript
// app/layout.tsx
import { Inter } from 'next/font/google'
import { Providers } from './providers'
import { Toaster } from '@/components/ui/toaster'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'DramaLing - Learn English with Drama',
description: 'Master English vocabulary through your favorite TV shows',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
{children}
)
}
```
### 2. 路由群組 Layout
```typescript
// app/(dashboard)/layout.tsx
import { redirect } from 'next/navigation'
import { getServerSession } from '@/lib/auth'
import { Sidebar } from '@/components/layout/Sidebar'
import { Header } from '@/components/layout/Header'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await getServerSession()
if (!session) {
redirect('/login')
}
return (
)
}
```
### 3. 動態路由
```typescript
// app/(dashboard)/flashcards/[id]/page.tsx
import { notFound } from 'next/navigation'
import { getFlashcard } from '@/lib/api/flashcards'
interface PageProps {
params: {
id: string
}
}
export default async function FlashcardPage({ params }: PageProps) {
const flashcard = await getFlashcard(params.id)
if (!flashcard) {
notFound()
}
return (
{flashcard.word}
{flashcard.translation}
{/* 詞卡詳情 */}
)
}
// 生成靜態參數(可選)
export async function generateStaticParams() {
const flashcards = await getPopularFlashcards()
return flashcards.map((card) => ({
id: card.id,
}))
}
```
### 4. 平行路由(Modal)
```typescript
// app/(dashboard)/@modal/(.)flashcards/[id]/page.tsx
import { Modal } from '@/components/ui/modal'
import FlashcardDetail from '@/components/FlashcardDetail'
export default function FlashcardModal({ params }: { params: { id: string } }) {
return (
)
}
// app/(dashboard)/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
>
)
}
```
## 🛡️ 路由保護
### 1. Middleware 認證檢查
```typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { getToken } from 'next-auth/jwt'
// 需要認證的路由
const protectedRoutes = [
'/dashboard',
'/flashcards',
'/decks',
'/learn',
'/progress',
'/settings',
]
// 已登入用戶不應訪問的路由
const authRoutes = ['/login', '/signup', '/forgot-password']
export async function middleware(request: NextRequest) {
const token = await getToken({ req: request })
const { pathname } = request.nextUrl
// 檢查受保護路由
const isProtectedRoute = protectedRoutes.some(route =>
pathname.startsWith(route)
)
if (isProtectedRoute && !token) {
const url = new URL('/login', request.url)
url.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(url)
}
// 已登入用戶重定向
const isAuthRoute = authRoutes.some(route =>
pathname.startsWith(route)
)
if (isAuthRoute && token) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
```
### 2. 客戶端路由守衛
```typescript
// components/auth/AuthGuard.tsx
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useStore } from '@/store'
interface AuthGuardProps {
children: React.ReactNode
fallback?: React.ReactNode
}
export function AuthGuard({ children, fallback }: AuthGuardProps) {
const router = useRouter()
const { isAuthenticated, isLoading, checkAuth } = useStore()
useEffect(() => {
checkAuth()
}, [])
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/login')
}
}, [isAuthenticated, isLoading, router])
if (isLoading) {
return fallback || Loading...
}
if (!isAuthenticated) {
return null
}
return <>{children}>
}
```
## 🔀 路由導航
### 1. 程式化導航
```typescript
// components/FlashcardList.tsx
'use client'
import { useRouter } from 'next/navigation'
export function FlashcardList() {
const router = useRouter()
const handleCardClick = (id: string) => {
router.push(`/flashcards/${id}`)
}
const handleCreateNew = () => {
router.push('/flashcards/new')
}
return (
// ...
)
}
```
### 2. Link 組件
```typescript
// components/Navigation.tsx
import Link from 'next/link'
export function Navigation() {
return (
)
}
```
### 3. 動態麵包屑
```typescript
// components/Breadcrumbs.tsx
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function Breadcrumbs() {
const pathname = usePathname()
const segments = pathname.split('/').filter(Boolean)
return (
)
}
```
## 📱 路由載入狀態
### 1. Loading UI
```typescript
// app/(dashboard)/flashcards/loading.tsx
export default function Loading() {
return (
{[...Array(6)].map((_, i) => (
))}
)
}
```
### 2. Error Boundary
```typescript
// app/(dashboard)/flashcards/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
Something went wrong!
{error.message}
)
}
```
## 🎯 路由最佳實踐
### 1. SEO 優化
```typescript
// app/(dashboard)/flashcards/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Flashcards | DramaLing',
description: 'Manage your vocabulary flashcards',
openGraph: {
title: 'Flashcards | DramaLing',
description: 'Learn English vocabulary with AI-powered flashcards',
},
}
// 動態 metadata
export async function generateMetadata({ params }: PageProps): Promise {
const flashcard = await getFlashcard(params.id)
return {
title: `${flashcard.word} | DramaLing`,
description: flashcard.translation,
}
}
```
### 2. 路由預載入策略
```typescript
// components/FlashcardGrid.tsx
import Link from 'next/link'
export function FlashcardGrid({ flashcards }: { flashcards: Flashcard[] }) {
return (
{flashcards.map((card, index) => (
))}
)
}
```
### 3. 查詢參數處理
```typescript
// app/(dashboard)/flashcards/page.tsx
import { Suspense } from 'react'
interface PageProps {
searchParams: {
page?: string
sort?: 'recent' | 'alphabetical' | 'difficulty'
filter?: string
}
}
export default function FlashcardsPage({ searchParams }: PageProps) {
const page = Number(searchParams.page) || 1
const sort = searchParams.sort || 'recent'
const filter = searchParams.filter
return (
}>
)
}
```
## 🚀 路由性能優化
### 1. 部分預渲染
```typescript
// app/(dashboard)/flashcards/page.tsx
export const dynamic = 'force-dynamic' // 動態渲染
export const revalidate = 60 // ISR:60 秒重新驗證
```
### 2. 路由分割
```typescript
// 使用動態導入減少初始載入
const FlashcardEditor = dynamic(
() => import('@/components/FlashcardEditor'),
{
loading: () => Loading editor...
,
ssr: false, // 客戶端渲染
}
)
```
### 3. 路由快取策略
```typescript
// app/api/flashcards/route.ts
export async function GET(request: Request) {
// 設定快取標頭
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=10, stale-while-revalidate=59',
},
})
}
```