feat: improve multilingual blog content handling and documentation

- Enhance content collections to support cross-language content matching
- Update blog post categories from "guide" to "product"
- Add documentation for multilingual blog implementation
- Modify category and blog post pages to handle localized content
- Update locale routing with more detailed language information
- Improve content relationship resolution in content collections
This commit is contained in:
javayhu 2025-03-07 00:54:15 +08:00
parent 6a43803d92
commit 11e1df01ed
9 changed files with 155 additions and 41 deletions

View File

@ -19,6 +19,8 @@ import { LOCALES, DEFAULT_LOCALE } from "@/i18n/routing";
/**
* Blog Author collection
*
* Authors are identified by their slug across all languages
*/
export const authors = defineCollection({
name: 'author',
@ -46,6 +48,8 @@ export const authors = defineCollection({
/**
* Blog Category collection
*
* Categories are identified by their slug across all languages
*/
export const categories = defineCollection({
name: 'category',
@ -73,17 +77,12 @@ export const categories = defineCollection({
/**
* Blog Post collection
*
* 1. For a blog post file at content/en/blog/2023/year-review.mdx:
* locale: en
* slug: /blog/2023/year-review
* slugAsParams: 2023/year-review
*
* 2. For a blog post at content/en/blog/first-post.mdx:
* 1. For a blog post at content/en/blog/first-post.mdx:
* locale: en
* slug: /blog/first-post
* slugAsParams: first-post
*
* 3. For a blog post at content/zh/blog/first-post.mdx:
* 2. For a blog post at content/zh/blog/first-post.mdx:
* locale: zh
* slug: /blog/first-post
* slugAsParams: first-post
@ -114,18 +113,33 @@ export const posts = defineCollection({
[rehypePrettyCode, prettyCodeOptions]
]
});
const blogAuthor = context
.documents(authors)
.find((a) => a.slug === data.author);
const blogCategories = context
.documents(categories)
.filter((c) => data.categories.includes(c.slug));
// Determine the locale from the file path or use the provided locale
const pathParts = data._meta.path.split(path.sep);
const localeFromPath = LOCALES.includes(pathParts[0]) ? pathParts[0] : null;
const locale = data.locale || localeFromPath || DEFAULT_LOCALE;
// Find the author by matching slug
const blogAuthor = context
.documents(authors)
.find((a) => a.slug === data.author && a.locale === locale) ||
context
.documents(authors)
.find((a) => a.slug === data.author);
// Find categories by matching slug
const blogCategories = data.categories.map(categorySlug => {
// Try to find a category with matching slug and locale
const category = context
.documents(categories)
.find(c => c.slug === categorySlug && c.locale === locale) ||
context
.documents(categories)
.find(c => c.slug === categorySlug);
return category;
}).filter(Boolean); // Remove null values
// Create a slug without the locale in the path
let slugPath = data._meta.path;
if (localeFromPath) {

View File

@ -4,7 +4,7 @@ description: IndieHub is the best all-in-one directory for indie hackers.
image: /images/blog/indiehub-og.png
date: 2024-11-24T12:00:00.000Z
published: true
categories: [news, guide]
categories: [news, product]
author: indiehub
---

View File

@ -4,7 +4,7 @@ description: MkSaaS is the best boilerplate for building AI SaaS websites.
image: /images/blog/mksaas-og.png
date: 2024-11-26T12:00:00.000Z
published: true
categories: [company, guide]
categories: [company, product]
author: mksaas
locale: en
---

View File

@ -4,7 +4,7 @@ description: IndieHub是一站式的独立开发者导航站。
image: /images/blog/indiehub-og.png
date: 2024-11-24T12:00:00.000Z
published: true
categories: [news, guide]
categories: [news, product]
author: indiehub
locale: zh
---

View File

@ -4,7 +4,7 @@ description: MkSaaS 是构建 AI SaaS 网站的最佳代码模板。
image: /images/blog/mksaas-og.png
date: 2024-11-26T12:00:00.000Z
published: true
categories: [company, guide]
categories: [company, product]
author: mksaas
locale: zh
---

88
docs/multilingual-blog.md Normal file
View File

@ -0,0 +1,88 @@
# 简化的多语言博客实现
本文档说明了项目中简化的多语言博客功能实现方式。
## 概述
多语言博客功能允许博客文章、分类和作者在不同语言中可用,同时使用相同的 slug 来标识相同的内容。
## 关键概念
### 1. 内容组织
内容按语言特定的目录组织:
```
content/
├── en/
│ ├── blog/
│ ├── category/
│ └── author/
└── zh/
├── blog/
├── category/
└── author/
```
### 2. 统一 Slug
每个内容项(文章、分类、作者)在所有语言中使用相同的 slug
- 英文分类:`slug: "news"`
- 中文分类:`slug: "news"`(而不是 "xinwen"
这样可以简化内容关系的处理,不需要额外的映射机制。
## 实现细节
### Content Collections 配置
Content Collections 在 `content-collections.ts` 中配置,包括:
1. 简单的 schema 定义,包含 `slug``locale` 字段
2. Transform 函数,用于:
- 从文件路径确定 locale
- 处理内容之间的关系
### 内容关系
当博客文章引用分类或作者时:
1. 首先尝试查找具有相同 locale 的匹配项
2. 如果找不到,则回退到匹配 slug
3. 这确保了跨语言的关系正确工作
### 语言切换
使用现有的全局语言选择器来切换语言,无需在博客页面上添加额外的语言切换器。
## 添加新内容
添加新的多语言内容时:
1. 在每个语言目录中创建内容
2. 在所有语言中使用相同的 slug
示例(分类):
```mdx
// content/en/category/news.mdx
---
slug: "news"
name: "News"
description: "Latest news and updates"
---
// content/zh/category/news.mdx
---
slug: "news"
name: "新闻"
description: "最新新闻和更新"
---
```
## 最佳实践
1. 在所有语言中保持 slug 一致
2. 确保每个语言的内容都有正确的 locale 属性
3. 测试不同语言之间的内容关系是否正确

View File

@ -11,18 +11,19 @@ import { NextPageProps } from "@/types/next-page-props";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
params: Promise<{ slug: string; locale: string }>;
}): Promise<Metadata | undefined> {
const resolvedParams = await params;
const { slug } = resolvedParams;
const { slug, locale } = resolvedParams;
// Find category with matching slug and locale
const category = allCategories.find(
(category) => category.slug === slug
(category) => category.slug === slug && category.locale === locale
);
if (!category) {
console.warn(
`generateMetadata, category not found for slug: ${slug}`,
`generateMetadata, category not found for slug: ${slug}, locale: ${locale}`,
);
return;
}
@ -52,12 +53,21 @@ export default async function BlogCategoryPage({
const startIndex = (currentPage - 1) * POSTS_PER_PAGE;
const endIndex = startIndex + POSTS_PER_PAGE;
// Find category with matching slug and locale
const category = allCategories.find(
(category) => category.slug === slug && category.locale === locale
);
// Filter posts by category and locale
const filteredPosts = allPosts.filter(
(post) =>
post.published &&
post.locale === locale &&
post.categories.some(category => category.slug === slug)
(post) => {
if (!post.published || post.locale !== locale) {
return false;
}
// Check if any of the post's categories match the current category slug
return post.categories.some(category => category && category.slug === slug);
}
);
// Sort posts by date (newest first)

View File

@ -158,18 +158,20 @@ export default async function BlogPostPage(props: NextPageProps) {
<div className="bg-muted/50 rounded-lg p-6">
<h2 className="text-lg font-semibold mb-4">Categories</h2>
<ul className="flex flex-wrap gap-4">
{post.categories?.map((category) => (
<li key={category.slug}>
<Link
href={{
pathname: "/blog/category/[slug]",
params: { slug: category.slug }
}}
className="text-sm link-underline"
>
{category.name}
</Link>
</li>
{post.categories?.filter(Boolean).map((category) => (
category && (
<li key={category.slug}>
<Link
href={{
pathname: "/blog/category/[slug]",
params: { slug: category.slug }
}}
className="text-sm link-underline"
>
{category.name}
</Link>
</li>
)
))}
</ul>
</div>

View File

@ -1,10 +1,10 @@
import { defineRouting } from "next-intl/routing";
export const DEFAULT_LOCALE = "en";
export const LOCALE_LIST: Record<string, string> = {
en: "🇬🇧 English",
zh: "🇨🇳 中文",
export const LOCALE_LIST: Record<string, { flag: string; name: string }> = {
en: { flag: "🇺🇸", name: "English" },
zh: { flag: "🇨🇳", name: "中文" },
};
export const DEFAULT_LOCALE = "en";
export const LOCALES = Object.keys(LOCALE_LIST);
/**