先補讀者路徑,再補元件。Astro 專案骨架至少要讓讀者知道網站在做什麼、從哪開始、怎麼找到內容,以及走到不存在的網址時怎麼回來。nav、hero、cards、404、footer 都要服務這條路徑。
這個 capstone 已經有 19 篇文章、搜尋、分頁、文章頁,以及 Actions、Auth、購物車等 demo;首頁卻仍是一條文章清單,header 只有站名,footer 還停在「最小可展示版」,也沒有自訂 404。讀者看得到內容,卻看不出這些頁面如何組成同一個網站。
先把既有 route 接成一條走得完的路:
首頁
├─ 依序閱讀 → /blog/page/1 → 文章 → 回文章列表
├─ 搜尋主題 → /blog → 篩選 → 文章
└─ 無效網址 → /404 → 首頁或搜尋
首頁先說明系列在做什麼,再把讀者導向兩種閱讀方式。Day 16:分頁與轉址建立的 /blog/page/1 適合從頭依序看;Day 14:文章搜尋 island建立的 /blog 適合已經知道關鍵字的人。文章頁的返回連結改指向分頁列表,避免把「回文章列表」連到已經改成入口的首頁。
404 也要接回這條路徑。頁面只寫「找不到」,讀者停在原地;補上首頁與搜尋兩個連結,人才走得回內容。
Day 4:Layout 與 Slot建立的 BaseLayout.astro 負責完整 HTML、SEO 中繼資料與 <slot />。這些責任留在 layout,header 與 footer 這兩塊跨頁共用區域則拆成元件:
---
import SiteFooter from '../components/SiteFooter.astro';
import SiteHeader from '../components/SiteHeader.astro';
interface Props {
title: string;
description?: string;
wide?: boolean;
}
const { wide = false } = Astro.props;
---
<body>
<SiteHeader />
<main class:list={{ 'site-main': true, 'site-main--wide': wide }}>
<slot />
</main>
<SiteFooter />
</body>
SiteHeader 與 SiteFooter 每一頁都會出現,屬於共用外框;hero、能力卡與最近文章只屬於首頁,留在 index.astro 組合。元件邊界照責任切:跨頁外框放 layout,該頁內容留給 page,card 只有重複呈現時才拆成元件。
wide 採 opt-in。文章與一般頁預設使用 48rem 閱讀寬度,只有首頁打開 74rem 展示寬度。讓全站 main 一起拉寬,首頁三欄 cards 是好排了,每篇文章的行長也跟著失控。
首頁已經能用 getCollection() 取得全部非草稿文章,不需要為 cards 建另一套資料來源:
---
const posts = await getCollection('blog', ({ data }) => !data.draft);
const latestPosts = posts
.toSorted(
(a, b) =>
b.data.pubDate.valueOf() - a.data.pubDate.valueOf() ||
b.data.day - a.data.day,
)
.slice(0, 6);
---
<HomeHero publishedCount={posts.length} />
{
latestPosts.map((post) => (
<ArticleCard
href={`/blog/${post.id}`}
day={post.data.day}
title={post.data.title}
description={post.data.description}
tags={post.data.tags}
/>
))
}
同一份 collection 資料同時提供 hero 的完成篇數與最近 6 篇 cards。Day 10:Content Collections建立的 schema 統一規範內容欄位;ArticleCard.astro 只接顯示所需欄位,不接整包 entry。
這些 cards 輸出普通 <article> 和 <a>。排序與 HTML 生成都在 build 時完成;搜尋頁的即時篩選才需要 Vue island 和 client state。把靜態 card 改成 Vue,只會多送一份瀏覽器 JavaScript。
實際 build 後,首頁 HTML 找不到 <astro-island>。nav、hero、能力卡、文章 cards 與 footer 都是零 hydration 的靜態頁。
課程把這一段稱為 Portfolio Project,但目前 repo 沒有 project collection,也沒有作品縮圖。為了湊三張卡就新增資料模型、圖片 pipeline 與假作品,只會提早帶進資料與圖片問題。
首頁呈現三條可以查證的 capstone 能力線:
| 能力線 | 已存在的成果 | Card 指向 |
|---|---|---|
| Content system | collection、文章 route、搜尋、分頁 | Content Collections 文章 |
| Interactive islands | Vue 搜尋、表單與登入狀態 | 搜尋 island 文章 |
| Server & data | Actions、Turso、middleware、Better Auth | Auth 策略文章 |
這三張「作品卡」帶讀者進入同一個 capstone 的三個切面,不把它們包裝成三個外部專案。Day 24:Better Auth 驗證策略等文章提供實作脈絡;theme toggle、正式部署與圖片策略仍留在各自的 Day。
另一種順序可以在一個上線中的多語系品牌官網看到。它已經有 /blog route,blog.scss 與 blog-post.scss 也寫好了 .blog-list、.blog-post、.date 這些規則;文章資料尚未進來,沒有 Content Collection,也沒有任何 Markdown。
於是 blog 頁面的實作是把首頁複製一份改名:同樣兩張硬編碼的圖、同樣的背景、幾乎相同的 scoped 樣式,元件裡只多一行 console.log('Blog')。那兩份寫好的樣式檔沒有被任何地方 import,留在專案裡不會生效。
這個順序在當時有其理由:route 和樣式不依賴內容,可以先開;文章正本要等編輯流程或 CMS 決定。這個殼卻容易被當成「已經做好」;讀者打開 /blog,看到的仍是首頁。
內容站先替內容建立正本與 schema(Day 10 到 Day 13 那條線),再讓 route 從資料長出來。內容還沒準備好時,先不要開那條 route,避免它指向不屬於自己的內容。
把自訂 404 放在 src/pages/404.astro,靜態 build 會產生 404.html,多數部署服務會自動使用。頁面仍套用 BaseLayout,導覽、SEO 基礎與 footer 不會因為錯誤路徑消失:
---
import BaseLayout from '../layouts/BaseLayout.astro';
export const prerender = true;
---
<BaseLayout title="找不到這一頁">
<h1>這條路徑沒有內容,但閱讀路徑還在。</h1>
<a href="/">回到首頁</a>
<a href="/blog">搜尋文章</a>
</BaseLayout>

兩個出口就夠。改成列出錯誤代碼或 router 技術細節,讀者還是回不到內容。
首頁展示區在寬螢幕使用三欄,中等寬度改兩欄,窄螢幕回到單欄。nav 也改成上下排列,連結列可以水平捲動,避免站名和三個入口互相擠壓。

樣式責任沿用 Day 6:scoped 與 global CSS的分法:顏色、寬度、間距 token 與 focus 樣式放全域;hero、nav、card 的外觀留在各自元件的 scoped <style>。調整單一 card 不會碰到文章排版;全站 token 的變更則同步影響所有頁面。
以下以 Node 24.16.0、Astro 7.1.1 於 2026-07-24 驗證:
npm run build 通過,產出首頁、20 篇文章、4 個分頁與 /404.html。<astro-island>,新增骨架沒有增加 hydration。/、/blog、/blog/page/1、文章頁與 /404 都保有同一組主要導覽。Astro layout 與 <slot /> 的現行寫法可查 Layouts 官方文件,自訂 404 規則見 Astro Pages 官方文件,內容查詢則見 Content Collections 官方文件。
首頁、nav、404 與 footer 現在指向同一組閱讀路徑,既有內容、互動與 server 功能也收進同一個入口。Day 18 接著處理圖片最佳化與媒體策略。
本日程式碼:step-17|只看這天的改動:step-16...step-17