个人博客 #

一、项目概述 #

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