Skip to content

Astro 路由与页面

文件路由基础#

🙋 如果你用过 Next.js 或 Nuxt,对”文件即路由”的概念应该不陌生。Astro 采用了类似的设计:src/pages/ 目录下的文件会自动映射成 URL 路由。

目录映射规则#

src/pages/
├── index.astro → /
├── about.astro → /about
├── blog/
│ ├── index.astro → /blog
│ └── first-post.astro → /blog/first-post
└── docs/
└── guide/
└── intro.astro → /docs/guide/intro

🎯 规则很简单:

支持的文件类型#

src/pages/ 目录支持多种文件类型:

文件类型用途示例
.astroAstro 组件页面about.astro
.mdMarkdown 页面post.md
.mdxMDX 页面(需集成)post.mdx
.html纯 HTML 页面legacy.html
.js/.tsAPI 端点api/data.ts

🔶 注意: .md.mdx 文件会自动使用默认布局渲染。如果想自定义布局,需要在 Frontmatter 中指定。

嵌套路由与 index 文件#

当目录和文件同名时,Astro 的处理方式:

src/pages/
├── blog.astro → /blog
└── blog/
├── index.astro → /blog (会和 blog.astro 冲突!)
└── post.astro → /blog/post

🔶 避免冲突: 不要同时创建 blog.astroblog/index.astro,选择其中一种方式即可。推荐使用目录 + index.astro 的方式,更便于组织子页面。

静态路由#

基本页面创建#

创建一个简单的静态页面:

src/pages/about.astro
---
import Layout from '../layouts/Layout.astro'
---
<Layout title="关于我">
<h1>关于我</h1>
<p>这是一个静态页面,构建时生成 HTML。</p>
</Layout>

访问 /about 就能看到这个页面。

多层级目录组织#

实际项目中,页面往往需要多级目录:

src/pages/
├── index.astro
├── blog/
│ ├── index.astro # 博客列表页
│ └── [slug].astro # 博客详情页(动态路由)
├── docs/
│ ├── index.astro # 文档首页
│ ├── getting-started.astro
│ └── api/
│ ├── index.astro # API 文档首页
│ └── reference.astro
└── legal/
├── privacy.astro
└── terms.astro

链接导航#

页面之间的导航使用普通的 <a> 标签:

src/components/Nav.astro
---
const links = [
{ href: '/', text: '首页' },
{ href: '/blog', text: '博客' },
{ href: '/about', text: '关于' },
]
---
<nav>
{links.map((link) => <a href={link.href}>{link.text}</a>)}
</nav>

🤔 为什么不用 <Link> 组件?

Astro 默认是静态站点,普通 <a> 标签就能实现页面跳转。如果你启用了 View Transitions,Astro 会自动拦截导航并提供平滑过渡,不需要特殊组件。

动态路由#

静态路由适合内容固定的页面,但博客文章、产品详情这类页面数量不确定,就需要动态路由了。

单参数路由 [param]#

用方括号包裹文件名,表示这是一个动态参数:

src/pages/blog/[slug].astro → /blog/:slug

🎯 这个文件可以匹配:

getStaticPaths() 函数详解#

对于静态构建(SSG),你需要告诉 Astro 所有可能的参数值:

src/pages/blog/[slug].astro
---
export function getStaticPaths() {
return [
{ params: { slug: 'hello-world' } },
{ params: { slug: 'astro-tutorial' } },
{ params: { slug: 'typescript-tips' } },
]
}
const { slug } = Astro.params
---
<h1>文章: {slug}</h1>

构建时,Astro 会为每个 params 对象生成一个 HTML 文件:

从实际数据生成路径#

实际项目中,路径参数通常来自数据源:

src/pages/blog/[slug].astro
---
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}))
}
const { post } = Astro.props
---
<h1>{post.data.title}</h1>

params 与 props 的区别#

getStaticPaths 返回的对象可以包含 paramsprops

{
params: { slug: 'hello' }, // 必须,用于构建 URL
props: { title: '...' }, // 可选,传递给页面的数据
}
属性用途类型限制
params构建 URL 路径只能是字符串
props传递数据给页面可以是任意类型

🎯 最佳实践: 把需要的数据通过 props 传递,避免在页面中再次查询。

---
// ❌ 不推荐:在页面中再次查询
export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map((post) => ({
params: { slug: post.id },
}))
}
const { slug } = Astro.params
const posts = await getCollection('blog')
const post = posts.find((p) => p.id === slug) // 重复查询
// ✅ 推荐:通过 props 传递
export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map((post) => ({
params: { slug: post.id },
props: { post }, // 直接传递
}))
}
const { post } = Astro.props // 直接使用
---

