Astro MangoCat主题开发日志

关于这个项目是怎么写出来的,其实是用AI手搓出来的,AI负责写功能,我负责写样式。 样式当然也不是凭空想出来的,而是借鉴一些主题项目: Pure Clarity 吐司气泡

虽然最终的样式是朝着我的预期方向发展的,但是在这个过程中也有很多问题不断堆积了下来,首先是markdown的样式格式,这个完完全全是手搓的,很多地方并不完美,我很希望有个插件能一次性解决这个问题,而不是需要什么样式就直接往markdown.css中写,代码就会越写越💩,其次是CSS堆积,因为我并没有使用原子样式TailWindCSS,用于约束布局和格式的样式都会写在global.css中,而单页面的专属样式则直接写在单页面组件中,虽然改是很好改,但是代码太多了…这是让我困扰的一点,这些问题往往会模糊我对项目整个结构的理解(可恶!写一个主题,CSS写成屎山了😠😠😠)

不过在后续的开发中,我会不断对其进行优化,同时也会整理出整个项目实现结构…

基于简洁的理念,我删除了很多实际浏览时几乎用不到的功能,比如:

  • 多选项翻页功能:我仅提供了翻页和显示总页数的功能,我个人认为翻第三页,第四页几乎用不到,还有一次性翻到最后一页的功能也用不到,如果要进行精准翻页,直接在URL中输入页码即可。
  • 标签页:我认为分类的权重更高,直接使用分类会更方便,当然了,我现在是这么认为的,后续可能会考虑添加标签页功能。
  • 文章封面:虽然我考虑过添加文章封面功能,但我目前还没有一个稳定的图床…

开发日志

[2026-02-26]

  1. 新增首页头Topcategory顶部快捷分类组件

  2. 新增头部Introduction介绍组件

  3. 删除搜索组件

  4. 调整整体宽度至900px

  5. 调整圆角幅度至12px

  6. 优化移动端导航栏UI样式

[2026-03-01] v1.5

  1. 替换图标库为Tabler
  2. 调整文字大小

[2026-03-04] v1.6

  1. 优化文章字体大小与颜色

  2. 优化文章头部信息样式

  3. 优化统一文字颜色

  4. 删除文章页内代码块阴影

  5. 删除文章页内代码块行号

[2026-03-04] v1.6a

  1. 修复移动端缩进样式

[2026-03-04] v1.7-v1.7c

  1. 优化行内代码样式

  2. 优化文章跳转链接样式

  3. 优化文章头部标题与信息栏样式

  4. 优化文章信息栏样式

  5. 优化部分字体大小样式

  6. 优化主页头部介绍组件样式

  7. 自定义显示主页分类导航卡片

  8. 修复container容器上下缩进问题

[2026-03-17] v2.0

  1. 重构导航栏

[2026-03-18] v2.0 - v2.6

  1. 优化导航栏LOGO样式
  2. 优化文章卡片布局样式
  3. 优化文章页头部样式
  4. 替换新的主页顶部介绍
  5. 优化导航栏按钮样式
  6. 优化文章部分排版样式
  7. 优化项目页样式
  8. 更换新字体

[2026-03-20] v3.0 - v4.0

  1. 重构文章头部样式:新增顶部封面

  2. 顶部介绍居中

  3. 新增关于页面

  4. 重构文章Markdown排版样式:github-markdown-css插件支持

  5. 删除友链页预设代码块

[2026-03-20] v4.0 - v4.2

  1. 升级Astro V6

  2. 重构文章集合,将id作为文章唯一标识并保留slug

  3. 修复暗色主题下切换路由卡白色主题帧问题

  4. 优化项目页面:新增Github贡献图标

  5. 优化关于页

  6. 优化文章顶部封面与信息栏样式

  7. 优化Markdown排版样式:调整字体间距,美化引用块,调整正文与标题大小。

