跳转到内容

博客

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...
字段 类型 必填 说明
title string 文章标题
description string 文章摘要描述
date string 发布日期(格式:YYYY-MM-DD)
category string 文章分类(单个分类)
image URL 封面图片 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 中使用博客系统,您可能想要探索这些相关功能: