# Story 1.10: Portfolio Implementation

**Status:** ready-for-dev
**Epic:** Epic 1 - Webflow to Payload CMS + Astro Migration
**Priority:** P1 (High)
**Estimated Time:** 8 hours

## Story

**作為**潛在客戶，
**我想要**看到 Enchun 過去的網站設計作品，
**以便**我能夠評估他們的設計能力並決定是否合作。

## Context

這是 Sprint 1 的核心前端故事之一。Portfolio 頁面展示了 Enchun 數位的設計能力和過往項目經驗，是潛在客戶評估公司能力的重要參考。

**Story Source:**
- `docs/prd/05-epic-stories.md` - Story 1.10
- `sprint-status.yaml` - Story 1-10-portfolio

**原始 HTML 參考：**
- [Source: research/www.enchun.tw/website-portfolio.html](../../research/www.enchun.tw/website-portfolio.html) - Portfolio 列表頁 (Webflow 原始)

**Dependencies:**
- Story 1-2-collections-definition (DONE) - Portfolio collection 已完成
- Story 1-4-global-layout - Header/Footer 組件需先完成

**原 Webflow URL 對應:**
- 舊: `/webdesign-profolio` → 新: `/portfolio` (作品列表)
- 舊: `/webdesign-profolio/[slug]` → 新: `/portfolio/[slug]` (作品詳情)

## Acceptance Criteria

### Part 1: Portfolio Listing Page (`/portfolio`)

1. **AC1 - 2-Column Grid Layout**: 作品以 2 欄式網格顯示
2. **AC2 - Card Information**: 每張卡片顯示:
   - 專案預覽圖 (image)
   - 專案標題 (title)
   - 專案描述 (description)
   - 標籤 (tags - 如 "一頁式銷售", "客戶預約")
3. **AC3 - Visual Fidelity**: 視覺還原度達 95%+ (對比 Webflow)
4. **AC4 - Responsive**: 手機版單欄顯示，平板/桌面 2 欄顯示

### Part 2: Portfolio Detail Page (`/portfolio/[slug]`)

5. **AC5 - Project Display**: 顯示完整專案資訊
6. **AC6 - Live Website Link**: 連結到實際網站 (url 欄位)
7. **AC7 - Additional Images**: 顯示額外專案圖片/輪播 (如可用)
8. **AC8 - Case Study Content**: 案例研究內容展示
9. **AC9 - Back to List**: 返回列表頁的連結

### Part 3: Content Integration

10. **AC10 - CMS Integration**: 從 Payload CMS Portfolio collection 獲取資料
11. **AC11 - SEO Metadata**: 每頁都有適當的 meta tags
12. **AC12 - 301 Redirect**: 舊 URL 正確重定向到新 URL

## Dev Technical Guidance

### Architecture Overview

Portfolio 頁面採用 Astro SSR 模式，在建置時從 Payload CMS 獲取所有 portfolio items 並生成靜態頁面。

### Portfolio Collection Schema (已完成)

```typescript
// apps/backend/src/collections/Portfolio/index.ts
{
  slug: 'portfolio',
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'slug', type: 'text' },
    { name: 'url', type: 'text' },           // Website URL
    { name: 'image', type: 'upload', relationTo: 'media' },
    { name: 'description', type: 'textarea' },
    { name: 'websiteType', type: 'select' }, // corporate/ecommerce/landing/brand/other
    { name: 'tags', type: 'array' },         // Array of { tag: string }
  ]
}
```

### Task 1.10.1: Design Portfolio Architecture (1h)

**目標**: 規劃 Portfolio 頁面的組件結構和資料流

**設計考量**:
1. **組件拆分**:
   - `PortfolioCard.astro` - 單一作品卡片組件
   - `PortfolioGrid.astro` - 作品網格容器
   - `PortfolioDetail.astro` - 作品詳情頁主組件

2. **資料獲取策略**:
   - 使用 Astro 的 `getStaticPaths()` 預生成所有作品頁面
   - 從 Payload CMS API 獲取完整資料

3. **URL 結構**:
   - 列表頁: `/portfolio` (或保留原有 `/website-portfolio`)
   - 詳情頁: `/portfolio/[slug]` (或 `/webdesign-profolio/[slug]`)

