前端改进方案
改进要求
- 二级分类:
- 每篇文章有两个分类:一级分类
first_level_category和 第二级分类second_level_category - 第一级分类是大致分类,按照文章的属性分类,例如
AI前沿,读诗分享,新闻快讯,数学专题,嵌入式板块,编程合集等等 - 第二级分类是该领域内的不同方向分类,例如
编程合集里有pyhton,C++,JS,VUE
- 每篇文章有两个分类:一级分类
- 卡片式呈现文章
- 取消目前呈现的”点击分类后以时间线Archive呈现文章的形式”,修改为类似主页呈现文章的卡片式页面
- 格式:主页 -> 一级分类列表 -> 二级分类列表 -> 文章列表
- 呈现形式:
- 一级分类列表:使用 yukina\src\layouts\ChipLayout.astro
- 二级分类列表:使用 yukina\src\layouts\ChipLayout.astro
- 点击进入文章列表时,用的 yukina\src\components\PostCard.astro 组件
改进方案
第 1 步:修改内容数据模型 (src/content.config.ts)
-
更新数据模型:将原有的单一分类字段
category: z.string().optional()替换为二级分类结构:first_level_category: z.string(), // 一级分类(必填) second_level_category: z.string(), // 二级分类(必填) -
组件接口适配:
-
PostCard组件 (
src/components/PostCard.astro):- 更新Props接口支持二级分类字段
- 修改分类链接生成逻辑,点击分类标签跳转到一级分类页面
- 保持文章标题/图片点击跳转到文章详情的行为不变
-
数据传递优化 (
src/utils/content.ts):- 新增
postCard接口,包含完整的卡片展示数据 - 统一数据传递格式:传递原始ID,由组件统一处理URL生成
- 确保HASH/RAW模式兼容性
- 新增
-
第 2 步:实现分类处理和路由系统
-
分类数据处理方法 (
src/utils/content.ts):-
getFirstCategories():
- 获取所有一级分类及其文章数量
- 生成一级分类页面所需的chip数据
- 支持
/categories/index.astro页面展示
-
getSecondCategories():
- 处理二级分类数据,构建完整分类路径
- 为每个二级分类收集对应的文章列表
- 重要优化:对文章按发布日期降序排序(最新在前)
- 生成postCard格式数据,包含分类信息
-
-
路由结构设计:
/categories/ # 一级分类列表 /categories/[first_category]/ # 二级分类列表 /categories/[first_category]/[second_category]/ # 文章列表 -
URL生成策略:
- 支持HASH和RAW两种slug模式
- 统一使用
IdToSlug()函数处理URL转换 - 解决URL重复前缀问题,避免404错误
第 3 步:优化页面布局和用户体验
-
布局组件选择:
-
一级分类页面 (
/categories/[first_category].astro):- 使用
ChipLayout.astro展示二级分类选项 - 显示每个二级分类下的文章数量
- 使用
-
二级分类页面 (
/categories/[first_category]/[second_category].astro):- 使用
PostCard.astro组件以卡片网格形式展示文章 - 响应式设计:手机单列,平板双列,桌面三列(可调整)
- 关键改进:文章按发布时间降序排列,最新文章显示在前
- 使用
-
-
数据流优化:
// 原始数据 → 处理函数 → 页面组件 MD文档 → getSecondCategories() → PostCard组件 // 关键改进:排序逻辑 category.posts.sort((a, b) => { return new Date(a.published) > new Date(b.published) ? -1 : 1; }); -
用户体验提升:
- 分类信息正确显示在PostCard组件中
- 点击分类标签导航到对应分类页面
- 最新文章优先显示,无需下滑查找
- URL结构清晰,支持直接访问和书签保存
改进效果
完成的功能
-
二级分类系统:
- ✅ 数据模型支持:
first_level_category和second_level_category - ✅ 路由结构:主页 → 一级分类列表 → 二级分类列表 → 文章列表
- ✅ URL模式支持:兼容HASH和RAW两种模式
- ✅ 数据模型支持:
-
组件和布局优化:
- ✅ 一级分类页面:使用
ChipLayout.astro展示分类卡片 - ✅ 二级分类页面:使用
PostCard.astro卡片式展示文章 - ✅ 响应式设计:手机单列,平板双列,桌面三列(可调整)
- ✅ 一级分类页面:使用
-
URL生成优化:
- ✅ 统一数据传递架构:原始ID传递,组件统一处理
- ✅ 模式无关设计:代码支持HASH/RAW模式无缝切换
- ✅ 404问题修复:解决URL重复前缀导致的路由错误
-
用户体验提升:
- ✅ 文章排序:分类页面按发布日期降序排列(最新在前)
- ✅ 分类信息显示:PostCard组件正确显示一级和二级分类
- ✅ 导航体验:点击分类标签跳转到一级分类页面
实际路由结构
/categories/ # 一级分类列表(ChipLayout)
├── /categories/blog理解文档/ # 二级分类列表(ChipLayout)
│ ├── /categories/blog理解文档/blog/ # 文章列表(PostCard网格)
│ └── /categories/blog理解文档/blogs/ # 文章列表(PostCard网格)
├── /categories/Examples/ # 二级分类列表(ChipLayout)
│ └── /categories/Examples/ex/ # 文章列表(PostCard网格)
└── /categories/数学理论/ # 二级分类列表(ChipLayout)
└── /categories/数学理论/数学/ # 文章列表(PostCard网格)
技术实现要点
-
数据接口设计:
// src/utils/content.ts export interface postCard { id: string; title: string; published: Date; first_level_category?: string; // 新增 second_level_category?: string; // 新增 tags?: string[]; description?: string; image?: string; readingMetadata?: { time: number; wordCount: number }; } -
关键函数:
getFirstCategories(): 处理一级分类数据getSecondCategories(): 处理二级分类数据,包含文章排序- 统一的URL生成逻辑:
IdToSlug()根据配置模式处理
-
排序优化:
// 二级分类页面文章按日期降序排序 category.posts.sort((a, b) => { const dateA = new Date(a.published); const dateB = new Date(b.published); return dateA > dateB ? -1 : 1; // 最新文章在前 });
文档分类管理
修改分类名操作步骤:
- 批量替换MD文档中的分类字段
- 重新构建项目:
npm run build - URL自动更新(RAW模式直观可读,HASH模式生成新hash)
注意事项:
- 修改分类名会导致URL变化
- 删除分类下所有文档会导致该分类页面消失
- 建议使用RAW模式,便于管理和调试
额外改进—添加浮动目录组件
功能升级:实现浮动式文章目录 (TOC) 组件
问题背景与方案确立
最初尝试使用 Typora 内置的 [TOC] 语法来生成目录,但是,该语法属于特定 Markdown 编辑器的私有扩展,并非通用标准,因此浏览器和 Astro 的构建工具无法将其解析为有效的目录。
为了在 Web 环境中构建一个健壮且灵活的目录,采用标准化的技术路径:在构建时提取文章标题数据,并将其传递给一个独立的前端组件进行动态渲染。
此方案核心优势在于解耦:文章内容(数据)与目录(表现)分离。这与直接在文章内注入 HTML 的 mdast-util-toc 等工具不同,此方法为实现浮动、高亮等复杂的 UI 交互提供了坚实的基础。
核心架构
该功能的实现依赖于 Astro 框架的原生能力和现代前端技术,主要包含三大模块:
-
数据提取 Astro 在处理 Markdown 内容时,通过其 render() 方法,会自动解析文章结构并返回一个 headings 数组。这是整个功能的数据源,无需任何外部 Remark 插件。每个标题对象包含:
- depth: 标题级别 (例如 h2 对应 2)。
- text: 标题的纯文本内容。
- slug: 根据标题文本生成的、URL友好的唯一ID,Astro 会自动将其作为 id 属性注入到对应的
<h2>等标签中。
-
组件渲染
创建一个独立的 Svelte 组件 (FloatingTOC.svelte),它接收 headings 数组作为唯一的 prop。组件的职责是遍历此数组,将其渲染为一个包含锚点链接(
<a href="#slug">)的列表。这种数据驱动的模式保证了组件的复用性和可维护性。 -
前端交互
-
锚点跳转:
利用浏览器原生功能。Astro 已为每个标题生成了 id,因此点击目录中的链接即可触发浏览器自带的页面内跳转。
-
浮动定位:
通过 CSS position: fixed 属性,将目录组件固定在视口的指定位置,使其不随页面滚动而移动。
-
动态高亮:
为提升用户体验,使用 Intersection Observer API 来高效监听文章中的标题元素。当某个标题进入预设的视口区域时,JavaScript 会为目录中对应的链接添加 active 状态类,从而实现滚动时目录的自动高亮,且性能开销极低。
-
可行性分析
完全正确且可行:
-
获取目录数据
- Astro 内置了标题提取功能,无需额外插件
- 通过 post.render() 自动获取 headings 数组
- 包含:depth(级别)、text(文本)、slug(锚点ID)
-
传递给浮动UI组件
- 使用Astro的数据驱动架构
- 将headings数组直接传递给Svelte组件
- 完全解耦,不影响文章内容
-
文章内容保持不变
- Astro自动为标题生成id属性(如
<h2 id="hello-world">) - 浏览器原生支持锚点跳转
#hello-world - 无需修改markdown源文件
- Astro自动为标题生成id属性(如
技术优势
比之前的mdast-util-toc方案更优:
- 之前的问题:使用外部插件,依赖复杂,容易出错
- 新方案优势:
- 使用Astro内置功能,零额外依赖
- 数据提取更可靠,无解析错误风险
- 代码更简洁,维护性更好
实现步骤(简化版)
1. 修改文章页面:获取Astro内置的headings数据
2. 创建Svelte组件:接收headings数组,渲染浮动TOC
3. 添加交互功能:滚动高亮、平滑跳转
关键点:不需要任何remark插件,完全基于Astro原生功能
实施详情
已完成
- 原生集成,零依赖:
- 完全基于 Astro 内置的 render() 方法提取 headings 数组,摒弃了 mdast-util-toc 等外部插件,消除了潜在的依赖冲突和解析错误风险。
- 独立的浮动组件:
- FloatingTOC.svelte 组件负责所有 UI 渲染,支持多级标题的缩进展示,与文章内容完全解耦。
- 高级交互体验:
- 智能高亮:基于 Intersection Observer API 实时高亮当前阅读区域对应的目录项。
- 精细的 UI/UX 设计:
- 定制化主题:采用muzimi颜色主题,并应用毛玻璃效果 (backdrop-filter) 提升视觉质感。
- 深色模式自适应:自动响应操作系统的颜色模式偏好。
- 响应式布局:在桌面端(>1200px)默认显示,在平板和移动设备上自动隐藏,以保证内容阅读的优先性。
技术实现要点
-
数据流程:
Markdown文件 → Astro render() → headings数组 → Svelte组件 -
核心文件:
// src/pages/posts/[...slug].astro const { Content, headings } = await render(entry); // 移到布局外部确保真正浮动 <> <PostLayout>...</PostLayout> <FloatingTOC headings={headings} client:load /> </> -
组件接口:
// src/components/FloatingTOC.svelte export let headings: Array<{ depth: number; // 标题级别 (1-6) text: string; // 标题文本 slug: string; // 锚点ID }> = []; -
滚动监听优化:
// 使用现代Intersection Observer API observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting && entry.boundingClientRect.top <= 100) { activeId = entry.target.id; } }); }, { rootMargin: '-100px 0px -80% 0px', threshold: 0 });
用户体验
访问效果:
- 桌面端:左侧显示浮动目录,随滚动智能高亮
- 平板/手机:自动隐藏,不影响阅读体验
- 交互方式:点击目录项跳转到对应标题
- 视觉反馈:当前阅读位置的标题高亮显示
注意事项:
- 完全基于浏览器原生功能,兼容性极佳
- 不修改markdown源文件,文档编辑不受影响
- 只在有标题的文章中显示,无标题文章不会出现空组件
新的问题:
问题一:浮动目录切换失效
问题描述:
目录组件能够正确出现,但是切换文章的时候面板内容不会切换,仍然是上一篇文章的目录,需要手动刷新页面才行。
原因分析:
因为 FloatingTOC 组件使用了 client:load 指令,在 Astro 的页面间导航时,JavaScript组件实例可能不会重新初始化。项目使用了 Swup 进行页面间的 SPA 导航,导致 Svelte 组件无法正确响应页面变化。
解决方案:放弃 Svelte 组件方案,改用纯 JavaScript 实现,并集成到 Swup 的页面切换钩子中。
关键实现:
- 在
ScriptSetup.astro中添加setupFloatingTOC()函数 - 集成到 Swup 的
content:replace钩子中,确保每次页面切换都重新创建TOC - 添加旧组件清理逻辑,防止内存泄漏
核心代码:
// ScriptSetup.astro
const setupFloatingTOC = () => {
// 清理旧的TOC组件
const existingTOC = document.querySelector('.floating-toc');
if (existingTOC) {
if ((existingTOC as any)._observer) {
(existingTOC as any)._observer.disconnect();
}
existingTOC.remove();
}
// 检查是否为文章页面
const isPostPage = window.location.pathname.startsWith('/posts/');
if (!isPostPage) return;
// 动态提取页面标题并创建TOC
const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'))
.filter(heading => heading.id)
.map(heading => ({
depth: parseInt(heading.tagName.charAt(1)),
text: heading.textContent?.trim() || '',
slug: heading.id
}));
// 创建并插入TOC DOM结构
// ... 完整实现见源码
};
// 集成到Swup钩子
document.addEventListener("swup:enable", () => {
window.swup.hooks.on("content:replace", setup);
});
问题二:搜索功能失效
问题描述:控制台报错 pagefind is not defined,搜索功能在开发和构建模式下都无法使用。
原因分析:
- 缺少Sharp依赖:Astro构建过程中因为缺少
sharp图像处理库导致构建失败,pagefind 索引无法生成 - 开发模式限制:pagefind 文件只在**构建(build)**时生成,开发模式下
/pagefind/pagefind.js路径不存在
解决思路:
- 安装缺失的依赖解决构建问题
- 为开发模式实现优雅降级方案
解决方案:
步骤1:修复构建依赖
pnpm add sharp
步骤2:改进pagefind加载逻辑
// NavBar.astro
async function loadPagefind() {
try {
const pagefind = await import("/pagefind/pagefind.js");
await pagefind.options({
excerptLength: 20,
});
pagefind.init();
window.pagefind = pagefind;
pagefind.search("");
console.log('Pagefind loaded successfully');
} catch (error) {
console.warn('Pagefind not available (development mode):', error.message);
// 开发模式降级:创建mock对象防止错误
window.pagefind = {
search: async () => ({ results: [] }),
options: async () => {},
init: () => {}
};
}
}
验证结果:
- 开发模式:
http://localhost:4323/运行无错误,使用mock搜索对象 - 构建模式:
[pagefind] Pagefind indexed 27 pages成功生成索引 - 预览模式:
http://localhost:4322/完整搜索功能正常工作
技术要点总结
- SPA导航兼容性:使用Swup的项目中,需要将组件逻辑集成到页面切换钩子中
- 资源依赖管理:确保构建时依赖完整,特别是图像处理相关的Sharp库
- 开发生产环境差异:为不同环境提供适当的降级方案,避免开发时的错误中断