可以。在共用 layout 的 <head> 放一個 <ClientRouter />,Astro 就會接手站內連結,讓多頁網站用 client-side
navigation 換頁,再用 View
Transitions 補上視覺連續感。這一行同時改變了 navigation 模型:document 不再每次完整重載,頁面 script 的執行時機也跟著改變。
範例把最小接法放進現有內容站,實測範圍包含 navigation lifecycle、原生 script、Vue island、route announcer 與 reduced
motion。版本基準是 Astro 7.1.1,查證與實測日期為 2026-07-24。
Day 17 建好的網站骨架由 BaseLayout.astro
包住 Header、main 與 Footer。原本點擊普通站內連結時,瀏覽器會載入下一份完整 HTML:
點擊 <a>
→ 載入新文件
→ Header / main / Footer 全部重建
→ 頁面 script 執行
→ island hydrate
這是標準多頁網站(MPA)的 navigation,流程簡單、可靠,不需要 router JavaScript。加入 View
Transitions 後,文章仍在 build 時預渲染;瀏覽器收到連結點擊後,則由 Astro 的 client
router 取得下一頁、交換 DOM 與更新 history。
另一種常見寫法是用 JavaScript 自己換頁。一個上線中的多語系品牌官網沒有使用astro:transitions,它的語言切換是這樣實作的:
// 把 pathname 第一段換成新語系,然後整頁重載
url.pathname = parts.join("/");
window.location.href = url.toString();
語言切換也是換頁,例如 /en/blog 換到 /cn/blog。window.location.href 賦值仍會走完整重載流程,觸發者從 <a>
變成 JavaScript。它也繞過了瀏覽器對連結的處理:沒有可以 hover 預覽的目標網址,中鍵開新分頁與右鍵複製連結都失效,爬蟲也看不到那條語系連結。
同一個專案的 layout 裡還有一行手動設定:
history.scrollRestoration = "manual";
history.scrollRestoration = 'manual'
會關閉瀏覽器的捲動位置還原,網站必須自行處理。換頁時的捲動處理也因此被拆到另一處設定。
語言切換先保留普通的 <a href>,讓瀏覽器照常處理,SEO 也能取得那條連結。加入 <ClientRouter />
後,站內連結的換頁才會由 Astro 的 client router 接手,捲動位置與 history 也由它管理,不必另設 scrollRestoration。<a>
是 ClientRouter 接手導覽的前提;若改用 window.location.href,這次換頁不會經過 ClientRouter。
Astro 7 從 astro:transitions 匯入 ClientRouter,放進所有目標頁共用的 <head>:
---
import { ClientRouter } from 'astro:transitions';
---
<html lang="zh-Hant">
<head>
<!-- title、meta、JSON-LD -->
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>
完整 document 已透過 Day 4 的 layout 與 slot集中到 BaseLayout.astro,所以<ClientRouter /> 只要在這個檔案加入一次,不必在每個頁面重複放置。
如果舊教學寫的是 <ViewTransitions />,不要直接照抄。Astro 5 已把它改名為<ClientRouter />,因為這個元件會啟用 client-side routing,功能超過單純呼叫瀏覽器的動畫 API。
共用 <main> 再指定一組清楚、可觀察的轉場:
<main transition:name="page-content" transition:animate="fade">
<slot />
</main>
transition:name 明確把前後頁面的 main 配成同一組;transition:animate="fade"
使用 Astro 內建的淡入淡出。即使兩個 directive 都不寫,Astro 仍會根據元素類型與 DOM 位置自動配對,產生預設轉場。
| Directive | 用途 | 範例設定 |
|---|---|---|
transition:name |
明確配對舊頁與新頁的元素 | main 使用 page-content |
transition:animate |
覆寫該組元素的預設動畫 | main 使用內建 fade |
transition:persist |
把同一個 DOM 或 island 帶到下一頁 | 不使用 |
transition:persist 會保留元件實例與 client
state,適合換頁時不能中斷的音樂播放器。搜尋條件、文章反應按鈕或一般頁面內容未必需要跨頁保留,因此範例不使用transition:persist。Day 8 的 Vue island會隨 DOM
swap 正常卸載,再在新頁 hydrate;轉場與狀態保留分開處理,結果比較容易判讀。
Day 7 的原生 script demo原本在 module 頂層抓 DOM、綁 click listener:
const button = document.getElementById("counter");
button?.addEventListener("click", () => {
// 更新計數
});
完整頁面 navigation 時,每份新 document 都會重新執行自己的 script。啟用 ClientRouter 後,已經執行過的 bundled module
script 不會因為同一個 <script>
再次出現在新頁就自動重跑。離開 demo 再返回時,畫面換成新的按鈕,舊 listener 仍綁在已被移除的按鈕上。
需要對新 DOM 初始化的程式,應改掛在 astro:page-load:
function setupNativeDemo() {
const button = document.getElementById("counter");
const count = document.getElementById("count");
if (!button || button.dataset.initialized === "true") return;
button.dataset.initialized = "true";
let clicks = 0;
button.addEventListener("click", () => {
clicks += 1;
if (count) count.textContent = String(clicks);
});
}
document.addEventListener("astro:page-load", setupNativeDemo);
astro:page-load 會在直接載入與後續每次 client
navigation 完成時觸發。初始化函式每次重新查詢目前 document 的元素,data-initialized 則避免同一個節點被重複綁定。
Astro 也提供 data-astro-rerun,可強迫 inline
script 每次 navigation 後重新執行。但它不該取代 lifecycle 判斷。需要操作新頁 DOM 時,用 astro:page-load
表達時機,通常更容易看懂,也更容易加入 guard 或 teardown。
ClientRouter 的 navigation lifecycle 順序如下:
astro:before-preparation
→ astro:after-preparation
→ astro:before-swap
→ astro:after-swap
→ astro:page-load
用 navigation 的三個階段理解會比較直接:
| 階段 | Event | 適合處理的事 |
|---|---|---|
| 準備下一頁 | before-preparation、after-preparation |
loading 狀態、包裝下一頁 loader |
| 交換 DOM | before-swap、after-swap |
修改新 document、調整 swap、處理 scroll |
| 新頁完成 | page-load |
查詢新 DOM、重新掛互動 |
before-preparation 發生在下一頁 request 送出前;after-preparation 代表下一頁已載入。before-swap
時新 document 已解析,但舊內容還沒換掉。after-swap 觸發時,history 與 scroll position 已更新。最後的 page-load
表示新頁已可見,blocking styles 與 scripts 也已完成。
/demos/view-transitions 使用一段原生 script 監聽五個 lifecycle 事件,將最近 20 筆記錄放進sessionStorage。從 demo 前往首頁,再按瀏覽器返回,畫面留下兩輪完整順序:

