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 * Blog Author collection
*
* Authors are identified by their slug across all languages
*/ */
export const authors = defineCollection({ export const authors = defineCollection({
name: 'author', name: 'author',
@ -46,6 +48,8 @@ export const authors = defineCollection({
/** /**
* Blog Category collection * Blog Category collection
*
* Categories are identified by their slug across all languages
*/ */
export const categories = defineCollection({ export const categories = defineCollection({
name: 'category', name: 'category',
@ -73,17 +77,12 @@ export const categories = defineCollection({
/** /**
* Blog Post collection * Blog Post collection
* *
* 1. For a blog post file at content/en/blog/2023/year-review.mdx: * 1. For a blog post at content/en/blog/first-post.mdx:
* locale: en
* slug: /blog/2023/year-review
* slugAsParams: 2023/year-review
*
* 2. For a blog post at content/en/blog/first-post.mdx:
* locale: en * locale: en
* slug: /blog/first-post * slug: /blog/first-post
* slugAsParams: 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 * locale: zh
* slug: /blog/first-post * slug: /blog/first-post
* slugAsParams: first-post * slugAsParams: first-post
@ -114,18 +113,33 @@ export const posts = defineCollection({
[rehypePrettyCode, prettyCodeOptions] [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 // Determine the locale from the file path or use the provided locale
const pathParts = data._meta.path.split(path.sep); const pathParts = data._meta.path.split(path.sep);
const localeFromPath = LOCALES.includes(pathParts[0]) ? pathParts[0] : null; const localeFromPath = LOCALES.includes(pathParts[0]) ? pathParts[0] : null;
const locale = data.locale || localeFromPath || DEFAULT_LOCALE; 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 // Create a slug without the locale in the path
let slugPath = data._meta.path; let slugPath = data._meta.path;
if (localeFromPath) { 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 image: /images/blog/indiehub-og.png
date: 2024-11-24T12:00:00.000Z date: 2024-11-24T12:00:00.000Z
published: true published: true
categories: [news, guide] categories: [news, product]
author: indiehub 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 image: /images/blog/mksaas-og.png
date: 2024-11-26T12:00:00.000Z date: 2024-11-26T12:00:00.000Z
published: true published: true
categories: [company, guide] categories: [company, product]
author: mksaas author: mksaas
locale: en locale: en
--- ---

View File

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

View File

@ -4,7 +4,7 @@ description: MkSaaS 是构建 AI SaaS 网站的最佳代码模板。
image: /images/blog/mksaas-og.png image: /images/blog/mksaas-og.png
date: 2024-11-26T12:00:00.000Z date: 2024-11-26T12:00:00.000Z
published: true published: true
categories: [company, guide] categories: [company, product]
author: mksaas author: mksaas
locale: zh 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({ export async function generateMetadata({
params, params,
}: { }: {
params: Promise<{ slug: string }>; params: Promise<{ slug: string; locale: string }>;
}): Promise<Metadata | undefined> { }): Promise<Metadata | undefined> {
const resolvedParams = await params; const resolvedParams = await params;
const { slug } = resolvedParams; const { slug, locale } = resolvedParams;
// Find category with matching slug and locale
const category = allCategories.find( const category = allCategories.find(
(category) => category.slug === slug (category) => category.slug === slug && category.locale === locale
); );
if (!category) { if (!category) {
console.warn( console.warn(
`generateMetadata, category not found for slug: ${slug}`, `generateMetadata, category not found for slug: ${slug}, locale: ${locale}`,
); );
return; return;
} }
@ -52,12 +53,21 @@ export default async function BlogCategoryPage({
const startIndex = (currentPage - 1) * POSTS_PER_PAGE; const startIndex = (currentPage - 1) * POSTS_PER_PAGE;
const endIndex = startIndex + 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 // Filter posts by category and locale
const filteredPosts = allPosts.filter( const filteredPosts = allPosts.filter(
(post) => (post) => {
post.published && if (!post.published || post.locale !== locale) {
post.locale === locale && return false;
post.categories.some(category => category.slug === slug) }
// 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) // 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"> <div className="bg-muted/50 rounded-lg p-6">
<h2 className="text-lg font-semibold mb-4">Categories</h2> <h2 className="text-lg font-semibold mb-4">Categories</h2>
<ul className="flex flex-wrap gap-4"> <ul className="flex flex-wrap gap-4">
{post.categories?.map((category) => ( {post.categories?.filter(Boolean).map((category) => (
<li key={category.slug}> category && (
<Link <li key={category.slug}>
href={{ <Link
pathname: "/blog/category/[slug]", href={{
params: { slug: category.slug } pathname: "/blog/category/[slug]",
}} params: { slug: category.slug }
className="text-sm link-underline" }}
> className="text-sm link-underline"
{category.name} >
</Link> {category.name}
</li> </Link>
</li>
)
))} ))}
</ul> </ul>
</div> </div>

View File

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