前端改进方案

改进要求

  1. 二级分类
    1. 每篇文章有两个分类:一级分类first_level_category 和 第二级分类 second_level_category
    2. 第一级分类是大致分类,按照文章的属性分类,例如AI前沿,读诗分享,新闻快讯,数学专题,嵌入式板块,编程合集等等
    3. 第二级分类是该领域内的不同方向分类,例如编程合集里有 pyhtonC++JSVUE
  2. 卡片式呈现文章
    1. 取消目前呈现的”点击分类后以时间线Archive呈现文章的形式”,修改为类似主页呈现文章的卡片式页面
  3. 格式:主页 -> 一级分类列表 -> 二级分类列表 -> 文章列表
  4. 呈现形式:
    1. 一级分类列表:使用 yukina\src\layouts\ChipLayout.astro
    2. 二级分类列表:使用 yukina\src\layouts\ChipLayout.astro
    3. 点击进入文章列表时,用的 yukina\src\components\PostCard.astro 组件

改进方案

第 1 步:修改内容数据模型 (src/content.config.ts)

  1. 更新数据模型:将原有的单一分类字段 category: z.string().optional() 替换为二级分类结构:

    first_level_category: z.string(),    // 一级分类(必填)
    second_level_category: z.string(),   // 二级分类(必填)
  2. 组件接口适配

    • PostCard组件 (src/components/PostCard.astro):

      • 更新Props接口支持二级分类字段
      • 修改分类链接生成逻辑,点击分类标签跳转到一级分类页面
      • 保持文章标题/图片点击跳转到文章详情的行为不变
    • 数据传递优化 (src/utils/content.ts):

      • 新增 postCard 接口,包含完整的卡片展示数据
      • 统一数据传递格式:传递原始ID,由组件统一处理URL生成
      • 确保HASH/RAW模式兼容性

第 2 步:实现分类处理和路由系统

  1. 分类数据处理方法 (src/utils/content.ts):

    • getFirstCategories():

      • 获取所有一级分类及其文章数量
      • 生成一级分类页面所需的chip数据
      • 支持 /categories/index.astro 页面展示
    • getSecondCategories():

      • 处理二级分类数据,构建完整分类路径
      • 为每个二级分类收集对应的文章列表
      • 重要优化:对文章按发布日期降序排序(最新在前)
      • 生成postCard格式数据,包含分类信息
  2. 路由结构设计

    /categories/                              # 一级分类列表
    /categories/[first_category]/             # 二级分类列表
    /categories/[first_category]/[second_category]/  # 文章列表
  3. URL生成策略

    • 支持HASH和RAW两种slug模式
    • 统一使用 IdToSlug() 函数处理URL转换
    • 解决URL重复前缀问题,避免404错误

第 3 步:优化页面布局和用户体验

  1. 布局组件选择

    • 一级分类页面 (/categories/[first_category].astro):

      • 使用 ChipLayout.astro 展示二级分类选项
      • 显示每个二级分类下的文章数量
    • 二级分类页面 (/categories/[first_category]/[second_category].astro):

      • 使用 PostCard.astro 组件以卡片网格形式展示文章
      • 响应式设计:手机单列,平板双列,桌面三列(可调整)
      • 关键改进:文章按发布时间降序排列,最新文章显示在前
  2. 数据流优化

    // 原始数据 → 处理函数 → 页面组件
    MD文档 → getSecondCategories() → PostCard组件
    
    // 关键改进:排序逻辑
    category.posts.sort((a, b) => {
      return new Date(a.published) > new Date(b.published) ? -1 : 1;
    });
  3. 用户体验提升

    • 分类信息正确显示在PostCard组件中
    • 点击分类标签导航到对应分类页面
    • 最新文章优先显示,无需下滑查找
    • URL结构清晰,支持直接访问和书签保存

改进效果

