Adding Chinese Support to Your Blog: A Practical Guide to Multilingual Website Expansion

Adding Chinese Support to Your Blog: A Practical Guide to Multilingual Website Expansion

Experience report on adding Chinese support to 40 posts. Parallel agent processing to update 120 files, SEO optimization, automation strategies - everything about expanding an Astro multilingual blog

Introduction

In November 2025, my blog reached an important milestone. I decided to add Chinese (Simplified) support to my technical blog that had been running in Korean, English, and Japanese.

Traffic from China had been steadily increasing, and I wanted to provide quality content to Chinese-speaking developers as well. However, adding Chinese versions to 40 posts, multilingualize all UI components, and optimize SEO was no small task.

In this article, I’ll share the entire process of adding a new language to an Astro-based multilingual blog in detail. From file structure design to large-scale translation automation and SEO optimization - I’ve transparently documented the problems encountered and solutions found in practice.

What You’ll Learn

  • Designing multilingual content structure using Astro Content Collections
  • How to automate updating 40 posts × 3 languages = 120 files with parallel agents
  • Language-specific sitemap, RSS feed, and metadata configuration strategies
  • UI component localization best practices
  • Automated workflow that reduced work time by 90% while maintaining translation quality

Section 1: File Structure Design

The most important aspect of expanding a multilingual blog is a clear and scalable file structure. Astro Content Collections is the perfect tool for this.

Language-Specific Folder Structure

src/content/blog/
├── ko/          # Korean
├── en/          # English
├── ja/          # Japanese
└── zh/          # Chinese (newly added)

Core Principles:

  1. Same filename: Save with the same name in all language folders
    • Example: ko/ai-agent-collaboration-patterns.md, zh/ai-agent-collaboration-patterns.md
  2. Automatic language detection via language code: Identify language by folder path (getCollection filtering)
  3. Shared assets: heroImage shared from src/assets/blog/ regardless of language

Actual Code Example

// src/pages/[lang]/blog/[...slug].astro
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const allPosts = await getCollection('blog');

  // Filter by language
  const languages = ['ko', 'en', 'ja', 'zh'];
  const paths = [];

  for (const lang of languages) {
    const langPosts = allPosts.filter(post => post.id.startsWith(`${lang}/`));

    for (const post of langPosts) {
      const slug = post.id.replace(`${lang}/`, '');
      paths.push({
        params: { lang, slug },
        props: { post }
      });
    }
  }

  return paths;
}

Benefits

  • Clear separation: Language-specific content physically separated for easy management
  • Easy maintenance: Can modify, add, or remove specific languages only
  • Type safety: Astro automatically generates types, supports TypeScript
  • Scalability: Just create a folder when adding new languages

Section 2: Content Collections Schema

Astro Content Collections ensures type safety for Frontmatter and allows systematic management of multilingual fields.

Frontmatter Structure

---
title: 'AI智能体协作模式:用5个专业Agent构建全栈应用'
description: 通过协调Architecture、Coding、Testing、Security、DevOps Agent构建生产级应用的实战指南
pubDate: '2025-10-16'
heroImage: ../../../assets/blog/ai-agent-collaboration-patterns-hero.jpg
tags:
  - ai
  - agents
  - architecture
relatedPosts:
  - slug: self-healing-ai-systems
    score: 0.94
    reason:
      ko: '자동화, AI/ML, 아키텍처 분야에서 유사한 주제를 다루며 비슷한 난이도입니다.'
      ja: 自動化、AI/ML、アーキテクチャ分野で類似したトピックを扱い、同程度の難易度です。
      en: 'Covers similar topics in automation, AI/ML, architecture with comparable difficulty.'
      zh: 涵盖自动化、AI/ML、架构等相似主题,难度相当。
---

A distinctive feature of my blog is providing recommendation reasons in 4 languages for each recommended post. This benefits both user experience and SEO.

// src/content.config.ts
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  type: 'content',
  schema: ({ image }) => z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: image().optional(),
    tags: z.array(z.string()).optional(),
    relatedPosts: z.array(
      z.object({
        slug: z.string(),
        score: z.number(),
        reason: z.object({
          ko: z.string(),
          ja: z.string(),
          en: z.string(),
          zh: z.string()
        })
      })
    ).optional()
  })
});

export const collections = { blog: blogCollection };

Schema Benefits

  • Build-time validation: Incorrect Frontmatter immediately detected as build failure
  • Automatic type generation: CollectionEntry<'blog'> type automatically generated
  • IDE support: Autocomplete and type checking in VSCode

Section 3: UI Component Localization

