个人博客 #
一、项目概述 #
1.1 功能需求 #
- 文章列表和详情页
- 分类和标签系统
- 文章搜索
- 评论功能
- RSS 订阅
- SEO 优化
1.2 技术栈 #
| 技术 | 用途 |
|---|---|
| Astro | 框架 |
| Content Collections | 内容管理 |
| Tailwind CSS | 样式 |
| TypeScript | 类型安全 |
二、项目结构 #
text
my-blog/
├── src/
│ ├── components/
│ │ ├── Header.astro
│ │ ├── Footer.astro
│ │ ├── PostCard.astro
│ │ ├── TagList.astro
│ │ └── Search.astro
│ ├── content/
│ │ ├── config.ts
│ │ └── blog/
│ │ └── *.md
│ ├── layouts/
│ │ ├── Layout.astro
│ │ └── PostLayout.astro
│ ├── pages/
│ │ ├── index.astro
│ │ ├── blog/
│ │ │ ├── index.astro
│ │ │ └── [slug].astro
│ │ ├── tags/
│ │ │ └── [tag].astro
│ │ └── rss.xml.js
│ └── styles/
│ └── global.css
├── public/
│ └── images/
├── astro.config.mjs
└── package.json
三、内容集合配置 #
3.1 定义 Schema #
typescript
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: ({ image }) => z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
updated: z.coerce.date().optional(),
cover: image().optional(),
coverAlt: z.string().optional(),
author: z.string().default('作者'),
tags: z.array(z.string()).default([]),
category: z.string(),
draft: z.boolean().default(false),
featured: z.boolean().default(false),
}),
});
export const collections = { blog };
3.2 创建文章 #
markdown
---
title: Astro
description: 使用 Astro 构建一个的个人博客网站,包含文章管理、分类标签、评论等功能。
date: 2024-01-15
author: 张三
tags:
- Astro
- 前端
category: 前端
featured: true
---
# Astro 入门指南
Astro 是一款现代化的静态站点生成器...
## 为什么选择 Astro?
Astro 具有以下优势...
四、页面实现 #
4.1 首页 #
astro
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
import PostCard from '../components/PostCard.astro';
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog'))
.filter(post => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const featuredPosts = posts.filter(post => post.data.featured).slice(0, 3);
const recentPosts = posts.slice(0, 6);
---
<Layout title="首页 - 我的博客">
<main>
<section class="hero">
<h1>欢迎来到我的博客</h1>
<p>分享技术、记录生活</p>
</section>
<section class="featured">
<h2>精选文章</h2>
<div class="posts-grid">
{featuredPosts.map(post => (
<PostCard post={post} featured />
))}
</div>
</section>
<section class="recent">
<h2>最新文章</h2>
<div class="posts-grid">
{recentPosts.map(post => (
<PostCard post={post} />
))}
</div>
</section>
</main>
</Layout>
4.2 文章列表页 #
astro
---
// src/pages/blog/index.astro
import Layout from '../../layouts/Layout.astro';
import PostCard from '../../components/PostCard.astro';
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog'))
.filter(post => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<Layout title="博客文章">
<main>
<h1>全部文章</h1>
<div class="posts-list">
{posts.map(post => (
<PostCard post={post} />
))}
</div>
</main>
</Layout>
4.3 文章详情页 #
astro
---
// src/pages/blog/[slug].astro
import Layout from '../../layouts/Layout.astro';
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content, headings } = await render(post);
---
<Layout title={post.data.title} description={post.data.description}>
<article class="post">
<header>
<h1>{post.data.title}</h1>
<div class="meta">
<time>{post.data.date.toLocaleDateString('zh-CN')}</time>
<span>·</span>
<span>{post.data.author}</span>
</div>
<div class="tags">
{post.data.tags.map(tag => (
<a href={`/tags/${tag}`} class="tag">{tag}</a>
))}
</div>
</header>
<div class="content">
<Content />
</div>
</article>
</Layout>
4.4 标签页 #
astro
---
// src/pages/tags/[tag].astro
import Layout from '../../layouts/Layout.astro';
import PostCard from '../../components/PostCard.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
const tags = [...new Set(posts.flatMap(post => post.data.tags))];
return tags.map(tag => ({
params: { tag },
props: {
posts: posts.filter(post => post.data.tags.includes(tag)),
},
}));
}
const { tag } = Astro.params;
const { posts } = Astro.props;
---
<Layout title={`标签: ${tag}`}>
<main>
<h1>标签: {tag}</h1>
<p>共 {posts.length} 篇文章</p>
<div class="posts-list">
{posts.map(post => (
<PostCard post={post} />
))}
</div>
</main>
</Layout>
五、组件实现 #
5.1 文章卡片 #
astro
---
// src/components/PostCard.astro
import type { CollectionEntry } from 'astro:content';
interface Props {
post: CollectionEntry<'blog'>;
featured?: boolean;
}
const { post, featured = false } = Astro.props;
---
<article class:list={['post-card', { featured }]}>
<a href={`/blog/${post.slug}`}>
<h2>{post.data.title}</h2>
<p class="description">{post.data.description}</p>
<div class="meta">
<time>{post.data.date.toLocaleDateString('zh-CN')}</time>
<span class="category">{post.data.category}</span>
</div>
</a>
</article>
<style>
.post-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1.5rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.post-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
.post-card.featured {
border-color: #2563eb;
}
h2 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.description {
color: #6b7280;
margin-bottom: 1rem;
}
.meta {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: #9ca3af;
}
</style>
5.2 导航头部 #
astro
---
// src/components/Header.astro
const navItems = [
{ label: '首页', href: '/' },
{ label: '博客', href: '/blog' },
{ label: '关于', href: '/about' },
];
const currentPath = Astro.url.pathname;
---
<header class="header">
<nav class="nav">
<a href="/" class="logo">我的博客</a>
<ul class="nav-list">
{navItems.map(item => (
<li>
<a
href={item.href}
class:list={['nav-link', { active: currentPath === item.href }]}
>
{item.label}
</a>
</li>
))}
</ul>
</nav>
</header>
<style>
.header {
background: white;
border-bottom: 1px solid #e5e7eb;
position: sticky;
top: 0;
z-index: 100;
}
.nav {
max-width: 1200px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: #2563eb;
}
.nav-list {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-link {
color: #4b5563;
transition: color 0.2s;
}
.nav-link:hover,
.nav-link.active {
color: #2563eb;
}
</style>
六、RSS 订阅 #
javascript
// src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: '我的博客',
description: '分享技术、记录生活',
site: context.site,
items: posts
.filter(post => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
.map(post => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.date,
link: `/blog/${post.slug}`,
})),
});
}
七、SEO 优化 #
7.1 布局组件 #
astro
---
// src/layouts/Layout.astro
interface Props {
title: string;
description?: string;
image?: string;
}
const { title, description = '默认描述', image } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalURL} />
{image && <meta property="og:image" content={image} />}
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
八、总结 #
个人博客项目要点:
text
┌─────────────────────────────────────────────────────┐
│ 博客项目要点 │
├─────────────────────────────────────────────────────┤
│ │
│ 📝 内容管理 Content Collections 组织文章 │
│ │
│ 📄 页面路由 文件路由自动生成 │
│ │
│ 🏷️ 分类标签 分类和标签系统 │
│ │
│ 📰 RSS 订阅 自动生成 RSS feed │
│ │
│ 🔍 SEO 优化 元数据和结构化数据 │
│ │
└─────────────────────────────────────────────────────┘
恭喜你完成了 Astro 文档的学习!现在你已经掌握了 Astro 的核心概念和实践技能,可以开始构建自己的项目了!
最后更新:2026-03-28