🌐

[Vue Router]URLの末尾のスラッシュの有無は統一するべき

2021/11/30に公開

概略

同じページから同じリンクで遷移したのに、URLの末尾のスラッシュの有無によって遷移先が異なる現象が起きるので、Vue Router のルーティング設定では末尾スラッシュの有無にも気をつけた方が良いよ、という考えを綴ります。

URLの末尾のスラッシュの有無の違い

下記のように2つのURLがあります。末尾にスラッシュ(/)があるかどうかのみ異なりますが、それ以外に何が異なるのでしょうか?

// 何が違う?
https://example.com/hoge
https://example.com/hoge/

これはこうだという決まりがあるというわけではありません。しかし慣習として、末尾にスラッシュがない場合はファイルを表し、末尾にスラッシュがある場合はディレクトリを表すことが多いです。

https://example.com/hoge (hogeというファイルへのアクセスを表す)
https://example.com/hoge/ (hogeというディレクトリへのアクセスを表す)

もしくは、ウェブページのURLの場合、スラッシュで終わるURLはそこにindex.htmlindex.phpなどといったインデックスファイルが省略されていることが多いです。

// この2つは同等
https://example.com/hoge/
https://example.com/hoge/index

両方あることで起こり得る問題

末尾のスラッシュの有無が異なる2つページが両方とも存在する場合でも、検索エンジンはきちんと異なるページだと認識してくれるようなので、SEO的な問題はないと思います。

しかし、人間にとってはどうでしょう。URLを手打ちしてアクセスした場合、末尾のスラッシュの打ち忘れで、想定していたページとは全く異なるページへアクセスしてしまうのは混乱の元です。

こういった問題が起こり得ますので、スラッシュの有無で異なるURLでも同じコンテンツを表示するようにした方が良いでしょう。

Vue Router における末尾のスラッシュ

Vue Router で下記のようなルートを定義したとしましょう。今回の説明には必要ないのでpath以外のプロパティは省略します。

const routes = [
  { path: '/hoge' },
  { path: '/fuga/' },
]

Vue Router は、定義したルートに対して末尾のスラッシュの有無に関わらずアクセスできるようになります。下記の4つのパスはすべてアクセス可能です。

/hoge
/hoge/
/fuga
/fuga/

スラッシュの有無で両方とも定義することはできません。同じパスとして解釈されるため、あとから指定した方が有効になります。

const routes = [
  { path: '/hoge' }, // こちらは無効。スラッシュの有無は同じパスとして解釈される
  { path: '/hoge/' }, // あとから定義したため、こちらが有効
]

Vue Router における問題

スラッシュの有無両方のパスを柔軟に設定してくれるのは、なんて親切なんだと思うかもしれません。しかし、重大な落とし穴があります。

末尾にスラッシュがある場合はディレクトリ、末尾にスラッシュがない場合はファイルとして解釈される、という慣習があるという話は先述しました。Vue Router の相対リンクもこの慣習に従っています。

/hoge というページの中で、下記のようなリンクを貼ったとします。

<router-link to="fuga"></router-link>

このリンクをクリックすると、/fuga へ遷移します。これは多くの場合に期待通りの挙動だと思います。

それでは、/hoge/ から同じリンクをクリックした場合はどうなるでしょうか?
なんと /fuga でも /fuga/ でもなく /hoge/fuga へと遷移します。まったく異なる想定外のページへ遷移しました。/hoge/hoge/ も同じページなのに、同じリンクをクリックして異なる挙動をしてしまうのです。

特殊なルーティング設定をしたわけではなく、公式ドキュメントにも基礎として載っているレベルのルーティング設定でこのようなことが起こり得てしまいます。

これらの問題は、相対パスではなく絶対パスで遷移するようにすれば起こりません。
しかし、相対パスの方が便利なケースが圧倒的に多いです。グローバルナビゲーションなどのような場所では絶対パスが良いですが、それ以外は基本的には相対パスを使用するのが自然でしょう。

末尾のスラッシュの有無を厳格に定義する

上記のような問題を未然に防ぐためにも、末尾のスラッシュの有無はどちらかに統一し、もう片方でのアクセスはできないようにしましょう。

strict オプションを仕様することで、末尾のスラッシュを厳格に定義できます。
今回は末尾のスラッシュはなしで統一するように設定をします。個人的にはスラッシュなしの方が運用しやすいと思います。

const routes = [
  { path: '/hoge', strict: true },
]

これで、/hoge へのアクセスのみが有効になり、 /hoge/ へのアクセスはできなくなります。

末尾のスラッシュありからなしへのリダイレクトを設定する

末尾のスラッシュありからなしへのリダイレクト設定を併用すると、万が一スラッシュありへアクセスしてしまった場合のフォールバックにすることができるので親切かもしれません。

const routes = [
  { path: '/hoge', strict: true },
  { path: '/hoge/', redirect: '/hoge' },
]

これで /hoge/ へアクセスしてしまった場合、URLは強制的に /hoge に書き換わります。

index を想定したルーティング

index.htmlなどにあたるページのルートを作成する場合は、普通に/fuga/indexのようなルーティングを定義して使用すればよいかと思います。
しかし、中にはindexは省略して/fuga/で運用したい人もいることでしょう。

今回は末尾のスラッシュなしで統一するということで進行していますが、末尾のスラッシュありを許容するのは index というパスが省略されていると想定するケースに留めた方が良いかと思います。

const routes = [
  { path: '/fuga/', strict: true }, // `/fuga/index` と同等の想定
  { path: '/fuga/item', strict: true }, // `/fuga/item`
]

/fuga/item から /fuga/ へ相対リンクで遷移する場合、./ で遷移できます。
/ で相対リンクしようとしてしまいがちですが、/ から始まるパスは絶対リンクになります。そのままルーティング全体の根底となるパスである / へ遷移してしまいますので、注意が必要です。

<router-link to="./">/fuga/</router-link>
<router-link to="./item">/fuga/item</router-link>

相対パスの場合は遷移先パスを ./ から始めるように統一しておくと混乱が少ないでしょう。

index が省略されていることを明示的にするために、/fuga/と一緒にわざわざ/fuga/index のルートも定義してしまうのも良いかもしれません。その場合は同じページのURLが2つできてしまうので、 /fuga/ へリダイレクトして片方に統一するようにします。

const routes = [
  { path: '/fuga/', strict: true }, // `/fuga/index` と同等の想定
  { path: '/fuga/index', redirect: '/fuga/' }, // `/fuga/index`
  { path: '/fuga/item', strict: true }, // `/fuga/item`
]

明示的になる以外の他の利点として、相対パスを ./ から始めなくても遷移できる手段ができます。

<router-link to="index">/fuga/</router-link>
<router-link to="item">/fuga/item</router-link>

設定が増える面倒と見合う対価かどうかはわかりません。そこまでしなくても良いような気はします。

参考

Discussion