### Task 1.10.2: Implement Portfolio Listing Page (2h)

**檔案**: `apps/frontend/src/pages/portfolio.astro`

```astro
---
import Layout from '../layouts/Layout.astro'
import { Payload } from '@payload-types'

// Payload CMS API 配置
const PAYLOAD_API_URL = import.meta.env.PAYLOAD_CMS_URL || 'http://localhost:3000/api'

// 獲取所有 portfolio items
async function getPortfolios() {
  try {
    const res = await fetch(`${PAYLOAD_API_URL}/portfolio?depth=1&limit=100`)
    if (!res.ok) throw new Error('Failed to fetch portfolios')
    const data = await res.json()
    return data.docs || []
  } catch (error) {
    console.error('Error fetching portfolios:', error)
    return []
  }
}

const portfolios = await getPortfolios()

// SEO 配置
const title = '網站設計作品 | 恩群數位行銷'
const description = '瀏覽恩群數位的網站設計作品集，包含企業官網、電商網站、活動頁面等專案案例。'
---

<Layout title={title} metaDescription={description}>
  <section class="portfolio-section">
    <div class="container">
      <header class="page-header">
        <h1 class="page-title">網站設計作品</h1>
        <p class="page-subtitle">精選案例，展現專業設計能力</p>
      </header>

      <div class="portfolio-grid">
        {portfolios.map((item) => (
          <a href={`/portfolio/${item.slug}`} class="portfolio-card">
            <div class="card-image">
              {item.image?.url ? (
                <img
                  src={item.image.url}
                  alt={item.image.alt || item.title}
                  loading="lazy"
                />
              ) : (
                <div class="image-placeholder">暫無圖片</div>
              )}
            </div>
            <div class="card-content">
              <h2 class="card-title">{item.title}</h2>
              <p class="card-description">{item.description}</p>
              <div class="card-tags">
                {item.tags?.map((tagItem) => (
                  <span class="tag">{tagItem.tag}</span>
                ))}
              </div>
              <span class="card-type">{getWebsiteTypeLabel(item.websiteType)}</span>
            </div>
          </a>
        ))}
      </div>

      {portfolios.length === 0 && (
        <div class="empty-state">
          <p>暫無作品資料</p>
        </div>
      )}
    </div>
  </section>
</Layout>

<style>
  .portfolio-section {
    padding: var(--spacing-3xl) 0;
  }

  .container {
    max-width: var(--container-max-width);
    margin: 0 auto;
    padding: 0 var(--container-padding);
  }

  .page-header {
    text-align: center;
    margin-bottom: var(--spacing-3xl);
  }

  .page-title {
    font-size: 2.5rem;
    font-weight: 700;
    color: var(--color-text-primary);
    margin-bottom: var(--spacing-md);
  }

  .page-subtitle {
    font-size: 1.125rem;
    color: var(--color-text-muted);
  }

  .portfolio-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--spacing-xl);
  }

  .portfolio-card {
    display: block;
    background: var(--color-surface);
    border-radius: var(--radius-lg);
    overflow: hidden;
    transition: all var(--transition-base);
    text-decoration: none;
    box-shadow: var(--shadow);
  }

  .portfolio-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-lg);
  }

  .card-image {
    aspect-ratio: 16 / 10;
    overflow: hidden;
    background: var(--color-gray-200);
  }

  .card-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--transition-slow);
  }

  .portfolio-card:hover .card-image img {
    transform: scale(1.05);
  }

  .image-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    color: var(--color-text-muted);
  }

  .card-content {
    padding: var(--spacing-lg);
  }

  .card-title {
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--color-text-primary);
    margin-bottom: var(--spacing-sm);
  }

  .card-description {
    color: var(--color-text-secondary);
    line-height: 1.6;
    margin-bottom: var(--spacing-md);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .card-tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-xs);
    margin-bottom: var(--spacing-sm);
  }

  .tag {
    background: var(--color-primary-light);
    color: white;
    padding: 0.25rem 0.5rem;
    border-radius: var(--radius-sm);
    font-size: 0.75rem;
    font-weight: 500;
  }

  .card-type {
    display: inline-block;
    font-size: 0.875rem;
    color: var(--color-primary);
    font-weight: 500;
  }

  .empty-state {
    text-align: center;
    padding: var(--spacing-3xl);
    color: var(--color-text-muted);
  }

  @media (max-width: 768px) {
    .portfolio-grid {
      grid-template-columns: 1fr;
    }

    .page-title {
      font-size: 1.875rem;
    }
  }
</style>

---
// Helper functions
function getWebsiteTypeLabel(type: string): string {
  const labels = {
    corporate: '企業官網',
    ecommerce: '電商網站',
    landing: '活動頁面',
    brand: '品牌網站',
    other: '其他'
  }
  return labels[type] || type
}
---
```

