diff --git a/src/Redirects/HandleRedirects.php b/src/Redirects/HandleRedirects.php index bb3b1d22..be722236 100644 --- a/src/Redirects/HandleRedirects.php +++ b/src/Redirects/HandleRedirects.php @@ -57,12 +57,23 @@ private function findRedirect(string $path, string $siteHandle): ?Redirect private function findExactMatch(string $path, string $siteHandle): ?Redirect { return RedirectFacade::query() - ->where('source', $path) + ->whereIn('source', $this->sourceVariations($path)) ->where('site', $siteHandle) ->where('enabled', true) ->first(); } + private function sourceVariations(string $path): array + { + if ($path === '/') { + return ['/']; + } + + $withoutTrailingSlash = rtrim($path, '/'); + + return [$withoutTrailingSlash, $withoutTrailingSlash.'/']; + } + private function findWildcardMatch(string $path, string $siteHandle): ?Redirect { return RedirectFacade::query() diff --git a/tests/Redirects/HandleRedirectsTest.php b/tests/Redirects/HandleRedirectsTest.php index 09393c78..2b7c68f7 100644 --- a/tests/Redirects/HandleRedirectsTest.php +++ b/tests/Redirects/HandleRedirectsTest.php @@ -2,6 +2,7 @@ namespace Tests\Redirects; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Queue; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Collection; @@ -50,6 +51,42 @@ public function it_redirects_with_302_response_code() ->assertStatus(302); } + #[Test] + public function it_redirects_when_visiting_url_with_trailing_slash() + { + Facades\Redirect::make() + ->id('abc') + ->source('/old-url') + ->destination('/new-url') + ->responseCode(301) + ->enabled(true) + ->save(); + + // The test HTTP client strips trailing slashes before dispatching, so the + // request is handled directly through the kernel to preserve it. + $response = $this->app->handle(Request::create('/old-url/', 'GET')); + + $this->assertEquals(301, $response->getStatusCode()); + $this->assertEquals('/new-url', parse_url($response->headers->get('Location'), PHP_URL_PATH)); + } + + #[Test] + public function it_redirects_when_rule_has_trailing_slash_but_url_does_not() + { + Facades\Redirect::make() + ->id('abc') + ->source('/old-url/') + ->destination('/new-url') + ->responseCode(301) + ->enabled(true) + ->save(); + + $this + ->get('/old-url') + ->assertRedirect('/new-url') + ->assertStatus(301); + } + #[Test] public function it_does_not_redirect_to_inactive_redirect() {