Astro MangoCat主题开发日志
关于这个项目是怎么写出来的,其实是用AI手搓出来的,AI负责写功能,我负责写样式。 样式当然也不是凭空想出来的,而是借鉴一些主题项目: Pure Clarity 吐司气泡 …
虽然最终的样式是朝着我的预期方向发展的,但是在这个过程中也有很多问题不断堆积了下来,首先是markdown的样式格式,这个完完全全是手搓的,很多地方并不完美,我很希望有个插件能一次性解决这个问题,而不是需要什么样式就直接往markdown.css中写,代码就会越写越💩,其次是CSS堆积,因为我并没有使用原子样式TailWindCSS,用于约束布局和格式的样式都会写在global.css中,而单页面的专属样式则直接写在单页面组件中,虽然改是很好改,但是代码太多了…这是让我困扰的一点,这些问题往往会模糊我对项目整个结构的理解(可恶!写一个主题,CSS写成屎山了😠😠😠)
不过在后续的开发中,我会不断对其进行优化,同时也会整理出整个项目实现结构…
基于简洁的理念,我删除了很多实际浏览时几乎用不到的功能,比如:
- 多选项翻页功能:我仅提供了翻页和显示总页数的功能,我个人认为翻第三页,第四页几乎用不到,还有一次性翻到最后一页的功能也用不到,如果要进行精准翻页,直接在URL中输入页码即可。
- 标签页:我认为分类的权重更高,直接使用分类会更方便,当然了,我现在是这么认为的,后续可能会考虑添加标签页功能。
- 文章封面:虽然我考虑过添加文章封面功能,但我目前还没有一个稳定的图床…
开发日志
[2026-02-26]
-
新增首页头Topcategory顶部快捷分类组件
-
新增头部Introduction介绍组件
-
删除搜索组件
-
调整整体宽度至900px
-
调整圆角幅度至12px
-
优化移动端导航栏UI样式
[2026-03-01] v1.5
- 替换图标库为Tabler
- 调整文字大小
[2026-03-04] v1.6
-
优化文章字体大小与颜色
-
优化文章头部信息样式
-
优化统一文字颜色
-
删除文章页内代码块阴影
-
删除文章页内代码块行号
[2026-03-04] v1.6a
- 修复移动端缩进样式
[2026-03-04] v1.7-v1.7c
-
优化行内代码样式
-
优化文章跳转链接样式
-
优化文章头部标题与信息栏样式
-
优化文章信息栏样式
-
优化部分字体大小样式
-
优化主页头部介绍组件样式
-
自定义显示主页分类导航卡片
-
修复
container容器上下缩进问题
[2026-03-17] v2.0
- 重构导航栏
[2026-03-18] v2.0 - v2.6
- 优化导航栏LOGO样式
- 优化文章卡片布局样式
- 优化文章页头部样式
- 替换新的主页顶部介绍
- 优化导航栏按钮样式
- 优化文章部分排版样式
- 优化项目页样式
- 更换新字体
[2026-03-20] v3.0 - v4.0
-
重构文章头部样式:新增顶部封面
-
顶部介绍居中
-
新增关于页面
-
重构文章Markdown排版样式:github-markdown-css插件支持
-
删除友链页预设代码块
[2026-03-20] v4.0 - v4.2
-
升级Astro V6
-
重构文章集合,将id作为文章唯一标识并保留slug
-
修复暗色主题下切换路由卡白色主题帧问题
-
优化项目页面:新增Github贡献图标
-
优化关于页
-
优化文章顶部封面与信息栏样式
-
优化Markdown排版样式:调整字体间距,美化引用块,调整正文与标题大小。
[2026-03-20] v4.2 - v4.4
-
调整封面位置:没有封面时,PC端信息栏居中,默认居左。
-
重构鱼塘页面:删除鱼塘组件,仅使用circle作为主体
-
重构鱼塘页面布局:采用新的排版方式和RSS解析逻辑
-
优化鱼塘页面样式:调整文章卡片间距。
-
优化markdown部分排版样式
[2026-03-20] v4.5
- 优化鱼塘的卡片的排列方式、采用JS Masonry瀑布流、删除随机排序功能
- 优化项目页样式:调整标题位置。
- 优化部分代码。
[2026-03-20] v4.6
- 新增Github-Markdown-Alerts提示语法功能
- 优化部分代码
[2026-03-25] v4.7
- 修复文章页脚翻页bug
- 修复封面闪烁bug
[2026-03-19] 从去年的12月份到现在,经历了从0.1Beta版到如今3.0正式版的迭代,MangoCat不论是样式还是设计思路都得到了很大提升,它越来越接近整个较为成熟的主题。
说说MangoCat的设计思路吧:其实从一开始,我完全是按照自己的基本思路进行开发的(这里的设计思路包括:组件布局的排版,UI设计风格和所需功能),但到0.8版本时我已经没有其他的设计思路了。
于是这个时候我开始去网上参考各种主题的设计风格,尝试着去学习和借鉴其他人的设计风格…结合目前的项目状况,最终形成了一个自己独特的设计风格:以简洁为基础,多的不要,少的补全。不仅是MangoCat这个主题,甚至其他项目我也会按照这种设计风格进行开发。
3月19日,截至当前,MangoCat的设计思路已经基本确定,样式和功能进行样式和功能进行优化即可,特别是文章Markdown的排版样式格式,我已经不想在这个项目上花太多时间了,我很想再去搞其他项目:到现在我连个图床都没有,个人主页也很敷衍,所以,我会尽全力完善MangoCat剩余的不足。
例如:
截至现在,MangoCat的框架已经基本稳定,后续我只需要对样式和功能进行优化即可,例如文章Markdown的排版样式格式,
CSS关键类
.container:通用容器,用于包裹页面的内容(格式化内容布局)。
.article-container:通用容器,用于包裹文章内容(格式化文章布局)。
.container基本上包揽了一切,它被直接套在Layout组件中,用于包裹页面的内容,所以所有布局都被该容器所约束,达到样式的统一管理。 .article-container则是用于包裹文章内容的容器,它被直接套在文章组件中,用于格式化文章的布局,当然该容器也被.container所约束,达到样式的统一管理。
布局组件
- 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>
- 我们可以写好一些组件,如导航栏(Header.astro)、页脚(Footer.astro)等,直接在布局组件中引入即可。
- 当我们将全局样式(如 normalize.css、animate.css 等)引入到布局组件中时,这些样式会应用到当前所有页面中。
- 可以理解为布局组件是一个模板,它定义了页面的通用结构和样式,而具体的页面内容则由插槽(
)来填充。 - 我们也可以通过将内容写在Layout组件中,例如将PostList组件写在Layout组件中,它将会被作为参数传递给Layout组件的插槽(
),随后被统一渲染出来。
---
import Layout from '../layouts/Layout.astro';
---
<Layout>
<PostList posts={paginatedPosts} />
</Layout>
共享组件
在 Astro 中,
Layouts目录(通常是 src/layouts)是一个专门用来存放布局组件的目录。布局组件是 Astro 中一种特殊的组件,它们定义了你的网站页面的通用结构和样式,比如 HTML 结构、头部、尾部、导航栏等等。
Layouts 目录的作用和意义:
-
统一页面结构: Layouts 目录允许你将网站的通用页面结构定义在一个或多个布局组件中。 这样,你就可以在多个页面中重复使用这些布局,而无需在每个页面中都编写相同的 HTML 结构。
-
代码复用: 通过使用布局组件,你可以避免代码重复,提高代码的可维护性。 如果你需要修改网站的整体结构,只需要修改相应的布局组件,所有使用该布局的页面都会自动更新。
-
简化页面开发: 布局组件可以让你专注于编写页面的核心内容,而不用关心页面的通用结构。 这可以大大提高开发效率。
-
SEO 友好: 通过在布局组件中设置统一的 HTML 结构、meta 标签和其他 SEO 相关的设置,你可以确保网站的每个页面都符合 SEO 的最佳实践。
布局组件的特点:
包含<slot />: 布局组件通常包含一个或多个
Layouts 目录的常见用法:
- 定义网站的通用结构: 在 Layouts 目录中创建一个 BaseLayout.astro 组件,定义网站的通用 HTML 结构、头部、尾部和导航栏。 创建不同的页面布局: 如果你需要不同的页面布局 (例如,博客文章页面和普通页面),可以在 Layouts 目录中创建多个不同的布局组件。 嵌套布局: 你可以将布局组件嵌套在一起,构建更复杂的页面结构。 简单示例:
共享html格式
- 创建一个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>
- 这其实就是一个html文件,只是在其中添加了一个
标签。 是 Web 组件(包括 Astro 组件)中的一个占位符,它定义了父组件应该在哪里渲染传递给子组件的内容。可以把它想象成一个“插槽”或者“空位”,父组件可以将内容“插入”到这个位置。 - 现在只需要将layout.astro组件引入到需要使用布局的页面中即可。
- 例如:在
src/pages/index.astro中引入布局组件
---
import Layout from '../layouts/Layout.astro';
...
---
<Layout>
<Header />
<div class="article-container">
<PostList posts={processedPosts} />
</div>
</Layout>
- 这样,导航栏和文章列表就会被渲染到Layout布局组件中在,这就相当于它们都共享了Layout布局组件的结构。
共享布局
新建一个components/Container.astro组件
- 这将作为一个统一布局,所有页面都将使用这个布局。
---
// Container组件,用于统一包装页面内容
---
<div class="container">
<slot />
</div>
- 例如:在
src/pages/index.astro中引入布局组件,导航栏和文章列表都将被渲染到Container组件中,这样就实现了布局的共享以达到布局样式的统一。 - 不仅如此这也降低了代码的重复度,提高了代码的可维护性。
---
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>
字数统计
-
代码来源于
Astro-Pure主题 -
保留了 CJK 支持: 代码包含了 CJK_RANGES、CJK_PUNCTUATION 和 isCJK 函数,用于正确处理中文、日文、韩文等字符。 这部分代码直接从你提供的 reading-time.ts 复制过来,保证了对 CJK 字符的准确计数。
-
改进的单词识别: 使用 /\S/.test(char) 来判断一个字符是否是“非空白字符”。 \S 是一个正则表达式,匹配任何非空白字符。 这种方式可以更准确地识别英文单词。 inWord 变量用来跟踪是否正在一个单词内部,避免重复计数。
-
更简洁的实现: 直接在 readingTime 函数中实现字数统计逻辑,而不再依赖 remark 和 strip-markdown。 虽然牺牲了一部分精度(无法完美去除 markdown 标记的影响,例如链接),但是可以避免引入额外的依赖,并且对于大多数情况来说,精度已经足够。
-
去除了不必要的代码: 删除了原 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();
- 将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插件,它提供了丰富的代码块样式和功能,比如语法高亮、行号显示、复制按钮等。
- 为Astro安装expressive-code插件
npm astro add astro-expressive-code
- 在
astro.config.mjs中引入expressive-code插件。
import { defineConfig } from 'astro/config';
import expressiveCode from 'astro-expressive-code';
export default defineConfig({
integrations: [
expressiveCode({
// 配置选项
}),
],
});
- 这样,所有的代码块就会自动应用expressive-code插件的样式和功能。
- 另外,如果只是安装了expressive-code还是不够的,它没法显示行号,我们需要额外安装
@expressive-code/plugin-line-numbers插件。 - 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
- 安装完成后,在
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],
}),
],
});
- 这样,所有的代码块就会自动显示行号了。
主题切换功能
Astro插件:astro-theme-toggle 轻松为你的 Astro 项目添加一个涟漪风格的主题切换动画。
-
这是 Astro 生态中专门用于主题切换的轻量插件,它能够轻松为你的 Astro 项目添加一个涟漪风格的主题切换动画。基于 Web API(matchMedia + localStorage)实现,支持自动检测系统主题、持久化存储用户选择,且集成简单。
-
安装插件
npm install astro-theme-toggle
- 引入插件只需将导入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>
- 该插件甚至不用配置图标,自带两个明暗图标,另外它缺少一个跟随系统主题的功能,不过这也不是问题,因为系统主题切换时,会自动触发主题切换动画。
过渡动画
- 使用的是CSS编写的原生过渡动画。
- 在
src/styles/global.css中定义了slide-in动画,该动画可以适配给所有需要滑动入效果的元素,比如文章列表、文章、友链等。 - 使用方法:只需为需要添加滑动入效果的元素添加
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;
/* 初始状态为透明 */
}
- 例如,为文章内容添加滑动入效果:
<!-- 为文章内容添加动画 -->
<div class="article-content animate-slide-in">
<Content/>
</div>
评论系统
- 评论系统使用的是Twikoo,它是一个基于云函数的评论系统,支持多种前端框架,比如Vue、React等。
- 提前配置好Twikoo的环境变量,包括环境ID、区域、数据库等…
- 我们在
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中实现:
- 导入依赖:导入配置、布局组件、内容集合和工具函数
- 获取文章数据:使用
getCollection('posts')获取所有博客文章 - 计算字数:使用
ReadingTime函数计算每篇文章的字数 - 排序和分页:按发布时间排序,并进行分页处理
- 渲染页面:使用
Layout和PostList组件渲染页面内容
// 读取文章数据
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变量,减少重复样式