[2026-03-20] v4.2 - v4.4

  1. 调整封面位置:没有封面时,PC端信息栏居中,默认居左。

  2. 重构鱼塘页面:删除鱼塘组件,仅使用circle作为主体

  3. 重构鱼塘页面布局:采用新的排版方式和RSS解析逻辑

  4. 优化鱼塘页面样式:调整文章卡片间距。

  5. 优化markdown部分排版样式

[2026-03-20] v4.5

  1. 优化鱼塘的卡片的排列方式、采用JS Masonry瀑布流、删除随机排序功能
  2. 优化项目页样式:调整标题位置。
  3. 优化部分代码。

[2026-03-20] v4.6

  1. 新增Github-Markdown-Alerts提示语法功能
  2. 优化部分代码

[2026-03-25] v4.7

  1. 修复文章页脚翻页bug
  2. 修复封面闪烁bug

[2026-03-19] 从去年的12月份到现在,经历了从0.1Beta版到如今3.0正式版的迭代,MangoCat不论是样式还是设计思路都得到了很大提升,它越来越接近整个较为成熟的主题。

说说MangoCat的设计思路吧:其实从一开始,我完全是按照自己的基本思路进行开发的(这里的设计思路包括:组件布局的排版,UI设计风格和所需功能),但到0.8版本时我已经没有其他的设计思路了。

于是这个时候我开始去网上参考各种主题的设计风格,尝试着去学习和借鉴其他人的设计风格…结合目前的项目状况,最终形成了一个自己独特的设计风格:以简洁为基础,多的不要,少的补全。不仅是MangoCat这个主题,甚至其他项目我也会按照这种设计风格进行开发。

3月19日,截至当前,MangoCat的设计思路已经基本确定,样式和功能进行样式和功能进行优化即可,特别是文章Markdown的排版样式格式,我已经不想在这个项目上花太多时间了,我很想再去搞其他项目:到现在我连个图床都没有,个人主页也很敷衍,所以,我会尽全力完善MangoCat剩余的不足。

例如:

  • 主页文章列表借鉴了:Pure
  • 导航栏与主页头像设计借鉴了:吐司气泡
  • 分类、归档页和文章格式借鉴的是:纸鹿摸鱼处

截至现在,MangoCat的框架已经基本稳定,后续我只需要对样式和功能进行优化即可,例如文章Markdown的排版样式格式,

CSS关键类

.container:通用容器,用于包裹页面的内容(格式化内容布局)。 .article-container:通用容器,用于包裹文章内容(格式化文章布局)。

.container基本上包揽了一切,它被直接套在Layout组件中,用于包裹页面的内容,所以所有布局都被该容器所约束,达到样式的统一管理。 .article-container则是用于包裹文章内容的容器,它被直接套在文章组件中,用于格式化文章的布局,当然该容器也被.container所约束,达到样式的统一管理。

布局组件

  1. layouts目录用于存放布局组件的目录,它作为一个的容器,用于包裹页面的内容。
---
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
const { pagetitle, children } = Astro.props;

import "../styles/golbal.css";
import "../styles/markdown.css";
import "animate.css";
---
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>#</title>
  </head>
  <body>
    <Header />
      <slot />
      <Footer />
    </div>
  </body>
</html>
  1. 我们可以写好一些组件,如导航栏(Header.astro)、页脚(Footer.astro)等,直接在布局组件中引入即可。
  2. 当我们将全局样式(如 normalize.css、animate.css 等)引入到布局组件中时,这些样式会应用到当前所有页面中。
  3. 可以理解为布局组件是一个模板,它定义了页面的通用结构和样式,而具体的页面内容则由插槽()来填充。
  4. 我们也可以通过将内容写在Layout组件中,例如将PostList组件写在Layout组件中,它将会被作为参数传递给Layout组件的插槽(),随后被统一渲染出来。
---
import Layout from '../layouts/Layout.astro';
---
<Layout>
      <PostList posts={paginatedPosts} />
</Layout>

共享组件

在 Astro 中,Layouts 目录(通常是 src/layouts)是一个专门用来存放布局组件的目录。布局组件是 Astro 中一种特殊的组件,它们定义了你的网站页面的通用结构和样式,比如 HTML 结构、头部、尾部、导航栏等等。

