DEV Community

Chris Shennan
Chris Shennan

Posted on

Decorate the Symfony router to add a trailing slash to all URLs

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

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 [];
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (2)

Collapse
 
xwero profile image
david duymelinck

If there is no solution for removing the trailing slashes with S3 static hosting; I suggest you use another hosting platform.

Why would you want to let you hosting platform dicate your url structure?

Collapse
 
chrisshennan profile image
Chris Shennan

Good question. I can think of a couple of reasons why using another host, or changing the trailing slash behaviour at the provider may not be viable. 

  • It's not your choice. - Most of us don't get a choice of where our application is hosted. Commercial agreements are already in place, often spanning multiple years so we have to work with what's available.
  • Red tape - A change in provider, or even a trailing slash configuration at the infrastructure level might require a change request to be raised with a separate DevOps teams. For something like changing the trailing slash policy, this will likely be at the bottom of their list. Managing this within your application can mean it's resolved in hours rather than days or weeks.
  • Complexity/Flexibility - This isn't just an AWS issue. Many static site host solutions have trailing slashes at the end of their URLs out of the box, and there are workarounds to remove them (including for AWS). Developers aren't always aware of the infrastructure that their application runs on (although a basic understanding of it is beneficial). Keeping the application routing within the application rather than a separate DevOps function, which is possibly not available for developers to see, reduces cognitive complexity and offers greater flexibility in the future (no request needs to be raised with DevOps for a change).
  • Convenience (and I fall into this) - I could use another host but Password Angel is one small project amongst several others using several AWS features. While I could separate it, having all my projects in one place greatly outweighs any benefit that would come from splitting them into multiple providers to overcome a trailing slash minor inconvenience.

Ultimately having a trailing slash or not isn't a big deal - search engines can handle both just fine and static site providers tend to automatically redirect non-trailing slash URLs to ones with trailing slashes i.e. passwordangel.co/terms will redirect to passwordangel.co/terms/ (so no broken links!) - what is important is consistency.

However the pros and cons of which provider to use and whether to use a trailing slash or not are beyond the scope of this article.  Having decided you want a trailing slash at the end of your URLs, this article is aimed at showing you how to achieve that with minimal effort and fuss.