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:
parent
6a43803d92
commit
11e1df01ed
@ -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) {
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
|
@ -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
|
||||
---
|
||||
|
@ -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
|
||||
---
|
||||
|
@ -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
88
docs/multilingual-blog.md
Normal 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. 测试不同语言之间的内容关系是否正确
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user