### Task 1.10.3: Implement Portfolio Detail Page (2h)

**檔案**: `apps/frontend/src/pages/portfolio/[slug].astro`

```astro
---
import Layout from '../../layouts/Layout.astro'

const PAYLOAD_API_URL = import.meta.env.PAYLOAD_CMS_URL || 'http://localhost:3000/api'

// 獲取單一 portfolio item
async function getPortfolio(slug: string) {
  try {
    const res = await fetch(`${PAYLOAD_API_URL}/portfolio?where[slug][equals]=${slug}&depth=1`)
    if (!res.ok) throw new Error('Failed to fetch portfolio')
    const data = await res.json()
    return data.docs?.[0] || null
  } catch (error) {
    console.error('Error fetching portfolio:', error)
    return null
  }
}

// 生成所有靜態路徑
export async function getStaticPaths() {
  try {
    const res = await fetch(`${PAYLOAD_API_URL}/portfolio?limit=100&depth=0`)
    if (!res.ok) return []

    const data = await res.json()
    const portfolios = data.docs || []

    return portfolios.map((item) => ({
      params: { slug: item.slug },
      props: { slug: item.slug }
    }))
  } catch (error) {
    console.error('Error in getStaticPaths:', error)
    return []
  }
}

const { slug } = Astro.props
const portfolio = await getPortfolio(slug)

// 404 處理
if (!portfolio) {
  return Astro.redirect('/404')
}

// SEO 配置
const title = `${portfolio.title} | 恩群數位網站設計作品`
const description = portfolio.description || `瀏覽 ${portfolio.title} 專案詳情`
---

<Layout title={title} metaDescription={description}>
  <article class="portfolio-detail">
    <div class="container">
      <!-- 返回連結 -->
      <a href="/portfolio" class="back-link">← 返回作品列表</a>

      <!-- 專案標題 -->
      <header class="project-header">
        <div class="project-meta">
          <span class="project-type">{getWebsiteTypeLabel(portfolio.websiteType)}</span>
        </div>
        <h1 class="project-title">{portfolio.title}</h1>
        <p class="project-description">{portfolio.description}</p>

        <!-- 標籤 -->
        {portfolio.tags && portfolio.tags.length > 0 && (
          <div class="project-tags">
            {portfolio.tags.map((tagItem) => (
              <span class="tag">{tagItem.tag}</span>
            ))}
          </div>
        )}
      </header>

      <!-- 主要圖片 -->
      <div class="project-hero-image">
        {portfolio.image?.url ? (
          <img
            src={portfolio.image.url}
            alt={portfolio.image.alt || portfolio.title}
            loading="eager"
          />
        ) : (
          <div class="image-placeholder">暫無圖片</div>
        )}
      </div>

      <!-- 案例詳情內容 -->
      <div class="project-content">
        <div class="content-section">
          <h2>專案介紹</h2>
          <p>此專案展示了我們在 {getWebsiteTypeLabel(portfolio.websiteType)} 領域的專業能力。</p>
        </div>

        <div class="content-section">
          <h2>專案連結</h2>
          {portfolio.url ? (
            <a href={portfolio.url} target="_blank" rel="noopener noreferrer" class="btn-primary">
              前往網站 →
            </a>
          ) : (
            <p class="text-muted">此專案暫無公開連結</p>
          )}
        </div>
      </div>
    </div>
  </article>
</Layout>

<style>
  .portfolio-detail {
    padding: var(--spacing-3xl) 0;
  }

  .container {
    max-width: 1000px;
    margin: 0 auto;
    padding: 0 var(--container-padding);
  }

  .back-link {
    display: inline-block;
    margin-bottom: var(--spacing-xl);
    color: var(--color-primary);
    text-decoration: none;
    font-weight: 500;
    transition: color var(--transition-fast);
  }

  .back-link:hover {
    color: var(--color-primary-hover);
  }

  .project-header {
    margin-bottom: var(--spacing-3xl);
    text-align: center;
  }

  .project-meta {
    margin-bottom: var(--spacing-md);
  }

  .project-type {
    display: inline-block;
    background: var(--color-primary-light);
    color: white;
    padding: 0.375rem 0.75rem;
    border-radius: var(--radius-full);
    font-size: 0.875rem;
    font-weight: 500;
  }

  .project-title {
    font-size: 2.5rem;
    font-weight: 700;
    color: var(--color-text-primary);
    margin-bottom: var(--spacing-md);
  }

  .project-description {
    font-size: 1.125rem;
    color: var(--color-text-secondary);
    max-width: 700px;
    margin: 0 auto var(--spacing-lg);
    line-height: 1.7;
  }

  .project-tags {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--spacing-sm);
  }

  .project-tags .tag {
    background: var(--color-surface2);
    color: var(--color-text-primary);
    padding: 0.375rem 0.75rem;
    border-radius: var(--radius-full);
    font-size: 0.875rem;
  }

  .project-hero-image {
    margin-bottom: var(--spacing-3xl);
    border-radius: var(--radius-xl);
    overflow: hidden;
    box-shadow: var(--shadow-lg);
  }

  .project-hero-image img {
    width: 100%;
    height: auto;
  }

  .image-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 16 / 10;
    background: var(--color-gray-200);
    color: var(--color-text-muted);
  }

  .project-content {
    display: grid;
    gap: var(--spacing-2xl);
  }

  .content-section h2 {
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--color-text-primary);
    margin-bottom: var(--spacing-md);
  }

  .content-section p {
    color: var(--color-text-secondary);
    line-height: 1.7;
  }

  .btn-primary {
    display: inline-block;
    background: var(--color-primary);
    color: white;
    padding: 0.75rem 1.5rem;
    border-radius: var(--radius-md);
    text-decoration: none;
    font-weight: 500;
    transition: all var(--transition-fast);
  }

  .btn-primary:hover {
    background: var(--color-primary-hover);
    transform: translateY(-2px);
  }

  .text-muted {
    color: var(--color-text-muted);
  }

  @media (max-width: 768px) {
    .project-title {
      font-size: 1.875rem;
    }

    .project-description {
      font-size: 1rem;
    }
  }
</style>

---
// Helper function
function getWebsiteTypeLabel(type: string): string {
  const labels = {
    corporate: '企業官網',
    ecommerce: '電商網站',
    landing: '活動頁面',
    brand: '品牌網站',
    other: '其他'
  }
  return labels[type] || type
}
---
```