Not just content, but UI components must also be localized to provide a complete user experience.

BuyMeACoffee Component

---
// src/components/BuyMeACoffee.astro
import type { Language } from '../lib/i18n/languages';

interface Props {
  lang: Language;
}

const { lang } = Astro.props;

const messages = {
  ko: {
    title: '글이 도움이 되셨나요?',
    description: '더 나은 콘텐츠를 작성하는 데 힘이 됩니다. 커피 한 잔으로 응원해주세요! ☕'
  },
  ja: {
    title: 'この記事は役に立ちましたか?',
    description: 'より良いコンテンツを作成するための力になります。コーヒー一杯で応援してください!☕'
  },
  en: {
    title: 'Was this helpful?',
    description: 'Your support helps me create better content. Buy me a coffee! ☕'
  },
  zh: {
    title: '这篇文章有帮助吗?',
    description: '您的支持能帮助我创作更好的内容。请我喝杯咖啡吧!☕'
  }
};

const message = messages[lang];
---

<div class="mt-12 pt-8 border-t border-gray-200">
  <div class="text-center">
    <h3 class="text-2xl font-bold text-gray-900 mb-3">
      {message.title}
    </h3>
    <p class="text-gray-600 mb-6 max-w-2xl mx-auto">
      {message.description}
    </p>
  </div>
</div>

BlogPostLanguageSwitcher Component

---
// src/components/BlogPostLanguageSwitcher.astro
const languages = {
  ko: { flag: '🇰🇷', label: '한국어', currentLabel: '한국어 (현재 페이지)' },
  ja: { flag: '🇯🇵', label: '日本語', currentLabel: '日本語(現在のページ)' },
  en: { flag: '🇺🇸', label: 'English', currentLabel: 'English (Current Page)' },
  zh: { flag: '🇨🇳', label: '中文', currentLabel: '中文(当前页面)' }  // Newly added
};

const sectionTitles = {
  ko: '다른 언어로 읽기',
  ja: '他の言語で読む',
  en: 'Read in Other Languages',
  zh: '阅读其他语言版本'  // Newly added
};
---

HeroSection Component

---
// src/components/HeroSection.astro (excerpt)
<h1 class="text-4xl sm:text-5xl md:text-6xl font-bold mb-6">
  {lang === 'ko' ? (
    <>
      <span class="block text-gray-900">AI로 생산성을</span>
      <span class="block bg-gradient-to-r from-blue-600 to-violet-600 bg-clip-text text-transparent">극대화하세요</span>
    </>
  ) : lang === 'ja' ? (
    <>
      <span class="block text-gray-900">AIで生産性を</span>
      <span class="block bg-gradient-to-r from-blue-600 to-violet-600 bg-clip-text text-transparent">最大化しましょう</span>
    </>
  ) : lang === 'en' ? (
    <>
      <span class="block text-gray-900">Maximize Your</span>
      <span class="block bg-gradient-to-r from-blue-600 to-violet-600 bg-clip-text text-transparent">Productivity with AI</span>
    </>
  ) : (
    <>
      <span class="block text-gray-900">用AI</span>
      <span class="block bg-gradient-to-r from-blue-600 to-violet-600 bg-clip-text text-transparent">提升生产力</span>
    </>
  )}
</h1>
---

Localization Patterns

  1. messages object: Manage messages by language key
  2. Language type: Ensure type safety with TypeScript
  3. Conditional rendering: Select appropriate message with lang prop

Section 4: Data File Translation

Structured data like blog improvement history also needs localization.

Improvement JSON Structure