Layouts 目录的作用和意义:

  1. 统一页面结构: Layouts 目录允许你将网站的通用页面结构定义在一个或多个布局组件中。 这样,你就可以在多个页面中重复使用这些布局,而无需在每个页面中都编写相同的 HTML 结构。

  2. 代码复用: 通过使用布局组件,你可以避免代码重复,提高代码的可维护性。 如果你需要修改网站的整体结构,只需要修改相应的布局组件,所有使用该布局的页面都会自动更新。

  3. 简化页面开发: 布局组件可以让你专注于编写页面的核心内容,而不用关心页面的通用结构。 这可以大大提高开发效率。

  4. SEO 友好: 通过在布局组件中设置统一的 HTML 结构、meta 标签和其他 SEO 相关的设置,你可以确保网站的每个页面都符合 SEO 的最佳实践。

布局组件的特点:

包含<slot />: 布局组件通常包含一个或多个 标签。 是一个占位符,用于指定应该在哪里渲染页面中的内容。 可以接收 Props: 布局组件可以接收 Props(属性),允许你根据不同的页面传递不同的数据。 例如,你可以在布局组件中定义一个 title Prop,用于设置页面的标题。

Layouts 目录的常见用法:

  • 定义网站的通用结构: 在 Layouts 目录中创建一个 BaseLayout.astro 组件,定义网站的通用 HTML 结构、头部、尾部和导航栏。 创建不同的页面布局: 如果你需要不同的页面布局 (例如,博客文章页面和普通页面),可以在 Layouts 目录中创建多个不同的布局组件。 嵌套布局: 你可以将布局组件嵌套在一起,构建更复杂的页面结构。 简单示例:

共享html格式

  1. 创建一个Layout.astro组件
---
import { SiteConfig} from '../config';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
const { title, favicon } = SiteConfig;
const { pagetitle, children } = Astro.props;

import '../styles/golbal.css';
import '../styles/markdown.css'; 
import 'animate.css';

const pageTitle = pagetitle || title;
---

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{pageTitle}</title>
    <link rel="icon" type="image/svg+xml" href={favicon} />
  </head>
  <body>
    <!-- 共享的Header组件 -->
    <Header />
    <slot />
    <!-- 共享的Footer组件 -->
    <Footer />
  </body>
</html>
  1. 这其实就是一个html文件,只是在其中添加了一个标签。
  2. 是 Web 组件(包括 Astro 组件)中的一个占位符,它定义了父组件应该在哪里渲染传递给子组件的内容。可以把它想象成一个“插槽”或者“空位”,父组件可以将内容“插入”到这个位置。
  3. 现在只需要将layout.astro组件引入到需要使用布局的页面中即可。
  4. 例如:在src/pages/index.astro中引入布局组件
---
import Layout from '../layouts/Layout.astro';
...
---
<Layout>
    <Header />
    <div class="article-container">
      <PostList posts={processedPosts} />
    </div>
</Layout>
  1. 这样,导航栏和文章列表就会被渲染到Layout布局组件中在,这就相当于它们都共享了Layout布局组件的结构。

共享布局

新建一个components/Container.astro组件

  1. 这将作为一个统一布局,所有页面都将使用这个布局。
---
// Container组件,用于统一包装页面内容
---
<div class="container">
    <slot />
</div>
  1. 例如:在src/pages/index.astro中引入布局组件,导航栏和文章列表都将被渲染到Container组件中,这样就实现了布局的共享以达到布局样式的统一。
  2. 不仅如此这也降低了代码的重复度,提高了代码的可维护性。
---
import Layout from '../layouts/Layout.astro';
import Container from '../components/Container.astro';
...
---
<Layout>
    <Container>
        <Header />
        <div class="article-container">
          <PostList posts={processedPosts} />
        </div>
    </Container>
</Layout>

