跳转到内容

Astro

3 篇包含标签 "Astro" 的文章

Design.md:让 AI 一致性进行前端 UI 设计的解决方案

Design.md:让 AI 一致性进行前端 UI 设计的解决方案

Section titled “Design.md:让 AI 一致性进行前端 UI 设计的解决方案”

在 AI 辅助前端开发时代,如何让 AI 生成的 UI 保持一致性?本文分享了我们基于 awesome-design-md 构建设计画廊站点的实践经验,以及如何创建结构化的 design.md 来指导 AI 进行规范化的 UI 设计。

用过 AI 写前端代码的朋友应该都有过类似的经历:同一个页面,让 AI 多生成几次,每次的风格都不一样。有时候是圆角有时候是方角,有时候间距是 8px 有时候又变成 16px,甚至同一个按钮在不同对话里长得都不一样。

这不仅仅是个别现象。随着 AI 辅助开发的普及,AI 生成的前端 UI 缺乏一致性已经成为一个普遍问题。不同的 AI 助手、不同的提示词,甚至同一助手在不同对话中,都会产生风格迥异的界面设计。这给产品迭代带来了巨大的维护成本。

问题的根源其实很简单:缺少一份权威的设计参考文档。传统的 CSS 样式文件只能告诉开发者”怎么实现”,却无法完整传达”为什么这样设计”以及”在什么场景下使用什么设计模式”。而对于 AI 来说,它更需要一个清晰的结构化描述来理解设计规范。

与此同时,开源社区已经有了一些很好的资源。VoltAgent/awesome-design-md 项目收集了大量知名公司的设计系统文档,每个目录包含 README.md、DESIGN.md 和预览 HTML。但这些都分散在上游仓库中,难以快速查阅和比较。

那能不能把这些资源整合起来,做成一个方便查阅的设计画廊,同时沉淀出一份结构化的 design.md 给 AI 用呢?

答案是肯定的。接下来分享一下我们的方案。

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个 AI 辅助开发平台,在开发过程中,我们也遇到了 AI 生成 UI 不一致的问题。为了解决这个问题,我们构建设计画廊站点并创建规范化的 design.md,本文就是这套方案的总结。

GitHub - HagiCode-org/site

先看一眼最终做出来的首页效果。首页把设计画廊入口、站点仓库、上游仓库和 HagiCode 的背景介绍收拢在同一个界面里,方便团队先建立统一上下文,再继续阅读具体条目。

Awesome Design MD Gallery 首页概览

在动手写代码之前,我们先来拆解一下这个问题的几个技术挑战。

内容源管理:如何统一分散的设计资源?

Section titled “内容源管理:如何统一分散的设计资源?”

上游的 awesome-design-md 仓库包含了大量设计文档,但我们需要一种方式把它纳入到我们的项目中。

方案:使用 git submodule

awesome-design-md-site
└── vendor/awesome-design-md # 上游资源(git submodule)

这样做有几个好处:

  • 版本可控:可以锁定特定的上游版本
  • 离线构建:不需要在构建时请求外部 API
  • 内容审阅:可以在 PR 中看到具体变更

数据标准化:不同文档结构怎么统一?

Section titled “数据标准化:不同文档结构怎么统一?”

不同公司的设计文档结构可能不同,有些缺少预览文件,有些命名不统一。我们需要在构建期进行标准化处理。

方案:构建期扫描并生成标准化条目