### Task 1.10.4: Implement Portfolio Filter (Optional, 1h)

**可選功能**: 依照 websiteType 或 tags 進行篩選

```typescript
// 在 portfolio.astro 中加入篩選功能
---
const websiteTypes = [
  { value: 'all', label: '全部' },
  { value: 'corporate', label: '企業官網' },
  { value: 'ecommerce', label: '電商網站' },
  { value: 'landing', label: '活動頁面' },
  { value: 'brand', label: '品牌網站' },
  { value: 'other', label: '其他' },
]

// 這裡可以加入客戶端 JavaScript 進行篩選
// 或使用 Astro 的 actions 進行伺服器端篩選
---
```

**篩選 UI 範例**:
```html
<div class="filter-bar">
  {websiteTypes.map((type) => (
    <button
      class={`filter-btn ${selectedType === type.value ? 'active' : ''}`}
      data-type={type.value}
    >
      {type.label}
    </button>
  ))}
</div>
```

### Task 1.10.5: Performance and Visual Testing (1h)

**Lighthouse 測試項目**:
- [ ] Performance 分數 >= 95
- [ ] Accessibility 分數 >= 95
- [ ] Best Practices 分數 >= 95
- [ ] SEO 分數 >= 95

**視覺還原測試**:
- [ ] 對比 Webflow 原站，視覺相似度 >= 95%
- [ ] 圖片尺寸和裁切方式一致
- [ ] 字體大小和行高一致
- [ ] 間距和布局一致

