Customize product URLs


This is an article for advanced users of Frontastic.

The URLs used to reference products are often crucial from an SEO point of view and Frontastic allows you to modify them. Here, we'll show you how to do this and what you need to keep in mind while doing it.

Custom product router

A ProductRouter is used to generate URLs for each product, this will then be used in the frontend and also for identifying products from the given route. You must include something which uniquely identifies the product in the URL, for example its SKU.

To overwrite the default ProductRouter you can use the default Symfony mechanism in the dependency injection container. To do this, just add the below lines to a services.xml in one of your project bundles (if there isn't a bundle yet, you can create one using bin/console frontastic:create:bundle MyDecorators, for example):

    <argument type="service" id="router" />                             
    <argument type="service" id="frontastic.catwalk.product_api" />     

In this case, we pass the configured Product API to the service because it comes in handy. The Symfony router is very useful to generate URLs, so we also pass it to our ProductRouter.

The ProductRouter class defines 2 methods:

  • generateUrlFrom – to generate a URL for a product
  • identifyFrom – to identify a product from a URL

Let's look at an example implementation:

namespace Acme\MyDecoratorsBundle\Routing;
use Frontastic\Catwalk\ApiCoreBundle\Domain\Context;
use Frontastic\Common\ProductApiBundle\Domain\Product;
use Frontastic\Common\ProductApiBundle\Domain\ProductApi;
use Frontastic\Common\ProductApiBundle\Domain\ProductApi\Query\ProductQuery;
use Frontastic\Common\ProductApiBundle\Domain\ProductApi\Query\SingleProductQuery;
use Frontastic\Common\ProjectApiBundle\Domain\Attribute;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Router;
use Frontastic\Catwalk\FrontendBundle\Routing\ObjectRouter\ProductRouter as FPRouter;

class ProductRouter extends FPRouter
    private $router;
    private $productApi;
    public function __construct(Router $router, ProductApi $productApi)
        $this->router = $router;
        $this->productApi = $productApi;
    public function generateUrlFor(Product $product)
        return $this->router->generate(
                'url' => $product->variants[0]->attributes['custom_slug'] ??
                'identifier' => $product->variants[0]->attributes['ean'] ??
    public function identifyFrom(Request $request, Context $context): ?string
        $result = $this->productApi->query(new ProductQuery([
            'locale' => $context->locale,
            'filter' => [
                new ProductApi\Query\TermFilter([
                    'handle' => 'variants.attributes.ean',
                    'attributeType' => Attribute::TYPE_TEXT,
                    'terms' => [$request->attributes->get('identifier')]
        if ($result->count >= 1) {
            return $result->items[0]->productId;
        $product = $this->productApi->getProduct(new SingleProductQuery([
            'locale' => $context->locale,
            'sku' => $request->attributes->get('identifier'),
        if (!$product) {
            return null;
        return $product->productId;

The 2 methods should always be mindful of what the other one is doing. In the example above, we optionally read a custom slug from the product attributes in the generateUrlFor method. This allows the attribute to be defined in the Product Information Management (PIM) System to overwrite a default slug. The slug isn't used to identify the product and could be anything. It may also contain slashes.

The route Frontastic.Frontend.Master.Product.view is defined as /{url}/p/{identifier} where url (the slug) could be anything. The only requirement here is the /p/ before the identifier – this allows us to find the identifier in the URL. The identifier must not contain any slashes.

The above example is also looking for an optional ean attribute in the first product variant which would then be used as its identifier. If we change the identifier we must also adapt the identifyFrom method to find products again by the changed identifier.

The identifyFrom method then uses our Product API to search for products with EAN which can be found in the identifier attribute in the current $request. This method should return the ID of the product is found.

Did this page help you?