Astro Portfolio 启动模板实战:用内容集合与动态路由构建个人作品集网站
本篇以 Astro 仓库内置的 Portfolio 启动模板 为主体,讲清该模板的安装方式、全部可用命令,并结合模板源码深入剖析其三大核心技术支柱:基于 glob loader 的 work 内容集合(Content Collections)、[...slug] 动态路由生成的项目详情页,以及纯 CSS 变量驱动的暗色主题与背景图懒加载机制。读完你可以完整掌握如何把这个模板改造为属于你自己的静态作品集站点,并理解其每个页面背后的数据流与渲染原理。
模板定位与安装方式
Portfolio 是 Astro 官方提供的一个面向设计师、开发者个人主页的启动模板(Starter Kit),其特点是不依赖任何第三方框架集成——站点内容(项目作品)以 Markdown 文件形式组织,通过内容集合做结构化校验,再经动态路由渲染为详情页。README 给出的官方安装命令为:
npm create astro@latest -- --template portfolio
该命令会通过 create-astro 将模板复制到你的新项目中。从模板的 package.json 可以看到其运行前提:
- 依赖仅有一个:
astro ^7.2.10,没有任何渲染器(React/Svelte 等)或第三方集成; engines要求 Node.js>=22.12.0;type: module,全部脚本(dev/build/preview/astro)分别对应astro dev、astro build、astro preview和裸astroCLI。
而 astro.config.mjs 的内容只有一个空的 defineConfig({})。这说明模板的站点能力完全来自 Astro 内置特性(内容集合、路由、静态构建),无需任何插件配置即可运行。README 也提示:熟悉 Astro 之后可以直接删除 README 文件开始自己的定制。
模板命令速查
以下命令均从项目根目录的终端执行,这是 README 原始命令表的完整继承,同时与 package.json 中的 scripts 字段一一对应:
| 命令 | 作用 |
|---|---|
npm install |
安装依赖 |
npm run dev |
启动本地开发服务器,地址为 localhost:4321 |
npm run build |
构建生产站点,产物输出到 ./dist/ |
npm run preview |
在部署前本地预览构建产物 |
npm run astro ... |
执行如 astro add、astro check 等 CLI 命令 |
npm run astro -- --help |
查看 Astro CLI 帮助 |
由于站点是纯静态生成的(无 SSR 配置、无集成),npm run build 之后即可将 dist/ 目录部署到任意静态托管环境。
项目结构总览
模板采用 Astro 标准目录约定,源码位于 src/ 下:
examples/portfolio/
├── public/
│ └── assets/ # 静态图片:头像、作品图、多分辨率背景图
├── src/
│ ├── components/ # 13 个 Astro 组件(Nav、Hero、Grid、ThemeToggle 等)
│ ├── content/work/ # 作品 Markdown 文件(含嵌套子目录)
│ ├── layouts/BaseLayout.astro # 全站布局:head/nav/footer + 背景层
│ ├── pages/ # 路由:index、work、work/[...slug]、about、404
│ ├── styles/global.css # 全局 CSS 变量(颜色/渐变/阴影/字号/字体)
│ └── content.config.ts # work 内容集合定义
├── astro.config.mjs
├── package.json
└── tsconfig.json
页面与文件的对应关系是:index.astro 对应首页 /,work.astro 对应作品列表页 /work/,work/[...slug].astro 为每个作品生成详情页 /work/<id>/,另有 about.astro 关于页和 404.astro 自定义 404 页。
内容集合:用 glob loader 与 Zod 校验作品数据
模板把"作品"抽象为一个名为 work 的内容集合,定义在 content.config.ts:
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
export const collections = {
work: defineCollection({
// Load Markdown files in the src/content/work directory.
loader: glob({ base: './src/content/work', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.coerce.date(),
tags: z.array(z.string()),
img: z.string(),
img_alt: z.string().optional(),
}),
}),
};
几个值得注意的实现细节:
- glob loader:
glob({ base: './src/content/work', pattern: '**/*.md' })会递归匹配src/content/work下的所有 Markdown 文件。模板特意放了一个嵌套文件 nested/duvet-genius.md 来演示这一点——子目录会保留在 entry 的id中(即nested/duvet-genius),因此详情页路由天然支持嵌套路径; - Zod schema:
title、description、publishDate、tags、img为必填,img_alt可选。publishDate使用z.coerce.date(),意味着 frontmatter 中写成字符串2019-10-02 00:00:00也会被强制转换为Date对象,这为后文的按日期排序打下基础; - 类型推断:由于 schema 存在,
CollectionEntry<'work'>类型会自动携带data.title、data.tags等精确字段类型,组件 props 校验无需手写。
一份真实的内容文件是 h20.md,其 frontmatter 完整展示了 schema 对应的字段写法:
---
title: h2.0
publishDate: 2019-10-02 00:00:00
img: /assets/stock-4.jpg
img_alt: Soft pink and baby blue water ripples together in a subtle texture.
description: |
We developed brand positioning and design assets for the launch
of a new colored water product.
tags:
- Design
- Branding
---
正文则是任意 Markdown,由详情页在构建时渲染为 HTML(见下节)。src/content/work/ 下共有 4 个示例作品(bloom-box.md、h20.md、markdown-mystery-tour.md、nested/duvet-genius.md)。
动态路由:为每个作品生成独立详情页
详情页 work/[...slug].astro 是模板中路由逻辑的核心,其 frontmatter 完整实现了"集合 → 路由 → 内容"三步链:
---
import { type CollectionEntry, getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
interface Props {
entry: CollectionEntry<'work'>;
}
// 为 src/content/ 中的每个 Markdown 文件生成一个页面
export async function getStaticPaths() {
const work = await getCollection('work');
return work.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
---
其工作流程为:
getStaticPaths()在构建期调用getCollection('work')取得全部 entry,将entry.id映射为slug路由参数,并把整个entry作为 props 传给页面。**通配符[...slug]语法使其能匹配nested/duvet-genius这类带斜杠的多段路径;render(entry)在构建时把 Markdown 正文渲染为 Astro 组件<Content />,再包裹进模板的BaseLayout、作品头图(entry.data.img)、标签 Pill(entry.data.tags.map(...))与描述文案中;- 由于 props 声明为
CollectionEntry<'work'>,entry.data的每个字段都获得与 content.config.ts 中 schema 一致的 TypeScript 类型。
也就是说:新增一个作品 = 在 src/content/work/ 下新增一个符合 schema 的 Markdown 文件,构建时自动多出对应详情页,无需修改任何页面代码。
列表侧同样由集合驱动:首页 index.astro 取"最近 4 个"精选作品:
const projects = (await getCollection('work'))
.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf())
.slice(0, 4);
作品列表页 work.astro 则用同样的 publishDate 倒序排序展示全部作品。两个页面的卡片均由 PortfolioPreview.astro 渲染——它接收 CollectionEntry<'work'>,输出指向 /work/${id} 的 <a class="card"> 链接,图片带 loading="lazy" decoding="async",alt 文本取 img_alt(缺省为空字符串)。
布局系统:BaseLayout 与导航的当前页高亮
全站共用 BaseLayout.astro 作为根布局,它接收可选的 title / description props 传给 <MainHead>(用于各页 SEO 元信息),内部结构为:
<html lang="en">
<head>
<MainHead title={title} description={description} />
</head>
<body>
<div class="stack backgrounds">
<Nav />
<slot />
<Footer />
</div>
<script>
// 文档完全加载后给 <html> 加 "loaded" 类(用于背景图懒加载,详见下文)
addEventListener('load', () => document.documentElement.classList.add('loaded'));
</script>
...
</body>
</html>
各页面通过向 <BaseLayout> 传入不同的 title/description 来区分页面,例如 about.astro 传入 title="About | Jeanine White"。
导航组件 Nav.astro 中有两处实现细节值得关注:
- 菜单项(
textLinks:Home / Work / About)与社交图标链接(iconLinks:Twitter / GitHub 等占位链接)以数据数组定义,iconLinks中的icon字段以keyof typeof iconPaths约束,只能取 IconPaths.ts 中已定义的图标名,改名改链只需编辑这两个数组; - 当前页高亮函数
isCurrentPage()用Astro.url.pathname.replace(import.meta.env.BASE_URL, '')先剔除base前缀,再归一化首尾斜杠后比较——这是站点部署在子路径(配置了base)时导航高亮仍正确工作的关键。
主题系统:零 JS 框架的暗色模式与背景图懒加载
模板的视觉层完全由 CSS 自定义属性驱动,不依赖 Tailwind 等工具类框架。global.css 在 :root 中集中声明了整套设计 token:
- 色板:
--gray-0到--gray-999的灰阶,以及--accent-light/regular/dark等强调色; - 渐变:
--gradient-subtle、--gradient-accent等,供卡片、文字描边复用; - 阴影、字号(
--text-sm至--text-5xl)、字体(Public Sans + Rubik)、过渡时长--theme-transition。
暗色模式通过在 <html> 上切换 theme-dark 类实现。切换控件 ThemeToggle.astro 用一段 <script> 注册了原生 Web Component:
class ThemeToggle extends HTMLElement {
constructor() {
super();
const button = this.querySelector('button')!;
const setTheme = (dark: boolean) => {
document.documentElement.classListdark ? 'add' : 'remove';
button.setAttribute('aria-pressed', String(dark));
};
button.addEventListener('click', () => setTheme(!this.isDark()));
setTheme(this.isDark());
}
isDark() {
return document.documentElement.classList.contains('theme-dark');
}
}
customElements.define('theme-toggle', ThemeToggle);
注意它是纯 Web Component + aria-pressed 无障碍状态,没有引入任何框架 island,页面整体保持"零客户端 JS 框架"的静态特性。主题相关的样式则通过 :global(.theme-dark) 选择器(如 ThemeToggle 的滑块动画)和 global.css 中 :root.theme-dark 的变量覆盖协同工作。
背景图的处理是模板另一个性能亮点,全部逻辑集中在 BaseLayout.astro:
- 首屏背景(
--bg-image-main)默认指向小尺寸的800w图片,媒体查询min-width: 50em时切换到1440w大图; - 折叠线以下(below the fold)的 subtle/footer 背景初始值为
linear-gradient(transparent, transparent)占位,只有当load事件触发、<html>被加上.loaded类后才切换为真实图片 URL——从源码注释看,这是刻意避免首屏加载视口外图片的懒加载策略; public/assets/backgrounds/中每个背景都提供800w/1440w两档 JPG 加 SVG 曲线变体,配合--bg-scale、--bg-gradient-size等变量按视口缩放;- 针对高对比度(强制颜色)用户,
@media (forced-colors: active)会直接关闭自定义背景与混合模式,保证可访问性降级。
首页 index.astro 的各 section 背景同样复用这套变量:.with-background::before 用伪元素叠加 noise.png 噪点纹理与 --hero-bg 渐变图,并通过 background-blend-mode 合成,实现"有质感但不额外请求资源"的视觉效果。
将模板改造为你自己的作品集
基于以上结构,定制路径非常清晰,且全程只改内容、不碰架构:
- 换身份:修改 Nav.astro 中的站点标题与
iconLinks社交链接;index.astro 中Hero的title/tagline、角色 Pill 文案,以及public/assets/portrait.jpg头像图; - 换配色:只改 global.css 的
--accent-*系列变量,全站强调色、渐变、按钮即联动更新; - 增删作品:在
src/content/work/下按 h20.md 的 frontmatter 格式新增/删除 Markdown 文件即可,列表与详情页由内容集合自动生成,publishDate决定排序位置; - 改内容文案:About 页各 section(Background / Education / Skills)、首页 Mentions 品牌列表(index.astro 中的数组)都是页面内直接可编辑的纯文本/数组。
构建部署方面,npm run build 产出的 dist/ 目录即可部署到任意静态托管;本地可用 npm run preview 在部署前验证效果。需要强调的是,以上能力的前提是遵循 content.config.ts 中定义的 schema——frontmatter 缺必填字段(如 publishDate、img)时,内容集合会在构建期报错而不是产出坏页面,这正是模板用 Zod 做契约的意图所在。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00