{
  "id": "01_related_posts_multilingual_upgrade",
  "date": "2025-10-08",
  "category": "feature",
  "title": {
    "ko": "관련 글 추천 다국어 업그레이드",
    "ja": "関連記事推薦多言語アップグレード",
    "en": "Related Posts Multilingual Upgrade",
    "zh": "相关文章推荐多语言升级"
  },
  "description": {
    "ko": "relatedPosts에 reason 필드 추가하여 4개 언어로 추천 이유 제공",
    "ja": "relatedPostsにreasonフィールドを追加し、4言語で推薦理由を提供",
    "en": "Added reason field to relatedPosts providing recommendation rationale in 4 languages",
    "zh": "在relatedPosts中添加reason字段,提供4种语言的推荐理由"
  },
  "metrics": {
    "ko": "전체 블로그 포스트(40개)의 relatedPosts에 다국어 reason 적용",
    "ja": "全ブログポスト(40件)のrelatedPostsに多言語reasonを適用",
    "en": "Applied multilingual reason to relatedPosts across all blog posts (40 posts)",
    "zh": "在所有博客文章(40篇)的relatedPosts中应用多语言reason"
  },
  "effort": {
    "ko": "중간 (약 4시간)",
    "ja": "中程度(約4時間)",
    "en": "Medium (About 4 hours)",
    "zh": "中等(约4小时)"
  },
  "roi": {
    "ko": "높음 (다국어 UX 대폭 향상)",
    "ja": "高(多言語UX大幅向上)",
    "en": "High (Significant multilingual UX improvement)",
    "zh": "高(多语言UX大幅提升)"
  },
  "lessons": {
    "ko": [
      "병렬 에이전트로 40개 포스트 일괄 업데이트 (5개씩 8배치)",
      "reason 필드를 각 언어별로 자연스럽게 번역하여 현지화 품질 향상"
    ],
    "ja": [
      "並列エージェントで40件のポストを一括更新(5件ずつ8バッチ)",
      "reasonフィールドを各言語で自然に翻訳し、ローカライゼーション品質向上"
    ],
    "en": [
      "Batch updated 40 posts using parallel agents (8 batches of 5)",
      "Improved localization quality by naturally translating reason field per language"
    ],
    "zh": [
      "使用并行智能体批量更新40篇文章(8批次,每批5篇)",
      "将reason字段自然翻译成各种语言,提高本地化质量"
    ]
  }
}

Consistent Multilingual Data

  • All fields provided in 4 languages: title, description, metrics, effort, roi, lessons
  • Same structure: Only language differs, JSON schema identical
  • Easy expansion: Just add keys to each JSON file when adding new language

Section 5: SEO Optimization

SEO for multilingual websites requires independent optimization per language.

Language-Specific Sitemap Generation

// src/pages/sitemap-zh.xml.ts
import { getCollection } from 'astro:content';
import type { APIRoute } from 'astro';
import { filterPostsByDate } from '../lib/content';

const SITE = 'https://jangwook.net';
const LANG = 'zh';

// Chinese static pages
const staticPages = [
  { path: '/zh', priority: 1.0, changefreq: 'weekly' },
  { path: '/zh/about', priority: 0.8, changefreq: 'monthly' },
  { path: '/zh/blog', priority: 0.9, changefreq: 'daily' },
  { path: '/zh/contact', priority: 0.7, changefreq: 'monthly' },
  { path: '/zh/social', priority: 0.7, changefreq: 'monthly' },
  { path: '/zh/improvement-history', priority: 0.6, changefreq: 'weekly' },
];

export const GET: APIRoute = async () => {
  // Filter Chinese blog posts only
  const allPosts = await getCollection('blog');
  const langPosts = filterPostsByDate(allPosts).filter(post => post.id.startsWith(`${LANG}/`));

  // Generate URLs
  const urls = [
    ...staticPages.map(page => ({
      loc: `${SITE}${page.path}`,
      lastmod: new Date().toISOString().split('T')[0],
      changefreq: page.changefreq,
      priority: page.priority,
    })),
    ...langPosts.map(post => {
      const slug = post.id.replace(`${LANG}/`, '');
      return {
        loc: `${SITE}/${LANG}/blog/${slug}/`,
        lastmod: (post.data.updatedDate || post.data.pubDate).toISOString().split('T')[0],
        changefreq: 'monthly' as const,
        priority: 0.7,
      };
    }),
  ];

  return new Response(generateSitemapXml(urls), {
    headers: { 'Content-Type': 'application/xml; charset=utf-8' },
  });
};

Adding Language-Specific Sitemaps to Main Sitemap

// src/pages/sitemap.xml.ts
const languageSitemaps = [
  { loc: `${SITE}/sitemap-ko.xml`, lastmod },
  { loc: `${SITE}/sitemap-en.xml`, lastmod },
  { loc: `${SITE}/sitemap-ja.xml`, lastmod },
  { loc: `${SITE}/sitemap-zh.xml`, lastmod }  // Newly added
];

RSS Feed Generation

// src/pages/rss-zh.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import { SITE_META } from '../consts';
import { filterPostsByDate } from '../lib/content';

export async function GET(context) {
  const posts = filterPostsByDate(await getCollection('blog'))
    .filter(post => post.id.startsWith('zh/'));

  return rss({
    title: SITE_META.zh.title,
    description: SITE_META.zh.description,
    site: context.site,
    items: posts.map((post) => ({
      title: post.data.title,
      pubDate: post.data.pubDate,
      description: post.data.description,
      link: `/zh/blog/${post.id.replace('zh/', '')}/`,
    })),
  });
}

SITE_META Configuration

