🦛

SvelteKitのSSGで多言語対応(多言語ルーティング)したメモ

2025/02/03に公開


SvelteKitでSSGを使用しているのですが、Optional parametersを使用して多言語対応させようとしたところnpm run dev(SSR相当と思われる)では動作するのにnpm run previewで動作せず微妙に四苦八苦しました。備忘メモとしてその対応を残しておきます。
[@sveltejs/kit@2.16.1, @sveltejs/adapter-static@3.0.8]

結論

  • ディレクトリ構成例
$ tree ./src/routes
./src/routes
├── [[lang]]
│   ├── hello
│   │   ├── +page.svelte
│   │   └── world
│   │       └── +page.svelte
│   └── +page.svelte
└── +layout.server.ts
  • 設定内容
+layout.server.ts
export const prerender = true;
export const ssr = true;
export const trailingSlash = "always";
svelte.config.js
...
const config = {
  ...
  kit: {
    adapter: adapter({
      ...
    }),
    prerender: {
      entries: ["*", "/en", "/ja"]
    }
  }
};

export default config;
  • 得られる成果物 (表示順は変更加工済)
./build/
$ tree ./build
./build
├── _app
│   ├── ...
│   ...
├── __data.json
├── index.html
├── hello
│   ├── __data.json
│   ├── index.html
│   └── world
│       ├── __data.json
│       └── index.html
├── en
│   ├── __data.json
│   ├── hello
│   │   ├── __data.json
│   │   ├── index.html
│   │   └── world
│   │       ├── __data.json
│   │       └── index.html
│   └── index.html
└── ja
    ├── __data.json
    ├── hello
    │   ├── __data.json
    │   ├── index.html
    │   └── world
    │       ├── __data.json
    │       └── index.html
    └── index.html

結論設定内容と追加的な対応

設定内容

  • 言語指定のない場合もページを表示したかったためOptional parametersを使用
    • 必ず/en/jaを指定するようにする場合は[lang]と指定する
      • langは変数名のため何でも可
  • SSGでは何も指定しないとDir/File出力時に[[lang]]部分に何が入るか不明になる
    • そのため許容する値をsvelte.config.jsprerender.entriesで指定する
    • svelte.config.jsではなく代わりに以下ファイルでも指定可能
      src/routes/[[lang]]/+page.server.ts
      import type { EntryGenerator } from "./$types";
      export const entries: EntryGenerator = () => {
        return [
          { lang: "ja" },
          { lang: "en" },
        ];
      };
      
  • ビルド時にどのようなディレクトリ構成を出力すべきか走査される
    • その走査処理のことをSvelteKitでは"crawl"と呼んでいる
    • crawlはSSR機能が有効の場合のみ機能する
      • 最低限のcrawlはSSR無効でも動作するように見える
      • 今回のようなOptional parametersがある場合は有効にする必要がある
    • そのためroutes/+layout.server.tsexport const ssr = true;を追加
    • crawlによりhrefの指定等を辿ってその先のページも含まれるようになる
      • prerender.entriesで走査起点を与えているという側面もある
      • href等で辿れない場合はprerender.entriesに含む必要がある
  • それでもうまくいかないため追加でtrailingSlash = "always"に設定する
    • デフォルトではtrailingSlash = "never"になっている
    • neverでなぜうまくいかないのかは不明
  • 上記ssrtrailingSlashを異なる設定値にした場合の出力はSSG構成にならない
    • 具体的には言語別ディレクトリの中にhello以下の構造が出力されない

各設定値でのビルド出力内容

  • ssr = true, trailingSlash = "always" -> 結論参照
  • ssr = true, trailingSlash = "never"
dir/file tree
$ tree ./build
./build
├── _app
│   ├── ...
│   ...
├── __data.json
├── index.html
├── en.html
├── ja.html
├── hello.html
├── world.html
├── hello
│   ├── __data.json
│   ├── world
│   │   └── __data.json
│   └── world.html
├── en
│   └── __data.json
├── ja
│   └── __data.json
└── world
    └── __data.json
  • ssr = false, trailingSlash = "always"
dir/file tree
$ tree ./build
./build
├── _app
│   ├── ...
│   ...
├── __data.json
├── index.html
├── hello
│   ├── __data.json
│   ├── index.html
│   └── world
│       ├── __data.json
│       └── index.html
├── en
│   ├── __data.json
│   └── index.html
└── ja
    ├── __data.json
    └── index.html
  • ssr = false, trailingSlash = "never"
dir/file tree
$ tree ./build
./build
├── _app
│   ├── ...
│   ...
├── __data.json
├── index.html
├── en.html
├── ja.html
├── hello.html
├── hello
│   ├── __data.json
│   ├── world
│   │   └── __data.json
│   └── world.html
├── en
│   └── __data.json
└── ja
    └── __data.json

htmlタグlang属性の設定

SEOにはあまり関係ないようだが綺麗になるようにしておく。

./src
$ tree ./src
./src
├── app.d.ts
├── app.html
├── hooks.server.ts
...
./src/app.html
<!doctype html>
<html lang="%lang%">
...
./src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";

export const handle: Handle = async ({ event, resolve }) => {
  const lang = event.url.pathname.startsWith("/ja") ? "ja" : "en";
  const response = await resolve(event, {
    transformPageChunk: ({ html }) => html.replace("%lang%", lang)
  });
  return response;
};

404等エラーページの設定

Cloudflare Pagesを使用しているため、その仕様に準拠したファイル名になっている。参考文献ではうまくいかないような記載があったが、現在の私の環境では+error.svelteの内容で表示されるため、現在は特に問題ない挙動になっているように思われる。

svelte.config.js
import adapter from "@sveltejs/adapter-static";
...
const config = {
  ...
  kit: {
    adapter: adapter({
      ...
      fallback: "/404.html",
      ...
    }),
    ...
  }
};

export default config;
./src/routes/+error.svelte
// you can design freely
<script lang="ts">
  import { page } from "$app/state";
</script>

<h1>{page.status} : {page.error?.message}</h1>

+page内でのlang判別

page.paramsの中に格納されるため、そこを参照することで判別可能。指定がない場合はundefinedになる模様。

+page.svelte
<script lang="ts">
  import { page } from "$app/state";
  let langValue = $derived(page.params.lang ?? "undefined");
</script>

<!-- "undefined" or "en" or "ja" -->
<p>{langValue}</p>

雑記

SSGと言ってもSPAファイルを複製しているだけだと思うので、もうちょっと簡単にできてほしかったです。ssr = trueでないとcrawlが有効にならないことは公式に記載はありますがTroubleshooting項目内だけだと思うので機能説明時にアピールしてほしかったのもあります。trailingSlash = "never"でうまくいかないのも謎です。

参考文献

Discussion