Kent Shen
← 所有文章

自己寫一個 60 行的前端路由,再用預先渲染解決搜尋引擎看不到內容的問題

  • React
  • SEO
  • 前端

這個網站是一個 React 單頁應用。它只有五個固定頁面加上部落格文章,用不到完整的路由套件,所以路由是我自己寫的,大約 60 行。

一個 History API 路由需要什麼

核心只有三件事:

  1. 知道現在的路徑。 用 useState 存著目前的路徑,初始值取自網址列。
  2. 監聽上一頁/下一頁。 註冊 popstate 事件,使用者按瀏覽器的返回鍵時更新路徑。
  3. 在頁面內導覽。 提供一個 navigate 函式,用 history.pushState 改網址,再更新狀態。

導覽用的 Link 元件很重要的一點,是它渲染的是真正的 <a href>。一般的左鍵點擊會被攔截成頁內導覽,但按住 Ctrl 或 Cmd 點擊、在新分頁開啟、複製連結位址,都跟普通連結一樣運作。很多自製路由會用 <div onClick> 取代連結,這樣會破壞這些使用者習慣,也對無障礙工具不友善。

還有一個細節:導覽後頁面要捲動到正確位置。如果網址帶有 #works 這樣的錨點,要等下一個畫面繪製完成後再捲過去,否則目標元素還沒渲染出來,就找不到。我用 requestAnimationFrame 等一個畫面。

未知路徑怎麼辦

路徑會先經過一個 normalizePath 函式:去掉結尾的斜線,如果它是已知的靜態路由或符合 /blog/某個文章 的格式就保留,否則一律當成首頁。這讓路由不會因為一個打錯的網址而渲染出空白畫面。

不過在文章層級,仍然要處理「網址格式正確但文章不存在」的情況,所以單篇文章頁面自己會顯示找不到的狀態。

單頁應用的老問題:看不到內容

SPA 的麻煩在於,伺服器回傳的永遠是同一份幾乎空白的 index.html,內容是瀏覽器執行 JavaScript 之後才長出來的。這會造成兩個問題:

  • 連結預覽:在社群或通訊軟體貼上某篇文章的連結,預覽卡片顯示的是整個網站的標題與說明,而不是這篇文章的。
  • 搜尋引擎:雖然現代的搜尋引擎能執行 JavaScript,但不保證每次都完整,也比較慢。

解法:建置時預先渲染

我的建置流程分成三步:先建置前端、再用同一份程式碼建置一個給伺服器端渲染用的版本,最後執行一支腳本,把每一個已知頁面(首頁、部落格列表、每一篇文章、關於與隱私頁)用 React 渲染成靜態 HTML 檔,寫進輸出資料夾,並順便產生 sitemap.xml。

這就是為什麼路由元件要接受一個 initialPath 參數:在建置時沒有瀏覽器,沒有網址列,路徑得從外部傳進來。在瀏覽器裡,這個參數不會用到,路徑永遠取自網址列。

預先渲染之後,每個頁面都有自己的 HTML、標題與說明,連結預覽與搜尋引擎抓到的就是完整內容。瀏覽器載入後,React 再接手變回互動式的應用。

為什麼不直接用現成的框架

Next.js 或 Astro 當然能做同樣的事,而且更完整。但對一個只有幾個頁面的個人網站,我更想要看得懂每一行程式碼。路由 60 行、預先渲染一支腳本,出問題時我知道去哪裡找。

何時該換成框架?當你開始需要資料預取、巢狀路由或權限控管時,自己寫就不划算了。在那之前,簡單的做法就足夠。