拡張子.html等、バニラPHPで作られたサイトのURLを維持してLaravelでリニューアルする

拡張子.html等を含むURLを維持してLaravelでリニューアル Laravel
※当サイトはアフィリエイト広告を掲載しています。

別の記事でも書きましたが、現在、バニラPHPで作られたサイトをリニューアルする案件を進めています。

そこで悩んだのがURL構成です。以下の様なURLが大量にあります。

  • /company/
  • /company/sub/
  • /company/history.html
  • /products/hoge/123

最後のだけ、mod_rewriteでパラメータを受け取っていますが、その他は典型的な通常のWebサイトです。

.htaccessのAddTypディレクティブにより、拡張子:.php, .htm .htmlでPHPが動いています。

これをLaravelにリニューアルすることになった際、迷ったのがURLを維持するか、そのままにするかどうか。

URLを維持するかどうかは改めて考えるとし、ひとまずはそれが可能かどうかをチェックしました。

同様の案件を抱える方の参考になればとメモしておきます。

Lara
Lara

結論から言えば、できます。

やったこと

実現したいことに対し、私が行ったのは以下の様な方法です。

ルートの指定

素直に考えると、普通はこんな風に指定してみますよね!?

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>
Lara
Lara

問題無く動く!良かった!

めでたしめでたし。……とはいきませんでした。

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>

/company/はフォルダ(ディレクトリ)では!?と思う方もいるかもしれませんが、これはpublic以下の実フォルダを意味します。ですので、スラッシュ無しにリダイレクトされていたわけですね。

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を維持する必要があったのか!?とも思い、改めて考えをまとめました。

Lara
Lara

もう決めている方は、ここから先はお読みいただく必要はありません。

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を維持するかはまだ検討中ですが、とにもかくにも「できる」ということが分かっただけでも収穫でした。

あまりこういった機会は多くはないかもしれませんが、それでも求められるケースはあると思います。実装時の参考にしていただければ幸いです。

コメント

タイトルとURLをコピーしました