使用 Next.js、React、MDX、Tailwind CSS、Shadcn/UI 和 TypeScript 打造現代化一頁式電商網站:完整開發指南


深入探討如何使用 Next.js 14/15、React 19、MDX、Tailwind CSS、Shadcn/UI 和 TypeScript 這套現代化技術堆棧,從零開始打造一個功能完整的一頁式電商網站。包含詳細實作步驟、最佳實踐和進階功能。
前言
在 2025 年的網頁開發領域中,打造一個高效能、美觀且功能完整的電商網站已經變得比以往更加重要。但是,什麼才是真正的 One Page Store?透過研究 Whirly Birdie 字體展示和 USB Club Transport 這樣的經典案例,我們發現真正的 One Page Store 不僅僅是技術實作,更是一種全新的電商藝術表達形式。
本文將深入探討如何使用 Next.js 14/15、React 19、MDX、Tailwind CSS、Shadcn/UI 和 TypeScript 這套現代化技術堆棧,從零開始打造一個真正意義上的沉浸式 One Page Store。
🎨 重新定義 One Page Store
傳統電商 vs. 真正的 One Page Store
傳統電商頁面的特點:
- 多產品展示目錄
- 功能性導向設計
- 標準化佈局模版
- 理性購買決策流程
真正的 One Page Store 特色:
- 單一產品聚焦 ✨ - 專注一個主打產品的完整故事
- 沉浸式體驗設計 🌟 - 創造情感連結和品牌認同
- 藝術化創意設計 🎪 - 獨特視覺效果和互動元素
- 完整情感旅程 📚 - 從認知到渴望到購買的流暢體驗
核心設計理念
真正的 One Page Store 就像是一個數位產品博物館,每個元素都是為了一個目標:讓訪客完全沉浸在產品的世界中,從理性認知轉化為情感渴望,最終完成購買行為。
🎯 One Page Store 的六大核心特色
1. 🎨 單一產品聚焦
真正的 One Page Store 不是產品目錄,而是一個產品的完整宇宙:
- 深度展示:從外觀、功能到使用場景的 360 度呈現
- 品牌塑造:創造產品的獨特身份和情感價值
- 故事敘述:透過設計語言訴說產品背後的故事
2. 🌟 沉浸式體驗設計
創造讓使用者「忘記時間」的體驗:
- 震撼的 Hero 區塊:全屏背景、漸變效果、動態文字
- 情感化文案:「重新定義你的聆聽體驗」而非「高品質耳機」
- 視覺衝擊力:大膽的色彩搭配和版式設計
3. 🎪 互動體驗元素
讓使用者「參與」而非僅僅「瀏覽」:
- 即時體驗:點擊按鈕切換產品效果展示
- 動畫回饋:滾動視差、淡入淡出、脈衝動畫
- 感官刺激:聲音、觸覺回饋(震動)等多感官體驗
4. 📚 完整銷售故事
從注意力到購買的情感旅程:
- 吸引注意 → 震撼的視覺和獨特價值主張
- 建立需求 → 展示痛點和解決方案
- 互動體驗 → 讓用戶「感受」產品價值
- 社會證明 → 真實用戶評價和成功案例
- 促成購買 → 緊迫感和無風險保證
5. ✨ 獨特的設計元素
區別於普通網頁的藝術化處理:
- 創意圖標系統:🔇🔋🎵⚡ 讓功能更生動
- 漸變文字效果:重點詞彙使用彩色漸變突出
- 流暢滾動體驗:各區塊間的平滑過渡
- 響應式互動:根據設備特性優化體驗
6. 🛒 無縫購買流程
消除所有購買摩擦:
- 一鍵購買:直接加入購物車並開啟結帳
- 信任標識:免費送貨、退款保證、保固承諾
- 緊迫感設計:限時特價、庫存提醒、節省金額顯示
技術堆棧深度解析
1. Next.js 15 - 強大的 React 框架
Next.js 是由 Vercel 開發的全端 React 框架,提供了檔案式路由、動態路由和條件渲染等核心功能。最新版本的 Next.js 15 帶來了許多重要的改進:
- App Router:提供更靈活的路由系統和佈局管理
- Server Components:預設使用伺服器元件,提升效能和 SEO
- Turbopack:更快的開發環境建置速度
- 內建圖片優化:自動優化圖片載入和顯示
2. React 19 - 現代化的 UI 函式庫
React 19 作為最新版本,帶來了許多效能優化和開發體驗的改進:
- 並行渲染:提升大型應用的渲染效能
- 自動批次更新:減少不必要的重新渲染
- 改進的錯誤邊界:更好的錯誤處理機制
3. MDX - 強大的內容管理解決方案
MDX 是 Markdown 的擴展,允許你在 Markdown 檔案中直接撰寫 JSX。這對於電商網站來說特別有用,因為:
- 豐富的產品描述:可以在產品描述中嵌入互動式元件
- 動態內容:輕鬆整合圖表、動畫和其他 React 元件
- 易於維護:內容與程式碼分離,便於非技術人員編輯
4. Tailwind CSS - 實用優先的 CSS 框架
Tailwind CSS 提供了一套完整的實用類別系統,讓你能夠快速建立美觀的介面:
- 快速開發:直接在 HTML 中使用預定義的類別
- 一致性設計:確保整個網站的視覺一致性
- 高度可自訂:可以根據品牌需求調整設計系統
5. Shadcn/UI - 現代化的元件庫
Shadcn/UI 不是傳統的元件庫,而是一個可以直接複製到專案中的元件集合。它的特點包括:
- 完全可自訂:每個元件的原始碼都在你的專案中
- 無障礙設計:所有元件都遵循 WCAG 標準
- 與 Tailwind 完美整合:使用 Tailwind CSS 進行樣式設計
6. TypeScript - 類型安全的 JavaScript
TypeScript 為 JavaScript 添加了靜態類型檢查,提供:
- 更少的執行時錯誤:在編譯時期就能發現大部分錯誤
- 更好的開發體驗:智慧型程式碼提示和自動完成
- 更容易重構:類型系統讓大規模重構變得更安全
詳細實作步驟
步驟 1:專案初始化與環境設置
首先,我們需要建立一個新的 Next.js 專案並設置所有必要的依賴項:
# 使用最新版本的 create-next-app 建立專案
npx create-next-app@latest my-store --typescript --tailwind --eslint --app --src-dir
# 進入專案目錄
cd my-store
# 安裝額外的依賴項
npm install @next/mdx @mdx-js/loader @mdx-js/react gray-matter reading-time
npm install zustand @tanstack/react-query
npm install framer-motion lucide-react
npm install @radix-ui/react-slot class-variance-authority clsx tailwind-merge
步驟 2:設置 Shadcn/UI
Shadcn/UI 的設置需要特別注意,因為它會直接影響整個專案的元件結構:
# 初始化 Shadcn/UI
npx shadcn@latest init
在初始化過程中,選擇以下選項:
- TypeScript:Yes
- 樣式:Default
- 基礎顏色:Slate(或根據品牌選擇)
- CSS 變數:Yes
接著安裝我們需要的元件:
# 安裝電商網站常用的元件
npx shadcn@latest add button card dialog sheet badge
npx shadcn@latest add input label textarea select
npx shadcn@latest add separator skeleton sonner
npx shadcn@latest add dropdown-menu navigation-menu
步驟 3:配置 MDX 支援
MDX 的配置對於管理產品內容至關重要。建立 next.config.mjs
:
import createMDX from '@next/mdx'
import remarkGfm from 'remark-gfm'
import rehypePrism from 'rehype-prism-plus'
const withMDX = createMDX({
options: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrism],
},
})
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['ts', 'tsx', 'mdx'],
images: {
domains: ['images.unsplash.com', 'cdn.shopify.com'],
},
experimental: {
mdxRs: true,
},
}
export default withMDX(nextConfig)
建立 mdx-components.tsx
來定義全域 MDX 元件:
import type { MDXComponents } from 'mdx/types'
import Image from 'next/image'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
h1: ({ children }) => (
<h1 className="text-4xl font-bold tracking-tight mb-4">{children}</h1>
),
h2: ({ children }) => (
<h2 className="text-3xl font-semibold mb-3">{children}</h2>
),
p: ({ children }) => (
<p className="text-muted-foreground leading-7 mb-4">{children}</p>
),
Image,
Button,
Card,
}
}
步驟 4:建立專案結構
一個組織良好的專案結構對於維護和擴展至關重要:
src/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ └── globals.css
├── components/
│ ├── ui/ # Shadcn/UI 元件
│ ├── store/ # 商店特定元件
│ │ ├── ProductCard.tsx
│ │ ├── ProductGrid.tsx
│ │ ├── CartSheet.tsx
│ │ ├── CheckoutDialog.tsx
│ │ └── SearchBar.tsx
│ └── layout/ # 佈局元件
│ ├── Header.tsx
│ ├── Footer.tsx
│ └── Hero.tsx
├── content/
│ └── products/ # MDX 產品檔案
│ ├── product-1.mdx
│ ├── product-2.mdx
│ └── product-3.mdx
├── lib/
│ ├── store.ts # Zustand 狀態管理
│ ├── api.ts # API 函數
│ └── utils.ts # 工具函數
├── types/
│ └── index.ts # TypeScript 類型定義
└── hooks/ # 自訂 Hooks
├── useCart.ts
└── useProducts.ts
步驟 5:定義資料類型
建立 src/types/index.ts
來定義專案中使用的所有類型:
export interface Product {
id: string
name: string
slug: string
price: number
salePrice?: number
description: string
shortDescription: string
images: string[]
category: string
tags: string[]
inStock: boolean
quantity: number
variants?: ProductVariant[]
features: string[]
specifications: Record<string, string>
reviews: Review[]
rating: number
createdAt: Date
updatedAt: Date
}
export interface ProductVariant {
id: string
name: string
price: number
image?: string
inStock: boolean
attributes: Record<string, string>
}
export interface Review {
id: string
userId: string
userName: string
rating: number
comment: string
createdAt: Date
helpful: number
}
export interface CartItem {
productId: string
variantId?: string
quantity: number
product: Product
variant?: ProductVariant
}
export interface ShippingInfo {
fullName: string
email: string
phone: string
address: string
city: string
state: string
zipCode: string
country: string
}
export interface Order {
id: string
items: CartItem[]
shipping: ShippingInfo
subtotal: number
tax: number
shipping: number
total: number
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
createdAt: Date
}
步驟 6:實作狀態管理
使用 Zustand 建立全域狀態管理,建立 src/lib/store.ts
:
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { CartItem, Product, ProductVariant } from '@/types'
interface CartStore {
items: CartItem[]
isOpen: boolean
// 購物車操作
addItem: (product: Product, variant?: ProductVariant, quantity?: number) => void
removeItem: (productId: string, variantId?: string) => void
updateQuantity: (productId: string, quantity: number, variantId?: string) => void
clearCart: () => void
// UI 操作
openCart: () => void
closeCart: () => void
// 計算屬性
getTotalItems: () => number
getSubtotal: () => number
getTax: (rate?: number) => number
getTotal: (taxRate?: number) => number
}
export const useCart = create<CartStore>()(
persist(
(set, get) => ({
items: [],
isOpen: false,
addItem: (product, variant, quantity = 1) => {
set((state) => {
const existingItem = state.items.find(
item => item.productId === product.id &&
item.variantId === variant?.id
)
if (existingItem) {
return {
items: state.items.map(item =>
item.productId === product.id && item.variantId === variant?.id
? { ...item, quantity: item.quantity + quantity }
: item
)
}
}
return {
items: [...state.items, {
productId: product.id,
variantId: variant?.id,
quantity,
product,
variant
}]
}
})
},
removeItem: (productId, variantId) => {
set((state) => ({
items: state.items.filter(
item => !(item.productId === productId && item.variantId === variantId)
)
}))
},
updateQuantity: (productId, quantity, variantId) => {
if (quantity <= 0) {
get().removeItem(productId, variantId)
return
}
set((state) => ({
items: state.items.map(item =>
item.productId === productId && item.variantId === variantId
? { ...item, quantity }
: item
)
}))
},
clearCart: () => set({ items: [] }),
openCart: () => set({ isOpen: true }),
closeCart: () => set({ isOpen: false }),
getTotalItems: () => {
return get().items.reduce((total, item) => total + item.quantity, 0)
},
getSubtotal: () => {
return get().items.reduce((total, item) => {
const price = item.variant?.price ?? item.product.salePrice ?? item.product.price
return total + (price * item.quantity)
}, 0)
},
getTax: (rate = 0.08) => {
return get().getSubtotal() * rate
},
getTotal: (taxRate = 0.08) => {
const subtotal = get().getSubtotal()
return subtotal + get().getTax(taxRate)
}
}),
{
name: 'shopping-cart',
storage: createJSONStorage(() => localStorage),
}
)
)
步驟 7:建立核心元件
產品卡片元件
建立 src/components/store/ProductCard.tsx
:
'use client'
import Image from 'next/image'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { ShoppingCart, Heart, Eye } from 'lucide-react'
import { Card, CardContent, CardFooter } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { useCart } from '@/lib/store'
import { Product } from '@/types'
import { cn } from '@/lib/utils'
interface ProductCardProps {
product: Product
onQuickView?: (product: Product) => void
}
export function ProductCard({ product, onQuickView }: ProductCardProps) {
const [isHovered, setIsHovered] = useState(false)
const [isFavorite, setIsFavorite] = useState(false)
const { addItem, openCart } = useCart()
const discount = product.salePrice
? Math.round(((product.price - product.salePrice) / product.price) * 100)
: 0
const handleAddToCart = () => {
addItem(product)
openCart()
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ y: -5 }}
>
<Card
className="relative overflow-hidden group cursor-pointer"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* 折扣標籤 */}
{discount > 0 && (
<Badge className="absolute top-2 left-2 z-10" variant="destructive">
-{discount}%
</Badge>
)}
{/* 收藏按鈕 */}
<Button
size="icon"
variant="ghost"
className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => setIsFavorite(!isFavorite)}
>
<Heart className={cn("h-4 w-4", isFavorite && "fill-current text-red-500")} />
</Button>
{/* 產品圖片 */}
<div className="relative aspect-square overflow-hidden bg-gray-100">
<Image
src={product.images[0]}
alt={product.name}
fill
className="object-cover transition-transform duration-300 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
{/* 快速預覽按鈕 */}
{isHovered && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="absolute inset-0 bg-black/40 flex items-center justify-center"
>
<Button
variant="secondary"
onClick={() => onQuickView?.(product)}
className="gap-2"
>
<Eye className="h-4 w-4" />
快速預覽
</Button>
</motion.div>
)}
</div>
<CardContent className="p-4">
<h3 className="font-medium text-lg mb-1 line-clamp-1">{product.name}</h3>
<p className="text-sm text-muted-foreground mb-2 line-clamp-2">
{product.shortDescription}
</p>
<div className="flex items-center gap-2 mb-2">
{product.salePrice ? (
<>
<span className="font-bold text-lg">${product.salePrice}</span>
<span className="text-sm text-muted-foreground line-through">
${product.price}
</span>
</>
) : (
<span className="font-bold text-lg">${product.price}</span>
)}
</div>
{/* 評分 */}
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<svg
key={i}
className={cn(
"h-4 w-4",
i < Math.floor(product.rating)
? "text-yellow-400 fill-current"
: "text-gray-300"
)}
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
<span className="text-sm text-muted-foreground ml-1">
({product.reviews.length})
</span>
</div>
</CardContent>
<CardFooter className="p-4 pt-0">
<Button
className="w-full"
onClick={handleAddToCart}
disabled={!product.inStock}
>
<ShoppingCart className="mr-2 h-4 w-4" />
{product.inStock ? '加入購物車' : '缺貨中'}
</Button>
</CardFooter>
</Card>
</motion.div>
)
}
購物車側邊欄元件
建立 src/components/store/CartSheet.tsx
:
'use client'
import Image from 'next/image'
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Minus, Plus, Trash2, ShoppingBag } from 'lucide-react'
import { useCart } from '@/lib/store'
import { motion, AnimatePresence } from 'framer-motion'
export function CartSheet() {
const {
items,
isOpen,
closeCart,
updateQuantity,
removeItem,
getTotalItems,
getSubtotal,
getTax,
getTotal
} = useCart()
const subtotal = getSubtotal()
const tax = getTax()
const total = getTotal()
return (
<Sheet open={isOpen} onOpenChange={closeCart}>
<SheetContent className="w-full sm:max-w-md">
<SheetHeader>
<SheetTitle className="flex items-center gap-2">
<ShoppingBag className="h-5 w-5" />
購物車 ({getTotalItems()})
</SheetTitle>
</SheetHeader>
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center h-[50vh] space-y-4">
<ShoppingBag className="h-16 w-16 text-muted-foreground" />
<p className="text-lg text-muted-foreground">您的購物車是空的</p>
<Button onClick={closeCart}>繼續購物</Button>
</div>
) : (
<>
<ScrollArea className="flex-1 mt-4 pr-4" style={{ height: 'calc(100vh - 280px)' }}>
<AnimatePresence mode="popLayout">
{items.map((item) => {
const price = item.variant?.price ?? item.product.salePrice ?? item.product.price
return (
<motion.div
key={\`\${item.productId}-\${item.variantId}\`}
layout
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="flex gap-4 py-4 border-b"
>
<div className="relative h-24 w-24 rounded-md overflow-hidden bg-gray-100">
<Image
src={item.product.images[0]}
alt={item.product.name}
fill
className="object-cover"
/>
</div>
<div className="flex-1 space-y-2">
<h4 className="font-medium line-clamp-1">{item.product.name}</h4>
{item.variant && (
<p className="text-sm text-muted-foreground">
{item.variant.name}
</p>
)}
<div className="flex items-center justify-between">
<span className="font-medium">${price}</span>
<div className="flex items-center gap-2">
<Button
size="icon"
variant="outline"
className="h-8 w-8"
onClick={() => updateQuantity(
item.productId,
item.quantity - 1,
item.variantId
)}
>
<Minus className="h-3 w-3" />
</Button>
<span className="w-8 text-center">{item.quantity}</span>
<Button
size="icon"
variant="outline"
className="h-8 w-8"
onClick={() => updateQuantity(
item.productId,
item.quantity + 1,
item.variantId
)}
>
<Plus className="h-3 w-3" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-destructive"
onClick={() => removeItem(item.productId, item.variantId)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</motion.div>
)
})}
</AnimatePresence>
</ScrollArea>
<div className="space-y-4 mt-6">
<Separator />
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>小計</span>
<span>${subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm">
<span>稅金</span>
<span>${tax.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm">
<span>運費</span>
<Badge variant="secondary">免運費</Badge>
</div>
<Separator />
<div className="flex justify-between font-semibold text-lg">
<span>總計</span>
<span>${total.toFixed(2)}</span>
</div>
</div>
<Button className="w-full" size="lg">
前往結帳
</Button>
<Button variant="outline" className="w-full" onClick={closeCart}>
繼續購物
</Button>
</div>
</>
)}
</SheetContent>
</Sheet>
)
}
步驟 8:建立真正的 One Page Store 內容結構
不同於傳統的產品描述,真正的 One Page Store 需要創造一個完整的產品宇宙。我們建立一個沉浸式的耳機體驗頁面:
---
id: "wavesound-pro"
name: "WaveSound Pro"
tagline: "重新定義你的聆聽體驗"
subtitle: "音質革命,從這裡開始"
price: 299.99
salePrice: 249.99
limitedOffer: "限時特價 - 48 小時內有效"
inStock: true
heroVideo: "/videos/wavesound-hero.mp4"
heroImage: "/images/wavesound-hero.jpg"
featureImages:
- "/images/anc-demo.jpg"
- "/images/battery-life.jpg"
- "/images/comfort-design.jpg"
- "/images/smart-features.jpg"
interactiveDemo: true
theme:
primary: "from-purple-600 to-blue-600"
accent: "#8B5CF6"
background: "#0F0F23"
---
import { InteractiveAudioDemo } from '@/components/store/InteractiveAudioDemo'
import { FeatureShowcase } from '@/components/store/FeatureShowcase'
import { CustomerReviews } from '@/components/store/CustomerReviews'
import { PurchaseSection } from '@/components/store/PurchaseSection'
# 🎧 沉浸式音質革命
想像一下,當世界安靜下來,只剩下純粹的音樂在你耳邊流淌...
**WaveSound Pro** 不只是一副耳機,它是你進入**另一個世界**的門戶。搭載革命性的主動降噪技術,讓你在喧囂的城市中找到屬於自己的寧靜空間。
## 🔇 體驗真正的寧靜
<InteractiveAudioDemo
demoTracks={[
{ name: '一般模式', file: '/audio/normal-mode.mp3' },
{ name: '降噪開啟', file: '/audio/anc-mode.mp3' }
]}
visualizer={true}
/>
*點擊上方按鈕,親自體驗降噪技術的神奇效果*
## ✨ 四大核心突破
<FeatureShowcase features={[
{
emoji: "🔇",
title: "AI 智慧降噪",
description: "95% 環境噪音消除,讓世界安靜下來",
interactive: "hover:scale-105 transform transition-all duration-300",
demo: "toggleNoise"
},
{
emoji: "🔋",
title: "超長續航",
description: "30 小時不間斷音樂,快充 15 分鐘續航 5 小時",
interactive: "animate-pulse",
counter: { from: 0, to: 30, suffix: "小時" }
},
{
emoji: "🎵",
title: "Hi-Res 音質",
description: "錄音室級別音質,每個細節都清晰可聞",
waveform: true
},
{
emoji: "⚡",
title: "瞬間連接",
description: "藍牙 5.0 技術,0.1 秒極速配對",
animation: "connectDevices"
}
]} />
## 🌟 真實用戶體驗
<CustomerReviews reviews={[
{
avatar: "/images/user-1.jpg",
name: "李小明",
rating: 5,
comment: "簡直是我用過最棒的耳機!降噪效果讓我在飛機上也能安靜休息。",
verified: true,
date: "2025-06-01"
},
{
avatar: "/images/user-2.jpg",
name: "陳美麗",
rating: 5,
comment: "音質超乎想像,聽古典樂的層次感非常豐富,而且戴起來很舒適。",
verified: true,
date: "2025-05-28"
},
{
avatar: "/images/user-3.jpg",
name: "王大衛",
rating: 5,
comment: "工作時戴著它,完全沒有外界干擾,效率提升了很多!",
verified: true,
date: "2025-05-25"
}
]} animated={true} />
## 🛒 立即擁有你的音質革命
<PurchaseSection
product={{
name: "WaveSound Pro",
originalPrice: 299.99,
salePrice: 249.99,
savings: 50,
urgency: "僅剩 23 個 - 48 小時限時特價",
guarantees: [
"30 天無條件退款",
"2 年全球保固",
"免費全球配送"
],
bonuses: [
"價值 $49 的專業收納盒",
"價值 $29 的飛機轉接頭"
]
}}
urgencyTimer={true}
oneClick={true}
/>
互動式元件實作
讓我們建立幾個關鍵的互動元件來實現真正的 One Page Store 體驗:
1. 音質對比體驗元件
// src/components/store/InteractiveAudioDemo.tsx
'use client'
import { useState, useRef } from 'react'
import { motion } from 'framer-motion'
import { Play, Pause, Volume2, VolumeX } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
interface AudioDemoProps {
demoTracks: { name: string; file: string }[]
visualizer?: boolean
}
export function InteractiveAudioDemo({ demoTracks, visualizer }: AudioDemoProps) {
const [activeTrack, setActiveTrack] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
const [audioLevel, setAudioLevel] = useState(0)
const audioRef = useRef<HTMLAudioElement>(null)
const togglePlay = async () => {
if (!audioRef.current) return
if (isPlaying) {
audioRef.current.pause()
setIsPlaying(false)
} else {
await audioRef.current.play()
setIsPlaying(true)
}
}
const switchTrack = (index: number) => {
setActiveTrack(index)
setIsPlaying(false)
if (audioRef.current) {
audioRef.current.src = demoTracks[index].file
}
}
return (
<Card className="p-8 bg-gradient-to-br from-gray-900 to-black text-white">
<div className="text-center mb-6">
<h3 className="text-2xl font-bold mb-2">🎧 親自體驗音質差異</h3>
<p className="text-gray-300">戴上耳機,感受降噪技術的魔力</p>
</div>
{/* 音軌選擇 */}
<div className="flex gap-4 justify-center mb-8">
{demoTracks.map((track, index) => (
<Button
key={index}
variant={activeTrack === index ? "default" : "outline"}
onClick={() => switchTrack(index)}
className={`px-6 py-3 transition-all duration-300 ${
activeTrack === index
? 'bg-purple-600 hover:bg-purple-700 transform scale-105'
: 'border-purple-400 text-purple-400 hover:bg-purple-600/20'
}`}
>
{activeTrack === index && isPlaying ? (
<VolumeX className="mr-2 h-4 w-4 animate-pulse" />
) : (
<Volume2 className="mr-2 h-4 w-4" />
)}
{track.name}
</Button>
))}
</div>
{/* 播放控制 */}
<div className="flex justify-center mb-6">
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Button
size="lg"
onClick={togglePlay}
className="w-16 h-16 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
>
{isPlaying ? (
<Pause className="h-6 w-6" />
) : (
<Play className="h-6 w-6 ml-1" />
)}
</Button>
</motion.div>
</div>
{/* 視覺化效果 */}
{visualizer && (
<div className="flex justify-center items-end gap-1 h-16 mb-4">
{[...Array(20)].map((_, i) => (
<motion.div
key={i}
className="bg-gradient-to-t from-purple-600 to-blue-400 w-2 rounded-full"
style={{
height: isPlaying ? `${Math.random() * 60 + 10}px` : '10px'
}}
animate={{
height: isPlaying
? [`${Math.random() * 60 + 10}px`, `${Math.random() * 60 + 10}px`]
: '10px'
}}
transition={{
duration: 0.5,
repeat: isPlaying ? Infinity : 0,
ease: "easeInOut"
}}
/>
))}
</div>
)}
<audio
ref={audioRef}
src={demoTracks[activeTrack]?.file}
onEnded={() => setIsPlaying(false)}
onTimeUpdate={(e) => {
const audio = e.target as HTMLAudioElement
setAudioLevel(audio.currentTime / audio.duration * 100)
}}
/>
</Card>
)
}
步驟 9:實作產品載入系統
建立 src/lib/products.ts
來處理 MDX 產品檔案的載入:
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { Product } from '@/types'
const productsDirectory = path.join(process.cwd(), 'src/content/products')
export async function getProductSlugs() {
const files = fs.readdirSync(productsDirectory)
return files.map(file => file.replace(/\.mdx$/, ''))
}
export async function getProductBySlug(slug: string): Promise<Product> {
const fullPath = path.join(productsDirectory, \`\${slug}.mdx\`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
// 計算評分(示例:隨機生成或從評論計算)
const rating = data.reviews
? data.reviews.reduce((acc: number, review: any) => acc + review.rating, 0) / data.reviews.length
: 4.5
return {
...data,
content,
rating,
reviews: data.reviews || [],
createdAt: new Date(data.createdAt || Date.now()),
updatedAt: new Date(data.updatedAt || Date.now()),
} as Product
}
export async function getAllProducts(): Promise<Product[]> {
const slugs = await getProductSlugs()
const products = await Promise.all(
slugs.map(slug => getProductBySlug(slug))
)
return products.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
}
export async function getProductsByCategory(category: string): Promise<Product[]> {
const products = await getAllProducts()
return products.filter(product => product.category === category)
}
export async function getFeaturedProducts(): Promise<Product[]> {
const products = await getAllProducts()
// 選擇有折扣或評分高的產品作為精選
return products
.filter(product => product.salePrice || product.rating >= 4.5)
.slice(0, 6)
}
export async function searchProducts(query: string): Promise<Product[]> {
const products = await getAllProducts()
const lowercaseQuery = query.toLowerCase()
return products.filter(product =>
product.name.toLowerCase().includes(lowercaseQuery) ||
product.description.toLowerCase().includes(lowercaseQuery) ||
product.tags.some(tag => tag.toLowerCase().includes(lowercaseQuery))
)
}
2. 震撼的 Hero 區塊元件
// src/components/store/HeroSection.tsx
'use client'
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import { Play, ShoppingCart, Clock, Star } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
export function HeroSection() {
const [timeLeft, setTimeLeft] = useState({
hours: 47,
minutes: 32,
seconds: 15
})
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev.seconds > 0) {
return { ...prev, seconds: prev.seconds - 1 }
} else if (prev.minutes > 0) {
return { ...prev, minutes: prev.minutes - 1, seconds: 59 }
} else if (prev.hours > 0) {
return { hours: prev.hours - 1, minutes: 59, seconds: 59 }
}
return prev
})
}, 1000)
return () => clearInterval(timer)
}, [])
return (
<section className="relative min-h-screen bg-gradient-to-br from-gray-900 via-purple-900 to-black overflow-hidden">
{/* 背景動態效果 */}
<div className="absolute inset-0">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl animate-pulse" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-blue-500/20 rounded-full blur-3xl animate-pulse delay-1000" />
</div>
<div className="relative z-10 container mx-auto px-4 py-20 text-center text-white">
{/* 限時優惠標籤 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6"
>
<Badge className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 text-sm font-bold">
⚡ 限時特價 - 立省 $50!
</Badge>
</motion.div>
{/* 主標題 */}
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-6xl md:text-8xl font-black mb-6"
>
<span className="bg-gradient-to-r from-purple-400 via-pink-500 to-blue-500 bg-clip-text text-transparent">
WaveSound
</span>
<br />
<span className="text-white">Pro</span>
</motion.h1>
{/* 副標題 */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="text-xl md:text-2xl mb-4 font-light"
>
重新定義你的聆聽體驗
</motion.p>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="text-lg text-gray-300 mb-8 max-w-2xl mx-auto"
>
音質革命,從這裡開始。95% 降噪效果,30 小時續航,讓你沉浸在純粹的音樂世界中。
</motion.p>
{/* 倒數計時器 */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.8 }}
className="flex justify-center gap-4 mb-8"
>
<div className="bg-black/50 backdrop-blur-md rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-red-400">{timeLeft.hours}</div>
<div className="text-xs text-gray-400">小時</div>
</div>
<div className="bg-black/50 backdrop-blur-md rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-red-400">{timeLeft.minutes}</div>
<div className="text-xs text-gray-400">分鐘</div>
</div>
<div className="bg-black/50 backdrop-blur-md rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-red-400">{timeLeft.seconds}</div>
<div className="text-xs text-gray-400">秒</div>
</div>
</motion.div>
{/* 價格展示 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.0 }}
className="mb-8"
>
<div className="flex items-center justify-center gap-4 mb-4">
<span className="text-4xl font-bold text-green-400">$249.99</span>
<span className="text-2xl text-gray-500 line-through">$299.99</span>
<Badge className="bg-green-600 text-white">省 $50</Badge>
</div>
<div className="flex items-center justify-center gap-2 text-yellow-400">
{[...Array(5)].map((_, i) => (
<Star key={i} className="h-5 w-5 fill-current" />
))}
<span className="text-white ml-2">4.9/5 (2,847 評價)</span>
</div>
</motion.div>
{/* 行動按鈕 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.2 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center"
>
<Button
size="lg"
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white px-8 py-4 text-lg font-bold transform hover:scale-105 transition-all duration-300 shadow-lg"
>
<ShoppingCart className="mr-2 h-5 w-5" />
立即購買 - 免費送貨
</Button>
<Button
size="lg"
variant="outline"
className="border-white text-white hover:bg-white hover:text-black px-8 py-4 text-lg font-semibold"
>
<Play className="mr-2 h-5 w-5" />
觀看展示影片
</Button>
</motion.div>
{/* 信任標識 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.4 }}
className="flex flex-wrap justify-center gap-6 mt-12 text-sm text-gray-400"
>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span>30 天退貨保證</span>
</div>
<div className="flex items-center gap-2">
<ShoppingCart className="h-4 w-4" />
<span>全球免費配送</span>
</div>
<div className="flex items-center gap-2">
<Star className="h-4 w-4" />
<span>2 年保固</span>
</div>
</motion.div>
</div>
{/* 產品圖片 */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.5, duration: 1 }}
className="absolute bottom-0 right-0 w-1/2 h-1/2 bg-gradient-to-tl from-purple-500/20 to-transparent"
>
<img
src="/images/wavesound-hero.png"
alt="WaveSound Pro"
className="w-full h-full object-contain transform rotate-12 hover:rotate-6 transition-transform duration-700"
/>
</motion.div>
</section>
)
}
3. 購買轉換區塊元件
// src/components/store/PurchaseSection.tsx
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { ShoppingCart, Shield, Truck, RotateCcw, Gift, Clock } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { useCart } from '@/lib/store'
interface PurchaseSectionProps {
product: {
name: string
originalPrice: number
salePrice: number
savings: number
urgency: string
guarantees: string[]
bonuses: string[]
}
urgencyTimer?: boolean
oneClick?: boolean
}
export function PurchaseSection({ product, urgencyTimer, oneClick }: PurchaseSectionProps) {
const [isAdding, setIsAdding] = useState(false)
const { addItem, openCart } = useCart()
const handlePurchase = async () => {
setIsAdding(true)
// 模擬加入購物車的過程
await new Promise(resolve => setTimeout(resolve, 1000))
// 這裡應該加入實際的產品物件
// addItem(productData)
if (oneClick) {
// 直接跳轉到結帳頁面
window.location.href = '/checkout'
} else {
openCart()
}
setIsAdding(false)
}
return (
<section className="py-20 bg-gradient-to-br from-purple-900 via-black to-gray-900">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="max-w-4xl mx-auto"
>
<Card className="p-8 md:p-12 bg-black/50 backdrop-blur-xl border-purple-500/30 text-white">
{/* 緊急性提醒 */}
<div className="text-center mb-8">
<Badge className="bg-red-600 text-white px-4 py-2 mb-4 animate-pulse">
🔥 {product.urgency}
</Badge>
<h2 className="text-4xl md:text-5xl font-bold mb-4">
立即擁有你的
<span className="bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">
音質革命
</span>
</h2>
</div>
<div className="grid md:grid-cols-2 gap-8 items-center">
{/* 左側:價格和保證 */}
<div className="space-y-6">
{/* 價格展示 */}
<div className="text-center md:text-left">
<div className="flex items-baseline gap-4 mb-2">
<span className="text-5xl font-bold text-green-400">
${product.salePrice}
</span>
<span className="text-2xl text-gray-500 line-through">
${product.originalPrice}
</span>
</div>
<p className="text-xl text-green-400 font-semibold">
立省 ${product.savings}!
</p>
</div>
{/* 保證項目 */}
<div className="space-y-3">
<h3 className="text-xl font-semibold mb-3 flex items-center gap-2">
<Shield className="h-5 w-5 text-green-400" />
無風險保證
</h3>
{product.guarantees.map((guarantee, index) => (
<div key={index} className="flex items-center gap-3">
<div className="w-2 h-2 bg-green-400 rounded-full" />
<span>{guarantee}</span>
</div>
))}
</div>
{/* 額外贈品 */}
<div className="space-y-3">
<h3 className="text-xl font-semibold mb-3 flex items-center gap-2">
<Gift className="h-5 w-5 text-purple-400" />
限時免費贈品
</h3>
{product.bonuses.map((bonus, index) => (
<div key={index} className="flex items-center gap-3">
<div className="w-2 h-2 bg-purple-400 rounded-full" />
<span>{bonus}</span>
</div>
))}
</div>
</div>
{/* 右側:購買按鈕和特色 */}
<div className="space-y-6">
{/* 主要購買按鈕 */}
<motion.div
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Button
size="lg"
onClick={handlePurchase}
disabled={isAdding}
className="w-full py-6 text-xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 transform transition-all duration-300 shadow-lg hover:shadow-xl"
>
{isAdding ? (
<div className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
處理中...
</div>
) : (
<div className="flex items-center gap-2">
<ShoppingCart className="h-6 w-6" />
{oneClick ? '一鍵購買' : '加入購物車'}
</div>
)}
</Button>
</motion.div>
{/* 信任標識 */}
<div className="grid grid-cols-3 gap-4 text-center">
<div className="flex flex-col items-center gap-2">
<Truck className="h-8 w-8 text-green-400" />
<span className="text-sm">免費送貨</span>
</div>
<div className="flex flex-col items-center gap-2">
<RotateCcw className="h-8 w-8 text-blue-400" />
<span className="text-sm">30天退貨</span>
</div>
<div className="flex flex-col items-center gap-2">
<Shield className="h-8 w-8 text-purple-400" />
<span className="text-sm">2年保固</span>
</div>
</div>
{/* 社會證明 */}
<div className="bg-gray-800/50 rounded-lg p-4 text-center">
<p className="text-sm text-gray-300 mb-2">
已有 <span className="text-green-400 font-bold">12,847</span> 人購買
</p>
<div className="flex justify-center gap-1">
{[...Array(5)].map((_, i) => (
<span key={i} className="text-yellow-400">⭐</span>
))}
<span className="ml-2 text-sm">4.9/5 平均評分</span>
</div>
</div>
{urgencyTimer && (
<div className="bg-red-900/30 border border-red-500/50 rounded-lg p-4 text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<Clock className="h-5 w-5 text-red-400" />
<span className="text-red-400 font-bold">限時優惠即將結束</span>
</div>
<p className="text-sm text-gray-300">
錯過這次優惠,下次恢復原價 ${product.originalPrice}
</p>
</div>
)}
</div>
</div>
</Card>
</motion.div>
</div>
</section>
)
}
步驟 10:建立完整的 One Page Store 頁面
整合所有元件,建立真正的 One Page Store src/app/page.tsx
:
import { Suspense } from 'react'
import { HeroSection } from '@/components/store/HeroSection'
import { InteractiveAudioDemo } from '@/components/store/InteractiveAudioDemo'
import { FeatureShowcase } from '@/components/store/FeatureShowcase'
import { CustomerReviews } from '@/components/store/CustomerReviews'
import { PurchaseSection } from '@/components/store/PurchaseSection'
import { FAQSection } from '@/components/store/FAQSection'
import { TrustBadges } from '@/components/store/TrustBadges'
import { SocialProof } from '@/components/store/SocialProof'
// 產品資料
const productData = {
name: "WaveSound Pro",
originalPrice: 299.99,
salePrice: 249.99,
savings: 50,
urgency: "僅剩 23 個 - 48 小時限時特價",
guarantees: [
"30 天無條件退款",
"2 年全球保固",
"免費全球配送"
],
bonuses: [
"價值 $49 的專業收納盒",
"價值 $29 的飛機轉接頭"
]
}
const audioTracks = [
{ name: '一般模式', file: '/audio/normal-mode.mp3' },
{ name: '降噪開啟', file: '/audio/anc-mode.mp3' }
]
const features = [
{
emoji: "🔇",
title: "AI 智慧降噪",
description: "95% 環境噪音消除,讓世界安靜下來",
interactive: "hover:scale-105 transform transition-all duration-300",
demo: "toggleNoise"
},
{
emoji: "🔋",
title: "超長續航",
description: "30 小時不間斷音樂,快充 15 分鐘續航 5 小時",
interactive: "animate-pulse",
counter: { from: 0, to: 30, suffix: "小時" }
},
{
emoji: "🎵",
title: "Hi-Res 音質",
description: "錄音室級別音質,每個細節都清晰可聞",
waveform: true
},
{
emoji: "⚡",
title: "瞬間連接",
description: "藍牙 5.0 技術,0.1 秒極速配對",
animation: "connectDevices"
}
]
const reviews = [
{
avatar: "/images/user-1.jpg",
name: "李小明",
rating: 5,
comment: "簡直是我用過最棒的耳機!降噪效果讓我在飛機上也能安靜休息。",
verified: true,
date: "2025-06-01"
},
{
avatar: "/images/user-2.jpg",
name: "陳美麗",
rating: 5,
comment: "音質超乎想像,聽古典樂的層次感非常豐富,而且戴起來很舒適。",
verified: true,
date: "2025-05-28"
},
{
avatar: "/images/user-3.jpg",
name: "王大衛",
rating: 5,
comment: "工作時戴著它,完全沒有外界干擾,效率提升了很多!",
verified: true,
date: "2025-05-25"
}
]
export default function OnePageStore() {
return (
<main className="min-h-screen">
{/* 1. 震撼的 Hero 區塊 */}
<HeroSection />
{/* 2. 互動式音質體驗 */}
<section className="py-20 bg-gray-900">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-white mb-4">
🔇 體驗真正的寧靜
</h2>
<p className="text-xl text-gray-300">
親自感受 95% 降噪效果的魔力
</p>
</div>
<InteractiveAudioDemo
demoTracks={audioTracks}
visualizer={true}
/>
</div>
</section>
{/* 3. 四大核心功能展示 */}
<section className="py-20 bg-black">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-white mb-4">
✨ 四大核心突破
</h2>
<p className="text-xl text-gray-300">
革命性技術,重新定義音質體驗
</p>
</div>
<FeatureShowcase features={features} />
</div>
</section>
{/* 4. 社會證明 - 真實用戶評價 */}
<section className="py-20 bg-gradient-to-br from-purple-900 to-gray-900">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-white mb-4">
🌟 12,847 位用戶的選擇
</h2>
<p className="text-xl text-gray-300">
聽聽他們怎麼說
</p>
</div>
<CustomerReviews reviews={reviews} animated={true} />
</div>
</section>
{/* 5. 社會證明數據 */}
<SocialProof />
{/* 6. FAQ 常見問題 */}
<FAQSection />
{/* 7. 最終購買轉換區 */}
<PurchaseSection
product={productData}
urgencyTimer={true}
oneClick={true}
/>
{/* 8. 信任標識 */}
<TrustBadges />
</main>
)
}
步驟 11:效能優化
1. 圖片優化
使用 Next.js 的 Image 元件和適當的載入策略:
// 建立圖片載入元件
import Image from 'next/image'
import { useState } from 'react'
import { cn } from '@/lib/utils'
interface OptimizedImageProps {
src: string
alt: string
className?: string
priority?: boolean
}
export function OptimizedImage({ src, alt, className, priority }: OptimizedImageProps) {
const [isLoading, setIsLoading] = useState(true)
return (
<div className={cn("relative overflow-hidden bg-gray-100", className)}>
<Image
src={src}
alt={alt}
fill
priority={priority}
className={cn(
"object-cover duration-700 ease-in-out",
isLoading ? "scale-110 blur-2xl grayscale" : "scale-100 blur-0 grayscale-0"
)}
onLoadingComplete={() => setIsLoading(false)}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
</div>
)
}
2. 程式碼分割和延遲載入
// 使用動態導入進行程式碼分割
import dynamic from 'next/dynamic'
// 延遲載入大型元件
const ProductQuickView = dynamic(
() => import('@/components/store/ProductQuickView'),
{
loading: () => <ProductQuickViewSkeleton />,
ssr: false
}
)
const ReviewSection = dynamic(
() => import('@/components/store/ReviewSection'),
{ loading: () => <ReviewSectionSkeleton /> }
)
3. 資料預取和快取
使用 React Query 進行資料管理:
// src/hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getAllProducts, searchProducts } from '@/lib/products'
export function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: getAllProducts,
staleTime: 5 * 60 * 1000, // 5 分鐘
cacheTime: 10 * 60 * 1000, // 10 分鐘
})
}
export function useProductSearch(query: string) {
return useQuery({
queryKey: ['products', 'search', query],
queryFn: () => searchProducts(query),
enabled: query.length > 2,
staleTime: 2 * 60 * 1000,
})
}
// 預取產品資料
export function usePrefetchProducts() {
const queryClient = useQueryClient()
return () => {
queryClient.prefetchQuery({
queryKey: ['products'],
queryFn: getAllProducts,
})
}
}
步驟 12:SEO 優化
建立 SEO 元件和結構化資料:
// src/components/seo/ProductJsonLd.tsx
export function ProductJsonLd({ product }: { product: Product }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.images,
offers: {
'@type': 'Offer',
price: product.salePrice || product.price,
priceCurrency: 'TWD',
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: product.rating,
reviewCount: product.reviews.length,
},
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
步驟 13:部署準備
環境變數設置
建立 .env.local
:
# API 端點
NEXT_PUBLIC_API_URL=https://api.yourstore.com
# 第三方服務
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
# 分析工具
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_FACEBOOK_PIXEL_ID=xxxxxxxxxxxxx
# 圖片 CDN
NEXT_PUBLIC_IMAGE_CDN=https://cdn.yourstore.com
建置優化配置
更新 next.config.mjs
:
const nextConfig = {
// ... 其他配置
// 生產環境優化
swcMinify: true,
compress: true,
// 圖片優化
images: {
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 60,
},
// 實驗性功能
experimental: {
optimizeCss: true,
optimizePackageImports: ['lucide-react', 'framer-motion'],
},
// HTTP 標頭
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
],
},
]
},
}
🚀 One Page Store 進階優化技巧
1. 即時搜尋功能
實作一個快速、響應式的搜尋功能對於提升使用者體驗至關重要:
// src/components/store/SearchCommand.tsx
'use client'
import { useEffect, useState } from 'react'
import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { useDebounce } from '@/hooks/useDebounce'
import { Product } from '@/types'
import { Search } from 'lucide-react'
export function SearchCommand({ products }: { products: Product[] }) {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 300)
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
product.description.toLowerCase().includes(debouncedSearch.toLowerCase())
)
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
placeholder="搜尋產品..."
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty>找不到相關產品</CommandEmpty>
<CommandGroup heading="產品">
{filteredProducts.slice(0, 5).map((product) => (
<CommandItem
key={product.id}
onSelect={() => {
// 導航到產品頁面
window.location.href = \`/products/\${product.slug}\`
setOpen(false)
}}
>
<Search className="mr-2 h-4 w-4" />
<span>{product.name}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
)
}
2. 進階篩選系統
建立多維度的產品篩選功能:
// src/components/store/ProductFilters.tsx
export function ProductFilters({
categories,
priceRange,
onFilterChange
}: ProductFiltersProps) {
const [filters, setFilters] = useState({
category: 'all',
minPrice: 0,
maxPrice: 9999,
inStock: false,
sortBy: 'featured'
})
return (
<aside className="w-64 space-y-6">
{/* 分類篩選 */}
<div>
<h3 className="font-semibold mb-3">商品分類</h3>
<RadioGroup
value={filters.category}
onValueChange={(value) => {
setFilters({ ...filters, category: value })
onFilterChange({ ...filters, category: value })
}}
>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<RadioGroupItem value="all" />
所有商品
</Label>
{categories.map((category) => (
<Label key={category} className="flex items-center gap-2">
<RadioGroupItem value={category} />
{category}
</Label>
))}
</div>
</RadioGroup>
</div>
{/* 價格範圍 */}
<div>
<h3 className="font-semibold mb-3">價格範圍</h3>
<div className="space-y-4">
<Slider
min={0}
max={1000}
step={10}
value={[filters.minPrice, filters.maxPrice]}
onValueChange={([min, max]) => {
setFilters({ ...filters, minPrice: min, maxPrice: max })
onFilterChange({ ...filters, minPrice: min, maxPrice: max })
}}
/>
<div className="flex justify-between text-sm">
<span>${filters.minPrice}</span>
<span>${filters.maxPrice}</span>
</div>
</div>
</div>
{/* 其他篩選選項 */}
<div>
<Label className="flex items-center gap-2">
<Checkbox
checked={filters.inStock}
onCheckedChange={(checked) => {
setFilters({ ...filters, inStock: checked as boolean })
onFilterChange({ ...filters, inStock: checked as boolean })
}}
/>
僅顯示有庫存商品
</Label>
</div>
</aside>
)
}
最佳實踐與技巧
1. 心理學導向的轉換優化
真正的 One Page Store 不只是技術實作,更是心理學的應用:
稀缺性和緊迫感
// 動態庫存顯示元件
export function InventoryCounter({ initialStock, productName }: InventoryCounterProps) {
const [stock, setStock] = useState(initialStock)
const [recentPurchases, setRecentPurchases] = useState(0)
useEffect(() => {
// 模擬實時購買
const interval = setInterval(() => {
if (Math.random() < 0.3) { // 30% 機率有人購買
setStock(prev => Math.max(0, prev - 1))
setRecentPurchases(prev => prev + 1)
}
}, 30000) // 每 30 秒檢查一次
return () => clearInterval(interval)
}, [])
return (
<div className="bg-red-900/20 border border-red-500/50 rounded-lg p-4 text-center">
<p className="text-red-400 font-bold mb-2">
⚠️ 僅剩 {stock} 個!
</p>
<p className="text-sm text-gray-300">
過去 1 小時內已有 {recentPurchases} 人購買 {productName}
</p>
</div>
)
}
社會證明動態顯示
// 實時購買通知
export function LivePurchaseNotifications() {
const [notifications, setNotifications] = useState<PurchaseNotification[]>([])
const mockPurchases = [
{ name: '李小明', location: '台北', time: '剛剛' },
{ name: '陳美麗', location: '高雄', time: '3 分鐘前' },
{ name: '王大衛', location: '台中', time: '5 分鐘前' }
]
useEffect(() => {
const interval = setInterval(() => {
const randomNotification = mockPurchases[Math.floor(Math.random() * mockPurchases.length)]
setNotifications(prev => [randomNotification, ...prev.slice(0, 2)])
}, 45000) // 每 45 秒顯示一次
return () => clearInterval(interval)
}, [])
return (
<div className="fixed bottom-4 left-4 z-50 space-y-2">
<AnimatePresence>
{notifications.map((notification, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
className="bg-green-600 text-white p-3 rounded-lg shadow-lg max-w-sm"
>
<p className="text-sm">
🎉 {notification.name} 在 {notification.location} {notification.time}購買了 WaveSound Pro
</p>
</motion.div>
))}
</AnimatePresence>
</div>
)
}
2. 情感連結設計模式
創造情感共鳴的設計元素:
故事化的產品介紹
// 故事化卷軸元件
export function StoryScrollSection() {
const storySteps = [
{
title: "🌃 凌晨 3 點的靈感",
content: "在這個安靜的深夜,我們的工程師正在細心調整每一個音頻參數...",
image: "/images/story-1.jpg",
emotion: "curiosity"
},
{
title: "🎵 第一次轟動",
content: "當第一首測試音樂播放時,整個實驗室都陷入了青靜...",
image: "/images/story-2.jpg",
emotion: "wonder"
},
{
title: "🎆 你的新世界",
content: "現在,這個技術奇蹟就在你的手中,等待開啟一個全新的音質世界...",
image: "/images/story-3.jpg",
emotion: "excitement"
}
]
return (
<section className="py-20">
{storySteps.map((step, index) => (
<motion.div
key={index}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 1 }}
className="min-h-screen flex items-center justify-center relative overflow-hidden"
>
<div className="container mx-auto px-4 grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<h2 className="text-4xl font-bold text-white">{step.title}</h2>
<p className="text-xl text-gray-300 leading-relaxed">{step.content}</p>
</div>
<div className="relative">
<img
src={step.image}
alt={step.title}
className="rounded-2xl shadow-2xl transform hover:scale-105 transition-transform duration-700"
/>
</div>
</div>
</motion.div>
))}
</section>
)
}
感官互動設計
// 觸覺回饋元件
export function HapticFeedbackButton({ children, intensity = 'medium', ...props }: HapticButtonProps) {
const triggerHaptic = (type: 'light' | 'medium' | 'heavy') => {
if ('vibrate' in navigator) {
const patterns = {
light: [10],
medium: [20],
heavy: [30, 10, 30]
}
navigator.vibrate(patterns[type])
}
}
return (
<Button
{...props}
onMouseDown={() => triggerHaptic(intensity)}
className={`transform active:scale-95 transition-all duration-150 ${props.className}`}
>
{children}
</Button>
)
}
// 聲音回饋系統
export function AudioFeedbackProvider({ children }: { children: React.ReactNode }) {
const playSound = (type: 'click' | 'success' | 'hover') => {
const audio = new Audio(`/sounds/${type}.mp3`)
audio.volume = 0.3
audio.play().catch(() => {}) // 忽略錯誤,有些瀏覽器需要用戶互動
}
return (
<div
onMouseDown={() => playSound('click')}
onMouseEnter={() => playSound('hover')}
>
{children}
</div>
)
}
3. 互動式產品展示
讓用戶「體驗」而非「觀看」產品:
// 360° 產品展示器
export function Product360Viewer({ images }: Product360ViewerProps) {
const [currentAngle, setCurrentAngle] = useState(0)
const [isRotating, setIsRotating] = useState(false)
const handleMouseMove = (e: React.MouseEvent) => {
if (!isRotating) return
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const percentage = x / rect.width
const angle = Math.floor(percentage * 360)
const imageIndex = Math.floor((angle / 360) * images.length)
setCurrentAngle(imageIndex)
}
return (
<div
className="relative w-full h-96 cursor-grab active:cursor-grabbing"
onMouseDown={() => setIsRotating(true)}
onMouseUp={() => setIsRotating(false)}
onMouseLeave={() => setIsRotating(false)}
onMouseMove={handleMouseMove}
>
<img
src={images[currentAngle]}
alt={`產品視角 ${currentAngle}`}
className="w-full h-full object-contain transition-opacity duration-100"
/>
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black/50 text-white px-3 py-1 rounded-full text-sm">
🔄 拖曳查看 360° 視角
</div>
</div>
)
}
🏆 One Page Store 最佳實踐指南
1. 設計心理學原則
應用用戶心理學原則提升轉換率:
認知偏差的運用
- 錯失厭避 (Loss Aversion):強調「節省」而非「優惠」
- 社會證明 (Social Proof):實時購買通知和評價數量
- 稀缺性 (Scarcity):限量和倒數計時
- 錨定效應 (Anchoring):先顯示高價,再顯示優惠價
情感設計模式
// 情感觸發器元件
const emotionalTriggers = {
curiosity: {
headline: "你能在 95% 的噪音中聽到什麼?",
color: "purple",
animation: "pulse"
},
urgency: {
headline: "僅剩 23 個,錯過就要等下次!",
color: "red",
animation: "shake"
},
belonging: {
headline: "加入 12,847 位音樂愛好者的選擇",
color: "blue",
animation: "glow"
}
}
2. 性能與轉換優化
針對 One Page Store 的特殊需求優化:
關鍵指標 (KPIs) 監控
// 轉換漏斗分析
const conversionFunnel = {
landingView: 100, // 著陸頁海查看
featureExplore: 60, // 功能探索
interactionDemo: 40, // 互動測試
reviewsRead: 30, // 閱讀評價
purchaseIntent: 15, // 點擊購買
checkoutComplete: 8 // 完成購買
}
// 熱力圖分析
const heatmapTracking = {
heroSection: 'primary-cta-clicks',
audioDemo: 'demo-interaction-time',
features: 'feature-hover-duration',
reviews: 'review-scroll-depth',
purchase: 'purchase-button-attention'
}
3. A/B 測試策略
持續優化轉換率的測試方法:
關鍵元素測試
// A/B 測試配置
const abTestVariants = {
heroHeadline: {
A: "重新定義你的聆聽體驗",
B: "一次性解決所有噪音問題",
C: "為什麼 12,847 人選擇 WaveSound Pro?"
},
primaryCTA: {
A: "立即購買",
B: "開始你的完美音質之旅",
C: "只要 $249,改變你的世界"
},
socialProof: {
A: "已有 12,847 人購買",
B: "4.9/5 星級評價,2,847 則評論",
C: "現在就有 234 人正在瀏覽這個頁面"
}
}
// 動態分流系統
export function useABTest(testName: string) {
const [variant, setVariant] = useState<string>('A')
useEffect(() => {
// 根據用戶 ID 或隨機分配變體
const userVariant = getUserVariant(testName)
setVariant(userVariant)
// 記錄分流事件
trackEvent('ab_test_assignment', {
testName,
variant: userVariant,
timestamp: Date.now()
})
}, [testName])
return variant
}
4. 無障礙與包容性設計
確保所有用戶都能享受完美體驗:
多媒體內容的替代方案
// 觸覺回饋的替代方案
export function AccessibleAudioDemo({ tracks }: AudioDemoProps) {
const [transcript, setTranscript] = useState('')
const [isHighContrast, setIsHighContrast] = useState(false)
return (
<div className={`audio-demo ${isHighContrast ? 'high-contrast' : ''}`}>
{/* 視覺指示器取代音頻 */}
<div className="visual-indicator" aria-live="polite">
<div className="volume-bars">
{[...Array(10)].map((_, i) => (
<div
key={i}
className={`bar ${isPlaying && i < volume * 10 ? 'active' : ''}`}
style={{ height: `${(i + 1) * 10}%` }}
/>
))}
</div>
</div>
{/* 字幕和譯本 */}
<div className="transcript-section">
<h3>音頻內容描述</h3>
<p>{transcript}</p>
</div>
{/* 高對比度模式 */}
<button
onClick={() => setIsHighContrast(!isHighContrast)}
aria-label="切換高對比度模式"
>
{isHighContrast ? '關閉' : '開啟'}高對比度
</button>
</div>
)
}
// 焦點管理
export function FocusManager({ children }: { children: React.ReactNode }) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Tab 鍵管理
if (e.key === 'Tab') {
const focusableElements = document.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
// 實作循環焦點邏輯
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])
return <div role="main">{children}</div>
}
🚀 部署與性能優化
Vercel 部署:專為 One Page Store 優化
Vercel 是部署 Next.js 應用程式的最佳選擇,特別適合 One Page Store 的需求:
- 全球 CDN 加速:確保全球用戶都能快速訪問
- 自動圖片優化:對於大量產品圖片至關重要
- 即時部署:快速更新優惠和產品資訊
- A/B 測試支持:優化轉換率
性能監控與轉換率優化
對於 One Page Store,關鍵指標不只是技術性能:
- Core Web Vitals:影響 SEO 和使用者體驗
- 互動元素效能:音頻播放、動畫效果的流暢度
- 轉換漏斗分析:從訪問到購買的每個步驟
- 用戶行為熱力圖:了解用戶在頁面上的互動節點
🎆 結論:你已經掌握了真正的 One Page Store 精髓
透過這篇文章,我們不僅學會了如何使用 Next.js、React、MDX、Tailwind CSS、Shadcn/UI 和 TypeScript 建立技術上先進的網站,更重要的是,我們重新定義了什麼才是真正的 One Page Store。
🎨 從技術實作到藝術創造
不同於傳統的電商網站,One Page Store 是一個整合的藝術作品:
- 單一產品聚焦 → 像 Whirly Birdie 專注於字體的美學展示
- 沉浸式體驗 → 像 USB Club Transport 創造的視覺震撼
- 情感連結 → 從理性認知轉化為情感渴望
- 完整故事 → 每個元素都為購買轉換服務
💪 技術堆棧的最佳實踐
我們選擇的這套技術堆棧完美平衡了:
- 開發效率 🚀 - Shadcn/UI 和 Tailwind 讓設計實作飛快
- 效能優異 ⚡ - Next.js 確保每個互動都流暢無礙
- 型別安全 🔒 - TypeScript 避免生產環境錯誤
- 內容管理 📝 - MDX 讓產品故事生動有趣
- 擴展性強 🌱 - 模組化設計支持未來成長
🎯 定制化建議
根據你的產品類型,可以進一步優化:
技術產品:
- 加強互動展示(像我們的耳機音質對比)
- 詳細的技術規格和測試數據
生活用品:
- 著重生活場景展示
- 真實用戶使用影片
服務產品:
- 案例研究和成果展示
- 客戶證言影片
🚀 下一步行動指南
- 立即開始:使用我們提供的代碼模版建立你的第一個 One Page Store
- 分析競品:研究 Whirly Birdie、USB Club Transport 等成功案例
- 測試優化:持續 A/B 測試不同的設計元素
- 收集回饋:定期分析用戶行為和轉換數據
🌟 最終思考
在 2025 年,成功的電商不再只是關於「賣東西」,而是關於「創造體驗」。One Page Store 代表了電商的未來:
- 品牌故事化 → 每個產品都是一個宇宙
- 技術藝術化 → 代碼成為創意表達的工具
- 互動體驗化 → 用戶不再是觀眾,而是參與者
當你掌握了這些技能和理念,你就不只是在建立一個網站,而是在創造一個數位體驗藝術作品。這就是真正的 One Page Store 的魅力所在 —— 它讓每一次訪問都成為一次難忘的旅程。
現在,開始創造你的數位傳奇吧! 🚀✨
Thanks for reading!
Found this article helpful? Share it with others or explore more content.