核心模块是 awesomeDesignCatalog.ts,负责:

  1. 扫描 vendor/awesome-design-md/design-md/* 目录
  2. 校验每个条目是否包含必需文件(README.md、DESIGN.md、至少一个预览文件)
  3. 提取并渲染 Markdown 内容为 HTML
  4. 生成标准化的条目数据
src/lib/content/awesomeDesignCatalog.ts
export interface DesignEntry {
slug: string;
title: string;
summary: string;
readmeHtml: string;
designHtml: string;
previewLight?: string;
previewDark?: string;
searchText: string;
}
export async function scanSourceEntries() {
// 扫描 vendor/awesome-design-md/design-md/*
// 校验文件完整性
// 生成标准化条目
}
export async function normalizeDesignEntry(dir: string) {
// 提取 README.md、DESIGN.md
// 解析预览文件
// 渲染 Markdown 为 HTML
}

静态站点架构:怎么在保持静态部署的同时提供动态搜索?

Section titled “静态站点架构:怎么在保持静态部署的同时提供动态搜索?”

既然是设计画廊,搜索功能是必须的。但 Astro 是静态站点生成器,怎么实现实时搜索呢?

方案:React island + URL 查询参数同步

src/components/gallery/SearchToolbar.tsx
export function SearchToolbar() {
const [query, setQuery] = useState('');
// URL 同步
useEffect(() => {
const params = new URLSearchParams(window.location.search);
setQuery(params.get('q') || '');
}, []);
// 实时过滤
const filtered = entries.filter(entry =>
entry.searchText.includes(query)
);
return <input value={query} onChange={e => {
setQuery(e.target.value);
updateURL(e.target.value);
}} />;
}

这样做的好处是保留了静态站点的可部署性(可以部署到任何静态托管服务),同时提供了即时过滤的用户体验。

设计文档化:怎么让 AI 理解并遵守设计规范?

Section titled “设计文档化:怎么让 AI 理解并遵守设计规范?”

这是整个方案的核心。我们需要创建一份结构化的 design.md,让 AI 能够理解并应用我们的设计规范。

方案:借鉴 ClickHouse DESIGN.md 的结构

ClickHouse 的 DESIGN.md 是一个很好的参考,它包含了:

  • Visual Theme & Atmosphere
  • Color Palette & Roles
  • Typography Rules
  • Component Stylings
  • Layout Principles
  • Depth & Elevation
  • Do’s and Don’ts
  • Responsive Behavior
  • Agent Prompt Guide

我们的做法是:结构参考,内容重写。保留 ClickHouse DESIGN.md 的章节结构,但把内容替换成我们自己项目实际使用的设计 token 和组件规范。

基于上述分析,我们的解决方案包含四个核心模块。

这是整个系统的基础,负责从上游资源中提取和标准化内容。

src/lib/content/awesomeDesignCatalog.ts
export async function scanSourceEntries(): Promise<DesignEntry[]> {
const designDir = 'vendor/awesome-design-md/design-md';
const entries: DesignEntry[] = [];
for (const dir of await fs.readdir(designDir)) {
const entryPath = path.join(designDir, dir);
if (await isValidDesignEntry(entryPath)) {
const entry = await normalizeDesignEntry(entryPath);
entries.push(entry);
}
}
return entries;
}
async function isValidDesignEntry(dir: string): Promise<boolean> {
const requiredFiles = ['README.md', 'DESIGN.md'];
for (const file of requiredFiles) {
if (!(await fileExists(path.join(dir, file)))) {
return false;
}
}
return true;
}

画廊界面包括三个主要部分:

首页:展示所有设计条目的卡片网格,每个卡片包含:

  • 设计条目标题和简介
  • 预览图(如果有)
  • 快速搜索高亮

详情页:聚合展示单个设计条目的完整信息:

  • README 文档
  • DESIGN 文档
  • 预览(支持明/暗主题切换)
  • 相邻条目导航

导航:支持返回画廊、浏览相邻条目

首页画廊使用高密度卡片布局,把不同来源的 design.md 条目平铺在一个统一的视觉框架里,方便快速对比品牌风格、按钮模式和排版节奏。

Awesome Design MD Gallery 设计卡片网格

进入具体条目后,详情页会把设计摘要和实时预览放在同一个页面中,减少在文档、预览和源码之间来回切换的成本。

Awesome Design MD Gallery 设计详情预览页

搜索功能基于客户端过滤,使用 URL 查询参数保持状态:

src/components/gallery/SearchToolbar.tsx
function SearchToolbar({ entries }: { entries: DesignEntry[] }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState(entries);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const initialQuery = params.get('q') || '';
setQuery(initialQuery);
filterEntries(initialQuery);
}, []);
const filterEntries = (searchQuery: string) => {
const filtered = entries.filter(entry =>
entry.searchText.toLowerCase().includes(searchQuery.toLowerCase())
);
setResults(filtered);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value);
filterEntries(value);
// 更新 URL(不触发页面刷新)
const newUrl = value
? `${window.location.pathname}?q=${encodeURIComponent(value)}`
: window.location.pathname;
window.history.replaceState({}, '', newUrl);
};
return (
<div className="search-toolbar">
<input
type="text"
value={query}
onChange={handleChange}
placeholder="搜索设计条目..."
/>
<span className="result-count">{results.length} 个结果</span>
</div>
);
}

这是整个方案的核心输出。我们在项目根目录创建 design.md,结构如下:

除了给 AI 消费的原始 design.md 内容,我们还把 README 和 DESIGN 两份文档放进同一个阅读界面,方便人工校对、复制片段和对照预览结果。

Awesome Design MD Gallery README 与 DESIGN 文档页

# Design Reference for [Project Name]
## 1. Visual Theme & Atmosphere
- 整体风格描述
- 设计哲学和原则
## 2. Color Palette & Roles
- 主色调、辅助色
- 语义化颜色(success、warning、error)
- CSS Variables 定义
## 3. Typography Rules
- 字体家族
- 字号层级(h1-h6, body, small)
- 行高和字重
## 4. Component Stylings
- 按钮样式规范
- 表单组件样式
- 卡片和容器样式
## 5. Layout Principles
- 间距系统
- 网格和断点
- 对齐原则
## 6. Depth & Elevation
- 阴影层级
- z-index 规范
## 7. Do's and Don'ts
- 常见错误和正确做法
## 8. Responsive Behavior
- 断点定义
- 响应式适配规则
## 9. Agent Prompt Guide
- 如何将本文档用于 AI 提示词
- 示例提示词模板

了解了方案之后,具体怎么实施呢?

第一步:初始化子模块

Terminal window
# 添加上游仓库为子模块
git submodule add https://github.com/VoltAgent/awesome-design-md.git vendor/awesome-design-md
# 初始化并更新子模块
git submodule update --init --recursive

第二步:创建内容管线

实现 awesomeDesignCatalog.ts,包括:

  • 文件扫描和校验逻辑
  • Markdown 渲染(使用 Astro 的内置渲染器)
  • 条目数据提取

第三步:构建画廊 UI

使用 Astro + React Islands 创建:

  • 首页画廊布局(卡片网格)
  • 设计卡片组件
  • 搜索工具栏
  • 详情页布局

第四步:编写设计文档

基于 ClickHouse DESIGN.md 结构,填充自己项目的实际设计 token。更新 README.md,添加指向 design.md 的链接。

安全性:Markdown 渲染需要过滤不安全的 HTML。Astro 的内置渲染器默认会过滤 script 标签,但仍需注意 XSS 风险。

性能:大量 iframe 预览可能影响首屏加载。建议使用 loading="lazy" 延迟加载预览内容。

维护性:design.md 需要与代码实现保持同步。建议在 CI 中添加检查,确保 CSS 变量在文档和代码中一致。

可访问性:确保颜色对比度符合 WCAG AA 标准(至少 4.5:1)。

创建 design.md 之后,怎么让 AI 真正用它呢?这里有几个实用技巧:

技巧一:在提示词中明确引用

请参考项目根目录的 design.md 文件,使用其中定义的设计规范来实现以下组件:
- 按钮:使用 primary 色调,圆角 8px
- 卡片:使用 elevation-2 阴影层级

技巧二:要求 AI 引用具体的 CSS 变量

实现一个导航栏,要求:
- 背景色使用 --color-bg-primary
- 边框使用 --color-border-subtle
- 文字使用 --text-color-primary

技巧三:在系统提示词中包含 design.md 内容

如果你的 AI 工具支持自定义系统提示词,可以将 design.md 的核心内容直接添加进去。

内容管线测试

  • 文件缺失场景(缺少 README.md 或 DESIGN.md)
  • 格式错误场景(Markdown 解析失败)
  • 空目录场景

搜索功能测试

  • 空结果处理
  • 特殊字符(如中文、emoji)
  • URL 同步验证

UI 组件测试

  • 明/暗主题切换
  • 响应式布局
  • 预览加载状态
Terminal window
# 1. 更新子模块到最新版本
git submodule update --remote
# 2. 重新构建站点
npm run build
# 3. 部署静态资源
npm run deploy

建议将子模块更新和构建部署自动化,可以在上游仓库更新时自动触发 CI 流程。

HagiCode 在开发过程中遇到的 AI 生成 UI 不一致问题,本质上是缺少结构化的设计参考文档。通过构建设计画廊站点和创建规范化的 design.md,我们成功解决了这个问题。

这套方案的核心价值在于:

  • 统一资源:整合分散的设计系统文档
  • 结构化规范:将设计规范以 AI 可理解的形式呈现
  • 持续维护:通过 git submodule 保持内容更新

如果你也在使用 AI 辅助前端开发,建议尝试一下这个方案。创建一份结构化的 design.md,不仅能提升 AI 生成代码的一致性,也能帮助团队内部保持设计规范的统一。


如果本文对你有帮助:

感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。 本内容采用人工智能辅助协作,最终内容由作者审核并确认。

基于 Starlight 文档站点接入 Microsoft Clarity 的完整实践指南

从数据洞察到用户增长:HagiCode 博客接入 Clarity Analytics 的完整指南

Section titled “从数据洞察到用户增长:HagiCode 博客接入 Clarity Analytics 的完整指南”

本文将分享如何在 Starlight 文档站点中优雅地接入 Microsoft Clarity,不仅能看清用户行为,还能确保隐私合规。这套方案是我们在 HagiCode 项目中实践总结出来的,希望能给同样在折腾数据统计的你一点参考。

以下代码展示了如何在 Astro 集成中根据环境变量动态注入 Microsoft Clarity 脚本,仅在生效时进行生产环境加载。

105 | interface Props {
106 | // 未来可扩展: 允许手动覆盖 Project ID
107 | projectId?: string;
108 | }
109 |
110 | const {
111 | projectId = import.meta.env.CLARITY_PROJECT_ID,
112 | } = Astro.props;
113 |
114 | const isProduction = import.meta.env.PROD;
115 | ---
116 |
117 | {isProduction && projectId && (
118 | <script is:inline define:vars={{projectId}}>
119 | (function(c,l,a,r,i,t,y){

文件:openspec/changes/archive/2026-01-30-microsoft-clarity-integration/design.md

在运营 HagiCode 的过程中,我们一直面临一个”盲盒”问题:我们产出内容,但不清楚用户是如何阅读的。虽然 GitHub 能看到 Star 数,但这太滞后了。我们需要知道:

  • 用户到底有没有看完我们的教程?
  • 那些复杂的配置文档,是在哪一步劝退了用户的?
  • 我们的 SEO 优化是否真的带来了有效流量?

市面上有很多分析工具,比如 Google Analytics(GA)和 Microsoft Clarity。GA 功能强大但配置复杂,且受到隐私法规(如 GDPR)的严格限制。而 Clarity 作为微软推出的免费热力图工具,不仅功能直观,而且在隐私合规上相对宽松,非常适合技术文档站点。

我们的目标很明确:在 HagiCode 的文档站点中无缝集成 Clarity,既要在所有页面生效,又要给用户留有”退出”的权利(隐私合规)。

HagiCode 主题初始化逻辑:优先读取本地存储,回退至系统偏好,默认暗色。

67 | function getInitialTheme(): Theme {
68 | // 1. 检查 localStorage
69 | const stored = localStorage.getItem('hagicode-theme');
70 | if (stored) return stored as Theme;
71 |
72 | // 2. 检测系统偏好
73 | const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
74 | if (systemDark) return 'dark';
75 |
76 | // 3. 默认暗色
77 | return 'dark';
78 | }
79 | ```
80 |
81 | ### 决策 3:主题应用方式
82 |
83 | **选择**:在 `<html>` 根元素设置 `data-theme` 属性
84 |
85 | **对比方案**
86 |

文件:openspec/changes/archive/2026-01-29-theme-toggle-implementation/design.md

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个基于 AI 的代码辅助工具,在开发过程中,我们需要维护大量的技术文档和博客。为了更好地理解用户需求,我们探索并实施了这套数据接入方案。

起初,我们在 Proposal 阶段讨论了多种集成方式。既然我们使用的是 Starlight(基于 Astro 的文档框架),最直观的想法是利用 Astro 的 Hooks。

我们首先尝试了修改 astro.config.mjs,计划在构建时注入 Clarity 脚本。虽然这种方式能保证全局覆盖,但缺乏灵活性——我们无法根据用户的偏好动态加载或卸载脚本。

考虑到用户体验和隐私控制,我们最终决定采用 组件覆盖 的方案。Starlight 允许开发者覆盖其内部组件,这意味着我们可以接管 <footer><head> 的渲染逻辑,从而精细控制 Clarity 的加载时机。

这里有一个小插曲:原本我们想创建一个名为 StarlightWrapper.astro 的布局包装器。但在实际调试中发现,Starlight 的路由机制并不会自动调用这个自定义 Wrapper,这导致脚本在部分页面失效。这算是一个典型的”想当然”踩坑经历,提醒我们必须深入理解框架的渲染流程,而不是盲目套用通用框架模式

为了确保 Clarity 脚本在所有页面(包括文档和博客)加载,并且不破坏原有的页面结构,我们选择了覆盖 Starlight 的 Footer 组件。

  1. 全局性:Footer 几乎在所有标准页面都会出现。
  2. 非侵入性:将脚本放在 Footer 区域(实际渲染在 body 底部)不会阻塞页面的关键渲染路径(LCP),对性能影响最小。
  3. 逻辑集中:可以在组件内部统一处理 Cookie 同意逻辑。

首先,你需要在 Microsoft Clarity 注册并创建一个新项目。获取你的 Project ID(类似 k8z2ab3xxx 这样的字符串)。

下面通过环境变量配置与日期判断代码,实现新年期间的逻辑控制,请参考具体实现。

46 | function isLunarNewYearPeriod() {
47 | const now = new Date();
48 | const year = now.getFullYear();
49 | const month = now.getMonth() + 1; // 1-12
50 | const day = now.getDate();
51 |
52 | // 2025年蛇年新年期间 (1月29日 - 2月12日)
53 | if (year === 2025) {
54 | if (month === 1 && day >= 29) return true;
55 | if (month === 2 && day <= 12) return true;
56 | }
57 | // 2026年马年新年期间 (2月17日 - 3月3日)
58 | if (year === 2026) {
59 | if (month === 2 && day >= 17) return true;
60 | if (month === 3 && day <= 3) return true;
61 | }
62 | return false;
63 | }
64 |
65 | const stored = localStorage.getItem('starlight-theme');

文件:src/pages/index.astro

为了安全起见,不要硬编码 ID。建议将 ID 存入环境变量。

在项目根目录创建 .env 文件:

Terminal window
# Microsoft Clarity ID
PUBLIC_CLARITY_ID="你的_Clarity_ID"

以下是监听系统主题变化的实现代码,展示了如何仅在未手动设置时跟随系统切换主题。

445 | const handleChange = (e: MediaQueryListEvent) => {
446 | // 仅在用户未手动设置时跟随系统
447 | if (!localStorage.getItem(THEME_KEY)) {
448 | setThemeState(e.matches ? 'dark' : 'light');
449 | }
450 | };
451 |
452 | mediaQuery.addEventListener('change', handleChange);
453 | return () => mediaQuery.removeEventListener('change', handleChange);
454 | }, []);
455 |
456 | return { theme, toggleTheme, setTheme: manuallySetTheme };
457 | }
458 | ```
459 |
460 | #### 3. `src/components/ThemeButton.tsx` - 按钮组件
461 |
462 | **职责**:渲染主题切换按钮,处理用户交互
463 |
464 | **组件接口**

文件:openspec/changes/archive/2026-01-29-theme-toggle-implementation/design.md

src/components/ 目录下创建文件 StarlightFooter.astro。Starlight 会自动识别这个文件并覆盖默认的 Footer。

核心代码逻辑如下:

src/components/StarlightFooter.astro
---
// 1. 引入原始组件以保留其默认功能
import DefaultFooter from '@astrojs/starlight/components/StarlightFooter.astro';
// 2. 获取环境变量
const clarityId = import.meta.env.PUBLIC_CLARITY_ID;
// 3. 定义简单的注入脚本(内联方式)
// 注意:生产环境建议将此逻辑抽离到单独的 .js 文件中以利用缓存
const initScript = `
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${clarityId}");
`;
---
<DefaultFooter {...Astro.props} />
{/* 仅在生产环境且 ID 存在时注入脚本 */}
{import.meta.env.PROD && clarityId && (
<script is:inline define:vars={{ clarityId }}>
{initScript}
</script>
)}

