昨天重整完資料之後,專輯詳情已經可以點版本、看各自的內容物了。
但如果我們點了aespa 的 Armageddon,展開它的詳情,關掉,再點 New Jeans 的 Supernatural,詳情再次展開,網址從頭到尾都是 localhost:3000。


【圖 1、2|點了兩張不同的專輯,網址都沒變】
我在點開某張專輯的情況下,複製網址、開一個無痕視窗貼上——打開的仍是首頁,不是那張專輯。
這時候會發現,如果我現在想把 aespa 《Armageddon》的專輯詳情傳給朋友,我要傳什麼?傳首頁,然後跟他說「你自己找一下」嗎?
這是之前提到的其中一個問題,今天來解決它。
首先,可以先看一個現在手邊就有的例子。鐵人賽選手列表的網址長這樣:
https://ithelp.ithome.com.tw/2026ironman/signup/list?group=vibe-coding
拆開來看,它有三段:
| 這一段 | 今天先叫它 | 在說什麼 |
|---|---|---|
https://ithelp.ithome.com.tw |
網站位置 | 哪一個網站 |
/2026ironman/signup/list |
路徑 | 這個網站裡的哪一頁 |
?group=vibe-coding |
參數 | 這一頁現在怎麼看(只看 Vibe Coding 組) |
今天要處理的是中間那段:路徑。 後面的參數比較像地址上附的一張備註,是明天的內容。
如果把網址想成地址:前面那段像是在說「要去哪一個地方」,後面的路徑才是在說「進去之後,走到哪裡」。而現在的問題是:我只有整個網站的地址,還沒有每張專輯自己的門牌。
現在我能傳給朋友的只有:
http://localhost:3000
等於只跟他說「去我的專案」。
(不過這個網址其實只有我自己的電腦打得開,因為 localhost 指的就是「這台電腦自己」。所以現在還不能真的傳給朋友使用;這裡我們先用同一台電腦開新的無痕視窗來測試。)
我真正想給的是 armageddon 這張專輯的位置,可能的網址會像這樣:
http://localhost:3000/albums/aespa-armageddon
└───── 網站位置 ─────┘└───────── 路徑 ────────┘
不只告訴他去哪個網站,還要告訴他網站裡的哪一個位置。(現在還在自己電腦上,前面那段 localhost:3000之後會換成別人也可以連線的網域,路徑的部分不會變——那是今天要做的東西。)
補充:之後還會遇到
/api/...這種網址,那是給程式讀資料用的。今天講的都是「給人看的頁面」,API 到第三幕會再說。
地址本身只是一串文字,還要有人知道「看到這個地址,要把人帶去哪裡」。就像郵差拿到一封信,會照著地址送到正確的位置。
網站裡負責這套對應規則的,就是路由:
/ → 首頁
/albums → 專輯列表
/albums/aespa-armageddon → Armageddon 的詳情
在 Next.js 裡,這件事不用另外設定。最基本的情況下,可以先理解成:資料夾的結構,就對應網址的結構,而資料夾裡的 page.tsx 決定這個網址能不能被打開。
換句話說,Next.js 已經先替這座城市訂好了編址規則——資料夾怎麼排,地址就跟著怎麼長,你不用再另外拿一張地圖把每條路重新登記一次。
app/
├── page.tsx → /
└── about/
└── page.tsx → /about
你只要照它的規矩把檔案放對位置,網址就出來了。這就是 Day 7 說的「framework 會先幫你把規則定好」其中一個例子,也是當初選 Next.js 的理由之一。
照上面的規則,要做出 25 張專輯的詳情頁,是不是得這樣:
app/albums/
├── aespa-armageddon/
│ └── page.tsx
├── newjeans-get-up/
│ └── page.tsx
├── ive-ive-switch/
│ └── page.tsx
└── ⋯⋯(再 22 個)
25 張還勉強。但如果之後有 2500 張呢?而且每新增一張專輯,都要開一個新資料夾、複製一次頁面。
這時候可以用動態路由:
app/
└── albums/
└── [slug]/
└── page.tsx
資料夾名稱外面那對方括號,意思是 「這一格可以換」 。
所以下面這些網址:
/albums/aespa-armageddon
/albums/newjeans-get-up
/albums/ive-ive-switch
全部都交給同一個 /albums/[slug]/page.tsx 處理。
回到地址。這些網址其實都長得一樣:
專輯區 / 某一張專輯
只有最後那一格會變。
[slug] 就像地址格式裡預留的那一格:有人打開 /albums/aespa-armageddon,這一格就填 aespa-armageddon;打開 /albums/newjeans-get-up,就換成 newjeans-get-up。
動態路由不是替每張專輯開一條新路,而是先定義「這一類地址要長什麼樣子」。
還記得昨天在型別裡加的那個 slug 欄位嗎?
slug: "aespa-armageddon"
它就是要填進上面那一格的東西。整個流程是這樣:
使用者打開 /albums/aespa-armageddon
↓
Next.js 把網址中間那段取出來
slug = "aespa-armageddon"
↓
到資料裡找 album.slug === slug 的那一筆
↓
找到 Armageddon
↓
顯示它的詳情
就像郵差:他不需要認得每一位住戶,只要拿著門牌去名冊裡查就好。
這也是為什麼昨天要規定 slug 不能重複:如果同一個社區裡有兩戶都掛 302,這個門牌就失去作用了。
資料裡其實也有 id,用 /albums/3 一樣找得到東西。但其實/albums/3 對程式來說沒問題,但對人來說沒那麼直觀——傳給朋友,他不會知道那是哪張專輯。/albums/aespa-armageddon 光看網址就多知道了一點。(但如果不需要從網址看出來,也可以用 id)
網址是會被複製、被貼出去、出現在搜尋結果裡的東西。它看起來像什麼,是有差別的(搜尋引擎主要還是看頁面的標題和內容,不是只看網址,完整的處理之後會提到)。
在動手之前,先做一個實驗。現在就在網址列打一個不存在的:
http://localhost:3000/banana