字数统计

  1. 代码来源于Astro-Pure主题

  2. 保留了 CJK 支持: 代码包含了 CJK_RANGES、CJK_PUNCTUATION 和 isCJK 函数,用于正确处理中文、日文、韩文等字符。 这部分代码直接从你提供的 reading-time.ts 复制过来,保证了对 CJK 字符的准确计数。

  3. 改进的单词识别: 使用 /\S/.test(char) 来判断一个字符是否是“非空白字符”。 \S 是一个正则表达式,匹配任何非空白字符。 这种方式可以更准确地识别英文单词。 inWord 变量用来跟踪是否正在一个单词内部,避免重复计数。

  4. 更简洁的实现: 直接在 readingTime 函数中实现字数统计逻辑,而不再依赖 remark 和 strip-markdown。 虽然牺牲了一部分精度(无法完美去除 markdown 标记的影响,例如链接),但是可以避免引入额外的依赖,并且对于大多数情况来说,精度已经足够。

  5. 去除了不必要的代码: 删除了原 reading-time.ts 中的 time 和 text 属性的计算,因为你的接口 ReadingTimeResults 中只需要 minutes 和 words。

import { readingTime } from './utils/Wordcount';

async function main() {
  const markdownContent = `
  # Hello World

  This is a paragraph with some **bold** and *italic* text.  你好世界。

  - List item 1
  - List item 2

  \`\`\`javascript
  console.log("Hello from a code block!");
  \`\`\`
  `;

  const readingTimeResult = await readingTime(markdownContent);
  console.log(`Reading Time: ${readingTimeResult.minutes} minutes`);
  console.log(`Word Count: ${readingTimeResult.words} words`);
}

main();
  1. 将wordCount导入到获取文章数据的页面(index.astro)
---
import Header from '../components/Header.astro';
import { getCollection } from 'astro:content';
import PostList from '../components/Postlist.astro';
+ import { readingTime } from '../utils/Wordcount';
import './styles/golbal.css';
import './styles/markdown.css'; 
import 'animate.css';

代码块配色与功能

文章中的代码块使用的是expressive-code插件,它提供了丰富的代码块样式和功能,比如语法高亮、行号显示、复制按钮等。

  1. 为Astro安装expressive-code插件
npm astro add astro-expressive-code
  1. astro.config.mjs中引入expressive-code插件。
import { defineConfig } from 'astro/config';
import expressiveCode from 'astro-expressive-code';

export default defineConfig({
  integrations: [
    expressiveCode({
      // 配置选项
    }),
  ],
});
  1. 这样,所有的代码块就会自动应用expressive-code插件的样式和功能。
  2. 另外,如果只是安装了expressive-code还是不够的,它没法显示行号,我们需要额外安装@expressive-code/plugin-line-numbers插件。
  3. expressive-code还提供了很多主题,我们可以在官网选择一个喜欢的主题,然后在astro.config.mjs中配置expressive-code插件,添加theme选项。
import { defineConfig } from 'astro/config';
import expressiveCode from 'astro-expressive-code';

export default defineConfig({
  integrations: [
    expressiveCode({
      theme: 'github-dark',
      plugins: [pluginLineNumbers(),],
    }),
  ],
});
npm i @expressive-code/plugin-line-numbers
  1. 安装完成后,在astro.config.mjs中配置expressive-code插件,添加lineNumbers插件。
import { defineConfig } from 'astro/config';
import expressiveCode from 'astro-expressive-code';
import lineNumbers from '@expressive-code/plugin-line-numbers';

export default defineConfig({
  integrations: [
    expressiveCode({
      plugins: [lineNumbers],
    }),
  ],
});
  1. 这样,所有的代码块就会自动显示行号了。

主题切换功能

Astro插件:astro-theme-toggle 轻松为你的 Astro 项目添加一个涟漪风格的主题切换动画。

  1. 这是 Astro 生态中专门用于主题切换的轻量插件,它能够轻松为你的 Astro 项目添加一个涟漪风格的主题切换动画。基于 Web API(matchMedia + localStorage)实现,支持自动检测系统主题、持久化存储用户选择,且集成简单。

  2. 安装插件

