How to URL: SEO URLs

We are currently implementing a big project with Shopware as a backend and Typo 3 as a frontend. One piece in this picture is to give Typo3 the possibility to get the cart and some other infos. Because Typo3 should be stateless, we don’t want it to handle store-api stuff. Instead we thought to implement our own little AJAX Controller

URLs in Controllers

The intuitiv way to build URLs in controller is to use $this->generateUrl($route, $parameters) the only Problem is, it does not generate SEO URLs.

How to SEO URL?

To understand how shopware generates SEO URLs we need to dig a little deeper. Let’s start with the

Twig SeoUrlFunctionExtension

<?php declare(strict_types=1);

namespace Shopware\Core\Framework\Adapter\Twig\Extension;

use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface;
use Shopware\Core\Framework\Log\Package;
use Symfony\Bridge\Twig\Extension\RoutingExtension;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

#[Package('core')]
class SeoUrlFunctionExtension extends AbstractExtension
{
    /**
     * @internal
     */
    public function __construct(
        private readonly RoutingExtension $routingExtension,
        private readonly SeoUrlPlaceholderHandlerInterface $seoUrlReplacer
    ) {
    }

    public function getFunctions(): array
    {
        return [
            new TwigFunction('seoUrl', $this->seoUrl(...), ['is_safe_callback' => $this->routingExtension->isUrlGenerationSafe(...)]),
        ];
    }

    public function seoUrl(string $name, array $parameters = []): string
    {
        return $this->seoUrlReplacer->generate($name, $parameters);
    }
}

After inspecting the code we understand, that Shopware is using it’s own URL generator (at least that is what I thought). The name SeoUrlPlaceholderHandlerInterface is weird for an URL generator, but not the weirdest I saw in my career.

We added the dependency and used the class. But the output was… weird. It looked like this:

124c71d524604ccbad6042edce3ac799/detail/3492ade3a3054e51a9e76fd373534aa7

It took a while to decrypt it, because we didn’t immediately start to read code, but guessed. And our guess was, that the first is a saleschannel id, then comes the info and the last is the product id. Nearly correct, except, the first is not the saleschannel id, but:

class SeoUrlPlaceholderHandler implements SeoUrlPlaceholderHandlerInterface
{
    final public const DOMAIN_PLACEHOLDER = '124c71d524604ccbad6042edce3ac799';

But why? And where is the URL!?

I have NO CLUE why they did it and couldn’t find anything, no ADR and no references to tickets which made sense to me.

But I can explain to you how it works. If you are using \Shopware\Storefront\Controller\StorefrontController::renderStorefront to render your twig template, one of the things which happen, pretty late in the method is:

$host = $request->attributes->get(RequestTransformer::STOREFRONT_URL);

$seoUrlReplacer = $this->container->get(SeoUrlPlaceholderHandlerInterface::class);
$content = $response->getContent();

if ($content !== false) {
    $response->setContent(
        $seoUrlReplacer->replace($content, $host, $salesChannelContext)
    );
}

As you can see the SeoUrlPlaceholder is expected to generate a placeholder – who would have guessed! And is later replaced with the correct SEO URL. So as long as you use the SeoUrlPlaceholderHandlerInterface Shopware takes care of the URLs.

Be careful with JSON

As explained above we implemented an AJAX controller, which returned JSON. So we thought we do the same as above:

$json = [
    'slug' => $this->seoUrlGenerator->generate('frontend.detail.page', ['productId' => $item->getId()])
]

$response = new JsonResponse($json);
$content = $response->getContent();

if ($content !== false) {
    $response->setContent($seoUrlReplacer->replace($content, $host, $salesChannelContext));
}

But it didn’t work. Maybe some of you already know the problem, we needed to dig into it again, if one generates JSON, the / are replaced with \/ and therefore the replacement doesn’t work.

So we used this solution:

$json = [
    'slug' => $this->seoUrlGenerator->replace(
        $this->seoUrlGenerator->generate('frontend.detail.page', ['productId' => $item->getId()]),
        $request->attributes->get(RequestTransformer::STOREFRONT_URL),
        $salesChannelContext
    ),
]

Hope it helps!

One thought on “How to URL: SEO URLs

  1. I have the same problem with the SEO URL.

    A customer has a plugin which simply calls this function in the Twig template {{ seoUrl(‘frontend.detail.page’, { ‘productId’: productId }) }} and the controller function returns a JsonResponse.

    I have now fixed the problem like this:
    SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler is added to the controller function via DI
    After $html = $this->renderView(…) I do the following

    $html = $seoUrlPlaceholderHandler->replace(
    $html,
    $request->getBaseUrl(),
    $salesChannelContext
    );

    Then the correct SEO URLs are in the HTML.

    Best wishes,
    Stefan

Leave a Reply