**響應式測試**:
- [ ] 手機版 (< 768px) 單欄顯示
- [ ] 平板版 (768px - 1024px) 2 欄顯示
- [ ] 桌面版 (> 1024px) 2 欄顯示，最大寬度限制

## File Structure

```
apps/frontend/src/
├── pages/
│   ├── portfolio.astro              ← CREATE (或修改 website-portfolio.astro)
│   └── portfolio/
│       └── [slug].astro              ← CREATE (或修改 webdesign-profolio/[slug].astro)
├── components/
│   ├── portfolio/
│   │   ├── PortfolioCard.astro       ← CREATE (可選，用於組件化)
│   │   ├── PortfolioGrid.astro       ← CREATE (可選)
│   │   └── PortfolioDetail.astro     ← CREATE (可選)
│   ├── Header.astro                  ← EXISTS (依賴)
│   └── Footer.astro                  ← EXISTS (依賴)
└── styles/
    └── theme.css                     ← EXISTS (共用樣式)
```

## Tasks / Subtasks

### Part 1: 設計與架構
- [ ] **Task 1.1**: 設計 Portfolio 頁面架構
  - [ ] 確認 URL 結構 (`/portfolio` vs `/website-portfolio`)
  - [ ] 規劃組件拆分策略
  - [ ] 設計資料獲取流程

### Part 2: Portfolio Listing Page
- [ ] **Task 2.1**: 實作 Portfolio 列表頁
  - [ ] 建立 `/portfolio.astro` (或修改現有檔案)
  - [ ] 從 Payload CMS 獲取資料
  - [ ] 實作 2 欄網格布局
  - [ ] 加入卡片 hover 效果

- [ ] **Task 2.2**: Portfolio 卡片組件
  - [ ] 顯示專案圖片
  - [ ] 顯示標題、描述
  - [ ] 顯示標籤和類型
  - [ ] 加入連結到詳情頁

- [ ] **Task 2.3**: 響應式設計
  - [ ] 手機版單欄
  - [ ] 平板/桌面 2 欄
  - [ ] 測試各裝置尺寸

### Part 3: Portfolio Detail Page
- [ ] **Task 3.1**: 實作 Portfolio 詳情頁
  - [ ] 建立 `/portfolio/[slug].astro`
  - [ ] 實作 `getStaticPaths()`
  - [ ] 獲取單一專案資料

- [ ] **Task 3.2**: 詳情頁內容
  - [ ] 專案標題和描述
  - [ ] 主要圖片展示
  - [ ] 標籤顯示
  - [ ] 外部網站連結

- [ ] **Task 3.3**: 導航功能
  - [ ] 返回列表連結
  - [ ] 404 處理 (無效 slug)

### Part 4: 篩選功能 (可選)
- [ ] **Task 4.1**: 實作篩選器
  - [ ] 按 websiteType 篩選
  - [ ] 按 tags 篩選
  - [ ] 客戶端狀態管理

### Part 5: 測試與優化
- [ ] **Task 5.1**: Lighthouse 測試
  - [ ] Performance >= 95
  - [ ] Accessibility >= 95
  - [ ] SEO >= 95

- [ ] **Task 5.2**: 視覺還原測試
  - [ ] 對比 Webflow 原站
  - [ ] 相似度 >= 95%
  - [ ] 修正視覺差異

- [ ] **Task 5.3**: SEO 設定
  - [ ] Meta title 和 description
  - [ ] Open Graph tags
  - [ ] 結構化資料 (可選)

## Testing Requirements

### Unit Tests

```typescript
// apps/frontend/src/components/portfolio/__tests__/PortfolioCard.spec.ts
import { describe, it, expect } from 'vitest'

describe('PortfolioCard', () => {
  const mockPortfolio = {
    title: '測試專案',
    slug: 'test-project',
    description: '這是一個測試專案',
    image: { url: '/test.jpg', alt: '測試圖片' },
    websiteType: 'corporate',
    tags: [{ tag: '企業官網' }, { tag: '響應式' }]
  }

  it('should render portfolio card with all required fields', () => {
    // 測試卡片正確渲染所有欄位
  })

  it('should link to correct detail page', () => {
    // 測試連結正確指向 /portfolio/[slug]
  })

  it('should display all tags', () => {
    // 測試所有標籤正確顯示
  })
})
```