剩余参数路由 [...spread]#

有时需要匹配任意深度的路径:

src/pages/docs/[...path].astro

这可以匹配:

src/pages/docs/[...path].astro
---
export function getStaticPaths() {
return [
{ params: { path: undefined } }, // /docs
{ params: { path: 'intro' } }, // /docs/intro
{ params: { path: 'api/reference' } }, // /docs/api/reference
]
}
const { path } = Astro.params
---
<h1>文档路径: {path || '首页'}</h1>

多参数路由#

可以在路径中使用多个动态参数:

src/pages/[lang]/blog/[slug].astro

匹配 /zh/blog/hello/en/blog/world

---
export function getStaticPaths() {
return [
{ params: { lang: 'zh', slug: 'hello' } },
{ params: { lang: 'en', slug: 'world' } },
]
}
const { lang, slug } = Astro.params
---

分页实现#

paginate() 函数使用#

Astro 内置了 paginate() 函数,简化分页实现:

src/pages/blog/[page].astro
---
import { getCollection } from 'astro:content'
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog')
// 按发布日期排序
const sortedPosts = posts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
)
// 每页 10 篇文章
return paginate(sortedPosts, { pageSize: 10 })
}
const { page } = Astro.props
---
<h1>博客 - 第 {page.currentPage}</h1>
<ul>
{
page.data.map((post) => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
</li>
))
}
</ul>

paginate() 会自动生成:

Page 对象属性详解#

paginate() 传递给页面的 page 对象包含丰富的信息:

interface Page<T> {
// 当前页的数据
data: T[]
// 分页元数据
start: number // 第一条数据的索引(从 0 开始)
end: number // 最后一条数据的索引
total: number // 总数据量
currentPage: number // 当前页码(从 1 开始)
size: number // 每页大小
lastPage: number // 最后一页页码
// 导航 URL
url: {
current: string // 当前页 URL
prev?: string // 上一页 URL(第一页时为 undefined)
next?: string // 下一页 URL(最后一页时为 undefined)
first?: string // 第一页 URL(当前就是第一页时为 undefined)
last?: string // 最后一页 URL(当前就是最后一页时为 undefined)
}
}

分页导航组件实现#

利用 page.url 实现分页导航:

src/components/Pagination.astro
---
interface Props {
page: {
currentPage: number
lastPage: number
url: {
prev?: string
next?: string
first?: string
last?: string
}
}
}
const { page } = Astro.props
const { currentPage, lastPage, url } = page
---
<nav class="pagination" aria-label="分页导航">
{
url.first && (
<a href={url.first} aria-label="第一页">
« 首页
</a>
)
}
{
url.prev && (
<a href={url.prev} aria-label="上一页">
‹ 上一页
</a>
)
}
<span class="current">
{currentPage} / {lastPage}
</span>
{
url.next && (
<a href={url.next} aria-label="下一页">
下一页 ›
</a>
)
}
{
url.last && (
<a href={url.last} aria-label="最后一页">
末页 »
</a>
)
}
</nav>
<style>
.pagination {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
margin-top: 2rem;
}
.pagination a {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
}
.pagination a:hover {
background-color: #f5f5f5;
}
.current {
font-weight: bold;
}
</style>

使用分页组件:

src/pages/blog/[page].astro
---
import Pagination from '../../components/Pagination.astro'
// ... getStaticPaths 和其他代码
const { page } = Astro.props
---
<ul>
{page.data.map((post) => <li>{post.data.title}</li>)}
</ul>
<Pagination {page} />

自定义分页 URL 格式#

默认分页 URL 是 /blog/1/blog/2,可以自定义:

---
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog')
return paginate(posts, {
pageSize: 10,
// 自定义 URL 格式
// /blog/page/1, /blog/page/2
})
}
---

如果想要 /blog/page/1 格式,调整文件位置:

src/pages/blog/page/[page].astro

SSR 动态路由#

前面讲的都是静态构建(SSG)模式,需要在构建时确定所有路由。如果内容是动态的(比如来自数据库),可以使用服务端渲染(SSR)。

静态 vs 服务端渲染模式#

特性SSG (静态)SSR (服务端)
构建时机构建时生成所有页面请求时动态渲染
性能极快(纯静态文件)取决于服务器
getStaticPaths必需不需要
数据实时性构建时确定每次请求获取最新
部署方式CDN / 静态托管Node.js 服务器

