布局 Layouts 复用页面骨架
本教程共 56 篇 · 第 13 篇 · 更新于 2026-08-07 · 约 10 分钟阅读
本节目标:学会用布局组件把每个页面都要重复的导航、底部和 head 抽出来,一次写好、处处复用。
上一章我们了解到,每个 .astro 页面通常都要输出一份完整的 HTML:有 <html>、<head>、<body>,里面还有导航栏、页脚。如果每个页面都手写一遍这些重复的东西,不仅累,改一处还得改全部。
Astro 的解法很优雅:布局(Layout)。布局本质上就是一个普通的 Astro 组件,专门用来提供可复用的页面骨架。
布局到底是什么
布局其实就是一种 Astro 组件。我们习惯把「提供公共界面元素」的组件叫布局。一个典型的布局会给页面提供两样东西:
- 一个页面外壳(page shell):也就是
<html>、<head>、<body>这些最外层的标签。 - 一个插槽(slot):插槽是个占位符,告诉 Astro「页面自己的内容塞在这里」。
除了这两点,布局跟别的组件没啥区别。它也能接收属性(props)、导入并使用别的组件、放进 UI 框架组件(如 React、Vue)、写客户端脚本。它甚至不一定非得是完整页面外壳,也能当作局部的界面模板。
不过有个规矩:如果布局里包含了页面外壳,那它的 <html> 元素必须是所有其他元素的「老祖宗」——也就是所有内容都得包在 <html> 里面。
Note「插槽(slot)」是 Astro 组件里的一个概念,对应英文
slot。它的作用就是占个位,等用这个组件的地方把内容填进来。本章后面会看到具体用法。
一个最简单的布局例子
下面这个布局组件,提供了导航、标题位置和一个插槽:
// src/layouts/MySiteLayout.astro
---
import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro';
const { title } = Astro.props;
---
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<BaseHead title={title}/>
</head>
<body>
<nav>
<a href="/">首页</a>
<a href="/posts/">文章</a>
<a href="/contact/">联系</a>
</nav>
<h1>{title}</h1>
<article>
<slot /> <!-- 你的页面内容会被注入这里 -->
</article>
<Footer />
</body>
</html>
注意看里面的 <slot />。它就像一个空抽屉,等会儿页面把自己的内容推进去。
布局一般放在 src/layouts/ 目录里,这算是约定俗成的习惯。但它不是强制的,你放项目任何地方都行,甚至可以用下划线开头的文件名把布局跟页面放同一目录。不过放 src/layouts/ 最清晰,新手照做就好。
页面怎么套用布局
页面想用这个布局,就把它 import 进来,然后把 <MySiteLayout> 当标签用,把内容写在标签内部:
// src/pages/index.astro
---
import MySiteLayout from '../layouts/MySiteLayout.astro';
---
<MySiteLayout title="首页">
<p>我的页面内容,被包在布局里啦!</p>
</MySiteLayout>
写在 <MySiteLayout>...</MySiteLayout> 中间的那段 <p>,最后会替换掉布局里的 <slot />。而 title="首页" 这种写法,是把 title 当作属性传给布局,布局里用 Astro.props.title 取出来填进 <h1>。
这样每个页面只写自己独有的内容,公共的导航、底部、head 全在布局里一次搞定。
用 TypeScript 给布局加类型
布局接收的属性,可以加上 TypeScript 类型,这样写代码时有智能提示,写错了编辑器也会提醒。
// src/components/MyLayout.astro
---
interface Props {
title: string;
description: string;
publishDate: string;
viewCount: number;
}
const { title, description, publishDate, viewCount } = Astro.props;
---
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="description" content={description}>
<title>{title}</title>
</head>
<body>
<header>
<p>发布于 {publishDate}</p>
<p>已被 {viewCount} 人浏览</p>
</header>
<main>
<slot />
</main>
</body>
</html>
有了 interface Props,如果哪个页面忘了传 viewCount 这种必填属性,编辑器就会报错。这对大项目特别有用,能早早发现疏漏。
Markdown 页面用布局
上一章提到,放在 src/pages/ 里的 .md 文件能直接当页面。但 Markdown 文件本身没有 <head>、没有导航,看起来光秃秃的。布局正好能给它补上这些。
做法是,在 Markdown 文件的 frontmatter 里写一个 layout 属性,指向某个 .astro 布局组件:
---
layout: ../layouts/BlogPostLayout.astro
title: "你好,世界!"
author: "码上学"
date: "2026-08-07"
---
这里是文章正文,写啥都行。
这里 layout 是 Astro 在 Markdown 场景里认识的唯一特殊属性,别的属性(如 title、author)都会作为正常数据传给布局。
配套的 Markdown 布局长这样:
// src/layouts/BlogPostLayout.astro
---
// 1. frontmatter 属性让布局拿到 Markdown 的元数据和正文数据
const { frontmatter } = Astro.props;
---
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<title>{frontmatter.title}</title>
</head>
<body>
<h1>{frontmatter.title} · 作者 {frontmatter.author}</h1>
<!-- 2. 渲染出来的 HTML 会被送进默认插槽 -->
<slot />
<p>写于:{frontmatter.date}</p>
</body>
</html>
一个 Markdown 布局会拿到两类信息:
frontmatter属性:Markdown 文件里写的所有元数据(标题、作者、日期等)。- 一个默认
<slot />:Markdown 被渲染成的 HTML 会嵌在这里。
Note这种
layout写法只适用于「放在src/pages/里、走文件路由的单个 Markdown 文件」。如果你是用后面会讲的内容集合(content collection)来批量管理文章,那套机制不同,不会认这个layout属性。
Markdown 布局能拿到哪些数据
布局组件通过 Astro.props 能拿到不少有用信息:
file:这个文件在硬盘上的绝对路径。url:这个页面的网址,比如/posts/hello。frontmatter:Markdown 或 MDX 文件里的全部 frontmatter 数据。headings:文章里所有标题(h1 到 h6)的列表,含层级、锚点和文字。rawContent():返回原始 Markdown 文本的函数。compiledContent():返回编译后 HTML 文本的异步函数。
如果你用 TypeScript,可以借助 MarkdownLayoutProps 这个辅助类型,把上面的属性都加上类型:
// src/layouts/BlogPostLayout.astro
---
import type { MarkdownLayoutProps } from 'astro';
type Props = MarkdownLayoutProps<{
title: string;
author: string;
date: string;
}>;
const { frontmatter, url } = Astro.props;
---
<html>
<head>
<meta charset="utf-8">
<link rel="canonical" href={new URL(url, Astro.site).pathname}>
<title>{frontmatter.title}</title>
</head>
<body>
<h1>{frontmatter.title} · {frontmatter.author}</h1>
<slot />
<p>写于:{frontmatter.date}</p>
</body>
</html>
有了它,frontmatter、url 等属性都有了类型保护,写起来更安心。
MDX 里手动导入布局
MDX 文件除了用 layout 属性,还能手动 import 一个布局组件来用。手动导入的好处是:可以传一些 frontmatter 里放不下、或没法放的数据。
// src/pages/posts/first-post.mdx
---
layout: ../../layouts/BaseLayout.astro
title: '我的第一篇 MDX'
publishDate: '2026-08-07'
---
import BaseLayout from '../../layouts/BaseLayout.astro';
export function fancyJsHelper() {
return "试试用 JS 函数做点事!";
}
<BaseLayout title={frontmatter.title} fancyJsHelper={fancyJsHelper}>
欢迎来到我的 Astro 博客,用 MDX 写!
</BaseLayout>
这里 fancyJsHelper 是个 MDX 里的 JS 函数,它通过属性传给了布局。布局里用 Astro.props.fancyJsHelper 取出来调用。
Warning用 MDX 时有个细节:一旦你手动导入布局,Astro 不会再自动给 MDX 页面加
<meta charset="utf-8">标签。所以你必须在布局里自己写上这个标签,否则中文等字符可能显示乱码。
布局也能嵌套
布局不见得非得包含一整页的 HTML。你可以把布局拆成更小的组件,再组合起来,做出更灵活的模板。这种「布局套布局」叫嵌套布局。
比如,一个 BlogPostLayout.astro 只负责文章的标题、日期、作者样式;而全站通用的 BaseLayout.astro 负责导航、页脚、SEO 标签、全局样式和字体。两者可以叠在一起用:
// src/layouts/BlogPostLayout.astro
---
import BaseLayout from './BaseLayout.astro';
const { frontmatter } = Astro.props;
---
<BaseLayout url={frontmatter.url}>
<h1>{frontmatter.title}</h1>
<h2>作者:{frontmatter.author}</h2>
<slot />
</BaseLayout>
嵌套布局和嵌套普通组件一模一样:把内层布局当标签用,想传的属性照常传。这样多个布局之间能共享代码,网站结构越做越大也不会乱。
本章小结
布局是 Astro 里「偷懒」的好工具:它把导航、页脚、head 这些每个页面都要重复的东西抽到一处,页面只写自己的独特内容,通过插槽(slot)填进去。布局能接收属性、能加 TypeScript 类型、能给 Markdown 文章当外壳、还能一层套一层地嵌套。
掌握了页面(第 12 章)和布局(本章),下一章我们正式进入路由:文件怎么变成网址,以及 Astro 的静态路由是怎么工作的。