### Integration Tests

```typescript
// apps/frontend/tests/e2e/portfolio.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Portfolio Pages', () => {
  test('should display portfolio listing page', async ({ page }) => {
    await page.goto('/portfolio')
    await expect(page.locator('h1')).toContainText('網站設計作品')
    await expect(page.locator('.portfolio-card')).toHaveCount(expect.any(Number))
  })

  test('should navigate to portfolio detail page', async ({ page }) => {
    await page.goto('/portfolio')
    const firstCard = page.locator('.portfolio-card').first()
    await firstCard.click()
    await expect(page).toHaveURL(/\/portfolio\/[^/]+$/)
  })

  test('should display portfolio detail with all information', async ({ page }) => {
    await page.goto('/portfolio/test-project')
    await expect(page.locator('.project-title')).toBeVisible()
    await expect(page.locator('.project-description')).toBeVisible()
    await expect(page.locator('.project-hero-image')).toBeVisible()
  })

  test('should return to listing from detail page', async ({ page }) => {
    await page.goto('/portfolio/test-project')
    await page.click('.back-link')
    await expect(page).toHaveURL('/portfolio')
  })

  test('should handle 404 for invalid slug', async ({ page }) => {
    await page.goto('/portfolio/invalid-slug')
    await expect(page).toHaveURL('/404')
  })
})
```

### Visual Regression Tests

```bash
# 使用 Playwright 截圖進行視覺測試
npx playwright test --project=chromium --update-snapshots
```

### Manual Testing Checklist

**Portfolio Listing Page**:
- [ ] 頁面正常載入，無 console 錯誤
- [ ] 所有 portfolio cards 正確顯示
- [ ] 圖片正確載入（使用 R2 URL）
- [ ] 標籤正確顯示
- [ ] 卡片 hover 效果正常
- [ ] 點擊卡片跳轉到正確的詳情頁
- [ ] 手機版顯示單欄
- [ ] 平板/桌面顯示 2 欄

**Portfolio Detail Page**:
- [ ] 頁面正常載入，無 console 錯誤
- [ ] 專案標題、描述正確顯示
- [ ] 主要圖片正確顯示
- [ ] 外部連結正確運作
- [ ] 返回列表連結正常運作
- [ ] 無效 slug 正確導向 404
- [ ] SEO meta tags 正確設置

## Risk Assessment

| Risk | Probability | Impact | Mitigation |
|------|------------|--------|------------|
| CMS API 無回應 | Low | Medium | 加入錯誤處理和 fallback |
| 圖片載入緩慢 | Medium | Low | 使用 loading="lazy"，優化圖片尺寸 |
| 視覺還原度不足 | Medium | Medium | 對比 Webflow 原站進行像素級測試 |
| 大量作品導致頁面過長 | Low | Low | 加入分頁或虛擬滾動 (未來優化) |
| 301 redirect 遺失 | Low | Medium | 確保 redirect map 包含所有舊 URL |

## Definition of Done

- [ ] `/portfolio` 列表頁實作完成
- [ ] `/portfolio/[slug]` 詳情頁實作完成
- [ ] 從 Payload CMS 正確獲取資料
- [ ] 2 欄網格布局正確顯示
- [ ] 響應式設計通過 (手機/平板/桌面)
- [ ] 所有連結正常運作
- [ ] SEO meta tags 正確設置
- [ ] Lighthouse 分數 >= 95 (Performance, Accessibility, SEO)
- [ ] 視覺還原度 >= 95%
- [ ] 單元測試通過
- [ ] E2E 測試通過
- [ ] 無 console 錯誤或警告
- [ ] 301 redirect 配置完成
- [ ] sprint-status.yaml 更新為 "done"

## Dev Agent Record

### Agent Model Used
*To be filled by Dev Agent*

### Debug Log References
*To be filled by Dev Agent*

### Completion Notes
*To be filled by Dev Agent*

### File List
*To be filled by Dev Agent*

## Change Log

| Date | Action | Author |
|------|--------|--------|
| 2026-01-31 | Story created (ready-for-dev) | SM Agent (Bob) |
