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
|
* 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) {
|
||||||
|
@ -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
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -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
|
||||||
---
|
---
|
||||||
|
@ -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
|
||||||
---
|
---
|
||||||
|
@ -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
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({
|
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)
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user