diff --git a/content-collections.ts b/content-collections.ts index 36d7544..208aac7 100644 --- a/content-collections.ts +++ b/content-collections.ts @@ -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) { diff --git a/content/en/blog/what-is-indiehub.mdx b/content/en/blog/what-is-indiehub.mdx index 4ac90e3..b3994e1 100644 --- a/content/en/blog/what-is-indiehub.mdx +++ b/content/en/blog/what-is-indiehub.mdx @@ -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 --- diff --git a/content/en/blog/what-is-mksaas.mdx b/content/en/blog/what-is-mksaas.mdx index a1c5f7c..7d0ddf4 100644 --- a/content/en/blog/what-is-mksaas.mdx +++ b/content/en/blog/what-is-mksaas.mdx @@ -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 --- diff --git a/content/zh/blog/what-is-indiehub.mdx b/content/zh/blog/what-is-indiehub.mdx index 43224f2..adb4d9b 100644 --- a/content/zh/blog/what-is-indiehub.mdx +++ b/content/zh/blog/what-is-indiehub.mdx @@ -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 --- diff --git a/content/zh/blog/what-is-mksaas.mdx b/content/zh/blog/what-is-mksaas.mdx index 9cbffe9..4f4f21c 100644 --- a/content/zh/blog/what-is-mksaas.mdx +++ b/content/zh/blog/what-is-mksaas.mdx @@ -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 --- diff --git a/docs/multilingual-blog.md b/docs/multilingual-blog.md new file mode 100644 index 0000000..2fb35bb --- /dev/null +++ b/docs/multilingual-blog.md @@ -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. 测试不同语言之间的内容关系是否正确 \ No newline at end of file diff --git a/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx b/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx index d9cad85..309922e 100644 --- a/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx +++ b/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx @@ -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 { 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) diff --git a/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx b/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx index a06968c..339df62 100644 --- a/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx +++ b/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx @@ -158,18 +158,20 @@ export default async function BlogPostPage(props: NextPageProps) {

Categories

diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts index 513f326..a186763 100644 --- a/src/i18n/routing.ts +++ b/src/i18n/routing.ts @@ -1,10 +1,10 @@ import { defineRouting } from "next-intl/routing"; -export const DEFAULT_LOCALE = "en"; -export const LOCALE_LIST: Record = { - en: "🇬🇧 English", - zh: "🇨🇳 中文", +export const LOCALE_LIST: Record = { + en: { flag: "🇺🇸", name: "English" }, + zh: { flag: "🇨🇳", name: "中文" }, }; +export const DEFAULT_LOCALE = "en"; export const LOCALES = Object.keys(LOCALE_LIST); /**