TL;DR

Astro 6.x 内置 i18n 支持,本文教你实现:URL 路由(/blog/post vs /en/blog/post)、内容双语组织、导航语言切换器、默认语言重定向。完成后博客支持中文和英文两套内容。

前提:

  • Astro 6.x 项目
  • 有两套内容(中文 + 英文)
  • 了解 Astro Content Collections

Step 1:理解两种路由策略

Astro i18n 支持两种 URL 结构:

策略 A:子路径模式(推荐)

/blog/my-post          → 中文
/en/blog/my-post       → 英文

策略 B:文件名模式

/src/pages/blog/my-post.md     → 中文
/src/pages/en/blog/my-post.md   → 英文

策略 A 更清晰,URL 结构统一,推荐使用。

Step 2:配置 astro.config.mjs

// astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: 'zh',
    locales: ['zh', 'en'],
    routing: {
      prefixDefaultLocale: false,  // 默认语言不加前缀(/blog 而非 /zh/blog)
    },
  },
});

prefixDefaultLocale: false 让中文 URL 不加 /zh 前缀,英文加 /en

Step 3:组织内容结构

src/content/blog/
  my-post/
    index.md          # 中文(默认)
    index.en.md       # 英文版本

Astro 会自动识别 index.en.md 为英文版本,index.md 为中文版本。

注意:这种方式要求中文和英文在同一目录下(同一 slug)。如果内容差异大(不是翻译关系),可以用不同 content collection。

Step 4:获取翻译版本

// 在文章页面中获取其他语言版本
const { entry, render } = Astro.props;
const lang = Astro.currentLocale;  // 'zh' 或 'en'

// 获取其他语言版本
const allEntries = await getCollection('blog');
const translations = allEntries.filter(e => 
  e.slug.replace(`/${lang}`, '') === entry.slug.replace(`/${lang}`, '') &&
  e.slug !== entry.slug
);

或者用 Astro 内置的 getRelativeLocaleUrl

---
// 获取当前页面的英文版本 URL
const enUrl = getRelativeLocaleUrl('en', currentPath);
---
<a href={enUrl}>English</a>

Step 5:语言切换器组件

---
// src/components/LanguageSwitcher.astro
const currentLang = Astro.currentLocale || 'zh';
const targetLang = currentLang === 'zh' ? 'en' : 'zh';
const currentPath = Astro.url.pathname;

// 去掉当前语言前缀,得到路径部分
let pathWithoutLang = currentPath;
if (currentPath.startsWith('/en')) {
  pathWithoutLang = currentPath.replace('/en', '') || '/';
} else if (currentPath.startsWith('/zh')) {
  pathWithoutLang = currentPath.replace('/zh', '') || '/';
}

// 如果默认语言不加前缀,切换到英文需要加 /en
const targetPath = targetLang === 'en' 
  ? `/en${pathWithoutLang}` 
  : pathWithoutLang;
---

<div class="lang-switcher">
  <a href={targetPath} class={targetLang === 'en' ? 'active' : ''}>
    EN
  </a>
  <span class="sep">|</span>
  <a href={pathWithoutLang === '/' ? '/' : pathWithoutLang} class={targetLang === 'zh' ? 'active' : ''}>
    中文
  </a>
</div>

<style is:inline>
  .lang-switcher { display: flex; gap: 4px; align-items: center; }
  .lang-switcher a { font-size: 14px; color: var(--text-secondary); text-decoration: none; }
  .lang-switcher a.active { color: var(--color-primary); font-weight: 600; }
  .lang-switcher .sep { color: var(--border); }
</style>

Step 6:默认语言重定向

如果用户访问 /blog/my-post,应该自动重定向到中文版本(默认语言)。这不需要额外配置,Astro i18n 默认行为就是这样。

但如果用户访问 /en/blog/my-post 时英文内容不存在,需要重定向到中文版本:

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware(async (context, next) => {
  const lang = context.currentLocale;
  const path = context.url.pathname;
  
  // 检查当前语言版本是否存在
  const allEntries = await getCollection('blog');
  const pathWithoutLang = path.replace(/^\/(zh|en)/, '') || '/';
  
  const hasThisLang = allEntries.some(e => e.slug.endsWith(path));
  
  if (!hasThisLang && lang === 'en') {
    // 英文版本不存在,重定向到中文
    return context.redirect(pathWithoutLang);
  }
  
  return next();
});

Step 7:导航中语言链接

在导航栏或 Footer 中加入语言切换:

---
// src/components/Header.astro
import LanguageSwitcher from './LanguageSwitcher.astro';
---
<nav>
  <a href="/">首页</a>
  <a href="/blog">博客</a>
  <a href="/about">关于</a>
  <LanguageSwitcher />
</nav>

Step 8:SEO 配置

每篇文章需要在 <head> 中添加 hreflang 声明:

---
// 在 BlogPost.astro 的 <head> 中
const enEntry = allEntries.find(e => e.slug === entry.slug.replace(/^\/zh/, '/en'));
---
{entry.slug.includes('/en') && (
  <link rel="alternate" hreflang="zh" href={entry.slug.replace('/en', '/zh')} />
)}
{enEntry && (
  <link rel="alternate" hreflang="en" href={`/en${entry.slug}`} />
)}

同时在 src/pages/sitemap.xml.ts 中生成多语言 sitemap:

// src/pages/sitemap.xml.ts
import { getCollection } from 'astro:content';

export async function GET(context) {
  const blog = await getCollection('blog');
  const baseUrl = context.site;

  const zhUrls = blog.map(entry => ({
    url: `${baseUrl}${entry.slug}`,
    changefreq: 'weekly',
    priority: entry.data.important ? 0.8 : 0.6,
  }));

  const enEntries = blog.filter(e => e.id.endsWith('.en.md'));
  const enUrls = enEntries.map(entry => ({
    url: `${baseUrl}/en${entry.slug}`,
    changefreq: 'weekly',
    priority: 0.6,
  }));

  return new Response([...zhUrls, ...enUrls], { status: 200 });
}

Trade-offs

优点代价
URL 结构清晰内容需要维护两份
SEO 友好写作工作量翻倍
用户体验好翻译质量难保证一致性
Astro 内置支持初次配置有复杂度

写在

双语支持不是技术问题,是内容策略问题。在开始之前问自己:你的读者真需要英文版本吗?

如果是为了 SEO,英文内容需要和中文内容一样丰富才有意义。半吊子翻译(中文内容 100 篇,英文内容 5 篇)对 SEO 没有帮助。

延伸阅读

种下你的想法

在花园里留下一条评论,和这篇文章一起生长。

COMMENTS