前往首頁的實測記錄是:
astro:before-preparation /demos/view-transitions/ → /
astro:after-preparation /demos/view-transitions/
astro:before-swap /demos/view-transitions/ → /
astro:after-swap /
astro:page-load /
瀏覽器返回時也依同一順序再走一次。普通 link navigation 與 history
traversal 都會進入相同 lifecycle,因此初始化程式不能只處理「使用者點了連結」這一種入口。
只看動畫很容易誤判,所以還要檢查 document 與 browser state:
window.day25DocumentMarker = "persists"。<a> 前往首頁。結果如下:
{
"marker": "persists",
"navigationEntries": 1,
"active": "首頁",
"announcement": "用 Astro 打造 Content-first 前端網站"
}
window marker 沒消失,document navigation
entry 仍只有 1 筆,表示沒有完整 reload;URL、title、H1 與 Header 則都換成新頁內容。檢查時也發現 Header 原有的路徑比對問題:preview
URL 是 /blog/,原本只比對 /blog,所以「搜尋文章」沒有 aria-current。把尾斜線正規化後,direct load 與 client
navigation 都能正確標示目前頁面。
production build 會多出一個 16,154 bytes、尚未 gzip 的 ClientRouter JavaScript chunk。這份 chunk 不大,仍是額外的 client
JavaScript;如果網站只需要標準 MPA navigation,就沒有必要為了「看起來比較現代」加入 ClientRouter。
官方文件指出,ClientRouter 內建 prefers-reduced-motion 支援。實測時固定 browser、route、viewport 與 production
build,只切換 reduced-motion。
在 astro:before-swap 取得 event.viewTransition,等待 viewTransition.ready 後讀取 animations:
| 條件 | 執行中的 View Transition animations |
|---|---|
| 一般 motion | 8 |
prefers-reduced-motion: reduce |
0 |
一般模式可看到 root 與 page-content 的 transition pseudo-elements,duration 分別為 250ms 與 180ms。切換 reduced
motion 後沒有任何 View Transition
animation;URL、內容交換與 lifecycle 仍正常完成。在 ClientRouter 中,少動偏好會停用轉場動畫,不會關閉 navigation。
ClientRouter 的保護範圍只包含它所管理的動畫。若另外加入 CSS
animation、canvas 或 JavaScript 動畫,仍要個別處理 reduced-motion。
ClientRouter 的 fallback 有三種值:
| 設定 | 不支援原生 View Transitions API 時 |
|---|---|
animate |
預設;Astro 模擬動畫,繼續 client navigation |
swap |
不動畫,直接交換 DOM,繼續 client navigation |
none |
退回完整頁面 navigation |
這裡未設定 fallback,因此採用官方預設的 animate。實測涵蓋支援 View
Transitions 的 Chrome;表格中的三種 fallback 行為來自 Astro 官方文件,其他瀏覽器路徑未實測。
傳統整頁載入會自然讓輔助科技知道頁面已改變;client-side navigation 必須補回這個訊號。ClientRouter 內建一個aria-live="assertive" route announcer,公告文字依序取:
<title>。<h1>。實測從 Day 24 回到首頁,live region 的文字等於首頁 <title>。<title> 同時服務 SEO 與 client navigation 的可及性。這和
Day 20 的 Endpoint/RSS不同:JSON、RSS、下載或外部目的地不屬於一般 HTML page
navigation,不能一概交給 client router。
開啟前先確認需求真的包含以下至少一項:
如果只想讓內容頁「快一點」,先量測再決定。ClientRouter 會增加 client
JavaScript,也要求逐一檢查既有 scripts;原生 browser navigation 已經夠用的站,保持 MPA 反而更省心。
ClientRouter 的範圍由 layout 決定。這個專案只有使用 BaseLayout
的正式頁加入 ClientRouter,bench 與獨立 MDX 頁不在同一個 shell;在一個 layout 加入 ClientRouter,不會自動覆蓋所有 route。
production build 與瀏覽器回歸結果如下:
/demos/view-transitions 完成預渲染。/blog 的 SearchFilter 與文章 ReactionButton 都能重新 hydrate。ClientRouter 能讓 MPA 擁有 SPA 般的換頁體驗。導入時還要驗證 script 重跑時機、狀態保留、fallback、少動偏好與 route
announcement;這些項目都經過 production build 與瀏覽器回歸驗證。
下一篇進入 Day 26 i18n。當換頁變得連續、語系也進入 URL 時,route、內容與 language
picker 的分工需要明確,避免兩套路徑逐漸分歧。
官方查證:View Transitions guide、View Transitions Router API、Astro 5 upgrade:
ViewTransitions改名為ClientRouter。