+
+
+ {isOpen() && (
+ <>
+
setIsOpen(false)} />
+
+ >
+ )}
+
+ >
+ );
+}
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx
new file mode 100644
index 0000000..f954e13
--- /dev/null
+++ b/src/components/ThemeToggle.tsx
@@ -0,0 +1,52 @@
+import { createSignal, onMount } from 'solid-js';
+
+export default function ThemeToggle() {
+ const [theme, setTheme] = createSignal<'light' | 'dark'>('light');
+ const [isAnimating, setIsAnimating] = createSignal(false);
+
+ onMount(() => {
+ const currentTheme = document.documentElement.getAttribute('data-theme') as 'light' | 'dark';
+ setTheme(currentTheme || 'light');
+ });
+
+ const toggleTheme = () => {
+ setIsAnimating(true);
+ setTimeout(() => setIsAnimating(false), 600);
+
+ const newTheme = theme() === 'light' ? 'dark' : 'light';
+ setTheme(newTheme);
+ document.documentElement.setAttribute('data-theme', newTheme);
+ localStorage.setItem('theme', newTheme);
+ };
+
+ return (
+
+ );
+}
diff --git a/src/consts.ts b/src/consts.ts
index 2c83f04..5d628f1 100644
--- a/src/consts.ts
+++ b/src/consts.ts
@@ -1,3 +1,7 @@
export const metadata = {
siteTitle: "42的博客",
+ siteDescription: "分享技术、思考与生活",
+ siteUrl: "https://ua42.com",
+ author: "42",
+ postsPerPage: 10,
}
diff --git a/src/content/config.ts b/src/content/config.ts
new file mode 100644
index 0000000..3bc917e
--- /dev/null
+++ b/src/content/config.ts
@@ -0,0 +1,18 @@
+import { defineCollection, z } from 'astro:content';
+
+const posts = defineCollection({
+ type: 'content',
+ schema: z.object({
+ title: z.string(),
+ description: z.string(),
+ pubDate: z.coerce.date(),
+ updatedDate: z.coerce.date().optional(),
+ author: z.string().default('42'),
+ image: z.string().optional(),
+ tags: z.array(z.string()).default([]),
+ category: z.string().default('未分类'),
+ draft: z.boolean().default(false),
+ }),
+});
+
+export const collections = { posts };
diff --git a/src/content/posts/markdown-test.md b/src/content/posts/markdown-test.md
new file mode 100644
index 0000000..a6cd4c2
--- /dev/null
+++ b/src/content/posts/markdown-test.md
@@ -0,0 +1,264 @@
+---
+title: "Markdown 渲染测试"
+description: "测试博客的 Markdown 渲染能力,包括各种元素和代码高亮"
+pubDate: 2024-11-21
+author: "42"
+tags: ["测试", "Markdown", "代码高亮"]
+category: "技术"
+draft: false
+---
+
+## 标题测试
+
+### 三级标题
+
+#### 四级标题
+
+##### 五级标题
+
+###### 六级标题
+
+## 段落和文本格式
+
+这是一个普通的段落。Lorem ipsum dolor sit amet, consectetur adipiscing elit. 中文和英文混排测试,确保行高和间距合理。
+
+这是另一个段落,包含**粗体文本**、*斜体文本*和`行内代码`。
+
+## 列表测试
+
+### 无序列表
+
+- 第一项
+- 第二项
+ - 嵌套项 2.1
+ - 嵌套项 2.2
+ - 更深层嵌套 2.2.1
+- 第三项
+
+### 有序列表
+
+1. 第一步
+2. 第二步
+ 1. 子步骤 2.1
+ 2. 子步骤 2.2
+3. 第三步
+
+## 链接测试
+
+这是一个[内联链接](https://example.com)。
+
+这是一个[带标题的链接](https://example.com "链接标题")。
+
+## 引用块测试
+
+> 这是一个引用块。
+>
+> 引用块可以包含多个段落。
+>
+> 引用块的样式应该与正文有明显区分。
+
+## 代码测试
+
+### 行内代码
+
+使用 `const` 和 `let` 声明变量,用 `console.log()` 输出日志。
+
+### JavaScript 代码块
+
+```javascript
+// JavaScript 示例
+function fibonacci(n) {
+ if (n <= 1) return n;
+ return fibonacci(n - 1) + fibonacci(n - 2);
+}
+
+const result = fibonacci(10);
+console.log(`Fibonacci(10) = ${result}`);
+
+// ES6+ 特性
+const numbers = [1, 2, 3, 4, 5];
+const doubled = numbers.map(n => n * 2);
+```
+
+### TypeScript 代码块
+
+```typescript
+// TypeScript 示例
+interface User {
+ id: number;
+ name: string;
+ email: string;
+}
+
+class UserService {
+ private users: User[] = [];
+
+ addUser(user: User): void {
+ this.users.push(user);
+ }
+
+ getUserById(id: number): User | undefined {
+ return this.users.find(user => user.id === id);
+ }
+}
+
+const service = new UserService();
+service.addUser({ id: 1, name: '张三', email: 'zhang@example.com' });
+```
+
+### Python 代码块
+
+```python
+# Python 示例
+def fibonacci(n):
+ """计算斐波那契数列第 n 项"""
+ if n <= 1:
+ return n
+ return fibonacci(n - 1) + fibonacci(n - 2)
+
+# 列表推导式
+squares = [x**2 for x in range(10)]
+
+# 字典
+user = {
+ 'name': '李四',
+ 'age': 25,
+ 'email': 'li@example.com'
+}
+
+print(f"用户: {user['name']}, 年龄: {user['age']}")
+```
+
+### CSS 代码块
+
+```css
+/* CSS 示例 */
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+.card {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 12px;
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease;
+}
+
+.card:hover {
+ transform: translateY(-5px);
+}
+```
+
+### HTML 代码块
+
+```html
+
+
+
+
+
+
示例页面
+
+
+
+
+
+ 文章标题
+ 文章内容...
+
+
+
+
+```
+
+### Go 代码块
+
+```go
+// Go 示例
+package main
+
+import (
+ "fmt"
+ "time"
+)
+
+type User struct {
+ ID int
+ Name string
+ Email string
+}
+
+func main() {
+ user := User{
+ ID: 1,
+ Name: "王五",
+ Email: "wang@example.com",
+ }
+
+ fmt.Printf("用户: %s (ID: %d)\n", user.Name, user.ID)
+
+ // 并发示例
+ go func() {
+ fmt.Println("并发执行")
+ }()
+
+ time.Sleep(time.Second)
+}
+```
+
+## 表格测试
+
+| 语言 | 类型 | 特点 | 流行度 |
+|-----------|---------|---------------|-------|
+| JavaScript | 动态 | 灵活,生态丰富 | ⭐⭐⭐⭐⭐ |
+| TypeScript | 静态 | 类型安全 | ⭐⭐⭐⭐ |
+| Python | 动态 | 简洁,易学 | ⭐⭐⭐⭐⭐ |
+| Go | 静态 | 高性能,并发 | ⭐⭐⭐⭐ |
+| Rust | 静态 | 内存安全,性能 | ⭐⭐⭐ |
+
+## 图片测试
+
+
+
+## 分隔线
+
+---
+
+## 混合内容测试
+
+让我们结合多种元素:
+
+1. **列表项包含代码**:使用 `Array.prototype.map()` 进行数组转换
+2. **列表项包含链接**:访问 [MDN 文档](https://developer.mozilla.org)
+3. **列表项包含强调**:这是*非常重要*的一点
+
+> **提示**:以上测试涵盖了常见的 Markdown 元素。
+>
+> 确保在亮色和暗色模式下都有良好的显示效果。
+
+```typescript
+// 综合示例
+interface Config {
+ theme: 'light' | 'dark';
+ language: string;
+ features: string[];
+}
+
+const config: Config = {
+ theme: 'dark',
+ language: 'zh-CN',
+ features: ['syntax-highlight', 'responsive', 'dark-mode']
+};
+```
+
+## 结尾
+
+这个测试文档涵盖了博客系统支持的主要 Markdown 功能。
diff --git a/src/content/posts/welcome.md b/src/content/posts/welcome.md
new file mode 100644
index 0000000..37ae548
--- /dev/null
+++ b/src/content/posts/welcome.md
@@ -0,0 +1,30 @@
+---
+title: "欢迎来到我的博客"
+description: "这是第一篇博客文章,介绍了博客的功能和技术栈。"
+pubDate: 2024-11-21
+author: "42"
+tags: ["博客", "Astro", "SolidJS"]
+category: "技术"
+draft: false
+---
+
+## 欢迎!
+
+这是使用 Astro + SolidJS 构建的现代化博客系统。
+
+### 技术特点
+
+- **服务端渲染**:基于 Astro 的 SSR 模式,提供极致的首屏加载速度
+- **响应式组件**:使用 SolidJS 构建高性能的交互式组件
+- **TypeScript**:完整的类型支持,提升开发体验
+- **内容管理**:使用 Content Collections 管理文章
+
+### 功能亮点
+
+1. 文章分类和标签系统
+2. 搜索功能
+3. RSS 订阅
+4. 响应式设计
+5. 暗色模式支持
+
+开始你的阅读之旅吧!
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
new file mode 100644
index 0000000..6c2b75d
--- /dev/null
+++ b/src/layouts/BaseLayout.astro
@@ -0,0 +1,56 @@
+---
+import '@/styles/base.css';
+import { metadata } from '@/consts';
+
+interface Props {
+ title?: string;
+ description?: string;
+ image?: string;
+}
+
+const {
+ title = metadata.siteTitle,
+ description = metadata.siteDescription,
+ image = `${metadata.siteUrl}/og-image.jpg`,
+} = Astro.props;
+
+const fullTitle = title === metadata.siteTitle ? title : `${title} - ${metadata.siteTitle}`;
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{fullTitle}
+
+
+
+
+
+
+
diff --git a/src/layouts/PostLayout.astro b/src/layouts/PostLayout.astro
new file mode 100644
index 0000000..82abec7
--- /dev/null
+++ b/src/layouts/PostLayout.astro
@@ -0,0 +1,193 @@
+---
+import BaseLayout from './BaseLayout.astro';
+import { formatDate } from '@/utils/date';
+import type { CollectionEntry } from 'astro:content';
+import SiteHeader from '@/components/SiteHeader.astro';
+import TableOfContents from '@/components/TableOfContents';
+import BackToTop from '@/components/BackToTop';
+import { extractHeadings } from '@/utils/extract-headings';
+import '@/styles/toc.css';
+
+interface Props {
+ post: CollectionEntry<'posts'>;
+ readingTime: string;
+}
+
+const { post, readingTime } = Astro.props;
+const { title, description, pubDate, author, tags, category } = post.data;
+const headings = extractHeadings(post.body);
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/about.astro b/src/pages/about.astro
new file mode 100644
index 0000000..e27e02b
--- /dev/null
+++ b/src/pages/about.astro
@@ -0,0 +1,283 @@
+---
+import BaseLayout from '@/layouts/BaseLayout.astro';
+import SiteHeader from '@/components/SiteHeader.astro';
+
+---
+
+
+
+
+
+
+
+ 关于本站
+
+
+ 技术栈
+ 本博客基于现代化的技术栈构建:
+
+ - Astro - 现代化的静态网站生成器
+ - SolidJS - 高性能的响应式UI框架
+ - TypeScript - 类型安全的开发体验
+ - Docker - 容器化部署方案
+
+
+
+
+ 关于作者
+ 42 - 一个热爱技术、喜欢折腾的开发者。
+ 在这里分享技术心得、项目经验和生活感悟。
+
+
+
+ 联系方式
+ 欢迎通过以下方式与我交流:
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/categories/[category].astro b/src/pages/categories/[category].astro
new file mode 100644
index 0000000..c0c5984
--- /dev/null
+++ b/src/pages/categories/[category].astro
@@ -0,0 +1,292 @@
+---
+import BaseLayout from '@/layouts/BaseLayout.astro';
+import { getCollection } from 'astro:content';
+import ArticleCard from '@/components/ArticleCard.astro';
+import ThemeToggle from '@/components/ThemeToggle';
+
+const { category } = Astro.params;
+
+if (!category) {
+ return Astro.redirect('/404');
+}
+
+const allPosts = await getCollection('posts', ({ data }) => !data.draft);
+const posts = allPosts
+ .filter(post => post.data.category === category)
+ .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
+
+if (posts.length === 0) {
+ return Astro.redirect('/categories');
+}
+---
+
+
+
+
+
+
+
+
+
+ {posts.map(post => (
+
+ ))}
+
+
+
+
+
+
+
+
diff --git a/src/pages/categories/index.astro b/src/pages/categories/index.astro
new file mode 100644
index 0000000..45c48d9
--- /dev/null
+++ b/src/pages/categories/index.astro
@@ -0,0 +1,358 @@
+---
+import BaseLayout from '@/layouts/BaseLayout.astro';
+import { getCollection } from 'astro:content';
+import ThemeToggle from '@/components/ThemeToggle';
+
+const posts = await getCollection('posts', ({ data }) => !data.draft);
+
+// 统计所有分类
+const categoryCount = new Map
();
+posts.forEach(post => {
+ const category = post.data.category;
+ categoryCount.set(category, (categoryCount.get(category) || 0) + 1);
+});
+
+// 按文章数量排序
+const sortedCategories = Array.from(categoryCount.entries())
+ .sort((a, b) => b[1] - a[1]);
+---
+
+
+
+
+
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 94a51dd..dac9694 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,33 +1,295 @@
---
-import Layout from "@/layouts/Layout.astro";
+import BaseLayout from '@/layouts/BaseLayout.astro';
+import { getCollection } from 'astro:content';
+import ArticleCard from '@/components/ArticleCard.astro';
+import SiteHeader from '@/components/SiteHeader.astro';
-const posts = [
- {
- title: "First Post",
- date: "2024-01-01",
- summary: "This is the summary of the first post.",
- },
-];
+const posts = await getCollection('posts', ({ data }) => !data.draft);
+const sortedPosts = posts.sort((a, b) =>
+ b.data.pubDate.getTime() - a.data.pubDate.getTime()
+);
+
+// 获取所有标签
+const allTags = [...new Set(posts.flatMap(post => post.data.tags))];
+const topTags = allTags.slice(0, 8);
---
-
-
- {
- posts.map((post) => (
-
- {post.title}
- {post.date}
- {post.summary}
-
- ))
- }
+
+
+
+
+
+
+
+
42的博客
+
分享技术、思考与生活
+
在这里记录开发过程中的思考,分享工程实践经验,探索技术的可能性。
+
+
+
+
{sortedPosts.length}
+
篇文章
+
+
+
{allTags.length}
+
个标签
+
+
+
+
+
+
+
最新文章
+
+ {sortedPosts.map(post => (
+
+ ))}
+
+
+
+
+
-
+
diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro
index c038505..98dbe00 100644
--- a/src/pages/posts/[...slug].astro
+++ b/src/pages/posts/[...slug].astro
@@ -1,9 +1,24 @@
---
-import Layout from "@/layouts/Layout.astro";
+import { getCollection, getEntry } from 'astro:content';
+import PostLayout from '@/layouts/PostLayout.astro';
+import { getReadingTime } from '@/utils/reading-time';
const { slug } = Astro.params;
+
+if (!slug) {
+ return Astro.redirect('/404');
+}
+
+const post = await getEntry('posts', slug);
+
+if (!post || post.data.draft) {
+ return Astro.redirect('/404');
+}
+
+const { Content } = await post.render();
+const readingTime = getReadingTime(post.body);
---
-
- Post Slug: {slug}
-
+
+
+
diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts
new file mode 100644
index 0000000..db31661
--- /dev/null
+++ b/src/pages/rss.xml.ts
@@ -0,0 +1,25 @@
+import rss from '@astrojs/rss';
+import { getCollection } from 'astro:content';
+import { metadata } from '@/consts';
+import type { APIContext } from 'astro';
+
+export async function GET(context: APIContext) {
+ const posts = await getCollection('posts', ({ data }) => !data.draft);
+
+ return rss({
+ title: metadata.siteTitle,
+ description: metadata.siteDescription,
+ site: context.site || metadata.siteUrl,
+ items: posts
+ .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
+ .map(post => ({
+ title: post.data.title,
+ description: post.data.description,
+ pubDate: post.data.pubDate,
+ link: `/posts/${post.slug}/`,
+ categories: [post.data.category, ...post.data.tags],
+ author: post.data.author,
+ })),
+ customData: `
zh-CN`,
+ });
+}
diff --git a/src/pages/search-index.json.ts b/src/pages/search-index.json.ts
new file mode 100644
index 0000000..e26edfb
--- /dev/null
+++ b/src/pages/search-index.json.ts
@@ -0,0 +1,14 @@
+import type { APIRoute } from 'astro';
+import { generateSearchIndex } from '@/utils/search-index';
+
+export const GET: APIRoute = async () => {
+ const searchIndex = await generateSearchIndex();
+
+ return new Response(JSON.stringify(searchIndex), {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'public, max-age=3600',
+ },
+ });
+};
diff --git a/src/pages/tags/[tag].astro b/src/pages/tags/[tag].astro
new file mode 100644
index 0000000..e8661a7
--- /dev/null
+++ b/src/pages/tags/[tag].astro
@@ -0,0 +1,407 @@
+---
+import BaseLayout from '@/layouts/BaseLayout.astro';
+import { getCollection } from 'astro:content';
+import { formatDate } from '@/utils/date';
+import { getReadingTime } from '@/utils/reading-time';
+import ThemeToggle from '@/components/ThemeToggle';
+
+export async function getStaticPaths() {
+ const posts = await getCollection('posts', ({ data }) => !data.draft);
+
+ const tags = new Set
();
+ posts.forEach(post => {
+ post.data.tags.forEach(tag => tags.add(tag));
+ });
+
+ return Array.from(tags).map(tag => ({
+ params: { tag },
+ props: {
+ posts: posts.filter(post => post.data.tags.includes(tag))
+ .sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
+ },
+ }));
+}
+
+const { tag } = Astro.params;
+const { posts } = Astro.props;
+---
+
+
+
+
+
+
diff --git a/src/pages/tags/index.astro b/src/pages/tags/index.astro
new file mode 100644
index 0000000..480d561
--- /dev/null
+++ b/src/pages/tags/index.astro
@@ -0,0 +1,346 @@
+---
+import BaseLayout from '@/layouts/BaseLayout.astro';
+import { getCollection } from 'astro:content';
+import SiteHeader from '@/components/SiteHeader.astro';
+
+const posts = await getCollection('posts', ({ data }) => !data.draft);
+
+// 统计所有标签
+const tagCount = new Map();
+posts.forEach(post => {
+ post.data.tags.forEach(tag => {
+ tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
+ });
+});
+
+// 按文章数量排序
+const sortedTags = Array.from(tagCount.entries())
+ .sort((a, b) => b[1] - a[1]);
+---
+
+
+
+
+
+
diff --git a/src/styles/base.css b/src/styles/base.css
index 5cb1dad..224f6cb 100644
--- a/src/styles/base.css
+++ b/src/styles/base.css
@@ -1,7 +1,171 @@
@import "modern-normalize";
@import "./reset.css";
+@import "./typography.css";
+@import "./enhancements.css";
:root {
--layout-width: 1200px;
+ --content-width: 680px;
+
+ /* 亮色模式 */
+ --bg-primary: #ffffff;
+ --bg-secondary: #fafaf9;
+ --bg-tertiary: #f5f5f4;
+ --text-primary: #18181b;
+ --text-secondary: #71717a;
+ --text-tertiary: #a1a1aa;
+ --text-heading: #09090b;
+ --link-color: #0891b2;
+ --link-hover: #0e7490;
+ --accent-color: #d97706;
+ --accent-secondary: #0891b2;
+ --success-color: #10b981;
+ --warning-color: #f59e0b;
+ --border-color: #e5e7eb;
+ --border-light: #f3f4f6;
+
+ /* 代码相关 */
+ --code-inline-bg: #f4f5f7;
+ --code-inline-color: #e91e63;
+ --code-inline-border: #e5e7eb;
+ --code-block-bg: #f6f8fa;
+ --code-block-border: #e5e7eb;
+ --code-line-number: #9ca3af;
+ --code-highlight-bg: #fff9db;
+
+ /* Markdown 元素 */
+ --quote-border: #3b82f6;
+ --quote-bg: #f8f9fa;
+ --table-border: #e5e7eb;
+ --table-header-bg: #f0f2f5;
+ --table-stripe-bg: #f8f9fa;
+ --link-underline: #3b82f6;
+
+ /* 间距规范 */
+ --spacing-xs: 0.25rem;
+ --spacing-sm: 0.5rem;
+ --spacing-md: 1rem;
+ --spacing-lg: 1.5rem;
+ --spacing-xl: 2rem;
+ --spacing-2xl: 2.5rem;
+ --spacing-3xl: 3rem;
+
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+
+ /* 装饰线条颜色(替代渐变) */
+ --accent-line: var(--accent-color);
+ --accent-line-secondary: var(--accent-secondary);
+
+ /* 圆角 */
+ --radius-sm: 6px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-xl: 16px;
+ --radius-full: 9999px;
+
+ /* 过渡 */
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+[data-theme="dark"] {
+ /* 暗色模式 */
+ --bg-primary: #09090b;
+ --bg-secondary: #18181b;
+ --bg-tertiary: #27272a;
+ --text-primary: #fafafa;
+ --text-secondary: #a1a1aa;
+ --text-tertiary: #71717a;
+ --text-heading: #fafafa;
+ --link-color: #06b6d4;
+ --link-hover: #22d3ee;
+ --accent-color: #fbbf24;
+ --accent-secondary: #06b6d4;
+ --success-color: #34d399;
+ --warning-color: #fbbf24;
+ --border-color: #27272a;
+ --border-light: #18181b;
+
+ /* 代码相关 */
+ --code-inline-bg: #1e293b;
+ --code-inline-color: #f472b6;
+ --code-inline-border: #334155;
+ --code-block-bg: #1e293b;
+ --code-block-border: #334155;
+ --code-line-number: #64748b;
+ --code-highlight-bg: #374151;
+
+ /* Markdown 元素 */
+ --quote-border: #60a5fa;
+ --quote-bg: #1e293b;
+ --table-border: #334155;
+ --table-header-bg: #1e293b;
+ --table-stripe-bg: #0f172a;
+ --link-underline: #60a5fa;
+
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6), 0 10px 10px -5px rgba(0, 0, 0, 0.5);
+
+ /* 暗色模式装饰线条 */
+ --accent-line: var(--accent-color);
+ --accent-line-secondary: var(--accent-secondary);
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+ font-size: 16px;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ scroll-behavior: smooth;
+}
+
+body {
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ transition: background-color var(--transition-base), color var(--transition-base);
+ min-height: 100vh;
+ position: relative;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+img,
+picture,
+video,
+canvas,
+svg {
+ display: block;
+ max-width: 100%;
+}
+
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+
+button {
+ cursor: pointer;
+ border: none;
+ background: none;
}
diff --git a/src/styles/enhancements.css b/src/styles/enhancements.css
new file mode 100644
index 0000000..d053e33
--- /dev/null
+++ b/src/styles/enhancements.css
@@ -0,0 +1,295 @@
+/* UI/UX 增强样式 */
+
+/* 选中文本样式 */
+::selection {
+ background: var(--accent-color);
+ color: white;
+}
+
+::-moz-selection {
+ background: var(--accent-color);
+ color: white;
+}
+
+/* 滚动条样式 */
+::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+ border-radius: var(--radius-sm);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--text-tertiary);
+ border-radius: var(--radius-sm);
+ transition: background var(--transition-base);
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--accent-color);
+}
+
+/* Focus 样式增强 */
+*:focus-visible {
+ outline: 2px solid var(--accent-color);
+ outline-offset: 2px;
+ border-radius: var(--radius-sm);
+}
+
+button:focus-visible,
+a:focus-visible {
+ outline: 2px solid var(--accent-color);
+ outline-offset: 2px;
+}
+
+/* 按钮基础样式增强 */
+button {
+ -webkit-tap-highlight-color: transparent;
+}
+
+/* 平滑过渡类 */
+.fade-in {
+ animation: fadeIn 0.3s ease-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+/* 链接悬停效果 */
+a {
+ position: relative;
+}
+
+/* 响应式图片 */
+img {
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: crisp-edges;
+}
+
+/* 代码块增强 */
+pre {
+ position: relative;
+}
+
+/* Shiki 代码块样式适配 */
+.astro-code,
+.shiki {
+ padding: 1.5rem;
+ border-radius: var(--radius-lg);
+ overflow-x: auto;
+ line-height: 1.6;
+ font-size: 0.875rem;
+ margin: 1.5rem 0;
+ box-shadow: var(--shadow-md);
+ border: 1px solid var(--code-block-border);
+}
+
+/* 代码块滚动条样式 */
+pre::-webkit-scrollbar,
+.astro-code::-webkit-scrollbar,
+.shiki::-webkit-scrollbar {
+ height: 8px;
+}
+
+pre::-webkit-scrollbar-track,
+.astro-code::-webkit-scrollbar-track,
+.shiki::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+pre::-webkit-scrollbar-thumb,
+.astro-code::-webkit-scrollbar-thumb,
+.shiki::-webkit-scrollbar-thumb {
+ background: var(--text-tertiary);
+ border-radius: var(--radius-sm);
+}
+
+pre::-webkit-scrollbar-thumb:hover,
+.astro-code::-webkit-scrollbar-thumb:hover,
+.shiki::-webkit-scrollbar-thumb:hover {
+ background: var(--accent-color);
+}
+
+/* 移动端代码块优化 */
+@media (max-width: 768px) {
+ .astro-code,
+ .shiki,
+ article pre {
+ padding: 1rem;
+ font-size: 0.8125rem;
+ border-radius: var(--radius-md);
+ }
+}
+
+/* 加载动画 */
+@keyframes shimmer {
+ 0% {
+ background-position: -1000px 0;
+ }
+ 100% {
+ background-position: 1000px 0;
+ }
+}
+
+.skeleton {
+ animation: shimmer 2s infinite;
+ background: linear-gradient(
+ to right,
+ var(--bg-secondary) 0%,
+ var(--bg-tertiary) 50%,
+ var(--bg-secondary) 100%
+ );
+ background-size: 1000px 100%;
+}
+
+/* 工具提示基础样式 */
+[data-tooltip] {
+ position: relative;
+ cursor: help;
+}
+
+[data-tooltip]::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-8px);
+ padding: 0.5rem 0.75rem;
+ background: var(--text-heading);
+ color: white;
+ border-radius: var(--radius-md);
+ font-size: 0.875rem;
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--transition-fast), transform var(--transition-fast);
+ z-index: 1000;
+}
+
+[data-tooltip]:hover::after {
+ opacity: 1;
+ transform: translateX(-50%) translateY(-4px);
+}
+
+/* 徽章样式 */
+.badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.625rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+ border-radius: var(--radius-full);
+ background: var(--accent-color);
+ color: white;
+}
+
+/* 分隔线增强 */
+hr {
+ margin: 2rem 0;
+ border: none;
+ height: 1px;
+ background: linear-gradient(
+ to right,
+ transparent,
+ var(--border-color),
+ transparent
+ );
+}
+
+/* 引用块增强样式 */
+blockquote {
+ position: relative;
+ isolation: isolate;
+}
+
+/* 响应式视频容器 */
+.video-container {
+ position: relative;
+ padding-bottom: 56.25%; /* 16:9 */
+ height: 0;
+ overflow: hidden;
+ border-radius: var(--radius-lg);
+}
+
+.video-container iframe,
+.video-container video {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+/* 打印样式优化 */
+@media print {
+ body {
+ background: white;
+ color: black;
+ }
+
+ .site-header,
+ .site-footer,
+ .theme-toggle,
+ nav {
+ display: none;
+ }
+
+ article {
+ max-width: 100%;
+ }
+
+ a {
+ color: black;
+ text-decoration: underline;
+ }
+
+ pre,
+ code {
+ border: 1px solid #ccc;
+ page-break-inside: avoid;
+ }
+}
+
+/* 减少动画偏好设置 */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* 暗色模式特定增强 */
+[data-theme="dark"] img {
+ opacity: 0.9;
+}
+
+[data-theme="dark"] img:hover {
+ opacity: 1;
+}
+
+/* 高对比度模式支持 */
+@media (prefers-contrast: high) {
+ :root {
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
+ --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
+ }
+
+ button,
+ a {
+ outline: 2px solid currentColor;
+ }
+}
diff --git a/src/styles/toc.css b/src/styles/toc.css
new file mode 100644
index 0000000..0bc29b2
--- /dev/null
+++ b/src/styles/toc.css
@@ -0,0 +1,263 @@
+/* 文章目录(TOC)样式 */
+
+/* 桌面端:侧边固定目录 */
+.toc-desktop {
+ position: fixed;
+ left: max(1rem, calc((100vw - var(--layout-width)) / 2 - 240px));
+ top: 120px;
+ width: 200px;
+ max-height: calc(100vh - 200px);
+ overflow-y: auto;
+ display: none;
+}
+
+@media (min-width: 1400px) {
+ .toc-desktop {
+ display: block;
+ }
+}
+
+.toc-title {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ margin-bottom: 1rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.toc-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.toc-item {
+ margin-bottom: 0.5rem;
+ position: relative;
+}
+
+.toc-item.level-3 {
+ padding-left: 1rem;
+}
+
+.toc-item a {
+ display: block;
+ padding: 0.25rem 0;
+ padding-left: 0.75rem;
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ text-decoration: none;
+ border-left: 2px solid transparent;
+ transition: all var(--transition-fast);
+ line-height: 1.5;
+}
+
+.toc-item a:hover {
+ color: var(--text-primary);
+ border-left-color: var(--text-tertiary);
+}
+
+.toc-item.active a {
+ color: var(--accent-color);
+ font-weight: 600;
+ border-left-color: var(--accent-color);
+ border-left-width: 3px;
+}
+
+/* 移动端:浮动按钮 + 抽屉 */
+.toc-mobile {
+ display: block;
+ position: fixed;
+ bottom: 1.5rem;
+ right: 1.5rem;
+ z-index: 100;
+}
+
+@media (min-width: 1400px) {
+ .toc-mobile {
+ display: none;
+ }
+}
+
+.toc-toggle {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: var(--accent-color);
+ color: white;
+ border: none;
+ box-shadow: var(--shadow-lg);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--transition-base);
+}
+
+.toc-toggle:hover {
+ transform: scale(1.1);
+ box-shadow: var(--shadow-xl);
+}
+
+.toc-toggle:active {
+ transform: scale(0.95);
+}
+
+.toc-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 200;
+ animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.toc-drawer {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ max-height: 60vh;
+ background: var(--bg-primary);
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+ box-shadow: var(--shadow-xl);
+ z-index: 201;
+ animation: slideUp 0.3s ease-out;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+.toc-drawer-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.25rem 1.5rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.toc-drawer-title {
+ font-size: 1.125rem;
+ font-weight: 700;
+ color: var(--text-heading);
+}
+
+.toc-drawer-close {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: none;
+ background: var(--bg-secondary);
+ color: var(--text-secondary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--transition-fast);
+}
+
+.toc-drawer-close:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.toc-drawer-list {
+ list-style: none;
+ padding: 1rem;
+ margin: 0;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.toc-drawer-item {
+ margin-bottom: 0.5rem;
+}
+
+.toc-drawer-item.level-3 {
+ padding-left: 1rem;
+}
+
+.toc-drawer-item a {
+ display: block;
+ padding: 0.75rem 1rem;
+ font-size: 1rem;
+ color: var(--text-secondary);
+ text-decoration: none;
+ border-radius: var(--radius-md);
+ transition: all var(--transition-fast);
+ border-left: 3px solid transparent;
+}
+
+.toc-drawer-item a:active {
+ transform: scale(0.98);
+}
+
+.toc-drawer-item:hover a {
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+.toc-drawer-item.active a {
+ background: var(--bg-secondary);
+ color: var(--accent-color);
+ font-weight: 600;
+ border-left-color: var(--accent-color);
+}
+
+/* 返回顶部按钮 */
+.back-to-top {
+ position: fixed;
+ bottom: 1.5rem;
+ right: 1.5rem;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: var(--accent-secondary);
+ color: white;
+ border: none;
+ box-shadow: var(--shadow-lg);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--transition-base);
+ z-index: 50;
+ animation: fadeIn 0.3s ease-out;
+}
+
+.back-to-top:hover {
+ transform: scale(1.1);
+ box-shadow: var(--shadow-xl);
+}
+
+.back-to-top:active {
+ transform: scale(0.95);
+}
+
+/* 当有TOC按钮时,调整返回顶部按钮位置 */
+@media (max-width: 1399px) {
+ .back-to-top {
+ bottom: 5.5rem;
+ }
+}
diff --git a/src/styles/typography.css b/src/styles/typography.css
new file mode 100644
index 0000000..71f93c4
--- /dev/null
+++ b/src/styles/typography.css
@@ -0,0 +1,267 @@
+/* 排版样式 */
+article {
+ line-height: 1.75;
+ color: var(--text-primary);
+ font-size: 1.125rem; /* 18px */
+}
+
+article h1,
+article h2,
+article h3,
+article h4,
+article h5,
+article h6 {
+ margin-top: 3em;
+ margin-bottom: 1em;
+ line-height: 1.4;
+ font-weight: 700;
+ color: var(--text-heading);
+ letter-spacing: -0.02em;
+ scroll-margin-top: 5rem;
+ position: relative;
+}
+
+article h1:first-child,
+article h2:first-child,
+article h3:first-child,
+article h4:first-child {
+ margin-top: 0;
+}
+
+article h1 {
+ font-size: 2.5rem;
+ color: var(--text-heading);
+ position: relative;
+ padding-bottom: 0.5rem;
+}
+
+article h1::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 60px;
+ height: 4px;
+ background: var(--accent-color);
+ border-radius: 2px;
+}
+
+article h2 {
+ font-size: 1.875rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid var(--border-color);
+}
+
+article h3 {
+ font-size: 1.5rem;
+ font-weight: 600;
+}
+
+article h4 {
+ font-size: 1.375rem;
+}
+
+article h5 {
+ font-size: 1.125rem;
+}
+
+article h6 {
+ font-size: 1rem;
+ color: var(--text-secondary);
+}
+
+article p {
+ margin-bottom: 1.75em;
+}
+
+article a {
+ color: var(--link-color);
+ text-decoration: none;
+ border-bottom: 2px solid transparent;
+ background-image: linear-gradient(var(--link-color), var(--link-color));
+ background-size: 0% 2px;
+ background-repeat: no-repeat;
+ background-position: left bottom;
+ transition: background-size var(--transition-base), color var(--transition-base);
+ font-weight: 500;
+}
+
+article a:hover {
+ color: var(--link-hover);
+ background-size: 100% 2px;
+}
+
+article ul,
+article ol {
+ margin-bottom: 1.5em;
+ padding-left: 2em;
+}
+
+article ul {
+ list-style-type: disc;
+}
+
+article ol {
+ list-style-type: decimal;
+}
+
+article li {
+ margin-bottom: 0.5em;
+ line-height: 1.8;
+}
+
+article li > ul,
+article li > ol {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+article ul ul {
+ list-style-type: circle;
+}
+
+article ul ul ul {
+ list-style-type: square;
+}
+
+article code {
+ padding: 0.25rem 0.5rem;
+ background: var(--code-inline-bg);
+ border-radius: var(--radius-sm);
+ font-size: 0.9em;
+ font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
+ color: var(--code-inline-color);
+ font-weight: 500;
+ border: 1px solid var(--code-inline-border);
+}
+
+article pre {
+ margin-bottom: 1.5em;
+ padding: 1.5em;
+ background: var(--code-block-bg);
+ border-radius: var(--radius-lg);
+ overflow-x: auto;
+ box-shadow: var(--shadow-md);
+ border: 1px solid var(--code-block-border);
+ position: relative;
+ line-height: 1.6;
+}
+
+article pre::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 4px;
+ height: 100%;
+ background: var(--accent-color);
+ border-radius: var(--radius-sm) 0 0 var(--radius-sm);
+}
+
+article pre code {
+ padding: 0;
+ background: none;
+ font-size: 0.875rem;
+ border: none;
+ color: inherit;
+ font-weight: 400;
+ display: block;
+}
+
+article blockquote {
+ margin: 2em 0;
+ padding: 1.5em 1.5em 1.5em 2em;
+ border-left: 6px solid var(--accent-color);
+ background: var(--quote-bg);
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-style: italic;
+ position: relative;
+ box-shadow: var(--shadow-sm);
+}
+
+article blockquote p {
+ margin-bottom: 0.5em;
+}
+
+article blockquote p:last-child {
+ margin-bottom: 0;
+}
+
+article img {
+ max-width: 100%;
+ height: auto;
+ border-radius: var(--radius-lg);
+ margin: 2em 0;
+ box-shadow: var(--shadow-lg);
+ transition: transform var(--transition-base), box-shadow var(--transition-base);
+}
+
+article img:hover {
+ transform: scale(1.01);
+ box-shadow: var(--shadow-xl);
+}
+
+article hr {
+ margin: 3em 0;
+ border: none;
+ height: 1px;
+ background: linear-gradient(to right, transparent, var(--border-color), transparent);
+}
+
+/* 表格样式 */
+article table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 2rem 0;
+ background: var(--bg-primary);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--table-border);
+}
+
+article thead {
+ background: var(--table-header-bg);
+}
+
+article th {
+ padding: 0.75rem 1rem;
+ text-align: left;
+ font-weight: 600;
+ color: var(--text-heading);
+ border-bottom: 2px solid var(--table-border);
+ font-size: 0.9375rem;
+}
+
+article td {
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--table-border);
+ color: var(--text-primary);
+}
+
+article tr:last-child td {
+ border-bottom: none;
+}
+
+article tbody tr {
+ transition: background var(--transition-fast);
+}
+
+article tbody tr:nth-child(even) {
+ background: var(--table-stripe-bg);
+}
+
+article tbody tr:hover {
+ background: var(--bg-secondary);
+}
+
+/* 移动端表格滚动 */
+@media (max-width: 768px) {
+ article table {
+ display: block;
+ overflow-x: auto;
+ white-space: nowrap;
+ -webkit-overflow-scrolling: touch;
+ }
+}
diff --git a/src/types/post.ts b/src/types/post.ts
new file mode 100644
index 0000000..69cb818
--- /dev/null
+++ b/src/types/post.ts
@@ -0,0 +1,19 @@
+import type { CollectionEntry } from 'astro:content';
+
+export type Post = CollectionEntry<'posts'>;
+
+export interface PostMetadata {
+ title: string;
+ description: string;
+ pubDate: Date;
+ updatedDate?: Date;
+ author: string;
+ image?: string;
+ tags: string[];
+ category: string;
+ draft: boolean;
+}
+
+export interface PostWithReadingTime extends Post {
+ readingTime: string;
+}
diff --git a/src/utils/date.ts b/src/utils/date.ts
new file mode 100644
index 0000000..b9201e7
--- /dev/null
+++ b/src/utils/date.ts
@@ -0,0 +1,15 @@
+export function formatDate(date: Date): string {
+ return new Date(date).toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+}
+
+export function formatDateShort(date: Date): string {
+ return new Date(date).toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ });
+}
diff --git a/src/utils/extract-headings.ts b/src/utils/extract-headings.ts
new file mode 100644
index 0000000..baba6f3
--- /dev/null
+++ b/src/utils/extract-headings.ts
@@ -0,0 +1,29 @@
+// 提取文章标题的工具函数
+export interface Heading {
+ id: string;
+ text: string;
+ level: number;
+}
+
+export function extractHeadings(content: string): Heading[] {
+ const headings: Heading[] = [];
+ const lines = content.split('\n');
+
+ for (const line of lines) {
+ // 匹配 Markdown 标题 (## 或 ###)
+ const match = line.match(/^(#{2,3})\s+(.+)$/);
+ if (match) {
+ const level = match[1].length;
+ const text = match[2].trim();
+ // 生成 ID(简单处理,实际 Astro 会自动生成)
+ const id = text
+ .toLowerCase()
+ .replace(/[^\w\u4e00-\u9fa5]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+
+ headings.push({ id, text, level });
+ }
+ }
+
+ return headings;
+}
diff --git a/src/utils/reading-time.ts b/src/utils/reading-time.ts
new file mode 100644
index 0000000..0b7b663
--- /dev/null
+++ b/src/utils/reading-time.ts
@@ -0,0 +1,19 @@
+export function getReadingTime(content: string): string {
+ const wordsPerMinute = 200;
+ const chineseCharsPerMinute = 300;
+
+ // 计算中文字符数
+ const chineseChars = (content.match(/[\u4e00-\u9fa5]/g) || []).length;
+
+ // 计算英文单词数
+ const englishWords = content
+ .replace(/[\u4e00-\u9fa5]/g, '')
+ .split(/\s+/)
+ .filter(word => word.length > 0).length;
+
+ const minutes = Math.ceil(
+ chineseChars / chineseCharsPerMinute + englishWords / wordsPerMinute
+ );
+
+ return `${minutes} 分钟`;
+}
diff --git a/src/utils/search-index.ts b/src/utils/search-index.ts
new file mode 100644
index 0000000..a7606bb
--- /dev/null
+++ b/src/utils/search-index.ts
@@ -0,0 +1,28 @@
+// 搜索索引生成工具
+import { getCollection } from 'astro:content';
+
+export interface SearchIndexItem {
+ id: string;
+ title: string;
+ description: string;
+ content: string;
+ category: string;
+ tags: string[];
+ date: string;
+ url: string;
+}
+
+export async function generateSearchIndex(): Promise {
+ const posts = await getCollection('posts', ({ data }) => !data.draft);
+
+ return posts.map(post => ({
+ id: post.slug,
+ title: post.data.title,
+ description: post.data.description,
+ content: post.body.slice(0, 500), // 仅索引前500字符
+ category: post.data.category,
+ tags: post.data.tags,
+ date: post.data.pubDate.toISOString(),
+ url: `/posts/${post.slug}`,
+ }));
+}