首页
/ Astro Portfolio 启动模板实战:用内容集合与动态路由构建个人作品集网站

Astro Portfolio 启动模板实战:用内容集合与动态路由构建个人作品集网站

2026-09-04 21:32:49作者:柏廷章Berta

本篇以 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 devastro buildastro preview 和裸 astro CLI。

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 addastro 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 loaderglob({ base: './src/content/work', pattern: '**/*.md' }) 会递归匹配 src/content/work 下的所有 Markdown 文件。模板特意放了一个嵌套文件 nested/duvet-genius.md 来演示这一点——子目录会保留在 entry 的 id 中(即 nested/duvet-genius),因此详情页路由天然支持嵌套路径;
  • Zod schematitledescriptionpublishDatetagsimg 为必填,img_alt 可选。publishDate 使用 z.coerce.date(),意味着 frontmatter 中写成字符串 2019-10-02 00:00:00 也会被强制转换为 Date 对象,这为后文的按日期排序打下基础;
  • 类型推断:由于 schema 存在,CollectionEntry<'work'> 类型会自动携带 data.titledata.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.mdh20.mdmarkdown-mystery-tour.mdnested/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);
---

其工作流程为:

  1. getStaticPaths() 在构建期调用 getCollection('work') 取得全部 entry,将 entry.id 映射为 slug 路由参数,并把整个 entry 作为 props 传给页面。** 通配符 [...slug] 语法使其能匹配 nested/duvet-genius 这类带斜杠的多段路径;
  2. render(entry) 在构建时把 Markdown 正文渲染为 Astro 组件 <Content />,再包裹进模板的 BaseLayout、作品头图(entry.data.img)、标签 Pill(entry.data.tags.map(...))与描述文案中;
  3. 由于 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

  1. 首屏背景(--bg-image-main)默认指向小尺寸的 800w 图片,媒体查询 min-width: 50em 时切换到 1440w 大图;
  2. 折叠线以下(below the fold)的 subtle/footer 背景初始值为 linear-gradient(transparent, transparent) 占位,只有当 load 事件触发、<html> 被加上 .loaded 类后才切换为真实图片 URL——从源码注释看,这是刻意避免首屏加载视口外图片的懒加载策略;
  3. public/assets/backgrounds/ 中每个背景都提供 800w / 1440w 两档 JPG 加 SVG 曲线变体,配合 --bg-scale--bg-gradient-size 等变量按视口缩放;
  4. 针对高对比度(强制颜色)用户,@media (forced-colors: active) 会直接关闭自定义背景与混合模式,保证可访问性降级。

首页 index.astro 的各 section 背景同样复用这套变量:.with-background::before 用伪元素叠加 noise.png 噪点纹理与 --hero-bg 渐变图,并通过 background-blend-mode 合成,实现"有质感但不额外请求资源"的视觉效果。

将模板改造为你自己的作品集

基于以上结构,定制路径非常清晰,且全程只改内容、不碰架构:

  1. 换身份:修改 Nav.astro 中的站点标题与 iconLinks 社交链接;index.astroHerotitle / tagline、角色 Pill 文案,以及 public/assets/portrait.jpg 头像图;
  2. 换配色:只改 global.css--accent-* 系列变量,全站强调色、渐变、按钮即联动更新;
  3. 增删作品:在 src/content/work/ 下按 h20.md 的 frontmatter 格式新增/删除 Markdown 文件即可,列表与详情页由内容集合自动生成,publishDate 决定排序位置;
  4. 改内容文案:About 页各 section(Background / Education / Skills)、首页 Mentions 品牌列表(index.astro 中的数组)都是页面内直接可编辑的纯文本/数组。

构建部署方面,npm run build 产出的 dist/ 目录即可部署到任意静态托管;本地可用 npm run preview 在部署前验证效果。需要强调的是,以上能力的前提是遵循 content.config.ts 中定义的 schema——frontmatter 缺必填字段(如 publishDateimg)时,内容集合会在构建期报错而不是产出坏页面,这正是模板用 Zod 做契约的意图所在。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384