完成的功能

  1. 二级分类系统

    • ✅ 数据模型支持:first_level_categorysecond_level_category
    • ✅ 路由结构:主页 → 一级分类列表 → 二级分类列表 → 文章列表
    • ✅ URL模式支持:兼容HASH和RAW两种模式
  2. 组件和布局优化

    • ✅ 一级分类页面:使用 ChipLayout.astro 展示分类卡片
    • ✅ 二级分类页面:使用 PostCard.astro 卡片式展示文章
    • ✅ 响应式设计:手机单列,平板双列,桌面三列(可调整)
  3. URL生成优化

    • ✅ 统一数据传递架构:原始ID传递,组件统一处理
    • ✅ 模式无关设计:代码支持HASH/RAW模式无缝切换
    • ✅ 404问题修复:解决URL重复前缀导致的路由错误
  4. 用户体验提升

    • ✅ 文章排序:分类页面按发布日期降序排列(最新在前)
    • ✅ 分类信息显示: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网格)

技术实现要点

  1. 数据接口设计

    // 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 };
    }
  2. 关键函数

    • getFirstCategories(): 处理一级分类数据
    • getSecondCategories(): 处理二级分类数据,包含文章排序
    • 统一的URL生成逻辑:IdToSlug() 根据配置模式处理
  3. 排序优化

    // 二级分类页面文章按日期降序排序
    category.posts.sort((a, b) => {
      const dateA = new Date(a.published);
      const dateB = new Date(b.published);
      return dateA > dateB ? -1 : 1; // 最新文章在前
    });

文档分类管理

修改分类名操作步骤

  1. 批量替换MD文档中的分类字段
  2. 重新构建项目:npm run build
  3. URL自动更新(RAW模式直观可读,HASH模式生成新hash)

注意事项

  • 修改分类名会导致URL变化
  • 删除分类下所有文档会导致该分类页面消失
  • 建议使用RAW模式,便于管理和调试

额外改进—添加浮动目录组件

功能升级:实现浮动式文章目录 (TOC) 组件

问题背景与方案确立

​ 最初尝试使用 Typora 内置的 [TOC] 语法来生成目录,但是,该语法属于特定 Markdown 编辑器的私有扩展,并非通用标准,因此浏览器和 Astro 的构建工具无法将其解析为有效的目录。

​ 为了在 Web 环境中构建一个健壮且灵活的目录,采用标准化的技术路径:在构建时提取文章标题数据,并将其传递给一个独立的前端组件进行动态渲染。

​ 此方案核心优势在于解耦:文章内容(数据)与目录(表现)分离。这与直接在文章内注入 HTML 的 mdast-util-toc 等工具不同,此方法为实现浮动、高亮等复杂的 UI 交互提供了坚实的基础。

核心架构

​ 该功能的实现依赖于 Astro 框架的原生能力和现代前端技术,主要包含三大模块:

  1. 数据提取 Astro 在处理 Markdown 内容时,通过其 render() 方法,会自动解析文章结构并返回一个 headings 数组。这是整个功能的数据源,无需任何外部 Remark 插件。每个标题对象包含:

    • depth: 标题级别 (例如 h2 对应 2)。
    • text: 标题的纯文本内容。
    • slug: 根据标题文本生成的、URL友好的唯一ID,Astro 会自动将其作为 id 属性注入到对应的 <h2> 等标签中。
  2. 组件渲染

    ​ 创建一个独立的 Svelte 组件 (FloatingTOC.svelte),它接收 headings 数组作为唯一的 prop。组件的职责是遍历此数组,将其渲染为一个包含锚点链接(<a href="#slug">)的列表。这种数据驱动的模式保证了组件的复用性和可维护性。

  3. 前端交互

    • 锚点跳转:

      ​ 利用浏览器原生功能。Astro 已为每个标题生成了 id,因此点击目录中的链接即可触发浏览器自带的页面内跳转。

    • 浮动定位:

      ​ 通过 CSS position: fixed 属性,将目录组件固定在视口的指定位置,使其不随页面滚动而移动。

    • 动态高亮:

      ​ 为提升用户体验,使用 Intersection Observer API 来高效监听文章中的标题元素。当某个标题进入预设的视口区域时,JavaScript 会为目录中对应的链接添加 active 状态类,从而实现滚动时目录的自动高亮,且性能开销极低。