【圖 3|Next.js 內建的 404 畫面】
你會看到全黑、跟你的網站長得完全不一樣的頁面,上面寫著 404。這是 Next.js 內建的。它只是顯示找不到頁面,但也就僅此 —— 使用者不知道發生什麼事,也不知道接下來能去哪。
404 的意思就是「找不到這個東西」。 但「找不到」其實有兩種,而且差別很重要。
剛剛的例子,是路不存在 ——「banana」這條路根本不存在。
等一下 albums/[slug] 做好之後,路就開了。那時候輸入路徑 /albums/banana,郵差找得到專輯區、地址格式也完全合法,但如果我的路徑是一張根本不存在的專輯呢?
技術上的說法是:這條動態路由匹配得到,但對應的資料不存在。而這種在後續會蠻常遇到的 —— slug 打錯一個字、哪天某張專輯被下架,都會走到這裡。
所以待會要順便做一個自己的 404 頁面 ,不是只顯示「找不到頁面」的畫面,而是給使用者一條路:回首頁,或是去看看其他專輯。(這部分主要是提升使用者體驗,並非新增功能)。這剛好呼應之前提到的:畫面正常的時候大家都會做,出事的時候才是差別。
前幾天留過一個伏筆:點一張專輯,到底要跳到新頁面,還是在原地打開 Drawer?現在每張專輯都準備有自己的 /albums/[slug] 了,這個問題就真的要做決定。
先把幾種做法攤開:
| 做法 | Drawer | 完整詳情頁 | 直接打開 /albums/[slug] 會看到 |
|---|---|---|---|
| A 只做詳情頁 | ✗ | ✓ | 完整詳情頁 |
| B 只做 Drawer | ✓ | ✗ | 列表 + Drawer |
| C 兩個都做 | ✓ | ✓ | 完整詳情頁 |
三種其實都做得到,差別是你希望使用者在什麼情況下看到什麼。(同時也可能需要考量搜尋引擎的爬蟲機器人看到什麼,之後 SEO 會細說)
從列表逛專輯的時候,Drawer 很方便。點一張、看完、關掉,還留在剛才的位置,可以繼續往下逛。
但如果是朋友丟給我一條《Armageddon》的連結,我不太希望打開之後先看到整個專輯列表,再從旁邊浮出一塊詳情。因為這時候我的目的很明確:我就是來看《Armageddon》的。
而且之後詳情頁可能還會長出更多東西:不同版本、相關專輯、投稿資料⋯⋯完整頁面的空間也比較充裕。所以我最後想要的是 C:兩個都做。
我要的行為其實很簡單:
1.從列表點進去:
/albums/aespa-armageddon
→ Drawer
2.直接輸入這個網址,或是重新整理:
/albums/aespa-armageddon
→ 完整詳情頁
注意,網址是同一個。不同的只是「你怎麼到這裡」。
Next.js 可以用 Intercepting Routes(攔截路由),通常搭配 Parallel Routes,做出這種效果。名字看起來有點複雜,但這篇先不用背資料夾怎麼排。我真正需要先講清楚的是:
從列表點擊時,我要 Drawer;直接進網址時,我要完整頁面。
剩下的實作方式,可以交給 AI 按照 Next.js 現在的做法處理。
其實也可以做成:
/?album=aespa-armageddon
讓首頁維持不變,只是在網址後面記住「現在打開哪張專輯」。但那就變成把專輯當成「列表目前的一個狀態」。這不是做不到,只是和我現在想要的產品不一樣:我希望每張專輯本身就是一個可以直接打開的地方,所以這次先用 /albums/[slug]。至於問號後面這種參數,明天才會正式處理。
做法並不唯一,取決於每個人的觀點、想法
Day 8 介紹麵包屑時說過:「現在還沒有,等每張專輯有自己的頁面才會出現。」今天出現了。
首頁 > 專輯 > Armageddon
它不是硬加上去的元件,而是從網站的層級長出來的 ,而路由結構剛好提供了很好的基礎。
而且做了攔截路由之後,它變得更需要了。想想看誰會看到完整的詳情頁:大多是點了別人分享的連結、直接落在這一頁的人。 他沒有上一頁可以按,也不知道這個網站還有什麼。麵包屑同時回答了他兩個問題:我在哪裡,以及上一層有什麼。
至於 Drawer 裡面,可以不用放。麵包屑顯示的通常是「我在網站的哪一層」,但 Drawer 打開的時候,使用者根本沒有換頁,也沒有往下走一層。那個問題不存在,而且點下去只能把 Drawer 關掉——跟右上角的叉叉重複。Drawer 需要的是標題和關閉,這樣就夠了。
元件不是不能放,是要看它在那個位置有沒有在回答問題。
另外,每一頁最好也有自己的標題。現在所有頁面的瀏覽器分頁大概都寫著同一行字,但專輯頁應該顯示專輯名稱——分享連結、被搜尋引擎收錄的時候都會用到,完整的處理在處理 SEO 的時候會提到。
我給 Codex 的 Prompt 如下:
目前專輯列表點擊卡片後,會在右側 Drawer 顯示專輯詳情,但網址不會改變。
我希望改成以下行為,請先不要修改程式,先告訴我你會怎麼做、需要新增或修改哪些檔案:
- 現有專輯列表全部顯示在首頁,我想做一個 /albums 的頁面,可以完整列出所有專輯、搜尋、篩選,首頁的調整為近期更新,只顯示其中幾張(可以有一個按鈕「查看所有專輯」進入albums頁面)
- 每張專輯都有 /albums/[slug] 的獨立網址,使用現有資料中的 slug 找到對應專輯
- 從專輯列表點擊卡片時,網址要變成該專輯的 /albums/[slug],但畫面仍以目前的 Drawer 呈現
- 直接輸入、分享或重新整理 /albums/[slug] 時,要顯示完整的專輯詳情頁,而不是 Drawer
- 請使用 Next.js App Router 適合這種情境的 Intercepting Routes;如果需要 Parallel Routes,可以一起使用
- Drawer 和完整詳情頁應共用同一份專輯詳情內容,不要複製兩套相同的 UI
- 找不到對應 slug 時,顯示自訂 404,並提供回到專輯列表的連結
- 完整詳情頁加入麵包屑「首頁 > 專輯 > 專輯名稱」,Drawer 不需要麵包屑
- 保留目前的版本切換與各版本內容物
- 瀏覽器上一頁應該能從 Drawer 回到原本的列表
- 這一輪先不要加入 query parameters,版本參數會在下一步處理
先說明你的實作方式、route 結構,以及預計修改的檔案;我確認後再開始修改。
一樣是我個人的習慣,先看清楚它要動什麼,再讓它動。 路由會牽扯到檔案結構,這時候更值得先問一次。
如果做出來的行為怪怪的,可以請它退一步:先只做詳情頁、確認能動,再單獨加上攔截。分小步做,出事的時候比較容易知道是哪一步壞的。
像這次我其實也有和他來回交流幾輪,他第一次改的版本也有Bug,又多了一兩輪修 Bug。
做完後檢查一下幾件事:
| 測什麼 | 應該要 |
|---|---|
| 從列表點 Armageddon | Drawer 打開,而且網址變成 /albums/aespa-armageddon |
| 這時候按重新整理 | 變成完整的詳情頁,不是 Drawer |
| 按上一頁 | Drawer 關掉,回到列表原本的位置 |
| 複製網址貼到無痕視窗 | 完整的詳情頁 |
| 回列表再點 Get Up | 顯示 Get Up,不是還停在上一張 |
打 /albums/itzy-tunnel-vision |
資料中還沒有的專輯,應該要出現自訂的 404 頁面,不是內建那一頁 |