npm install astro-theme-toggle
  1. 引入插件只需将导入ThemeScript和Toggle组件,并插入到Layout布局组件中即可使用。
---
---
import { ThemeScript } from "astro-theme-toggle/components";
import { Toggle } from "astro-theme-toggle/components";
---

<div class="header-container">
    <header class="header">
        <a href="/" class="logo"><h1>MangoCat</h1></a>
        <ThemeScript />
        <Toggle class="theme-toggle" style={{ width: "18px", height: "18px" }} />
    </header>
</div>
  1. 该插件甚至不用配置图标,自带两个明暗图标,另外它缺少一个跟随系统主题的功能,不过这也不是问题,因为系统主题切换时,会自动触发主题切换动画。

过渡动画

  1. 使用的是CSS编写的原生过渡动画。
  2. src/styles/global.css中定义了slide-in动画,该动画可以适配给所有需要滑动入效果的元素,比如文章列表、文章、友链等。
  3. 使用方法:只需为需要添加滑动入效果的元素添加animate-slide-in类即可。
/* 定义slide-in动画 */
@keyframes slide-in {
    from {
        opacity: 0;
        transform: translateY(10px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* 应用slide-in动画的类 */
.animate-slide-in {
    animation: slide-in 0.3s ease-out forwards;
    opacity: 0;
    /* 初始状态为透明 */
}
  1. 例如,为文章内容添加滑动入效果:
<!-- 为文章内容添加动画 -->
<div class="article-content animate-slide-in">
	 <Content/>
</div>

评论系统

  1. 评论系统使用的是Twikoo,它是一个基于云函数的评论系统,支持多种前端框架,比如Vue、React等。
  2. 提前配置好Twikoo的环境变量,包括环境ID、区域、数据库等…
  3. 我们在src/components/Comment.astro中引入Twikoo组件,配置好环境变量,即可在博客文章中添加评论功能。
---
import { CommentConfig } from '../config';

const envId = CommentConfig.twikoo.envId;
const path = CommentConfig.twikoo.path;
---

<div class="mt-8">
  <h2 class="mb-4 text-lg font-medium text-[var(--title-color)] dark:text-[var(--title-color)]">评论</h2>
  <div 
    id="tcomment" 
    class="twikoo-container rounded-lg border-zinc-200 p-4 dark:border-zinc-700"
    data-env-id={envId}
    data-path={path || 'auto'}
  >
    <div class="flex items-center justify-center py-8 text-zinc-500 dark:text-zinc-400">
      <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500 mr-2"></div>
      正在加载评论...
    </div>
  </div>
</div>

<!-- 使用 defer 属性延迟执行,确保 DOM 就绪 -->
<script 
  src="https://cdn.jsdelivr.net/npm/twikoo@1.6.44/dist/twikoo.all.min.js"
  defer
></script>

<!-- 内联脚本处理初始化 -->
<script is:inline>
  // 全局变量存储重试次数
  window.twikooRetryCount = 0;
  window.maxRetries = 20; // 最大重试次数

  function waitForTwikoo(callback, retryCount = 0) {
    if (typeof window.twikoo !== 'undefined') {
      callback();
    } else if (retryCount < window.maxRetries) {
      setTimeout(() => waitForTwikoo(callback, retryCount + 1), 200);
    } else {
      console.error('Twikoo failed to load after maximum retries');
      showErrorMessage();
    }
  }

  function showErrorMessage() {
    const containers = document.querySelectorAll('#tcomment');
    containers.forEach(container => {
      if (container) {
        container.innerHTML = `
          <div class="text-center py-8 text-red-500">
            <p>评论系统加载失败</p>
            <button onclick="location.reload()" class="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">
              重新加载
            </button>
          </div>
        `;
      }
    });
  }

  function initializeTwikoo() {
    const container = document.getElementById('tcomment');
    
    if (!container) {
      console.warn('Twikoo container (#tcomment) not found');
      return;
    }

    const envId = container.getAttribute('data-env-id');
    const path = container.getAttribute('data-path');
    
    if (!envId) {
      console.error('Twikoo envId not provided');
      container.innerHTML = '<div class="text-center py-8 text-red-500">评论系统配置错误</div>';
      return;
    }

    try {
      // 清理容器
      container.innerHTML = '';
      
      // 初始化 Twikoo
      window.twikoo.init({
        envId: envId,
        el: '#tcomment',
        path: path === 'auto' ? location.pathname : path,
        lang: 'zh-CN'
      });
      
      console.log('Twikoo initialized successfully');
    } catch (error) {
      console.error('Failed to initialize Twikoo:', error);
      container.innerHTML = `
        <div class="text-center py-8 text-red-500">
          <p>评论系统初始化失败</p>
          <p class="text-sm mt-1">${error.message}</p>
          <button onclick="location.reload()" class="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm">
            重新加载
          </button>
        </div>
      `;
    }
  }

  function handlePageInit() {
    // 重置重试计数
    window.twikooRetryCount = 0;
    
    // 等待 Twikoo 脚本加载完成后初始化
    waitForTwikoo(initializeTwikoo);
  }

  // 首次加载处理
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', handlePageInit);
  } else {
    // DOM 已经准备就绪,延迟一点执行以确保所有脚本都加载完成
    setTimeout(handlePageInit, 100);
  }

  // Astro ViewTransitions 支持
  document.addEventListener('astro:after-swap', () => {
    console.log('Page swapped, reinitializing Twikoo');
    setTimeout(handlePageInit, 150);
  });

  // 页面可见性变化处理(可选)
  document.addEventListener('visibilitychange', () => {
    if (!document.hidden) {
      const container = document.getElementById('tcomment');
      if (container && container.children.length === 0) {
        console.log('Page became visible, checking Twikoo');
        setTimeout(handlePageInit, 100);
      }
    }
  });
</script>

<style is:global>
  .twikoo-container {
    font-family: inherit;
    min-height: 200px;
  }
  
  .dark .twikoo-container {
    background-color: transparent;
  }
  
  /* 输入框样式 */
  .dark .tk-content textarea,
  .dark .tk-input input {
    background-color: rgb(39 39 42) !important;
    border-color: rgb(63 63 70) !important;
    color: rgb(228 228 231) !important;
  }
  
  .dark .tk-content textarea:focus,
  .dark .tk-input input:focus {
    border-color: rgb(96 165 250) !important;
  }
  
  /* 按钮样式 */
  .dark .tk-submit {
    border-color: rgb(63 63 70) !important;
    color: rgb(228 228 231) !important;
  }
  

  
  /* 评论区域样式 */
  .dark .tk-comment,
  .dark .tk-replies-wrap {
    background-color: transparent !important;
    border-color: rgb(63 63 70) !important;
  }
  
  .dark .tk-comment .tk-main {
    color: rgb(228 228 231) !important;
  }
  
  .dark .tk-comment .tk-meta span {
    color: rgb(161 161 170) !important;
  }
  
  /* 链接样式 */
  .dark .tk-comment a {
    color: rgb(96 165 250) !important;
  }
  
  .dark .tk-comment a:hover {
    color: rgb(147 197 253) !important;
  }
  
  /* 表情包容器 */
  .dark .tk-owo-container {
    background-color: rgb(39 39 42) !important;
    border-color: rgb(63 63 70) !important;
  }
  
  /* 标签和额外信息 */
  .dark .tk-tag,
  .dark .tk-extras {
    color: rgb(161 161 170) !important;
  }
  
  /* 字体和边框圆角 */
  .tk-comment,
  .tk-content,
  .tk-input {
    font-family: 'Geist', system-ui, sans-serif !important;
  }
  
  .tk-content textarea,
  .tk-input input,
  .tk-submit,
  .tk-comment,
  .tk-owo-container {
    border-radius: 0.5rem !important;
  }
  
  .tk-comment {
    margin-bottom: 1rem !important;
  }
  
  /* 加载状态 */
  .dark .tk-loading {
    color: rgb(161 161 170) !important;
  }
  
  /* 加载动画 */
  .animate-spin {
    animation: spin 1s linear infinite;
  }
  
  @keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
</style>

博客完整实现原理

架构设计与核心技术

本博客主题基于Astro框架构建,采用了现代化的静态站点生成(SSG)架构。Astro的核心优势在于其组件系统的灵活性,允许开发者使用多种前端框架(React、Vue等)或直接使用原生HTML、CSS和JavaScript。

1.1 核心技术栈

  • Astro:静态站点生成框架,负责页面渲染和构建
  • TypeScript:提供类型安全,提高代码可维护性
  • Markdown:博客内容的编写格式
  • CSS:样式设计,支持响应式布局
  • Astro Icon:图标系统,支持Material Symbols和其他图标库
  • astro-theme-toggle:主题切换插件,实现明暗模式

项目结构

src/
├── content/          # 博客文章和内容文件
│   ├── posts/        # Markdown格式的博客文章
│   ├── about.md      # 关于页面内容
│   ├── link.md       # 友链页面内容
│   └── config.ts     # 内容配置
├── layouts/          # 布局组件
│   └── Layout.astro  # 主布局组件
├── components/       # 可复用组件
│   ├── Header.astro  # 导航栏组件
│   ├── Footer.astro  # 页脚组件
│   ├── Postlist.astro # 文章列表组件
│   └── Container.astro # 内容容器组件
├── pages/            # 页面入口
│   ├── index.astro   # 首页
│   ├── about.astro   # 关于页面
│   └── posts/        # 文章详情页
├── styles/           # 样式文件
├── utils/            # 工具函数
│   ├── Readtime-Wordcount.ts # 字数统计和阅读时间计算
│   └── types.ts      # TypeScript接口定义
└── config.ts         # 站点配置

2. 配置系统

博客采用集中式配置管理,通过src/config.ts文件统一管理站点的各项配置:

// 站点配置接口定义 (src/utils/types.ts)
export interface SiteConfigType {
  title: string;          // 站点标题
  author: string;         // 作者名称
  favicon: string;        // 网站图标
  desc: string;           // 站点描述
  siteUrl: string;        // 站点URL
  PaginationConfig: {
    POSTS_PER_PAGE: number;  // 每页文章数量
  };
  Categories: {           // 分类配置
    [key: string]: CategoryConfig;
  };
  NavConfig: {            // 导航配置
    name: string;
    path: string;
  }[];
}

配置系统的优势在于:

  • 集中管理,便于维护
  • 类型安全,通过TypeScript接口确保配置格式正确
  • 组件间共享,避免重复定义

3. 内容管理

博客使用Astro的Content Collections功能管理文章内容,所有博客文章存储在src/content/posts/目录下,采用Markdown格式编写,包含YAML前置元数据:

---
title: 从零开发一个Astro博客主题
description: 这是一个从零开始开发的Astro博客主题,用于展示我的学习笔记和技术分享。
published: 2025-12-06 16:27:41
tags: [C++, 数据结构]                               # 添加分类
category: 学习笔记 
slug: "83x33k23"
---

4. 页面渲染流程

4.1 首页渲染

首页渲染流程主要在src/pages/index.astro中实现:

  1. 导入依赖:导入配置、布局组件、内容集合和工具函数
  2. 获取文章数据:使用getCollection('posts')获取所有博客文章
  3. 计算字数:使用ReadingTime函数计算每篇文章的字数
  4. 排序和分页:按发布时间排序,并进行分页处理
  5. 渲染页面:使用LayoutPostList组件渲染页面内容
// 读取文章数据
const posts = await getCollection('posts');

// 计算每篇文章的字数
const processedPosts = await Promise.all(
  posts.map(async (post) => {
    const readingTimeResults = await ReadingTime(post.body);
    return {
      ...post,
      data: {
        ...post.data,
        wordCount: readingTimeResults.words, // 添加 wordCount 字段
      },
    };
  })
);

// 按发布时间排序
processedPosts.sort((a, b) => new Date(b.data.published).getTime() - new Date(a.data.published).getTime());
// 分页处理
const paginatedPosts = processedPosts.slice(startIndex, startIndex + POSTS_PER_PAGE);

4.2 文章列表渲染

PostList组件负责渲染文章列表,主要功能包括:

  • 格式化文章信息(标题、描述、分类、标签等)
  • 根据分类配置显示不同的图标和颜色
  • 格式化字数统计(如将1234字显示为1.2k字)
  • 显示发布日期

5. 核心功能实现

5.1 字数统计与阅读时间

Readtime-Wordcount.ts工具实现了准确的字数统计和阅读时间计算,特别支持CJK(中文、日文、韩文)字符:

  • 使用字符范围检查识别CJK字符
  • 跳过CJK标点符号,避免重复计数
  • 使用正则表达式识别英文单词
  • 提供可配置的每分钟阅读字数(默认200字/分钟)
export async function ReadingTime(markdown: string = '', wordsPerMinute: number = 200): Promise<ReadingTimeResults> {
  let words = 0;
  let inWord = false;

  for (let i = 0; i < markdown.length; i++) {
    const char = markdown[i];
    if (isCJK(char)) {
      words++;
      // 跳过后续的CJK标点符号
      while (i + 1 < markdown.length && CJK_PUNCTUATION.test(markdown[i + 1])) {
        i++;
      }
      inWord = false;
    } else if (/\S/.test(char)) {
      if (!inWord) {
        words++;
        inWord = true;
      }
    } else {
      inWord = false;
    }
  }

  const minutes = Math.ceil(words / wordsPerMinute);
  return { minutes, words };
}

5.2 分类系统

博客实现了灵活的分类系统,通过config.ts中的Categories配置定义不同分类的图标和颜色:

Categories: {
  '随笔': { icon: 'material-symbols:edit-document-rounded', color: '#ec4f4fff' },
  '感言': { icon: 'material-symbols:kid-star-outline', color: '#30afa7ff' },
  '日常': {icon: 'material-symbols:edit-note-rounded',color: '#c03f99ff'},
  '学习笔记': {icon: 'material-symbols:code-rounded', color: '#36bd41ff'}
}

PostList组件中,通过getCategoryConfig函数获取分类配置,并根据配置显示相应的图标和颜色。

5.3 响应式设计

博客采用响应式设计,通过CSS媒体查询适配不同屏幕尺寸:

  • 移动端:单列布局,导航栏折叠
  • 平板和桌面端:多列布局,完整导航栏
@media (max-width: 768px) {
  .footer {
    padding: 0 20px;
  }
  /* 其他移动端样式 */
}

5.4 主题切换

使用astro-theme-toggle插件实现明暗模式切换:

  • 自动检测系统主题
  • 支持用户手动切换
  • 本地存储用户偏好设置
  • 平滑的过渡动画
<ThemeScript />
<Toggle class="theme-toggle" style={{ width: "30px", height: "30px" }} />

6. 组件化设计

博客采用组件化设计,将页面拆分为多个可复用组件:

  • Layout.astro:主布局组件,包含HTML结构、头部和尾部
  • Header.astro:导航栏组件,包含站点标题和导航链接
  • Footer.astro:页脚组件,包含版权信息和社交媒体链接
  • PostList.astro:文章列表组件,渲染文章卡片
  • Container.astro:内容容器组件,统一页面布局

组件化设计的优势在于:

  • 代码复用,减少重复
  • 易于维护和更新
  • 清晰的职责划分
  • 提高开发效率

7. 性能优化

博客通过多种方式进行性能优化:

  • 静态站点生成:提前生成HTML文件,减少客户端渲染
  • 图片优化:使用适当大小的图片,支持现代图片格式
  • 组件懒加载:仅在需要时加载组件
  • 代码分割:将代码分成小块,按需加载
  • CSS优化:使用CSS变量,减少重复样式

评论

正在加载评论...