可行性分析

完全正确且可行:

  1. 获取目录数据

    • Astro 内置了标题提取功能,无需额外插件
    • 通过 post.render() 自动获取 headings 数组
    • 包含:depth(级别)、text(文本)、slug(锚点ID)
  2. 传递给浮动UI组件

    • 使用Astro的数据驱动架构
    • 将headings数组直接传递给Svelte组件
    • 完全解耦,不影响文章内容
  3. 文章内容保持不变

    • Astro自动为标题生成id属性(如 <h2 id="hello-world">
    • 浏览器原生支持锚点跳转 #hello-world
    • 无需修改markdown源文件

技术优势

比之前的mdast-util-toc方案更优:

  • 之前的问题:使用外部插件,依赖复杂,容易出错
  • 新方案优势:
    • 使用Astro内置功能,零额外依赖
    • 数据提取更可靠,无解析错误风险
    • 代码更简洁,维护性更好

实现步骤(简化版)

1. 修改文章页面:获取Astro内置的headings数据
2. 创建Svelte组件:接收headings数组,渲染浮动TOC
3. 添加交互功能:滚动高亮、平滑跳转

关键点:不需要任何remark插件,完全基于Astro原生功能

实施详情

已完成

  1. 原生集成,零依赖:
    • 完全基于 Astro 内置的 render() 方法提取 headings 数组,摒弃了 mdast-util-toc 等外部插件,消除了潜在的依赖冲突和解析错误风险。
  2. 独立的浮动组件:
    • FloatingTOC.svelte 组件负责所有 UI 渲染,支持多级标题的缩进展示,与文章内容完全解耦。
  3. 高级交互体验:
    • 智能高亮:基于 Intersection Observer API 实时高亮当前阅读区域对应的目录项。
  4. 精细的 UI/UX 设计:
    • 定制化主题:采用muzimi颜色主题,并应用毛玻璃效果 (backdrop-filter) 提升视觉质感。
    • 深色模式自适应:自动响应操作系统的颜色模式偏好。
    • 响应式布局:在桌面端(>1200px)默认显示,在平板和移动设备上自动隐藏,以保证内容阅读的优先性。

技术实现要点

  1. 数据流程

    Markdown文件 → Astro render() → headings数组 → Svelte组件
  2. 核心文件

    // src/pages/posts/[...slug].astro
    const { Content, headings } = await render(entry);
    
    // 移到布局外部确保真正浮动
    <>
      <PostLayout>...</PostLayout>
      <FloatingTOC headings={headings} client:load />
    </>
  3. 组件接口

    // src/components/FloatingTOC.svelte
    export let headings: Array<{
      depth: number;    // 标题级别 (1-6)
      text: string;     // 标题文本
      slug: string;     // 锚点ID
    }> = [];
  4. 滚动监听优化

    // 使用现代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 的页面切换钩子中。

关键实现

  1. ScriptSetup.astro 中添加 setupFloatingTOC() 函数
  2. 集成到 Swup 的 content:replace 钩子中,确保每次页面切换都重新创建TOC
  3. 添加旧组件清理逻辑,防止内存泄漏

核心代码

// 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,搜索功能在开发和构建模式下都无法使用。

原因分析

  1. 缺少Sharp依赖:Astro构建过程中因为缺少 sharp 图像处理库导致构建失败,pagefind 索引无法生成
  2. 开发模式限制:pagefind 文件只在**构建(build)**时生成,开发模式下 /pagefind/pagefind.js 路径不存在

解决思路

  1. 安装缺失的依赖解决构建问题
  2. 为开发模式实现优雅降级方案

解决方案

步骤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/ 完整搜索功能正常工作

技术要点总结

  1. SPA导航兼容性:使用Swup的项目中,需要将组件逻辑集成到页面切换钩子中
  2. 资源依赖管理:确保构建时依赖完整,特别是图像处理相关的Sharp库
  3. 开发生产环境差异:为不同环境提供适当的降级方案,避免开发时的错误中断
Author

JuyaoHuang

Publish Date

10 - 01 - 2025