Sistine Starter

博客系统

创建和管理多语言博客内容

博客系统

Sistine Starter 包含一个强大的博客系统,支持 MDX、多语言、分类和作者管理。

系统架构

目录结构

app/[locale]/(marketing)/blog/
├── page.tsx                          # 博客列表页
├── [slug]/
│   └── page.tsx                      # 博客文章页

content/blog/
├── article-1/
│   ├── en.mdx                        # 英文版本
│   ├── zh.mdx                        # 中文版本
│   └── thumbnail.jpeg                # 文章缩略图
├── article-2/
│   ├── en.mdx
│   ├── zh.mdx
│   └── thumbnail.jpeg
└── [...更多文章]

lib/
├── blog.ts                           # 博客加载和处理逻辑
└── blog-manifest.generated.ts        # 自动生成的博客清单

创建博客文章

1. 文件结构

content/blog/ 下创建文件夹:

content/blog/my-first-article/
├── en.mdx                  # 英文内容
├── zh.mdx                  # 中文内容
└── thumbnail.jpeg          # 缩略图 (推荐 1200×630px)

2. Frontmatter 格式

en.mdx:

export const blog = {
  author: { name: "Your Name", src: "/avatar.jpeg" },
  date: "2025-01-15",
  title: "How to Build AI SaaS Products",
  description: "Learn how to build production-ready AI SaaS applications using Sistine Starter",
  image: "https://images.unsplash.com/photo-xxxxx",
};

export const metadata = {
  title: blog.title,
  description: blog.description,
  openGraph: {
    images: [blog.image],
  },
};

export default (props) => <BlogLayout blog={blog} {...props} />;

# How to Build AI SaaS Products

文章内容...

zh.mdx:

export const blog = {
  author: { name: "你的名字", src: "/avatar.jpeg" },
  date: "2025-01-15",
  title: "如何构建 AI SaaS 产品",
  description: "学习如何使用 Sistine Starter 构建生产级 AI SaaS 应用",
  image: "https://images.unsplash.com/photo-xxxxx",
};

export const metadata = {
  title: blog.title,
  description: blog.description,
  openGraph: {
    images: [blog.image],
  },
};

export default (props) => <BlogLayout blog={blog} {...props} />;

# 如何构建 AI SaaS 产品

文章内容...

3. Frontmatter 字段说明

字段类型说明示例
authorObject作者信息{ name: "John", src: "/avatar.jpg" }
datestring发布日期 (YYYY-MM-DD)"2025-01-15"
titlestring文章标题"How to Build AI SaaS"
descriptionstring文章摘要"Complete guide..."
imagestringOG 图像 URL"https://images..."
categorystring (可选)分类标签"Technology"
tagsstring[] (可选)文章标签["AI", "SaaS"]

博客加载逻辑

lib/blog.ts

interface Blog {
  author: { name: string; src: string };
  date: string;
  title: string;
  description: string;
  image?: string;
}

// 获取所有博客
export async function getAllBlogs(locale: string = 'en') {
  // 动态导入所有博客模块
  // 按发布日期降序排列
  // 返回博客元数据数组
}

// 获取指定博客
export async function getBlogModule(slug: string, locale: string) {
  // 根据 slug 和 locale 获取博客 MDX 模块
  // 返回包含 blog、metadata、default 的对象
}

// 获取博客清单
export async function getBlogManifest() {
  // 返回所有可用的 blog slug
}

自动生成博客清单

# 运行此脚本自动生成 blog-manifest.generated.ts
pnpm generate:blog

生成的 lib/blog-manifest.generated.ts 包含所有博客的元数据。

博客列表页

app/[locale]/(marketing)/blog/page.tsx:

import { getAllBlogs } from '@/lib/blog';
import { getTranslations, getLocale } from 'next-intl/server';
import { Metadata } from 'next';

export async function generateMetadata({
  params
}: {
  params: { locale: string }
}): Promise<Metadata> {
  const t = await getTranslations({ locale: params.locale, namespace: 'seo' });

  return {
    title: t('blog.title'),
    description: t('blog.description'),
  };
}

