這個網站是怎麼做的?
// 完整閱讀此頁面約 8 分鐘這個網站本身就是第一個作品集項目。 它要回答的問題只有一個:一個 Design Engineer,如何把一份 Pencil 設計稿轉化為架構清楚、可維護的 React 前端實作?
以下記錄的不是功能清單,而是每個關鍵取捨背後的理由。 設計稿的數值怎麼被提取成 token、token 又怎麼約束元件、元件如何被分解、RWD 斷層如何劃定、瀏覽器相容問題如何面對、上線如何控制、設計如何被優化 —— 每一步其實都有取捨,而這個網站就是我把那些取捨實際做出來的結果。
Code First,但不是 Design Last。
這個網站的設計工程流程不是所有變更都從設計稿開始。結構性與工程性的變更,例如元件拆分、RWD 斷層、瀏覽器相容性修補,會先發生在 .tsx 檔案裡;等實作穩定後,再回推更新 Pencil 設計稿(.pen)。這樣做是為了避免「設計稿改了,但程式碼沒有跟上」的飄移。取捨是設計稿不一定永遠是最新版本,但程式碼會維持為最終落地的依據。
但視覺質感的打磨,會走另一條路。像色彩要不要再淡一點、間距要不要再鬆一點、陰影是否太重、畫面是不是更有呼吸感,這些判斷需要快速比較、反覆觀看,也需要設計者自己的感受介入。對我來說,這類調整在 Pencil 裡完成,通常比直接從程式碼端修改更有效率;即使有 AI 輔助,微妙的視覺判斷也還是需要回到畫面裡確認。定稿後,再把結果落回 token、元件與樣式檔。
所以這套流程其實是分流的:工程結構以程式碼先行,視覺細節以設計稿打磨,最後再回到同一套實作裡收斂。所有 token 數值都必須能追溯回 Pencil 裡的設計鎖點,或後續明確的視覺調整,而不是在程式碼裡憑空發明數字。這讓「為什麼間距是 28px 而不是 24px」有明確答案:它來自一次設計決策,而不是某次隨手調整。
維護同步需要紀律,不只是工具。每次修改 token,tokens.ts、globals.css @theme、docs/design.md 三個檔案必須同步更新。三者任一落後,設計意圖就開始靜默地走偏。
設計不是一次定稿,是來回磨出來的。
想優化某處時,我習慣先用程式碼把概念做出來 —— 真實的互動與動態,只有跑起來才看得準,靜態稿騙不了手感。如果視覺還要再磨,才把那個局部丟回 Pencil,請它產出幾個方向的變體並列比較,必要時人工介入調整。定案後再回寫程式碼。Pencil 是探索與溝通的畫布,程式碼始終是唯一真實來源。
首頁「關於我」那顆展開按鈕 ↗就是這樣磨出來的:先有可運作的 read_more() 初稿,再用 Pencil 來回試 chip 尺寸、箭頭、hover 態與間距的變體,挑定後回寫 —— 設計與工程不是兩段交棒,而是在同一個開發迭代裡來回校準,把「能動、看起來對、可上線」收成一件事。
命名看用途,不看顏色代碼。
Token 的命名策略不是描述顏色本身,而是描述它在介面中的用途。調色盤不叫「淺藍、深灰」,而是分成四個語意群組:背景層(base / soft / contact)、前景層次(strong → faint 五級)、強調色(含 hover / active 狀態)、邊框。
這讓元件開發時的決定從「我要用哪個藍」變成「這裡需要 accent 還是 border-soft」 —— 選擇空間縮小,決定速度加快。間距 scale 刻意不走標準倍數:第 4 階是 18px,第 5 階是 28px —— 這些數字不是照表套出來的,而是設計反覆調整後留下來的結果。
職責邊界,比急著抽象更重要。
Hero section 是這個網站裡拆分邏輯最密集的地方。Hero.tsx不負責把所有畫面細節都渲染出來,它只做一件事:判斷目前要使用哪套版型,然後把內容交給對應的子元件。容器元件的責任是編排與分派,不是把所有視覺邏輯都收進自己身上。
桌機與手機的版型差異大到無法靠同一套 CSS 優雅處理。我的取捨是維護兩棵獨立的 render tree,讓子元件透過 variant 接收情境,各自處理自己的佈局。這會增加一些重複程式碼,但換來的是明確的責任邊界:子元件不用知道lg 斷點在哪裡,也不用在 children 裡藏一堆斷點判斷。
同樣的原則也用在 navbar 上。NavBarBase只保留 sticky 與毛玻璃外殼,各頁透過 children 注入自己的 inner row —— 共用外殼、各自決定列裡放什麼。這和 variant 是處理差異的一體兩面:variant 讓同一個元件依情境在內部分叉,children 則把會變的部分整個交給呼叫端。首頁與 case study 的 navbar 結構差異太大,硬抽成同一個元件反而會變成錯誤抽象。與其急著共用,不如先把差異看清楚,只抽出穩定、可共用、且不造成視覺差異的最小部分。這樣的 refactor 才是真的降低複雜度,而不是把複雜度藏進元件裡。
1Hero.tsx// 46 行,版型路由 + navbar 統合2 ├─ NavBarBase// sticky frosted 外殼(ADR-009)3 ├─ hidden min-[1440px]:flex// 桌機 topbar4 └─ HeroTopBar// 導覽5 ├─ min-[1440px]:hidden// 手機 topbar6 └─ HeroTopBar variant='mobile'7 ├─ <section overflow-hidden>// min-[1440px]:mt-[-96px]8 ├─ hidden min-[1440px]:block// 桌機絕對佈局 1440×9609 ├─ HeroBigText// 負片跑馬燈,left-1/2 -ml-[50vw] 全出血10 ├─ HeroPhoto// 照片11 └─ HeroCorners// 職銜與地點12 ├─ min-[1440px]:hidden// 手機/平板流動堆疊13 ├─ HeroPhoto variant='mobile'14 ├─ HeroBigText variant='mobile'15 └─ HeroCorners variant='mobile'
斷層在哪裡,就在那裡劃定。
RWD 最大的問題通常不在最小或最大的螢幕,而是在中間那些容易被忽略的寬度。1024–1439px 之間,Hero 的絕對佈局會被 overflow-hidden 裁切;768–913px 之間,BeyondResume 的雙欄 flex 加上 md:px-12,會意外撐出橫向捲軸。這兩個問題都是在 8 個視窗寬度的手動驗證裡發現,而且修法剛好相反:一個要重新劃定 Hero 的斷點,一個要收斂內容寬度與 padding。
SEO 基礎設施維持最小化,只做真的需要的部分:robots、sitemap、canonical URL 與 OG Meta。OG Image 走 edge runtime 動態生成,不預先產出靜態圖片;sitemap 的 lastModified 每次 build 自動更新,不需要手動維護。
上線後的跨環境驗收,又補上了設計稿看不到的問題。iOS Safari favicon 的快取沒有可預期的過期時間,cache key 又吃完整 URL。已經污染在舊裝置上的快取不受我們控制,與其追著清,不如把範圍收斂成可控的部分 —— 確保新裝置正確顯示,並在網址後加上 query(cache key 含 query,等於另開一筆 entry)來驗證。跑馬燈則遇到 WebKit compositing bug,偶發閃爍無法完全消除;最後用 translate3d 降低發生頻率,並把它標記成已知妥協。至於微互動,一律用純 CSS,避免常駐 requestAnimationFrame—— 我曾經因為粒子迴圈讓單一分頁吃到 9GB。從那之後我更保守看待動畫:不是跑得起來就好,還要確認它不會吃掉使用者的資源。
值得記錄的細節。
技術選型本身很單純:Next.js 16 App Router、TypeScript、Tailwind v4 與 Vercel。真正值得記錄的,不是用了哪些工具,而是上線後那些容易被忽略的細節怎麼處理。第一個例子,是 PDF 更新日期。
Vercel 部署時,git checkout 會重置檔案的 mtime,所以如果直接讀取 resume.pdf 的修改時間,production 上永遠會顯示部署當天,而不是履歷真正更新的時間。我的解法是在 next.config.ts 裡,於 build time 執行 git log -1 --format=%cd --date=format:%Y.%m,從 git commit 紀錄抓出 PDF 最後更新的月份,再注入成環境變數。這樣不管網站重新部署幾次,畫面上顯示的都會是那份 PDF 最後被更新的月份,而不是部署時間。
另一個細節是上線開關。正式網域 hire.lukelin.dev/* 前面先掛一層 Cloudflare Worker 作為 coming-soon gate:啟用時回應 503,並加上 noindex,避免搜尋引擎收錄半成品;停用時,流量就直接回到 Vercel,正式上線。這個做法讓上線切換不需要重新 build,也不需要重新部署。staging 的 vercel.app URL 不經 Cloudflare,仍然可以獨立驗收。
看完了?如果你也在找一個能把設計判斷落到前端實作的人,歡迎聯絡。
← 回首頁瀏覽履歷