关键点解析

  • is:inline:告诉 Astro 不要处理这个 script 标签内的内容,直接输出到 HTML。这对第三方统计脚本至关重要,否则 Astro 的打包优化可能会导致脚本失效。
  • define:vars:这是 Astro 3+ 的特性,允许在作用域内安全地注入变量。
  • import.meta.env.PROD:确保在本地开发时(除非为了调试)不产生无效统计,保持数据纯净。

仅仅加上代码是不够的,特别是在 GDPR 管辖区域。我们需要尊重用户的选择。

HagiCode 的做法是提供一个简单的开关。虽然这不是全功能的 Cookie Banner,但对于纯展示的技术文档站点来说,通常属于”必要”或”统计”类 Cookie,可以通过隐私声明告知并默认开启,或者在 Footer 链接到隐私设置页面。

如果需要更严谨的控制,你可以结合 localStorage 来记录用户的选择:

本文将介绍用于主题切换与持久化的 TypeScript 工具函数,通过类型安全与环境检测实现严谨控制。

367 | export function getInitialTheme(): Theme;
368 | export function getSystemTheme(): Theme;
369 | export function setTheme(theme: Theme): void;
370 | export function applyTheme(theme: Theme): void;
371 | ```
372 |
373 | **设计原则**
374 | - **纯函数**:无副作用(除了 `setTheme``applyTheme`
375 | - **类型安全**:完整的 TypeScript 类型推导
376 | - **环境检测**:SSR 安全(`typeof window` 检查)
377 | - **单一职责**:每个函数只做一件事
378 |
379 | **关键实现**
380 | ```typescript
381 | export function getInitialTheme(): Theme {
382 | if (typeof window === 'undefined') return 'dark';
383 |
384 | const stored = localStorage.getItem(THEME_KEY);
385 | if (stored === 'light' || stored === 'dark') return stored;
386 |

文件:openspec/changes/archive/2026-01-29-theme-toggle-implementation/design.md

// 简单示例:检查用户是否拒绝统计
const consent = localStorage.getItem('clarity_consent');
if (consent !== 'denied') {
// 执行上面的 Clarity 初始化代码
window.clarity('start', clarityId);
}

在将这套方案落地到 HagiCode 的过程中,我们总结了几个容易被忽视的细节:

  1. StarlightWrapper.astro 是个陷阱: 如前所述,不要试图去创建一个全局 Wrapper 来注入脚本,这在 Starlight 中行不通。老老实实覆盖特定组件(如 StarlightFooter.astroStarlightHead.astro)才是正解。

  2. 脚本位置的性能考量: 虽然 Clarity 建议放在 <head> 中以确保数据准确性,但对于文档站点,首屏加载速度(LCP)直接影响了 SEO 和用户留存。我们选择了放在 Footer(Body 底部),这会轻微丢失极少量”秒退”用户的数据,但换来了更快的页面加载体验,这是一个值得的权衡。

  3. 开发环境的干扰: 一定要加上 import.meta.env.PROD 判断。在开发模式下,你会频繁刷新页面,这会产生大量无意义的测试数据,污染你的 Clarity 仪表盘。

部署完成后,你可以在 Clarity 控制台查看实时数据。通常在几分钟内,你就能看到用户的heatmap(热力图)和 recordings(录屏)。

对于 HagiCode 来说,通过这些数据我们发现:

  • 很多用户会反复查看”快速开始”章节,说明我们的安装指引可能还不够直观。
  • “API 参考”页面的停留时间最长,证实了我们核心用户群体的需求。

接入 Microsoft Clarity 并不需要复杂的服务端改造,也不需要引入沉重的 SDK。

利用 Starlight 的组件覆盖机制,我们仅通过一个轻量级的 StarlightFooter.astro 组件,就实现了全局数据统计。这种”微集成”的方式,既保证了代码的整洁,又赋予了我们洞察用户行为的能力。

如果你也在运营技术类项目,特别是像 HagiCode 这样需要不断迭代文档的项目,强烈建议尝试接入 Clarity。数据会告诉你,用户真正的痛点在哪里。


感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮👍,让更多的人看到本文。

本内容采用人工智能辅助协作,经本人审核,符合本人观点与立场。

Docusaurus 3.x 到 Astro 5.x 迁移实战:利用 Islands 架构实现性能与构建速度双重提升

从 Docusaurus 3.x 到 Astro 5.x:HagiCode 站点迁移实战复盘

Section titled “从 Docusaurus 3.x 到 Astro 5.x:HagiCode 站点迁移实战复盘”

本文复盘了我们将 HagiCode 官方网站从 Docusaurus 3.x 迁移至 Astro 5.x 的全过程。我们将深入探讨如何通过 Astro 的 Islands 架构解决性能瓶颈,同时保留现有的 React 组件资产,实现构建速度与加载性能的双重提升。

2026 年 1 月,我们对 HagiCode 的官方站点进行了一次”心脏移植手术”——将核心框架从 Docusaurus 3.x 全面迁移至 Astro 5.x。这不是一次冲动的大重构,而是经过深思熟虑的技术抉择。

在迁移前,我们的站点虽然功能完善,但逐渐显露出一些”富贵病”:构建产物体积臃肿、JavaScript 负载过高,且页面加载速度在复杂文档页面下不够理想。作为一个 AI 代码助手项目,HagiCode 需要频繁更新文档和功能介绍,构建效率直接影响发布速度。同时,我们希望站点对搜索引擎(SEO)更加友好,以便让更多开发者发现这个项目。

为了解决这些痛点,我们做了一个大胆的决定:整个构建系统推倒重来,迁移到 Astro。这个决定带来的变化,可能比你想象的还要大——稍后我会具体说。

本文分享的站点迁移方案,来自我们在 HagiCode 项目中的实践经验。

HagiCode 是一款致力于提升开发效率的 AI 代码助手,我们不仅关注核心功能的迭代,同样重视开发者体验。这次站点的重构,也是为了让用户在浏览文档和官网时能获得极致的加载速度。

在 React 生态中,Docusaurus 一直是文档站点的”标准答案”。它开箱即用,插件丰富,社区活跃。但是,随着 HagiCode 功能的增加,我们也感受到了它的局限性:

  1. 性能瓶颈:Docusaurus 本质上是一个 React SPA(单页应用)。哪怕你是写纯静态页面,客户端也需要加载 React 运行时并进行水合,这对于简单的文档页面来说太重了。
  2. 资源体积:即便页面内容很少,打包后的 JS 体积也相对固定,这对移动端用户和网络较差的环境不够友好。
  3. 灵活性不足:虽然也能扩展,但在构建流程的定制上,我们渴望拥有更底层的控制权。

Astro 的出现正好解决了这些问题。它提供了一个全新的”岛屿架构”(Islands Architecture):默认情况下,Astro 生成零 JavaScript 的静态 HTML,只有需要交互的组件才会”激活”并加载 JS。这意味着我们的站点大部分内容都是纯 HTML,速度极快。

迁移不是简单的复制粘贴,而是思维模式的转变。我们从 Docusaurus 的”全 React 模式”切换到了 Astro 的”Core + Islands”模式。

首先,我们需要从 docusaurus.config.ts 转向 astro.config.mjs。这不仅是文件名的变化,更是路由和构建逻辑的重写。

在 Docusaurus 中,一切皆插件;而在 Astro 中,一切皆集成。我们需要重新定义站点的基础路径、构建输出模式(静态 vs SSR)以及资源压缩策略。

迁移前:

docusaurus.config.ts
export default {
title: 'HagiCode',
url: 'https://hagicode.com',
baseUrl: '/',
// ... 更多配置
};

迁移后:

astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
integrations: [react()],
site: 'https://hagicode.com',
base: '/',
// 针对静态资源的优化配置
build: {
inlineStylesheets: 'auto',
},
});

