LogoShipSaaS

博客

学习如何创建、管理和自定义博客文章

ShipSaaS 内置一个基于 Content Collections 构建的博客系统。

博客系统架构

博客系统使用 Content Collections 构建,在项目根目录的 content-collections.ts 中定义集合配置。

content

blog

hello-world.md

getting-started.md

deploy-to-production.md

src

routes/blog

index.tsx

$slug.tsx

components/blog

blog-card.tsx

blog-grid.tsx

blog-pagination.tsx

lib

blog.ts

content-collections.ts

数据源配置

博客系统在 content-collections.ts 文件中使用 defineCollection 定义数据集合:

content-collections.ts

import { defineCollection, defineConfig } from '@content-collections/core';
import { z } from 'zod';

const blog = defineCollection({
  name: 'blog',
  directory: 'content/blog',
  include: '**/*.md',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.string(),
    category: z.string(),
    content: z.string(),
    image: z.url(),
  }),
  transform: (doc) => ({
    ...doc,
    slug: doc._meta.path,
  }),
});

export default defineConfig({
  collections: [blog],
});

然后在 src/lib/blog.ts 中使用 content-collections 提供的数据进行查询:

src/lib/blog.ts

import { allBlogs } from 'content-collections';
import type { Blog } from 'content-collections';

export type BlogPost = Blog & { slug: string };

export function getSortedPosts(): BlogPost[] {
  return [...(allBlogs as BlogPost[])].sort(
    (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
  );
}

export function getPostBySlug(slug: string): BlogPost | undefined {
  return (allBlogs as BlogPost[]).find((p) => p.slug === slug);
}

export function getPaginatedPosts(page: number) {
  const sorted = getSortedPosts();
  // ... pagination logic
}

创建博客内容

添加新博客文章

content/blog 目录中创建新的 .md 文件:

content/blog/my-first-post.md

---
title: My First Blog Post
description: This is a brief description of my first blog post.
date: 2026-01-15
category: Tutorial
image: https://example.com/images/blog/my-first-post.jpg
---

# Introduction

This is my first blog post. You can use **Markdown** here.

## Section 1

Some content here...

## Section 2

More content here...

Frontmatter 字段说明

字段类型必填说明
titlestring文章标题
descriptionstring文章摘要描述
datestring发布日期(格式:YYYY-MM-DD)
categorystring文章分类(单个分类)
imageURL封面图片 URL

ShipSaaS 的博客使用 .md Markdown 文件格式,每篇文章只有一个 category(字符串),不是多分类数组。

路由和页面

博客使用 TanStack Router 文件路由系统:

  • src/routes/blog/index.tsx:博客列表页,支持分页
  • src/routes/blog/$slug.tsx:博客文章详情页

src/routes/blog/index.tsx

import { createFileRoute } from '@tanstack/react-router';
import { getPaginatedPosts } from '@/lib/blog';

export const Route = createFileRoute('/blog/')({
  loader: ({ location }) => {
    const page = Number(new URLSearchParams(location.search).get('page')) || 1;
    return getPaginatedPosts(page);
  },
  component: BlogListPage,
});

src/routes/blog/$slug.tsx

import { createFileRoute, notFound } from '@tanstack/react-router';
import { getPostBySlug } from '@/lib/blog';

export const Route = createFileRoute('/blog/$slug')({
  loader: async ({ params }) => {
    const post = getPostBySlug(params.slug);
    if (!post) throw notFound();
    return post;
  },
  component: BlogPostPage,
});

自定义

配置分页

src/config/website.ts 中配置每页显示的文章数量:

src/config/website.ts

export const websiteConfig: WebsiteConfig = {
  // ...other config
  blog: {
    enable: true,
    paginationSize: 6,
  },
  // ...other config
}

更改博客文章卡片布局

src/components/blog/blog-card.tsx 中自定义博客卡片组件:

src/components/blog/blog-card.tsx

import type { BlogPost } from '@/lib/blog';

interface BlogCardProps {
  post: BlogPost;
}

export function BlogCard({ post }: BlogCardProps) {
  return (
    <div className="group flex flex-col border rounded-lg overflow-hidden h-full bg-card shadow-sm hover:shadow-md transition-shadow">
      <h3>{post.title}</h3>
      <p>{post.description}</p>
      <span>{post.category}</span>
      <time>{post.date}</time>
      {/* ... rest of the component */}
    </div>
  );
}

自定义博客文章数据结构

要向博客文章添加新字段:

  1. content-collections.ts 中修改 schema
  2. 更新组件以显示新字段

示例:添加"精选"字段

content-collections.ts

const blog = defineCollection({
  name: 'blog',
  directory: 'content/blog',
  include: '**/*.md',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.string(),
    category: z.string(),
    content: z.string(),
    image: z.url(),
    // Add new field
    featured: z.boolean().default(false),
  }),
  transform: (doc) => ({
    ...doc,
    slug: doc._meta.path,
  }),
});

然后,您可以在博客文章中使用此字段:

content/blog/important-post.md

---
title: Important Announcement
description: Read this important announcement
date: 2026-01-15
category: Announcement
image: https://example.com/images/blog/announcement.jpg
featured: true
---

Content here...

程序化查询文章

您可以使用 src/lib/blog.ts 中的工具函数查询文章:

import { getSortedPosts, getPostBySlug, getPaginatedPosts } from '@/lib/blog';

// Get all posts sorted by date
const allPosts = getSortedPosts();

// Get a specific post by slug
const post = getPostBySlug('hello-world');

// Get paginated posts
const { posts, totalPages, currentPage } = getPaginatedPosts(1);

构建过程

博客系统使用 content-collections 的构建过程:

  1. 开发:在开发过程中 content-collections 会监视 content/ 目录的变化并自动重新生成
  2. 构建:Vite 构建时自动处理 content-collections,无需额外命令
  3. 生成的文件.content-collections 目录包含生成的 TypeScript 文件

content-collections 与 Vite 集成,开发和构建过程完全自动化,无需手动运行额外命令。

最佳实践

  1. 使用高质量图片:为博客文章使用适当大小和优化的图片
  2. 一致的分类:在文章列表中保持一致的分类名称
  3. 清晰的元数据:编写清晰的标题和描述以提高 SEO 效果
  4. 结构化内容:在博客文章内容中使用适当的标题和段落
  5. 日期格式:使用 YYYY-MM-DD 格式的日期字符串
  6. 使用 Zod 验证:使用 Zod 进行类型安全的内容验证

下一步

现在您了解了如何在 ShipSaaS 中使用博客系统,您可能想要探索这些相关功能:

On this page