iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Vibe Coding

《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》系列 第 10 篇

【Day 10|標上座標】動態路由與 404:為什麼我傳出去的網址,只會回到首頁?

  • 分享至 

  • xImage
  •  

昨天重整完資料之後,專輯詳情已經可以點版本、看各自的內容物了。

但如果我們點了aespa 的 Armageddon,展開它的詳情,關掉,再點 New Jeans 的 Supernatural,詳情再次展開,網址從頭到尾都是 localhost:3000。

https://ithelp.ithome.com.tw/upload/images/20260924/20178017S7viezTL0Q.png
https://ithelp.ithome.com.tw/upload/images/20260924/20178017wqbyrYTskB.png
【圖 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 欄位嗎?

slug: "aespa-armageddon"

它就是要填進上面那一格的東西。整個流程是這樣:

使用者打開 /albums/aespa-armageddon
            ↓
Next.js 把網址中間那段取出來
slug = "aespa-armageddon"
            ↓
到資料裡找 album.slug === slug 的那一筆
            ↓
找到 Armageddon
            ↓
顯示它的詳情

就像郵差:他不需要認得每一位住戶,只要拿著門牌去名冊裡查就好。
這也是為什麼昨天要規定 slug 不能重複:如果同一個社區裡有兩戶都掛 302,這個門牌就失去作用了。

為什麼不直接用 id?

資料裡其實也有 id,用 /albums/3 一樣找得到東西。但其實/albums/3 對程式來說沒問題,但對人來說沒那麼直觀——傳給朋友,他不會知道那是哪張專輯。/albums/aespa-armageddon 光看網址就多知道了一點。(但如果不需要從網址看出來,也可以用 id)

網址是會被複製、被貼出去、出現在搜尋結果裡的東西。它看起來像什麼,是有差別的(搜尋引擎主要還是看頁面的標題和內容,不是只看網址,完整的處理之後會提到)。


五、如果網址亂打呢?

在動手之前,先做一個實驗。現在就在網址列打一個不存在的:

http://localhost:3000/banana

https://ithelp.ithome.com.tw/upload/images/20260924/20178017MTUja1zZ2o.png
【圖 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?

從列表逛專輯的時候,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 的時候會提到。


八、請 AI 動手

我給 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 頁面,不是內建那一頁

https://ithelp.ithome.com.tw/upload/images/20260924/20178017MwQ0TBOQSL.png
【圖 4|從列表上操作,網址跟著改變】

https://ithelp.ithome.com.tw/upload/images/20260924/20178017HcOw9KhpZo.png
【圖 5|重整或是複製網址到新視窗開啟,可進入詳情頁,且此頁包含麵包屑】

https://ithelp.ithome.com.tw/upload/images/20260924/20178017vvevMtkdBk.png
【圖 6|切到其他專輯,網址也跟著改變】

https://ithelp.ithome.com.tw/upload/images/20260924/20178017TmBWEMH1Q5.png
【圖 7|網址slug部分輸入資料內沒有的專輯,出現客製化404頁面】

這些測試都在同一台電腦上。如果你在手機上輸入 http://localhost:3000 ,會發現打不開。真正手機上的測試和網路環境,過幾天會提到。


十、換個領域:什麼樣的畫面該有自己的網址?

目前的例子都是以專輯的角度出發的,但其實換個領域,也可以用類似的思考模式去想,例如:

/products/iphone-17  //商品
/articles/what-is-nextjs  //文章
/orders/20260923001   //訂單

這幾個頁面有一個共同點:使用者會想直接打開它、傳給別人,或是之後再回到同一個地方。所以替它們保留一個穩定的網址,是有價值的。

不過網址也不一定要對應到「一個東西」。購物車可以是 /cart,結帳流程也可以拆成 /checkout/shipping、/checkout/payment——它們不是什麼具體的物件,但使用者可能會想回到那一步,或是重新整理之後可能不該從頭操做。

所以判斷的方式其實只有一句:這個畫面,使用者會不會想直接回到它(如上一頁)、或是分享給別人看? 會的話,就值得給它一個網址。反過來,像「確定要刪除嗎」這種暫時跳出來的視窗,開著或關著通常就不需要變成一個網址。

這樣做還有一個附帶的好處:網站的結構會變清楚,對搜尋引擎也比較友善——不過那不只是有沒有網址的問題,之後會再說。


結語與明日預告

今天做的事,說穿了只有一件:讓每一張專輯,變成一個可以指出來的地方。

在今天之前,這個網站以網址觀點來看,只有一個「地方」,所有東西都擠在裡面。詳細的專輯內容,不能分享、不能收藏,搜尋引擎也看不到。

今天解決的是「我要去哪一張專輯」。但同一張專輯裡還有版本。「Superbeing 版的配置」要不要也有自己的網址?還有回到列表:我搜尋了什麼、篩了哪些條件,這些又該記在哪?重新整理之後還會在嗎?

這些東西,也沒有說不能做成一個新頁面,但相對來說比較不適合變成一個新頁面。明天來看網址的另一半:參數。

我們明天見。


上一篇
【Day 09|清點物資】資料模型與 TypeScript 型別:能顯示,不代表資料是對的
下一篇
【Day 11|留下備註】網址參數:重新整理之後,為什麼我的選擇消失了?
系列文
《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言