这是迁移中最头疼的部分。我们现有的站点有很多 React 组件(比如 Tabs 组件、代码高亮、反馈按钮等)。直接扔掉太可惜,全都保留又会导致 JS 负载过重。

HagiCode 采用了渐进式水合策略:

  • 纯静态组件:对于展示型内容(如页眉、页脚、纯文本文档),重写为 Astro 组件(.astro 文件),在构建时直接渲染为 HTML。
  • 交互式岛屿:对于必须保留交互的组件(如主题切换器、Tabs 切换、代码块复制按钮),保留 React 实现,并添加 client:loadclient:visible 指令。

例如,我们的文档中常用的 Tabs 组件:

src/components/Tabs.jsx
import { useState } from 'react';
import './Tabs.css'; // 引入样式
export default function Tabs({ items }) {
const [activeIndex, setActiveIndex] = useState(0);
// ... 状态逻辑
return (
<div className="tabs-wrapper">
{/* 渲染逻辑 */}
</div>
);
}

在 Markdown 中使用时,我们明确告诉 Astro:“这个组件需要 JS”:

src/content/docs/example.mdx
import Tabs from '../../components/Tabs.jsx';
<!-- 只在组件进入视口时才加载 JS -->
<Tabs client:visible items={...} />

这样,非视口内的交互组件不会抢占带宽,极大地优化了首屏加载速度。

