TanStarter Docs

博客

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

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

博客系统架构

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

hello-world.md
getting-started.md
deploy-to-production.md
index.tsx
$slug.tsx
blog-card.tsx
blog-grid.tsx
blog-pagination.tsx
content.ts
blog.ts
content-collections.ts

数据源配置

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

content-collections.ts
import { defineCollection, defineConfig } from '@content-collections/core';
import { z } from 'zod';
import { renderMarkdown } from './scripts/lib/markdown';

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.string().min(1),
  }),
  transform: async (doc) => {
    const { content, ...metadata } = doc;
    return {
      ...metadata,
      ...getLocaleSlug(doc._meta.path),
      markdown: await renderMarkdown(content),
    };
  },
});

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

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

src/lib/blog.ts
import '@tanstack/react-start/server-only';
import { allBlogs } from 'content-collections';
import type { Blog } from 'content-collections';
import { websiteConfig } from '@/config/website';
import { baseLocale, getLocale, type Locale } from '@/lib/locale';

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

export type BlogSummary = Omit<BlogPost, 'markdown' | '_meta'>;

const DEFAULT_PAGE_SIZE = 6;

function getPageSize(): number {
  return websiteConfig.blog?.paginationSize ?? DEFAULT_PAGE_SIZE;
}

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

export function getPostBySlug(
  slug: string,
  locale: Locale = getLocale()
): BlogPost | undefined {
  const posts = allBlogs as BlogPost[];
  return (
    posts.find((p) => p.slug === slug && p.locale === locale) ??
    posts.find((p) => p.slug === slug && p.locale === baseLocale)
  );
}

export function getPaginatedPosts(
  page: number,
  locale: Locale = getLocale()
): {
  posts: BlogSummary[];
  totalPages: number;
  currentPage: number;
} {
  const pageSize = getPageSize();
  const sorted = getSortedPosts(locale);
  const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
  const currentPage = Math.max(1, Math.min(page, totalPages));
  const start = (currentPage - 1) * pageSize;
  return {
    posts: sorted
      .slice(start, start + pageSize)
      .map(({ markdown, _meta, ...summary }) => summary),
    totalPages,
    currentPage,
  };
}

export function getPostLocales(slug: string): Locale[] {
  return (allBlogs as BlogPost[])
    .filter((item) => item.slug === slug)
    .map((item) => item.locale);
}

创建博客内容

添加新博客文章

在 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✅文章分类(单个分类)
imagestring✅封面图片 URL

TanStarter 的博客使用 .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 { getBlogPage } from '@/api/content';
import { getLocale } from '@/lib/locale';

export const Route = createFileRoute('/blog/')({
  loader: ({ location }) => {
    const page = Number(new URLSearchParams(location.search).get('page')) || 1;
    return getBlogPage({ data: { page, locale: getLocale() } });
  },
  component: BlogListPage,
});
src/routes/blog/$slug.tsx
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getBlogPost } from '@/api/content';
import { getLocale } from '@/lib/locale';

export const Route = createFileRoute('/blog/$slug')({
  loader: async ({ params }) => {
    const post = await getBlogPost({
      data: { slug: params.slug, locale: getLocale() },
    });
    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 { BlogSummary } from '@/lib/blog';

export function BlogCard({ post }: { post: BlogSummary }) {
  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.string().min(1),
    // Add new field
    featured: z.boolean().default(false),
  }),
  transform: async (doc) => {
    const { content, ...metadata } = doc;
    return {
      ...metadata,
      ...getLocaleSlug(doc._meta.path),
      markdown: await renderMarkdown(content),
    };
  },
});

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

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 中的工具函数查询文章。该模块只能在服务端使用,请在服务端函数(例如 src/api/content.ts 中的函数)里调用,不要直接在路由 loader 或组件中调用:

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 进行类型安全的内容验证

下一步

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

最后更新于

本页目录