From ad1050ff786bfbb2b70bb06b23fbf4f52805ac6f Mon Sep 17 00:00:00 2001 From: Guajiro <276488307+Guajir0-code@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:56:49 -0300 Subject: [PATCH] fix(routes): constrain year and month params to numbers /{year}/{month} and /{year}/{month}/{slug} carried no constraints, so they matched any 2- and 3-segment path. /foo/bar rendered an empty YearMonthPosts page with HTTP 200 instead of 404, producing unlimited indexable soft-404s and feeding a cache entry per distinct path. Constrain year to 4 digits and month to 01-12, mirroring the WordPress permalink structure the routes were modelled on. Co-Authored-By: Claude Opus 5 --- phpunit.xml | 3 +- routes/web.php | 12 +++- tests/Feature/ArchiveRouteConstraintsTest.php | 55 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 tests/Feature/ArchiveRouteConstraintsTest.php diff --git a/phpunit.xml b/phpunit.xml index c09b5bc..61c031c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,7 +22,8 @@ - + + diff --git a/routes/web.php b/routes/web.php index bb65cc6..f6a3e9a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,5 +8,13 @@ Route::get('/author/{author}', [PostController::class, 'authorPosts'])->name('author.posts'); Route::get('/category/{category}', [PostController::class, 'categoryPosts'])->name('category.posts'); Route::get('/tag/{slug}', [PostController::class, 'tagPosts'])->name('tag.posts'); -Route::get('/{year}/{month}', [PostController::class, 'yearMonthPosts'])->name('year-month.posts'); -Route::get('/{year}/{month}/{slug}', [PostController::class, 'show'])->name('post'); +// Mirrors the WordPress permalink structure (%year%/%monthnum%/%postname%). +// Without the constraints these two act as catch-alls for any 2- and +// 3-segment path, answering 200 with an empty archive instead of 404. +Route::get('/{year}/{month}', [PostController::class, 'yearMonthPosts']) + ->where(['year' => '[0-9]{4}', 'month' => '0[1-9]|1[0-2]']) + ->name('year-month.posts'); + +Route::get('/{year}/{month}/{slug}', [PostController::class, 'show']) + ->where(['year' => '[0-9]{4}', 'month' => '0[1-9]|1[0-2]']) + ->name('post'); diff --git a/tests/Feature/ArchiveRouteConstraintsTest.php b/tests/Feature/ArchiveRouteConstraintsTest.php new file mode 100644 index 0000000..d83450a --- /dev/null +++ b/tests/Feature/ArchiveRouteConstraintsTest.php @@ -0,0 +1,55 @@ +get($path)->assertNotFound(); +})->with([ + '/foo/bar', + '/abcd/ef', + '/wp-admin/setup-config.php', + '/assets/app', +]); + +it('rejects out-of-range months', function (string $path) { + $this->get($path)->assertNotFound(); +})->with([ + '/2026/00', + '/2026/13', + '/2026/99', + '/2026/7', // month must be zero padded, as WordPress emits it + '/2026/007', +]); + +it('rejects malformed years', function (string $path) { + $this->get($path)->assertNotFound(); +})->with([ + '/26/07', + '/20260/07', + '/2026a/07', +]); + +it('rejects the same malformed paths on the single post route', function (string $path) { + $this->get($path)->assertNotFound(); +})->with([ + '/foo/bar/some-slug', + '/2026/13/some-slug', + '/2026/7/some-slug', +]); + +it('still matches well formed archive paths', function () { + // Reaching the controller means the route matched. The controller then hits + // wp_posts, which does not exist here, so anything other than 404 proves + // the constraint let the request through. + $this->get('/2026/07')->assertStatus(500); +}); + +it('does not shadow the named static routes', function () { + expect(route('year-month.posts', ['year' => '2026', 'month' => '07'])) + ->toEndWith('/2026/07'); + + expect(route('post', ['year' => '2026', 'month' => '07', 'slug' => 'meu-post'])) + ->toEndWith('/2026/07/meu-post'); +});