// src/consts.ts
export const SITE_META = {
  ko: {
    title: 'EffiFlow - 한국어',
    description: 'AI와 개발에 관한 생각을 기록합니다'
  },
  en: {
    title: 'EffiFlow - English',
    description: 'Exploring AI and Development'
  },
  ja: {
    title: 'EffiFlow - 日本語',
    description: 'AIと開発についての考察'
  },
  zh: {  // Newly added
    title: 'EffiFlow - 中文',
    description: '记录关于AI与开发的思考'
  }
};

SEO Checklist

  • ✅ Language-specific independent sitemap (sitemap-zh.xml)
  • ✅ Register language-specific sitemaps in main sitemap index
  • ✅ Language-specific RSS feed (rss-zh.xml)
  • <html lang="zh"> tag configuration
  • ✅ Open Graph and Twitter Card meta tags (per language)
  • ✅ Canonical URL configuration

Section 6: Large-Scale Translation Automation

Adding Chinese versions to 40 posts and adding Chinese reason to relatedPosts in existing Korean/English/Japanese versions was impossible to do manually.

Problem Definition

  • 40 posts × 3 existing languages (ko, en, ja) = 120 file updates
  • Add reason.zh field to relatedPosts (average 3 per post)
  • Maintain Chinese translation quality
  • Need to reduce work time

Parallel Agent Approach

graph TD
    A[40 Posts] --> B[Split into 9 batches<br/>5 posts each]
    B --> C[Batch 1: 5 posts]
    B --> D[Batch 2: 5 posts]
    B --> E[Batch 3: 5 posts]
    B --> F[... Batch 9]

    C --> G[Agent 1]
    C --> H[Agent 2]
    C --> I[Agent 3]
    C --> J[Agent 4]
    C --> K[Agent 5]

    G --> L[Read zh version]
    L --> M[Extract/translate reason.zh]
    M --> N[Update ko, en, ja files]

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#f0f0ff
    style G fill:#e8f5e9
    style N fill:#fff9c4

Agent Workflow

Each agent performs these steps:

  1. Read Chinese version: Parse relatedPosts in zh/[post-name].md
  2. Extract reason.zh:
    • Use as-is if already exists
    • Auto-translate from reason.ko/en/ja if missing
  3. Update 3 language files:
    • Add reason.zh to ko/[post-name].md
    • Add reason.zh to en/[post-name].md
    • Add reason.zh to ja/[post-name].md

Actual Implementation Example (Claude Code)

# Execute Batch 1 (5 posts in parallel)
@agent1: "Extract Chinese reason from ai-agent-collaboration-patterns post and add to ko/en/ja files"
@agent2: "Extract Chinese reason from ai-agent-persona-analysis post and add to ko/en/ja files"
@agent3: "Extract Chinese reason from ai-agent-notion-mcp-automation post and add to ko/en/ja files"
@agent4: "Extract Chinese reason from astro-scheduled-publishing post and add to ko/en/ja files"
@agent5: "Extract Chinese reason from claude-code-best-practices post and add to ko/en/ja files"

# Execute Batch 2 (next 5 posts)
...

Results

  • Work time reduction: Expected 8 hours → Actual 50 minutes (90% reduction)
  • Consistent translation: Unified Chinese reason style patterns
    • “适合作为下一步学习资源,通过…相连接”
    • “从…角度提供补充内容”
  • Zero errors: No omissions or typos from automation
  • Parallel processing: 5 agents running simultaneously maximized speed

Section 7: Translation Strategy

The goal was localization, not just simple translation.

Translation Logic

// Pseudocode
function extractOrTranslateReason(post, relatedPost) {
  // 1. Use existing Chinese reason if present
  if (relatedPost.reason.zh) {
    return relatedPost.reason.zh;
  }

  // 2. Translate from existing reason if missing
  const sourceReason = relatedPost.reason.ko || relatedPost.reason.en || relatedPost.reason.ja;

  // 3. Natural Chinese translation with Claude LLM
  const translatedReason = await translateToNaturalChinese(sourceReason);

  return translatedReason;
}

Chinese Style Guidelines

Use consistent patterns:

  • “适合作为下一步学习资源” (Suitable as next-step learning resource)
  • “通过X相连接” (Connected through X)
  • “从Y角度提供补充内容” (Provides supplementary content from Y perspective)

Technical terms:

  • Agent → 智能体 (Intelligent agent)
  • Automation → 自动化 (Automation)
  • Architecture → 架构 (Architecture)
  • Performance → 性能 (Performance)