【圖 4|從列表上操作,網址跟著改變】

【圖 5|重整或是複製網址到新視窗開啟,可進入詳情頁,且此頁包含麵包屑】

【圖 6|切到其他專輯,網址也跟著改變】

【圖 7|網址slug部分輸入資料內沒有的專輯,出現客製化404頁面】
這些測試都在同一台電腦上。如果你在手機上輸入
http://localhost:3000,會發現打不開。真正手機上的測試和網路環境,過幾天會提到。
目前的例子都是以專輯的角度出發的,但其實換個領域,也可以用類似的思考模式去想,例如:
/products/iphone-17 //商品
/articles/what-is-nextjs //文章
/orders/20260923001 //訂單
這幾個頁面有一個共同點:使用者會想直接打開它、傳給別人,或是之後再回到同一個地方。所以替它們保留一個穩定的網址,是有價值的。
不過網址也不一定要對應到「一個東西」。購物車可以是 /cart,結帳流程也可以拆成 /checkout/shipping、/checkout/payment——它們不是什麼具體的物件,但使用者可能會想回到那一步,或是重新整理之後可能不該從頭操做。
所以判斷的方式其實只有一句:這個畫面,使用者會不會想直接回到它(如上一頁)、或是分享給別人看? 會的話,就值得給它一個網址。反過來,像「確定要刪除嗎」這種暫時跳出來的視窗,開著或關著通常就不需要變成一個網址。
這樣做還有一個附帶的好處:網站的結構會變清楚,對搜尋引擎也比較友善——不過那不只是有沒有網址的問題,之後會再說。
今天做的事,說穿了只有一件:讓每一張專輯,變成一個可以指出來的地方。
在今天之前,這個網站以網址觀點來看,只有一個「地方」,所有東西都擠在裡面。詳細的專輯內容,不能分享、不能收藏,搜尋引擎也看不到。
今天解決的是「我要去哪一張專輯」。但同一張專輯裡還有版本。「Superbeing 版的配置」要不要也有自己的網址?還有回到列表:我搜尋了什麼、篩了哪些條件,這些又該記在哪?重新整理之後還會在嗎?
這些東西,也沒有說不能做成一個新頁面,但相對來說比較不適合變成一個新頁面。明天來看網址的另一半:參數。
我們明天見。