コンテンツにスキップ
svelte-vitals
Esc
↑↓移動↵開く⌘Jプレビュー
このページの内容

correctness/base-path-navigation · Root-relative navigation under a base path

ルート相対リンクをハードコードすると kit.paths.base ではなくドメインのルートを指すため、base path の配下ではアプリの外に出てしまい、本番で 404 になります。

重大度: warning · カテゴリ: correctness

チェック内容

対象になるのは base path を設定しているプロジェクトだけです(Vite 設定の sveltekit({ paths: { base } })、または svelte.config.* の kit.paths.base。前者が優先されます)。その場合に、ハードコードされたルート相対リテラルで書かれたナビゲーションを3つの箇所で検出します。

<a href="/about">About</a>
goto('/dashboard');
redirect(303, '/login');

base: '/docs' の下では、これらは /about・/dashboard・/login というドメインのルート、つまりアプリの外を指してしまい、本番環境で404になります。

base path の読み取り方は SvelteKit 自身と同じです。Vite の設定に sveltekit({ paths: { base } }) の引数があればそちらを(この場合 svelte.config は無視されます。SvelteKit 自身も警告を出します)、無ければ svelte.config.js/.ts の kit.paths.base を見ます。

設定側で値を計算している場合、よくある base: dev ? '' : '/repo' のような形も対象です。少なくともどれかの環境では base 配下で配信されるためです。発火しないのは、配信される base になりうる分岐が 1 つもない場合だけです。base が無い場合、明示的に base: '' の場合(トップレベルの const base = '' を paths: { base } で渡す形も含みます)、計算式の各分岐がリテラルの ''(dev ? '' : '')か SvelteKit が読み込みを拒否する値である場合が該当します。拒否される値とは / で始まらないもの、または / で終わるもの(tauri ? './' : '' など)で、その base の下でアプリが配信されることはありません。拒否される分岐の隣に有効な分岐があれば(tauri ? './' : '/repo')対象になります。

base: '/docs' の下の href="/docs/about" のように、リテラルの base で始まっているリテラルはアプリ内を指すため報告しません。ただし base を変えると壊れるので、移植性のある書き方は引き続き resolve() です。base が計算値の場合は先頭が base と一致するか判定できないため、ルート相対のリテラルはすべて報告します。

検出は静的なリテラルだけを対象にします。そのため正しい書き方が誤検出されることはありません。href="{base}/about"、href={resolve('/about')}、goto(resolve('/about'))、goto(`${base}/about`) はいずれも文字列リテラルではなく動的な式だからです。

なぜ重要か

この不具合は開発環境では見えません。base path は通常デプロイ先でだけ適用されるため、手元では base が '' になり、ハードコードされたリンクはすべて動いてしまいます。他のツールも教えてくれません。コンパイラにはただの属性に見え、svelte-check が検査するのは文字列の型で、実行時の解決結果ではないからです。結果として「デプロイしたら全部のリンクが404」という形で表面化します。

修正方法

$app/paths の resolve() でパスを包みます。

<script>
  import { resolve } from '$app/paths';
</script>

<a href={resolve('/about')}>About</a>
import { resolve } from '$app/paths';
import { goto } from '$app/navigation';
import { redirect } from '@sveltejs/kit';

goto(resolve('/dashboard')); // コンポーネントや .svelte.ts モジュールで
redirect(303, resolve('/login')); // load 関数や form action で

resolve()(SvelteKit 2.26以降)が base path を前置してくれます。ルートIDを渡せばルートパラメータの埋め込みも行います。非推奨になった base と resolveRoute の置き換えです。

制限事項

対象外:

  • <form action="/…">、fetch('/api/…')、静的アセット(<img src="/logo.png">、<link href>)。アセットも同じように壊れますが、修正には resolve() ではなく asset() を使うため別のルールに委ねています。
  • 動的なパス全般、<svelte:element this="a">、名前空間インポートの goto/redirect(import * as nav from '$app/navigation')。
  • 静的に読めない sveltekit() の引数(別ファイルからインポートした設定オブジェクトなど)。推測せずに沈黙します。
  • 素の .ts/.js モジュールの goto()。収集対象は .svelte・.svelte.ts・.svelte.js に限られます。
  • src/hooks.client.ts や src/hooks.ts の redirect()。Kit モジュール側の収集対象から外れています。

モードによる違い

ありません。このルールが読むのは同じ .svelte / .ts のソースファイルなので、CLI、Vite プラグインのビルド、ライブダッシュボードの静的ベースラインのいずれでも結果は同一で、レンダリング済み HTML の解析で再評価されることもありません。--route で実行範囲を絞ると、このルールは動きません。コンポーネントスコープのルールには、検出を紐づけるルートが無いためです。

無効化

export default {
  rules: {
    'correctness/base-path-navigation': 'off'
  }
};