3. 样式系统的适配:CSS Modules 到 Scoped

Section titled “3. 样式系统的适配:CSS Modules 到 Scoped”

Docusaurus 默认支持 CSS Modules,而 Astro 推崇使用 Scoped CSS(通过 <style> 标签)。两者的核心思想都是隔离样式,但语法不同。

在 HagiCode 的迁移中,我们将大部分复杂的 CSS Modules 拆解为 Astro 的 Scoped 样式。这其实是件好事,因为在 .astro 文件中,样式和模板写在同一个文件里,维护起来更加直观。

改造前:

Tabs.module.css
.wrapper { background: var(--ifm-background-color); }

改造后 (Astro Scoped):

Tabs.astro
<div class="tabs-wrapper">
<slot />
</div>
<style>
.tabs-wrapper {
/* 直接使用 CSS 变量,适配主题 */
background: var(--bg-color);
padding: 1rem;
}
</style>

同时,我们统一了全局 CSS 变量系统,利用 Astro 的环境感知能力,确保暗色模式在不同页面间的切换无缝衔接。

在 HagiCode 的实际迁移过程中,我们遇到了不少坑,这里挑几个最典型的分享一下。

HagiCode 支持子路径部署(比如部署到 GitHub Pages 的子目录)。在 Docusaurus 中,它自动处理 baseUrl。但在 Astro 中,处理图片链接和 API 请求时,我们需要更小心。