Natural expressions:

  • “有助于理解…” (Helps understand…)
  • “提供实用的…” (Provides practical…)
  • “深入探讨…” (Explores in depth…)

Translation Quality Validation

  • Consistency check: Same technical terms translated identically
  • Length balance: Maintain similar length to other language reasons
  • Contextual appropriateness: Accurately convey original intent

Section 8: Validation and Testing

Thorough validation is essential for multilingual sites.

Type Checking

# Astro type check
npm run astro check

# Result: ✅ 0 errors, 0 warnings

Build Validation

# Production build
npm run build

# Result:
# - 40 posts × 4 languages = 160 pages generated
# - Sitemaps generated: sitemap-ko.xml, sitemap-en.xml, sitemap-ja.xml, sitemap-zh.xml
# - RSS feeds generated: rss-ko.xml, rss-en.xml, rss-ja.xml, rss-zh.xml

Manual Testing Checklist

Content:

  • ✅ All Chinese posts render correctly
  • ✅ heroImage displays identically across all language versions
  • ✅ Chinese reason in relatedPosts displays properly
  • ✅ Language switch links work

UI:

  • ✅ BuyMeACoffee component shows Chinese message
  • ✅ BlogPostLanguageSwitcher shows Chinese flag (🇨🇳) and “中文”
  • ✅ HeroSection Chinese headline renders

SEO:

  • sitemap-zh.xml accessible and contains correct URLs
  • rss-zh.xml accessible and contains correct posts
  • ✅ Main sitemap.xml includes sitemap-zh.xml
  • ✅ Open Graph meta tags have Chinese title/description

Common Issues and Solutions

Issue 1: Frontmatter schema mismatch

Error: Invalid frontmatter in blog/zh/post.md

Solution: Modify to exactly match schema in src/content.config.ts

Issue 2: Image path error

Error: Could not find image at ../../../assets/blog/image.jpg

Solution: Recheck relative path, verify image file exists

Issue 3: Specific language missing during build Solution: Verify ‘zh’ is included in language array of getStaticPaths()

Section 9: Results and Lessons Learned

Quantitative Results

Work Time:

  • Expected manual time: About 8 hours
    • 120 files × 4 minutes average = 480 minutes
  • Actual time taken: About 50 minutes
    • Automation script creation: 20 minutes
    • 9 batch executions: 30 minutes
  • Time saved: 90%

Code Reduction:

  • Improved maintainability by removing repetitive code
  • Adhered to DRY principle through component localization

Scalability:

  • Same pattern can be applied when adding 5th language
  • Established workflow for simultaneous 4-language generation when creating new posts

Qualitative Lessons

1. File structure determines everything

  • Incorrect initial design makes later modifications difficult
  • Language-specific folder structure provides both clarity and scalability

2. Parallel agents are a game changer

  • Significantly reduced work time with 5 agents running simultaneously
  • Eliminated human errors through consistent automation

3. Type safety is essential

  • Build-time validation with Astro Content Collections schema
  • Ensured IDE support and refactoring safety with TypeScript

4. SEO should be independent per language

  • Optimized for search engines with language-specific sitemaps and RSS feeds
  • Improved CTR by localizing metadata for each language

5. Localization is not translation

  • Consider cultural context, not just simple translation
  • Technical terms consistent, descriptions natural

Future Plans

  • Automated deployment pipeline: Auto-generate 4 languages when creating posts
  • Translation quality monitoring: Collect user feedback and improve
  • Additional language support: Consider Spanish, French, etc.
  • Language-specific traffic analysis: Measure per-language performance with GA4

Conclusion

Adding Chinese support to an Astro-based blog required systematic planning and automation as the core.

Key Summary

  1. Clear file structure: Separate content by language-specific folders
  2. Type safety: Build-time validation with Content Collections schema
  3. UI localization: Apply messages object pattern to components
  4. SEO optimization: Language-specific sitemaps, RSS feeds, metadata
  5. Parallel automation: 90% time savings with 5 agents running simultaneously
  6. Localization quality: Consider cultural context beyond simple translation

Closing

Expanding a multilingual website isn’t just about adding translations. It’s a comprehensive task requiring consideration of architecture, SEO, user experience, and automation.

I hope this article provides practical help for your multilingual projects. Questions and feedback are always welcome!


Related Resources:

Read in Other Languages

Was this helpful?

Your support helps me create better content. Buy me a coffee! ☕

About the Author

JK

Kim Jangwook

Full-Stack Developer specializing in AI/LLM

Building AI agent systems, LLM applications, and automation solutions with 10+ years of web development experience. Sharing practical insights on Claude Code, MCP, and RAG systems.