Chris Shennan
Posted on July 5, 2024
I recently noticed an issue between the links that Symfony generated for Password Angel and the actual links that are in use. When Symfony builds the URL there are no trailing slashes i.e. /terms
, however, as Password Angel is hosted in an S3 bucket as a static site a trailing slash is part of the live URL i.e. /terms/
. This causes 2 problems:-
- Unnecessary redirections - All links in the page will refer to the link version without the trailing slash and then the user will need to be redirected to the version with the trailing slash.
-
The canonical URLs are invalid - As I'm using Symfony to generate the canonical URL for each page, it generated the link version without the trailing slash. This may cause SEO issues as search engines will
- visit
/terms
- be redirected to
/terms/
- be informed the original page is at
/terms
- ... go to step 1 - infinite loop ...
- visit
Solution - Decorate the Symfony Router
To resolve this I created a decorator for the Symfony default router and have overridden the generate
method to add a slash to the end of the URL. It also checks for the presence of ?
which would indicate there are query string parameters and in this situation, I am inserting the /
before the ?
as we want /terms/?utm_campaign=...
and not /terms?utm_campaign=.../
.
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Router;
use Symfony\Component\Routing\RouterInterface;
#[AsDecorator('router.default')]
class TrailingSlashUrlGenerator implements RouterInterface, WarmableInterface
{
public function __construct(
private readonly Router $urlGenerator,
) {}
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH): string
{
// Original URL
$url = $this->urlGenerator->generate($name, $parameters, $referenceType);
// Add the slash before any query string parameters
$pos = strpos($url, '?');
if ($pos !== false) {
$parts = explode('?', $url, 2);
if (str_ends_with($parts[0], '/') === false) {
$parts[0] .= '/';
return implode('?', $parts);
}
}
// Add the slash at the end of the URL
if (str_ends_with($url, '/') === false) {
$url .= '/';
}
return $url;
}
public function match(string $pathinfo): array
{
return $this->urlGenerator->match($pathinfo);
}
public function getRouteCollection(): RouteCollection
{
return $this->urlGenerator->getRouteCollection();
}
public function setContext(RequestContext $context): void
{
$this->urlGenerator->setContext($context);
}
public function getContext(): RequestContext
{
return $this->urlGenerator->getContext();
}
public function warmUp(string $cacheDir, ?string $buildDir = null): array
{
return [];
}
}
Note: To host Password Angel as a static site on S3, I have written a Symfony command to generate static versions of all the pages (all 4 of them) and these are uploaded to S3. Let me know if you're interested and I'll post up how the Symfony command works.
Originally published at https://chrisshennan.com/blog/decorate-the-symfony-router-to-add-a-trailing-slash-to-all-urls
Posted on July 5, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.