我们引入了环境变量机制来统一管理:

// 在构建脚本中处理路径
const getBasePath = () => import.meta.env.VITE_SITE_BASE || '/';

切记,不要在代码中硬编码 / 开头的路径。在开发环境和生产环境,或者配置了 base 路径后,这会导致资源 404。

我们的旧站点有一些 Node.js 脚本(用于自动抓取 Metrics 数据、更新 sitemap 等),它们是用 CommonJS (require) 写的。Astro 和现代构建工具全面拥抱 ES Modules (import/export)。

如果你也有类似的脚本,记得把它们全部重构为 ES Modules。这是大势所趋,早点改了早点省心。

// 旧方式
const fs = require('fs');
// 新方式
import fs from 'fs';

搜索引擎已经收录了 HagiCode 旧的 Docusaurus 页面。如果直接切到 Astro,URL 结构发生变化,会导致大量 404,权重大跌。

我们在 Astro 中配置了重定向规则:

astro.config.mjs
export default defineConfig({
redirects: {
'/docs/old-path': '/docs/new-path',
// 批量映射旧链接到新链接
}
});

或者在服务器配置层面处理。确保旧链接能 301 重定向到新地址,这对 SEO 至关重要。

从 Docusaurus 迁移到 Astro,对 HagiCode 来说,不仅仅是一次框架升级,更是一次对”性能优先”理念的实践。

我们的收获:

  • 极致的 Lighthouse 分数:迁移后,HagiCode 站点的性能评分轻松接近满分。
  • 更快的构建速度:Astro 的增量构建让我们文档更新的发布时间缩短了一半。
  • 保留了灵活性:通过 Islands 架构,我们没有牺牲任何交互功能,依然可以在需要的地方使用 React。

如果你也在维护文档型站点,并且深受打包体积或加载速度的困扰,不妨试试 Astro。虽然迁移过程需要动动手术(比如把 PCode 的名字改成 HagiCode,把组件一个个挖过来),但换来的是如丝般顺滑的用户体验,绝对值得。

本文分享的构建系统,正是我们在开发 HagiCode 过程中实际踩坑、实际优化出来的方案。如果你觉得这套方案有价值,说明我们的工程实力还不错——那么 HagiCode 本身也值得关注一下。

如果本文对你有帮助,欢迎来 GitHub 给个 Star,公测已经开始啦!


感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮👍,让更多的人看到本文。

本内容采用人工智能辅助协作,经本人审核,符合本人观点与立场。