Sabigara

Next.jsで複数言語対応のブログ記事を書く

このブログのヘッダーの一番右を見てもらえばわかるように、複数言語でコンテンツを公開できるようにしてみた。

next-18next を使えば基本的には特にハマることもなく実装できたが、ローカルのマークダウンファイルのディレクトリ構造から適切なURL (/ja/blog/<slug>)を指定する方法にちょっと悩んだ。

マークダウンファイルのディレクトリ構造

もともと言語ごとに /blog/en/<slug>.md というディレクトリ構造とそれを反映したURLになっていたのでそれを流用することにした。ただしNext.jsのInternationalized Routing を使用するので、URLは /en/blog/<slug> のようにロケールが先に来るようになる。

仕様

getStaticPaths

ブログ記事のページはすべて /pages/blog/[...slug].tsx で生成する。

getStaticPaths には引数として全ての locales が渡されるので、これを使って全ての locale + slug の組み合わせを網羅する。

/pages/blog/[...slug].tsx
type Post = {
  slug: string;
  locale: "en" | "ja";
};

export async function getStaticPaths({ locales }: GetStaticPathsContext) {
  return {
    paths: locales.flatMap((locale) =>
      getBlogPosts().map((p) => ({
        params: {
          slug: p.slug.split("/"),
        },
        locale,
      }))
    ),
    fallback: false,
  };
}
INFO

この p.slug はファイルパスから先頭の /(en|ja) を削除したものを使用している。

もし翻訳が存在する記事のみを静的生成したい場合は次のようにする。

export async function getStaticPaths() {
  return {
    paths: getBlogPosts().map((p) => ({
      params: {
        slug: p.slug.split("/"),
      },
      locale: p.locale,
    })),
    fallback: "blocking",
  };
}

getStaticProps

getStaticProps には localeslug が渡されるので、それを使ってデータを取得する。もしその言語への翻訳がない場合は元の言語で表示する。その際に別のロケールにリダイレクトされるわけではないので、記事の内容とその他のコンテンツ(フッターのテキストなど)で言語が不一致になる。これは基本的には望ましい挙動だと思う。

/pages/blog/[...slug].tsx
export const getStaticProps = async ({
  locale,
  params,
}: GetStaticPropsContext) => {
  const slug = (params.slug as string[]).join("/");
  const posts = getBlogPosts();
  const post =
    posts.find(
      // Fallback to untranslated version if exists
      (post) => post.slug === slug && post.locale === locale
    ) ?? posts.find((post) => post.slug === slug);

  if (!post) {
    return {
      notFound: true,
    };
  }

  return {
    props: {
      post,
      ...(await serverSideTranslations(locale, ["common", "blog"])),
    },
  };
};

TODO