启用 SSR 模式#

astro.config.ts 中配置:

astro.config.ts
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'
export default defineConfig({
output: 'server', // 或 'hybrid'
adapter: node({
mode: 'standalone',
}),
})

Astro.params 在 SSR 中的使用#

SSR 模式下,不需要 getStaticPaths,直接读取参数:

src/pages/blog/[slug].astro
---
// SSR 模式:不需要 getStaticPaths
const { slug } = Astro.params
// 从数据库或 API 获取数据
const response = await fetch(`https://api.example.com/posts/${slug}`)
if (!response.ok) {
return Astro.redirect('/404')
}
const post = await response.json()
---
<h1>{post.title}</h1>
<div set:html={post.content} />

重定向与 404 处理#

处理不存在的路由:

src/pages/blog/[slug].astro
---
const { slug } = Astro.params
const post = await getPostBySlug(slug)
// 文章不存在时重定向到 404
if (!post) {
return Astro.redirect('/404')
}
---

或者返回 404 状态码:

---
const post = await getPostBySlug(slug)
if (!post) {
return new Response('Not Found', {
status: 404,
statusText: 'Not Found',
})
}
---

混合模式 (Hybrid)#

大多数页面静态生成,少数页面 SSR:

astro.config.ts
export default defineConfig({
output: 'hybrid', // 默认静态
adapter: node({ mode: 'standalone' }),
})

在需要 SSR 的页面添加:

src/pages/api/search.astro
---
export const prerender = false // 禁用预渲染,启用 SSR
const query = Astro.url.searchParams.get('q')
const results = await searchPosts(query)
---

实战:博客列表分页#

让我们看看当前博客项目是如何实现文章列表和分页的。

当前项目的文章列表实现分析#

文章详情页 src/pages/blog/[...slug].astro

src/pages/blog/[...slug].astro
---
import RenderPost from '~/components/views/RenderPost.astro'
import { getFilteredPosts } from '~/utils/data'
export async function getStaticPaths() {
const posts = await getFilteredPosts('blog')
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}))
}
const { post } = Astro.props
---
<RenderPost {post} isCommentable={true} />

🎯 几个关键点:

  1. 使用 [...slug] 剩余参数,可以匹配任意深度的路径
  2. getFilteredPosts 是封装的工具函数,获取并过滤文章
  3. 通过 props 传递完整的 post 对象,避免重复查询

getStaticPaths 配合内容集合#

工具函数 getFilteredPosts 的实现思路:

src/utils/data.ts
import { getCollection } from 'astro:content'
export async function getFilteredPosts(collection: string) {
const posts = await getCollection(collection)
// 过滤草稿(生产环境)
const filtered = import.meta.env.PROD
? posts.filter((post) => !post.data.draft)
: posts
// 按日期排序
return filtered.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
)
}

分类/标签页面的动态生成#

为每个分类生成独立页面:

src/pages/blog/category/[category].astro
---
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const posts = await getCollection('blog')
// 提取所有分类
const categories = [...new Set(posts.map((post) => post.data.category))]
return categories.map((category) => ({
params: { category },
props: {
posts: posts.filter((post) => post.data.category === category),
},
}))
}
const { category } = Astro.params
const { posts } = Astro.props
---
<h1>分类: {category}</h1>
<ul>
{
posts.map((post) => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
</li>
))
}
</ul>

标签页面类似,但一篇文章可以有多个标签:

src/pages/blog/tag/[tag].astro
---
export async function getStaticPaths() {
const posts = await getCollection('blog')
// 提取所有标签(扁平化)
const tags = [...new Set(posts.flatMap((post) => post.data.tags || []))]
return tags.map((tag) => ({
params: { tag },
props: {
posts: posts.filter((post) => post.data.tags?.includes(tag)),
},
}))
}
---

路由优先级#

当多个路由模式可能匹配同一个 URL 时,Astro 按以下优先级选择:

  1. 静态路由 > 动态路由
  2. 参数少的 > 参数多的动态路由
  3. 具名参数 > 剩余参数
/posts/create # 静态路由,优先级最高
/posts/[id] # 单参数动态路由
/posts/[...slug] # 剩余参数,优先级最低

下一步#

🎯 现在你已经掌握了 Astro 的路由系统:

下一篇文章,我们将学习 Astro 的组件系统,包括:

参考资料#