export default async function BlogPage({
  params
}: {
  params: { locale: string }
}) {
  const locale = await getLocale();
  const blogs = await getAllBlogs(locale);

  return (
    <div className="container py-12">
      <h1 className="text-4xl font-bold mb-4">博客</h1>
      <p className="text-muted-foreground mb-12">
        关于 AI SaaS 开发的最新文章和技巧
      </p>

      {/* 首页特色文章 (前两篇) */}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
        {blogs.slice(0, 2).map((blog) => (
          <BlogCard key={blog.slug} blog={blog} featured />
        ))}
      </div>

      {/* 其他文章 */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {blogs.slice(2).map((blog) => (
          <BlogCard key={blog.slug} blog={blog} />
        ))}
      </div>
    </div>
  );
}

博客文章页

app/[locale]/(marketing)/blog/[slug]/page.tsx:

import { getBlogModule, getBlogManifest } from '@/lib/blog';
import { getLocale } from 'next-intl/server';

export async function generateStaticParams() {
  const manifest = await getBlogManifest();

  // 为每个 slug 和 locale 组合生成路由
  return manifest.flatMap(({ slug }) => [
    { locale: 'en', slug },
    { locale: 'zh', slug },
  ]);
}

export async function generateMetadata({
  params
}: {
  params: { slug: string; locale: string }
}) {
  const blog = await getBlogModule(params.slug, params.locale);

  return {
    title: `${blog.title} | Sistine Starter`,
    description: blog.description,
    openGraph: {
      images: [blog.image],
    },
  };
}

export default async function BlogPostPage({
  params
}: {
  params: { slug: string; locale: string }
}) {
  const blog = await getBlogModule(params.slug, params.locale);
  const Content = blog.default;

  return (
    <article className="container py-12 max-w-3xl">
      {/* 文章头部 */}
      <header className="mb-8">
        <h1 className="text-5xl font-bold mb-4">{blog.title}</h1>
        <div className="flex items-center gap-4 text-muted-foreground">
          <img
            src={blog.author.src}
            alt={blog.author.name}
            className="w-10 h-10 rounded-full"
          />
          <div>
            <p className="font-semibold text-foreground">{blog.author.name}</p>
            <time dateTime={blog.date}>
              {new Date(blog.date).toLocaleDateString(params.locale)}
            </time>
          </div>
        </div>
      </header>

      {/* 文章缩略图 */}
      {blog.image && (
        <img
          src={blog.image}
          alt={blog.title}
          className="w-full h-96 object-cover rounded-lg mb-8"
        />
      )}

      {/* 文章内容 */}
      <div className="prose dark:prose-invert max-w-none">
        <Content />
      </div>
    </article>
  );
}

MDX 组件和特性

1. 代码高亮

\`\`\`typescript
export const blog = {
  title: "My Article",
  date: "2025-01-15",
};
\`\`\`

2. 表格

| 功能 | 说明 |
|------|------|
| AI对话 | 实时流式对话 |
| 图像生成 | 图生图功能 |
| 视频生成 | 文本或图像生成视频 |

3. 自定义组件

<Callout type="info">
这是一个信息提示框
</Callout>

<Callout type="warning">
这是一个警告提示框
</Callout>

最佳实践

1. 文章标题

推荐:

  • 清晰、具体
  • 包含关键词
  • 50-60 字符

避免:

  • 太长或太短
  • 含糊其辞
  • 全大写

2. 发布日期格式

始终使用 ISO 8601 格式 (YYYY-MM-DD):

date: "2025-01-15"  // ✅ 正确
date: "Jan 15, 2025" // ❌ 错误

3. 作者头像

  • 尺寸: 至少 200×200px
  • 格式: JPG, PNG 或 WebP
  • 路径: 放在 public/ 目录下

4. 缩略图

  • 尺寸: 1200×630px (OG 标准)
  • 文件大小: < 500KB
  • 格式: JPG 或 PNG
  • 命名: 使用描述性名称

多语言文章

管理翻译

content/blog/my-article/
├── en.mdx          # 英文版本
├── zh.mdx          # 中文版本
└── thumbnail.jpeg  # 共用缩略图

自动语言选择

博客系统会根据 URL locale 参数自动选择对应语言:

  • /en/blog/my-article → 加载 en.mdx
  • /zh/blog/my-article → 加载 zh.mdx

语言切换链接

export function LanguageSwitcher({ slug, locale }) {
  const otherLocale = locale === 'en' ? 'zh' : 'en';

  return (
    <a href={`/${otherLocale}/blog/${slug}`}>
      {otherLocale === 'en' ? 'English' : '中文'}
    </a>
  );
}

常见问题

1. 文章不显示

原因:

  • 文件夹名称与 slug 不匹配
  • 缺少 frontmatter
  • 博客清单未更新

解决:

# 重新生成博客清单
pnpm generate:blog

2. 缩略图未显示

原因:

  • 图片格式不支持
  • 路径错误
  • 文件不存在

解决:

  • 使用支持的格式 (JPG, PNG, WebP)
  • 验证文件位置: content/blog/article-name/thumbnail.jpeg

3. 多语言内容不同步

原因:

  • 忘记创建翻译文件
  • Frontmatter 字段不一致

解决:

# 检查文件是否齐全
ls content/blog/my-article/
# 应输出: en.mdx zh.mdx thumbnail.jpeg

部署清单

  • 所有文章都有 en.mdx 和 zh.mdx
  • Frontmatter 字段完整且格式正确
  • 文章日期使用 YYYY-MM-DD 格式
  • 缩略图大小为 1200×630px
  • 作者头像大小 >= 200×200px
  • 运行 pnpm generate:blog 生成清单
  • 在本地预览验证所有文章正常显示
  • 检查 SEO 元数据正确生成

下一步