別の記事でも書きましたが、現在、バニラPHPで作られたサイトをリニューアルする案件を進めています。
そこで悩んだのがURL構成です。以下の様なURLが大量にあります。
- /company/
- /company/sub/
- /company/history.html
- /products/hoge/123
最後のだけ、mod_rewriteでパラメータを受け取っていますが、その他は典型的な通常のWebサイトです。
これをLaravelにリニューアルすることになった際、迷ったのがURLを維持するか、そのままにするかどうか。
URLを維持するかどうかは改めて考えるとし、ひとまずはそれが可能かどうかをチェックしました。
同様の案件を抱える方の参考になればとメモしておきます。
結論から言えば、できます。
やったこと
実現したいことに対し、私が行ったのは以下の様な方法です。
ルートの指定
素直に考えると、普通はこんな風に指定してみますよね!?
Route::get('/company/', function () {
return view('web.company.index');
})->name('company.index');
Route::get('/company/sub/', function () {
return view('web.company.sub.index');
})->name('company.sub.index');
Route::get('/company/history.htm', function () {
return view('web.company.history');
})->name('company.history');
Route::get('products/{hoge}/{id}', [ProductController::class, 'detail'])
->name('products.detail')->where([
'hoge' => '[a-z]{3,9}',
'id' => '[1-9][0-9]{0,3}',
])->name('products.detail);
URL直打ちで動作確認
以下、全て問題なく目的のビュー・コントローラーが利用され、“表示”できました。
- /company/
- /company/sub/
- /company/history.html
- /products/hoge/123
ブレードからのリンクで動作確認
以下の様に、いろいろな方法でブレード内から指定しても、一見、問題なく表示されました。拡張子.html
にしても同様に問題無しです。
<ul>
<li><a href="/company/">HTMLで指定</a></li>
<li><a href="{{ route('company.index') }}">route()で指定</a></li>
<li><a href="{{ url('/company/') }}">url()で指定</a></li>
</ul>
問題無く動く!良かった!
めでたしめでたし。……とはいきませんでした。
Laravelで発生した問題
スラッシュが消される問題
route()
やurl()
で指定すると、Laravelが勝手に末尾のスラッシュを消してしまうようです。
先ほどのブレードは以下の様に出力されていました。でも、いずれでも表示(動作)は問題ないんですよね。
<ul>
<li><a href="/company/">HTMLで指定</a></li>
<li><a href="http://example.jp/company">route()で指定</a></li>
<li><a href="http://example.jp/company">url()で指定</a></li>
</ul>
スラッシュ無しに301リダイレクトされる
URLは変えたくないから、仕方ないけどHTMLで指定すればいいよね!?ともいきませんでした。
/company/
にリクエストすると、スラッシュ無しの/company
に301リダイレクトされてしまうのです。きちんと表示できているように見えて、これは罠ですね。
とは言えそのページ内で/company/
としてcanonical指定すれば問題ないかもと思いますが、301でわざわざ/company
に移動させておいてそれはなんだか気持ちが悪い。。
見えてきた課題
というわけで、何も考えずに従来のURLをLaravelで引き継ぐのは難しいです。
以下の2点の課題が見えてきました。
- スラッシュ付きでアクセスされた際に、スラッシュ無しにリダイレクトさせない
- route()などでスラッシュがついてほしい
回避策
いろいろと試行錯誤した結果、問題は解決できました。
スラッシュ無しにリダイレクトさせない
public\.htaccess
で指定されていることが問題でした。
以下のように、12行目に「Redirect Trailing Slashes If Not A Folder…」とあるとおり、フォルダでない場合は末尾のスラッシュを外してリダイレクトする指定があります。
なので13~15行目をコメントアウトすれば、OKです。
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Redirect Trailing Slashes If Not A Folder...
# RewriteCond %{REQUEST_FILENAME} !-d
# RewriteCond %{REQUEST_URI} (.+)/$
# RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
route()でスラッシュを付ける:案1
route()
の後に、スラッシュを手動で入れるのは大変です。ですから、そういったヘルパー関数を使う案も考えました。
function route_with_slash($name, $parameters = [], $absolute = true)
{
// Laravelのroute()関数でURLを生成
$url = route($name, $parameters, $absolute);
// 末尾にスラッシュがなければ追加
if (substr($url, -1) !== '/') {
return $url . '/';
}
return $url;
}
これでやりたいことは実現出来ましたが、なんだかなぁという感じです。
<a href="{{ route_with_slash('company.index') }}">route()で呼び出し</a>
なのでこの案は却下しました。
route()でスラッシュを付ける:案2
Laravelでは、UrlGenerator.php
というクラスでURLを作っているようなので、それをオーバーライドする方法を考えてみます。
app\Routing\UrlGenerator.php
というファイルを作ります(設置場所はお好きな所へどうぞ)。
今回は以下のコードのように、特定の拡張子で終わる場合を除き、スラッシュを付けるようにしてみました。
<?php
namespace App\Routing;
use Illuminate\Routing\UrlGenerator as BaseUrlGenerator;
class UrlGenerator extends BaseUrlGenerator
{
/**
* Format the given URL segments into a single URL.
*
* @param string $root
* @param string $path
* @param \Illuminate\Routing\Route|null $route
* @return string
*/
public function format($root, $path, $route = null)
{
// 呼び出されたURLを標準の方法でフォーマット
$url = parent::format($root, $path, $route);
// 末尾が特定の拡張子(.htm, .html, .jpg, .png など)で終わる場合はスラッシュを追加しない
if ($this->isFile($url)) {
return $url;
}
// 末尾にスラッシュがなければ追加
if (!empty($path) && substr($url, -1) !== '/') {
return $url . '/';
}
return $url;
}
/**
* Check if the URL appears to be a file (based on extension).
*
* @param string $url
* @return bool
*/
protected function isFile($url)
{
// 対象となるファイル拡張子のパターン
$extensions = ['htm', 'html', 'jpg', 'jpeg', 'png', 'gif', 'pdf', 'css', 'js'];
// URLが特定の拡張子で終わっているかどうかをチェック
$pattern = '/\.(' . implode('|', $extensions) . ')$/i';
return preg_match($pattern, $url);
}
}
次に、app\Providers\AppServiceProvider.php
からこのクラスを使うように登録します。
このあたりの登録部分は私もあまり詳しく無く、ChatGPT先生から教えてもらいました。
問題なく動いている(バインドされている)と思われますが、自己責任でお願いします。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Cache;
use Illuminate\Pagination\Paginator;
// 追加
use App\Routing\UrlGenerator;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// ここでカスタムUrlGeneratorをバインド
$this->app->singleton('url', function ($app) {
$routes = $app['router']->getRoutes();
$request = $app->rebinding('request', function ($app, $request) {
$app['url']->setRequest($request);
});
return new UrlGenerator($routes, $request, $app['config']['app.asset_url']);
});
}
キャッシュをクリアしておきます。
php artisan route:clear
これで思惑通りに動作しました。私はこの方がスマートで気に入りました。
そもそもURLを維持する必要があったか
本ページでは旧サイトのURLを維持する方法を考えてきました。結果的に問題は解決したのですが、なんだかもやもやするところもあり、ふりかえってみることに。
そもそもよく考えれば、本当にURLを維持する必要があったのか!?とも思い、改めて考えをまとめました。
もう決めている方は、ここから先はお読みいただく必要はありません。
301リダイレクトすれば問題ない!?
旧URLにアクセスされた際、きちんと301リダイレクトをしておけば損失は最小限に済むはずです。
SEO的な問題もほぼ無いと認識しています。
RewriteRule ^company/history.htm$ /company/history [L,R=301]
canonicalしておけば問題ない!?
先ほどちょっと気持ちが悪いといいましたが、http://example.jp/company
にリダイレクトされてしまっても、該当ページで以下の様にcanonicalで正規化すれば、Googleはスラッシュ付きでページを認識してもらえるはずです。
<link rel="canonical" href="http://example.jp/company/">
じゃあ何が問題!?
自分でもよく分からなかったので(汗)自問自答してみました。
- 「いいね」がリセットされる→近年は重要性も低下してはいるとは思う
- 影響をあたえたくない→リニューアル後にアクセスが減ったら自分のせいにされるので変えたくない
- リダイレクト指定をずっと残すのもいや→自分で管理するので余計な記述を増やしたくない
- .htaccessにいろいろ書くとレスポンスに影響しそう→実際には体感できるほどではないとは思う
何が決め手というわけではありませんが、総合的に現状のURLを残そうと思ったようです。
今回は自分に決定権がある案件のため、もう少し考えてみようと思います。
まとめ
Laravelでも、バニラPHP時代のURLをそのままでリニューアルできることがわかりました。
結局、URLを維持するかはまだ検討中ですが、とにもかくにも「できる」ということが分かっただけでも収穫でした。
あまりこういった機会は多くはないかもしれませんが、それでも求められるケースはあると思います。実装時の参考にしていただければ幸いです。
コメント