Day 3 的結尾留了一個問題:檔案變成網址之後,每一頁都要 header、footer、同一套外框。如果每個 .astro 檔案都複製一遍,改個導覽列連結,就得一起改 30 個檔案。
用一個 layout 就能解決。在 Astro,layout 是普通的 .astro 元件,負責「把別人的內容包起來」。把每頁相同的外框寫在 layout 裡,中間用 <slot /> 留一個洞;每一頁只要提供自己的內容,就會共用這套外框。
如果你寫過 React,可以把 <slot /> 大致對照成 {children},layout 收到的 title、description 則是 props;Vue 的 <slot> 連名稱都相同。這些都是熟悉的「共用外框」概念,在 Astro 只是換成 layout 與 slot 的寫法。
先看它最小的骨架長怎樣。把每頁共用的東西寫進去,需要「放內容」的地方擺一個 <slot />:
---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
description?: string;
}
const { title, description } = Astro.props;
---
<html lang="zh-Hant">
<head>
<title>{title}</title>
<meta name="description" content={description} />
</head>
<body>
<header><nav>…每頁共用的導覽列…</nav></header>
<main>
<slot /> <!-- 每頁不同的內容,從這裡掉進來 -->
</main>
<footer>…每頁共用的頁尾…</footer>
</body>
</html>
這段使用的仍是一般 .astro 元件語法。interface Props 宣告「我收哪些資料」,Astro.props 取得傳入的值;<slot /> 則標出「呼叫我的人,內容要放在哪」。
這個檔案放在 src/layouts/,不是 src/pages/。Day 3 提過,只有 src/pages 裡的檔案才是路由;放在 src/layouts/ 的 layout 不會產生網址,而是由頁面 import 使用。
layout 會從外面收到兩種東西,分別走 props 和 slot:
<BaseLayout title="關於我" description="..."> 這些寫在標籤上的屬性,進到 Astro.props,是給 layout 拿去填 <title>、<meta> 用的值。<BaseLayout>…</BaseLayout> 這對標籤「中間」的所有東西,會掉進 <slot /> 的位置。這個實作專案的首頁就是這樣用的(src/pages/index.astro):
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="用 Astro 打造 Content-first 前端網站" description="...">
<h1>文章列表</h1>
<ul>…文章一篇篇列出來…</ul>
</BaseLayout>
title 和 description 寫在屬性上,會成為資料;中間的 <h1> 和 <ul> 是內容,會出現在 layout 的 <slot /> 位置。首頁只提供自身內容,不必再處理 header、footer 或 <head>,因為這些共用外框已集中寫在 layout 裡。
layout 要放多少內容,由頁面需求決定。同一個 <slot /> 機制可以用在功能較多的 layout,也可以用在只留最小外框的 layout。這個 repo 裡的兩個 layout 正好示範這兩種做法:
BaseLayout.astro |
BenchLayout.astro |
|
|---|---|---|
| 行數 | 約 130 行 | 29 行 |
<slot /> |
有(1 行) | 有(1 行) |
| 收 props | title/description/type/image/article | 只有 title/description |
<head> 裡有什麼 |
canonical、Open Graph、Twitter card、BlogPosting JSON-LD | 幾乎只有 title 和 charset |
| 用途 | capstone 正式頁,集中處理整站 SEO | 測試頁的最小外框 |
BaseLayout.astro 有 130 行,其中只有一行 <slot /> 用來「放內容」。剩下的 129 行都是外框,以及 <head> 裡的 SEO 設定:canonical 網址、社群分享用的 Open Graph、供搜尋引擎讀取的 JSON-LD 結構化資料。每篇文章都需要這些設定,但不必各自重寫。集中到 layout 後,30 篇文章共用同一份 SEO 設定,修改也只需動一個檔案。
每頁都需要、又不該重複的內容,就集中到 layout。Astro 官方也把 layout 定位為「用來提供頁面共用結構的元件」;header、footer 與共用樣式等重複元素可以放在其中,以減少重複並維持每頁一致(來源:Astro 官方 Layouts 文件,查證日 2026-07-21)。這個專案集中的是 SEO。日後換網域或補 meta 標籤時,只需修改 layout,不必逐一修改 30 個頁面。
BenchLayout.astro 則是另一種做法:29 行,同樣有一個 <slot />,但不放 SEO,只保留測試頁需要的最小外框。
<slot /> 只標示「內容放這」。layout 的複雜度由頁面需求決定:內容站的正式頁可以使用完整的 BaseLayout,測試頁則用一層薄殼即可。
前面的範例都只有「一個洞」。有些頁面版型會需要多個內容位置,例如標題在頂部、側欄在右邊、內文在中間。這時可以替 slot 命名:
<!-- layout 裡:開好幾個具名的洞 -->
<header><slot name="title" /></header>
<aside><slot name="aside" /></aside>
<main><slot /></main> <!-- 沒寫 name 的是預設洞 -->
<!-- 頁面裡:用 slot="名字" 對應塞進去 -->
<MyLayout>
<h1 slot="title">關於我</h1>
<nav slot="aside">…側欄…</nav>
<p>沒標 slot 的內容,掉進預設的那個洞。</p>
</MyLayout>
<slot>沒填就顯示這行</slot> 還可以提供預設值:如果呼叫端沒有放入對應內容,就顯示標籤中間的文字。
這個實作專案目前每頁只需要一個內容區,因此使用單一預設 <slot />。等到 Day 17 組合網站骨架、需要分開安排頭部/側欄/內文時,再使用 named slots。(named slot 的寫法依 Astro 官方 Slots 文件,基準 Astro v7、查證日 2026-07-21;上面是說明用的最小範例。)
<slot />。 你包在 <Layout>…</Layout> 中間的內容會整段消失,因為沒有洞,內容無處可去。頁面「白掉一塊」先查這個。src/pages/ 後,它會成為一個可存取的網址;layout 應放在 src/layouts/(或 src/components/),再由頁面 import 使用。.astro 和 TypeScript interface;如果漏傳 title 或傳入錯誤型別,建置階段就會指出,不必等到使用者開啟頁面後才發現。這也是 interface Props 的用途。驗收時,請確認自己能說清楚三件事:layout 是普通的 .astro 元件;資料走 props,內容走 slot;同一個 <slot /> 可以用在完整的 BaseLayout,也可以用在精簡的 BenchLayout,複雜度由頁面需求決定。
打開任何一個 Astro 專案,看到 src/layouts/ 裡的檔案,你都能指出哪裡是共用外框、<slot /> 在哪、它收哪些 props。
layout 是元件,頁面也是元件。Day 5 會比較 .astro 元件與 React 元件,說明 .astro 何時連一行 JavaScript 都不用送到瀏覽器。
本日程式碼:step-04|只看這天的改動:step-03...step-04