OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

Astro v4のi18nで言語別のRSSフィードを配信する

唐突なAstro話。

Astroのv4からi18n(国際化)機能が標準利用できるようになりました。

docs.astro.build

ただ、この場合に @astrojs/rss で言語別にフィードを配信しようと思ったらどうやるのかわからず苦戦したのでメモ。

簡単に、Astroの多言語化について

詳細は先のリンク先に書いているとおりだが、Astroで言語別にURLを分ける場合のやり方を簡単に。

Astroのconfig設定

astro.config.mjs の defineConfig() に下記を追加

  i18n: {
    defaultLocale: 'ja',
    locales: ['ja', 'en', 'zh'],
    routing: {
      prefixDefaultLocale: true
    }
  }

ここではデフォルト言語も他の言語と同じパス構成にするために、 prefixDefaultLocale: true を指定した。

言語定数と言語別のリソースリストを作っておく

対応する言語の一覧の定数を作る。型も作っておくと便利。

export const LANGUAGE_KEYS = [ "ja", "en", "zh" ] as const;

export type LanguageKey = typeof LANGUAGE_KEYS[number];

そして言語別の固定メッセージ。

interface Resource {
    siteTitle: string;
    siteDescription: string;
    home: string;
}

export const RESOURCES: { [key in LanguageKey]: Resource } = {
    ja: {
        siteTitle: "ほげほげサイト",
        siteDescription: "ほげほげなサイトです。",
        home: "ホーム"
    },
    en: { ... },
    zh: { ... }
} as const;

記事ファイルのフォルダ構成

フォルダ構成はたとえばこんな感じ。 (代表でindexページと記事ページのイメージ)

src/
  └ pages/
    ├ index.html   ・・・(1)
    └ [lang]/
      ├ index.html   ・・・(2)
      └ posts/
        └ [slug].astro  ・・・(3)

ルートの index.html (1) はデフォルト言語の index.html にリダイレクトするようにしておく。

<meta http-equiv="refresh" content="0;url=/ja/" />

あわせて、URLのパスから言語を特定するヘルパメソッドを作っておく。

export const Language = {
    fromUrl: (url: URL): LanguageKey => {
        const [, langInUrl] = url.pathname.split('/');
        const lang = LANGUAGE_KEYS.find(l => l.toLowerCase() === langInUrl.toLowerCase());
        return lang ?? "ja";
    }
}

動的ルーティング

言語別のindex.html (2) や記事ページ (3) の実装。

静的サイト(SSG)の場合は getStaticPaths() で全言語分のURLを出力する。

export async function getStaticPaths() {
  return LANGUAGE_KEYS.map(lang => {
    return { params: { lang } };
  })
}

このastroファイル内で言語を取得する場合、先ほどのヘルパメソッドを使ってもいいが、 Astro.params からでも取れる。

const { lang } = Astro.params;

画面内のテキストやリンクURL・記事一覧など、言語に応じて出し分ける。

<a href={getHomeLink(lang)}>{RESOURCES[lang].home}</a>

htmlの言語属性

一番外側でhtmlのlang属性を指定しているところも、言語別に指定を変えておくのが良い。

const lang = getLangFromUrl(Astro.url);
---
<html lang={lang}>
   ...
</html>

だいたいこんな感じ。

RSSフィードの多言語化

やっと本題。

まず @astrojs/rss をインストール。

npm install @astrojs/rss

通常は pages 直下に feed.xml 的なファイルを作ると思うが、これも言語別のフォルダに作成する。
言語の数だけフィードファイルも用意する形になる。

src/
  └ pages/
    ├ index.html
    └ [lang]/
      ├ index.html
      ├ feed.xml.ts    ・・・これ
      └ posts/
        └ [slug].astro

これもやはり getStaticPaths() を実装。

export async function getStaticPaths() {
  return LANGUAGE_KEYS.map(lang => {
    return { params: { lang } };
  })
}

そして GET メソッドの実装。
これはastroファイルでないので Astro.URL 見れないし、SSGだと window.location も見れないのでどうしようかと思ったが、context パラメータから取得できた。

先ほどのヘルパメソッドを定義していた Language にメソッドを追加

    fromApiContext: (context: APIContext): LanguageKey => {
        const langParam = context.params.lang;
        const lang = LANGUAGE_KEYS.find(l => l.toLowerCase() === langParam?.toLowerCase());
        return lang ?? "ja";
    }

GETメソッド。

export async function GET(context: APIContext) {
  // 言語を取得
  const lang = Language.fromApiContext(context)

  // 言語の記事一覧を取得
  const posts = await getPosts(lang);

  return rss({
    title: RESOURCES[lang].siteTitle,
    description: RESOURCES[lang].siteDescription,
    site: context.site,
    // languageタグを追加しておく
    customData: `<language>${lang}</language>`,
    items: posts.map((post) => ({
      link: post.url,
      title: post.title,
      description: post.description,
      pubDate: post.date,
    }))
  })
}

こんな感じでとりあえずできた。
正攻法なのかどうかは不明。。