ЧПУ (SEF URLs) в Symfony 3 — автогенерация slug, настройка и маршрутизация

  • Tutorial
Всем доброго времени суток!

Третьего дня мне понадобилось провести блиц вебинар на тему ЧПУ в Symfony. Вообще время вебинара у меня ограничено двумя часами, при этом я должен был рассказать еще и про автогенерацию CRUD функционала (scaffolding) в той же Symfony, и про простейший способ создать постраничность. Это создало проблему, так как я знаю как сделать ЧПУ «ручками», не прибегая к автоматизированным под эту задачу инструментам, но рассказ получился бы долгий и оказались бы затянутыми в обсуждение лишние темы. Поэтому я пошел спрашивать у Интернета как сделать все проще. И вот я оказался в той редкой ситуации, когда такая популярная платформа как Symfony не имеет банального обучающего материала на тему «ЧПУ в три клика». Смотрел так же и на английском языке, но там тоже пусто (может плохо искал — время было ограничено). В общем я справился с поиском разрозненного материала по данной теме, а так же со сбором его в единое повествование, так что почему бы не поделиться со всеми?



Терминология
Я не знаю кто будет читать мою статью, так что для начала разберемся в терминологии.

ЧПУ — аббревиатура от «Человекопонятные URL». На английский переводится как Friendly URL или Semantic URL. Однако чаще используется как аналогичная аббревиатура: SEF URLs — Search Engine Friendly URLs.

Что дает вам ЧПУ?

Самое очевидное — это то, что URL-ы вашего сайта будут понятны пользователю. Зачем, только, ему их читать? Большинство клиентов моих заказчиков даже не подозревают о наличии адресной строки браузера. Если есть сомнения, то посмотрите сколько результатов выведет запрос в Гугл «Где находится адресная строка браузера».

Однако есть неоспоримые плюс — правильно составленные ЧПУ являются одним из важных элементов SEO оптимизации, благодаря которой странички вашего сайта будут появляться в поисковике на первой странице. Для этого URL на вашем сайте должны содержать релевантную для поисковика информацию о страничках, на которые они ведут и иметь продуманную вложенность. Все это замечательно, но речь в этой статье не о SEO оптимизации. Предполагается, что вы уже решили получить ЧПУ на своем сайте и дополнительная мотивация вам уже не требуется.

ЧПУ, не ЧПУ

ЧПУ URL-ы — это адреса страниц описывающие всю необходимую информацию о запрашиваемой у сервера странице в виде сегментов пути, то есть GET параметры в таком URL-е большая редкость.

Обычно можно найти шаблоны пути подобные таким:
http(s)://Домен/slug-категории/slug-подкатегории/slug-товара-или-статьи
http(s)://Домен/Профайл/slug-владельца-профайла

Тут появляется еще один термин — Slug, который важен для дальнейшего понимания статьи:

Slug — (из Викисловаря) альтернативная дружественная к восприятию человеком — буквенно-цифровая часть универсального адреса интернет-ссылки (URL) к рубрицируемому содержимому. То есть, если по простому, то slug заменяет всяческие признаки и id-шники ресурсов нашего сайта в URL-е на человекопонятный текст.

Разберем пример

Кому и так ясно что-такое ЧПУ — мотаем дальше.

Разбор примера как могли бы выглядеть URL-ы сайта, если их доработать до ЧПУ
На примере сайта магазина rozetka.com.ua (первый сайт, который попался под руку). ЧПУ тут в зачаточном состоянии. Давайте попробуем их ссылки довести до ума вручную:

Я зашел на страницу «Мячи для настольного тенниса» и адрес в ее оказался:
rozetka.com.ua/t_balls/c81265

Явно, что «c81265» первым символом указывает на то, что запрашиваемый объект — категория товаров, а число после него — id категории в базе данных.

Переделав под ЧПУ у на получилось бы просто:
rozetka.com.ua/t_balls

Просто удалили id-шник? Как же так? А как же контентные страницы (http://rozetka.com.ua/contacts/)?
Да никаких проблем. Просто поставьте все контентные страницы, так чтобы текущий путь в запросе сверялся в первую очередь с ними. В Symfony это делается всего лишь тем, что маршруты для этих путей объявлены первыми.

Если и так не получается или у вас на сайте есть еще что-то важное кроме контентных страниц и категорий товаров, то делаем более однозначный путь:
rozetka.com.ua/category/t_balls

Далее я перешел на сам продукт «Мячи для настольного тенниса Donic Elit 1* 6 шт белый (618016): rozetka.com.ua/198578/p198578

Вот тут просто беда. ЧПУ даже перестало пахнуть.

Как должен был выглядеть такой URL? В зависимости от того как у вас устроен сайт может быть несколько вариантов. По степени загруженности URL сегментов, уменьшающих неоднозначность пути:


Здесь:

t_balls — slug категории
donic-elit-1-6-beliy — slug продукта

Думаю с наглядностью мы закончили.

Как получить ЧПУ в Symfony

Объяснять буду на примере свежей установки Symfony. На момент написания была взята версия Symfony 3.3.0. Предполагается, что вы установили Symfony и сконфигурировали доступ к базе данных.

Прежде чем начнется суть да дело нужно подружить нашу Symfony 3.3.0 с phpunit, чтобы она не валилась после автогенерации контроллеров. Дополните composer.json проекта двумя строчками:

composer.json
...
    "require-dev": {
        ...
        "phpunit/phpunit": "^6.2.1"
        ...
    },
...
    "config": {
        "platform": {
            "php": "7.0.15"
        },
        ...
    },
...

И произведите обновление зависимостей:

composer update

Или так, если у вас композер архивом лежит в проекте:

php composer.phar update

Генерируем внутри бандла AppBundle сущность продукта консольной командой:

php bin/console doctrine:generate:entity --entity=AppBundle:Product --fields="name:string description:text seller:string publishDate:datetime slug:string(length=128 nullable=false unique=true)" -q

Наверняка вы заметили, что помимо остальных полей имеется интересное поле slug. Я сделал его уникальным, и без возможности быть равным null. Дело в том, что в нашем новом проекте мы должны будем иметь возможность выбирать товары из базы данных как по id-шникам, так и по slug-ам. Slug теперь — наш второй после id уникальный идентификатор записи.

Для удобства изложения и для вашего удобства тестирования мною изложенного материала сгенерируем CRUD контроллер на основе сущности AppBundle:Product, созданой на предыдущем шаге. Для этого выполним консольные команды:

php bin/console doctrine:database:create #создаем базу данных
php bin/console doctrine:schema:create #создаем структуру данных в базе данных

php bin/console doctrine:generate:crud --entity="AppBundle:Product" --route-prefix=products --with-write -n #генерируем CRUD контроллер

Теперь после запуска сервера

php bin/console server:run localhost:2020

Мы можем посетить страницу http://localhost:2020/products/ и увидеть пустой список продуктов да ссылку на страницу создания нового продукта:



Повременим с созданием новых продуктов. Ведь нас ждет подключение расширений Doctrine.

Подключение поведенческих расширений Doctrine

Почему нам нужны расширения Doctrine? Разве мы сами не можем генерировать slug для продукта? В целом да. Все это можно сделать собственными руками: генерировать slug на основе поля или набора полей, заботиться об уникальности slug-а, иметь всегда в виду необходимость его заполнения, иначе сайт рухнет. Но мы не ради этого здесь собрались. Так что читаем официальную документацию по тому как пользоваться расширениями Doctrine:

How to use Doctrine Extensions

Тут нам советуют использовать бандл StofDoctrineExtensionsBundle, который обеспечит корректное подключение расширений Doctrine. Читаем документацию по нему:

StofDoctrineExtensionsBundle

Устанавливаем бандл StofDoctrineExtensionsBundle:

composer require stof/doctrine-extensions-bundle

Подключаем скачанный бандл:

app/AppKernel.php
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...
            new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
        );
        // ...
    }
    // ...
}

Из всего богатства вовлеченных нами в проект расширений Doctrine нам нужно всего одно — Sluggable behavior extension. Так что сконфигурируем StofDoctrineExtensionsBundle таким образом, чтобы это расширение было включено:

app/config/config.yml
...
stof_doctrine_extensions:
    default_locale: en_US
    orm:
        default:
            sluggable : true
...

Расширение Sluggable behavior extension подключено. Надо теперь указать ему, что именно от него требуется. Для этого почитаем по нему документацию:

Sluggable behavior extension for Doctrine 2

Оказывается, нам не так уж и много нужно сделать. Всего-то нужно в сущности продукта подключить класс аннотаций, которые предоставляет нам расширение, да указать этими аннотациями полю Product:slug, что оно должно автоматически заполняться как slug на основе полей, которые мы выберем:

src/AppBundle/Entity/Product.php
...
use Gedmo\Mapping\Annotation as Gedmo;
...

/**
 * Product
 *
 * @ORM\Table(name="product")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
 */
class Product
{
...
     /**
     * @var string
     *
     * @Gedmo\Slug(fields={"name"})
     * @ORM\Column(name="slug", type="string", length=128, nullable=false, unique=true)
     */
    private $slug;
...
}

Здесь я указал аннотацией @Gedmo\Slug(fields={"name"}), что я хочу, чтобы slug генерировался на основании поля name. Можно указать несколько полей, чтобы они конкантинировались при генерации. Например, часто вместо с именем сущности указывают дату создания: @Gedmo\Slug(fields={"publishDate", "name"}).

Пора создавать продукты. Но перед этим нужно убрать лишнее поле из формы, ведь поле slug Doctrine будет заполнять самостоятельно:

src/AppBundle/Form/ProductType.php
...
class ProductType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name')->add('description')->add('seller')->add('publishDate'); //Удалили ->add('slug')
    }
    ...
}

Заходим на форму создания продукта (http://localhost:2020/products/new)


Сохраняем и видим, что slug сгенерирован. Он годен для использования в маршрутах вашего приложения:



Остается проверить ЧПУ на деле.

Первый ЧПУ маршрут

Сделаем все по простому. А именно — переделаем маршруты products_show и products_edit:



таким образом, чтобы они показывали нам продукт не по id-нику, а по slug-у. Маршрут products_delete менять не будем, так как он не виден ни пользователю, ни поисковику.

src/AppBundle/Controller/ProductController.php
...
class ProductController extends Controller
{
     ...
     /**
     * Finds and displays a product entity.
     *
     * @Route("/{slug}", name="products_show")
     * @Method("GET")
     * @param string $slug
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function showAction(string $slug)
    {
        $product = $this->getDoctrine()
            ->getRepository('AppBundle:Product')
            ->findOneBySlug($slug);
        $deleteForm = $this->createDeleteForm($product);

        return $this->render('product/show.html.twig', array(
            'product' => $product,
            'delete_form' => $deleteForm->createView(),
        ));
    }

    /**
     * Displays a form to edit an existing product entity.
     *
     * @Route("/{slug}/edit", name="products_edit")
     * @Method({"GET", "POST"})
     * @param Request $request
     * @param string $slug
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
     */
    public function editAction(Request $request, string $slug)
    {
        $product = $this->getDoctrine()
            ->getRepository('AppBundle:Product')
            ->findOneBySlug($slug);

        $deleteForm = $this->createDeleteForm($product);
        $editForm = $this->createForm('AppBundle\Form\ProductType', $product);
        $editForm->handleRequest($request);

        if ($editForm->isSubmitted() && $editForm->isValid()) {
            $this->getDoctrine()->getManager()->flush();

            return $this->redirectToRoute('products_edit', array('slug' => $product->getSlug()));
        }

        return $this->render('product/edit.html.twig', array(
            'product' => $product,
            'edit_form' => $editForm->createView(),
            'delete_form' => $deleteForm->createView(),
        ));
    }
    ...
}

app/Resources/views/product/index.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Products list</h1>

    <table>
        <thead>
            <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Description</th>
                <th>Seller</th>
                <th>Publishdate</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
        {% for product in products %}
            <tr>
                <td><a href="{{ path('products_show', { 'slug': product.slug }) }}">{{ product.id }}</a></td>
                <td>{{ product.name }}</td>
                <td>{{ product.description }}</td>
                <td>{{ product.seller }}</td>
                <td>{% if product.publishDate %}{{ product.publishDate|date('Y-m-d H:i:s') }}{% endif %}</td>
                <td>
                    <ul>
                        <li>
                            <a href="{{ path('products_show', { 'slug': product.slug }) }}">show</a>
                        </li>
                        <li>
                            <a href="{{ path('products_edit', { 'slug': product.slug }) }}">edit</a>
                        </li>
                    </ul>
                </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>

    <ul>
        <li>
            <a href="{{ path('products_new') }}">Create a new product</a>
        </li>
    </ul>
{% endblock %}


app/Resources/views/product/show.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Product</h1>

    <table>
        <tbody>
            <tr>
                <th>Id</th>
                <td>{{ product.id }}</td>
            </tr>
            <tr>
                <th>Name</th>
                <td>{{ product.name }}</td>
            </tr>
            <tr>
                <th>Description</th>
                <td>{{ product.description }}</td>
            </tr>
            <tr>
                <th>Seller</th>
                <td>{{ product.seller }}</td>
            </tr>
            <tr>
                <th>Publishdate</th>
                <td>{% if product.publishDate %}{{ product.publishDate|date('Y-m-d H:i:s') }}{% endif %}</td>
            </tr>
            <tr>
                <th>Slug</th>
                <td>{{ product.slug }}</td>
            </tr>
        </tbody>
    </table>

    <ul>
        <li>
            <a href="{{ path('products_index') }}">Back to the list</a>
        </li>
        <li>
            <a href="{{ path('products_edit', { 'slug': product.slug }) }}">Edit</a>
        </li>
        <li>
            {{ form_start(delete_form) }}
                <input type="submit" value="Delete">
            {{ form_end(delete_form) }}
        </li>
    </ul>
{% endblock %}

Получилось так:



Теперь маршрут на детальный просмотр продукта выглядит так: @Route("/{slug}", name="products_show")

Маршрут на редактирование продукта: @Route("/{slug}/edit", name="products_edit")

Уникальность slug-ов
Вопрос заданный мне в комментариях пользователем psycho-coder сподвиг меня дополнить статью. А что, если я захочу создать несколько продуктов с одинаковым наименованием? Ведь Symfony позволяет это сделать. Что будет тогда со slug-ами, которые пишутся в поле с уникальным ключом в базе данных?

Как я говорил выше, Doctrine Sluggable behavior extension берет на себя ответственность по построению уникальных slug-ов.

Для примера я создал три раза подряд продукт с одним и тем же именем: „Что-то осмысленное“. Автоматически сгенерированные slug-и получились такими:

  • chto-to-osmyslennoe
  • chto-to-osmyslennoe-1
  • chto-to-osmyslennoe-2


Если этот вариант не нравится, то можно для поля slug указать генерацию на основе не одного поля, а двух. Пример подобной аннотации для поля slug:

@Gedmo\Slug(fields={"name", "publishDate"})

Трижды создаем продукт с промежутком в одну минуту и получаем slug-и:

  • chto-to-osmyslennoe-2015-05-05-04-04
  • chto-to-osmyslennoe-2015-05-05-04-05
  • chto-to-osmyslennoe-2015-05-05-04-06


Если и это не нравится, то придумываем свой вариант и делимся в комментариях

Напоследок

Мы добились нашей цели:

  • slug генерируется автоматически при сохранении сущности
  • маршруты работают с учетом slug вместо id-шника
  • Поле slug в базе данных обладает уникальным ключом, что позволяет нивелировать тормоза при выборке продуктов по этому полю

Пора идти закупаться пивом и думать как это все воплотить в большом проекте. Если я был вам полезен, значит я рад быть вам полезен.

Архив с Symfony проектом, созданным в процессе написания статьи прикладываю тут.

Кстати, 3d картинку рендерил сам специально для этой статьи. Мне она понравилась, да и сил много не отняла.

Всем хороших маршрутов!
Метки:
Поделиться публикацией
Комментарии 19
  • –1
    Никогда не понимал смысл SEF URLs в контексте удобства юзера. Особенно сейчас, когда почти в каждом мессенджере или редакторе генерируется превью ссылки.

    Чем https://example.com/ochen-krutoi-razdel/ochen-krutaya-statya-kotoryu-nuzhno-obyazatelino-prochitat лучше https://example.com/201701/11 для юзера?
    • +1

      Посмотрите хотя бы на выдачу Гугла по какому-нибудь запросу. Красиво и понятно составленные ссылки могут иметь решающее значение при выборе ссылки для перехода. Это не значит что нужно делать по вот такому примеру как вы написали. Оба эти варианта оставляют желать лучшего.

      • 0
        могут иметь решающее значение при выборе ссылки для перехода

        а могут и не быть. Это утверждение нуждается в проверке а не в голословных утверждениях. Ибо больше похоже на культ карго.


        С учетом того что в выборке отображается заголовок в нормальном человеческом виде, дескрипшен и т.д. я не вижу ни одной причины загоняться по формату ссылок.

        • 0

          Как вы верно замечаете что ЧПУ сейчас не в тренде, но это не значит что можно на них забить и полностью игнорировать. Нужно найти баланс между "загоняться" и совсем забросить. Ведь это ещё одна строка в поисковой выдаче, на которую вы влияете! Разве это разумно вот так просто отказываться от возможностей, которые дают поисковики?

          • +1
            Нужно найти баланс между "загоняться" и совсем забросить.

            Давайте составим требования к URL:


            1. уникальный идентификатор ресурсов, желательно постоянный.
            2. чем короче тем лучше
            3. по нему должно быть легко понятно "что внутри" с точки зрения чтения эксес логов.

            Хорошие примеры урлов:


            /posts/2017/06/23/123323
            /products/123134
            /products/123134-kakoyto-product
            /users/DQ4ADQsCCwACDgsLBAUGDg

            Плохие примеры урлов:


            /raznoie/samoy-krutoy-product-v-cataloge

            Последний пример плох тем что он:


            • не гарантирует нам постоянство — если поменялся слаг — поменяется и урл, надо хэндлить редиректы (чего обычно не происходит), излишняя сложность
            • может быть длинным и сложным
            • по логам ничего не понятно
            • 0

              Вот это уже похоже на взвешенный подход. Вам бы статью про ЧПУ написать.

              • 0
                Последний пример плох тем что он:

                не гарантирует нам постоянство — если поменялся слаг — поменяется и урл, надо хэндлить редиректы (чего обычно не происходит), излишняя сложность

                Если поменялся slug на ту страницу, для которой требовался ЧПУ, то это уже другая страница, которую нужно индексировать по новому в подавляющем большинстве случаев. Вы сами подумайте — вы поменяли название продукта/статьи/еще чего-то. Так что о редиректах тут речь идет только в том случае, когда ЧПУ применяется неуместно.

                может быть длинным и сложным

                Если полный адрес страницы укладывается в 128 символов, то кого это волнует? Да зачастую и более длинные ссылки мало кого раздражают. Длинный адрес — не является сложным. Плохо составленный адрес даже будучи коротким может вынести мозг почище 256-ти символьного.

                по логам ничего не понятно

                Логи нужно заполнять так, чтобы было понятно. А главное заполнять их исходя из понимания того как вы их будете потом читать и какая вам понадобится информация для понимания и в каком виде. И уж точно тут мало что зависит от длины адреса строки.
                Если говорить про стандартные Apache логи, то чем вам облегчит чтение при урле типа
                /products/123134

                ?
                123134 — это такой же ID как и slug. В случае если в урле есть еще и slug категории, то у вас есть два ID для выборки интересующей статьи, которые, кстати, еще из логов уже могут указать на тематику страницы с данным урлом. В итоге, на сколько я понимаю, вы недовольны тем, что вам придется делать на одно действие больше при выборки из базы данных статьи, вызвавшей сбой в программе? На месте вашего работодателя (если таковой имеется) я бы подумал — повышать вам ЗП или нет.

                Пока все из того, что я прочитал в ваших комментариях, относиться к тому, что вам проще работать с числовыми идентификаторами сущностей. Программистов/сисадминов в этом винить трудно, но речь идет не о нашем удобстве, а о том, что можно продукт сделать лучше.
                • 0
                  Если поменялся slug на ту страницу, для которой требовался ЧПУ, то это уже другая страница, которую нужно индексировать по новому в подавляющем большинстве случаев. Вы сами подумайте — вы поменяли название продукта/статьи/еще чего-то.

                  а теперь подумайте. Я скопировал ссылку и скинул ее кому-то. Этот кто-то спустя какое-то время (месяц) кликает по ней и получает 404. Да, редиректы определенно не нужны.


                  Причин для изменения "урла" тут множество. Это может быть не только изменение продукта. К примеру продукт относился к какой-то общей категории, и у нас появилась необходимость разделить эту общую категорию на две более специализированные. В этом случае продукт переносится в новую категорию и соответственно меняется ссылка.


                  Если полный адрес страницы укладывается в 128 символов, то кого это волнует?

                  а если не укладывается? Если вот было хорошо, укладывалось, но это не покрывает все 100% кейсов.


                  Плохо составленный адрес даже будучи коротким может вынести мозг почище 256-ти символьного.

                  Проведите исследование, приведите статистику, какой процент пользователей "читает" урлы.


                  И уж точно тут мало что зависит от длины адреса строки.

                  вопрос не в длине, а в отсутствии необходимости учитывать контекст. Все должно быть максимально явно.


                  В случае если в урле есть еще и slug категории, то у вас есть два ID для выборки интересующей статьи

                  Какой толк мне от второго идентификатора если для нахождения статьи мне достаточно одного?


                  которые, кстати, еще из логов уже могут указать на тематику страницы с данным урлом.

                  Те задачи где надо учитывать тематику больше подходят под описание задач аналитики. И по сути имея идентификаторы можно легко выгрести самую разнообразную статистику.


                  В логах же интересует что делал человек. Скажем у нас есть ошибка при добавлении товара в корзину. И каждому пользователю присваивается некий идентификатор. Видя ошибку мы можем просто грепнуть по идентификатору и глянуть какие именно продукты он пытался купить и тд.


                  Конечно пример надуманный, но от этого толка явно больше.


                  На месте вашего работодателя (если таковой имеется) я бы подумал — повышать вам ЗП или нет.

                  когда речь идет о изменении оплаты труда, всегда стоит думать.


                  Программистов/сисадминов в этом винить трудно, но речь идет не о нашем удобстве, а о том, что можно продукт сделать лучше.

                  Ответьте на вопрос, КАК такая вещи как ЧПУ поможет сделать продукт лучше. Польза для пользователей весьма сомнительна, количество потенциальных проблем зависящих от контекста задачи может сильно повлиять на стоимость этой фичи для бизнеса. Причем бизнесу как таковому профита от этого может и не быть вовсе.


                  Пока все из того, что я прочитал в ваших комментариях, относиться к тому, что вам проще работать с числовыми идентификаторами сущностей.

                  Из того что я прочитал все относится к тому что ЧПУ нужны, но зачем — ответа я так и не получил. Урлы читают? нет. Урлы учитываются при ранжировании поисковиками?


                  Есть огромная разница между "урлами с семантикой" и то о чем вы говорите. Семантика урлов нужна, но для того что бы у них была эта самая семантика совершенно не нужно пихать название продукта в урл.

                  • 0
                    1. ID сущности постоянен
                    2. Slug сущности может меняется

                    Поэтому, делать ссылки только со slug неправильно. Это может привести к следующим проблемам:


                    • Slug поменялся и ссылка отдает 404.
                      Это плохо для пользователя и для поисковиков. При частой смене slug можно и бан словить.
                    • Slug сущности А сменился, а slug сущности Б сменился на slug сущности А.
                      Теперь по старой ссылке отдается другой контент. Это плохо для пользователя и для SEO.
                    • Смена slug затрудняет идентификацию сущности в логах.

                    Ещё Google рекомендует в ссылках указывать числовой идентификатор (лень искать ссылку).


                    Решение описанных проблем это использование в URL, и ID и slug. И выглядеть это будет так:


                    /article/{id}-{slug}

                    Что это на даёт:


                    1. Мы гарантируем уникальность ссылок даже при смене slug.
                    2. Ссылки соответствуют рекомендациям Google.
                    3. В логах мы увидим ID сущности.
                    4. ЧПУ у нас сохраняется.
                    5. Мы можем избавится от 404 из-за смены slug.

                    Вы наверное уже догадались как избавится от 404, но я всё же поясню.
                    В контроллер выбираем сущность по ID из запроса. Проверяем текущий slug в сущности и slug из запроса. Если не совпадают, делаем 301 редирект на правильный slug.


                    Лёгким мановением руки решили все проблемы)))

        • –1
          а в контексте юзера никто и не рассматривает. seo-шники выдумали.
          • +1
            Согласен с alexkbs
            Оба эти варианта оставляют желать лучшего.


            Из моей статьи:
            URL на вашем сайте должны содержать релевантную для поисковика информацию о страничках, на которые они ведут и иметь продуманную вложенность


            Ваш длинный URL содержит вредную избыточность. В примере моей статьи приведен лишь базовый пример работы со slug-ами. В действительности нужно давать возможность их «причесывать» под SEO контентменеджеру сайта, и/или самому автору статьи.

            Ну а на счет того, что для пользователя большой разницы нет, ЧПУ-не ЧПУ, — об этом я и сам написал в статье. И с вами согласен. Однако есть большое НО: Если пользователь продвинутый (умеет печатать не только указательными пальцами), то на ссылки внимание он обращает и получает из них много информации. Например я обращаю внимание. Особенно когда хожу по ссылкам в поисковике.
            • –1
              то на ссылки внимание он обращает и получает из них много информации.

              приведите ссылки на исследования в этой области.


              Например я обращаю внимание.

              на что именно вы обращаете внимание? читаете транслит?

            • 0

              https://example.com/ochen-krutoi-razdel/ochen-krutaya-statya-kotoryu-nuzhno-obyazatelino-prochitat — в этой ссылке я могу удалить ochen-krutaya-statya-kotoryu-nuzhno-obyazatelino-prochitat и попасть в раздел. К тому же я примерно понимаю где я нахожусь. )


              Здесь https://example.com/201701/11 удаляя "11" я не могу даже западозрить куда попаду.

            • 0
              А как быть на странице фильтрации и сортировки? Там очень много параметров и их комбинации
              • 0
                Все зависит от ситуации. В большинстве случаев, даже на сайтах с хорошим ЧПУ и SEO оптимизацией, я вижу, что параметры фильтрации передаются на сервер банальными GET параметрами.

                Если фильтрация и сортировка подразумевает сотню параметров, то в общем-то и нет смысла сувать это все в ЧПУ, так как такой URL уже приобретает характер служебного URL. То есть такого URL, который индексировать поисковик не должен, да и пользователю лишний раз его показывать не стоит.

                Но если параметров фильтрации не больше 20-ти и одновременно из них не может быть выбрано больше 5-ти, то это можно оформить в виде ЧПУ ссылок.

                Сами подумайте, ведь ЧПУ типа:
                http(s)://Домен/slug-категории/slug-подкатегории/

                уже является ЧПУ с фильтрацией. Здесь фильтрация по категории.

                Значит здесь думать нужно так: если в результате фильтрации получаются URL-ы, которые вы в гипотетической смтуации могли бы вставить в гипотетическую менюшку и такая менюшка была бы удобна для пользователя, то подобная фильтрация заслуживает быть ЧПУ. А вот как это реализовать — это другой вопрос. Один экшин в симфони может сопоставляться с большим количеством маршрутов:
                     /**
                     * @Route("/search/category/{category_slug}/", name="search_by_category")
                     * @Route("/search/date-from/{date-from}/date-to/{date-to}", name="search_by_date")
                     * @Method({"GET"})
                     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
                     */
                    public function searchAction(Request $request, string $category_slug = null, $date-from = null, $date-to = null )
                    {
                        ...
                    }
                


                Либо еще как-то эксперементировать с маршрутами
                • 0
                  параметры фильтрации передаются на сервер банальными GET параметрами

                  Всегда считал, что так правильнее если эту ссылку нужно кому-то скинуть. Иначе будет примерно так: вот ссылка на раздел, там выбери такие-то параметры фильтра и где-то там на третьем экране нужное что-то…

                  Еще есть вопрос по поводу уникальности slug: если есть товар с одинаковым наименованием, то как быть?
                  Или один товар в разных категориях, или две подкатегориями с одинаковым названием в разных категориях?
                  В общем проблема имеет место быть и простым уникальным индексом не решается.
                  • 0
                    Вообще продукты могут быть с одинаковым именем даже в одной категории. Этому пример — большинство наших интернет витрин. Два смартфона с одной ценой и с одним наименованием, различающиеся только цветом корпуса, часто кладутся в одну категорию. Так что озвученная вами проблема даже ближе к повседневности, чем вы хотели показать.

                    Но, как я говорил в статье, Doctrine Sluggable behavior extension берет на себя ответственность по построению уникальных slug-ов.

                    Я сейчас создал три раза подряд продукт с одним и тем же именем: «Что-то осмысленное». Автоматически сгенерированные slug-и получились такими:
                    • chto-to-osmyslennoe
                    • chto-to-osmyslennoe-1
                    • chto-to-osmyslennoe-2


                    Если этот вариант не нравится, то можно для поля slug указать генерацию на основе не одного поля, а на основе двух. Например:

                    @Gedmo\Slug(fields={"name", "publishDate"})

                    Но это громоздкие slug-и получатся, так что надо под конкретный случай разбирать.

                    Возможность иметь продукты с одинаковым наименованием и slug-ом в двух разных категориях я не рассматривал.
                  • +1
                    уже является ЧПУ с фильтрацией. Здесь фильтрация по категории.

                    а теперь сделайте мне так что бы можно было выводить продукты из 5-ти различных категорий. И скажите что удобнее. сделать /products?category=1,2,5,12 или /products/category1-category2-category3... и потом еще хэндлить редиректы (названия то могут поменяться, но ссылки должны сохраняться, а тут могут быть коллизии).


                    Я это к чему. Основная характеристика ссылок — они должны быть постоянными. Или хотя бы по максимуму постоянными. Не должно быть такого что я поделился ссылкой, контент менеджер переименовал категорию и ссылка стала невалидной. А это уже влечет за собой огромное количество технических проблем и неимоверное усложенение. Причем профит от этого действа мягко скажем сомнительный.


                    В целом же любая фильтрация и тд. это уже служебная ссылка. Как правило индексируется все же детали по продукту, есть возможность подсказывать краулерам что является основным контентом и т.д. ЧПУ было в тренде лет 10 назад, сейчас в этом нет никакого смысла.

                    • 0
                      Не хочется признавать, но в подавляющем большинстве случаев действительно ЧПУ окажется лишним. Об этом говорится и на странице недавнего ныне популярного у SEO-шников исследования

                      На самом деле урл не является предметом семантического, то есть смыслового анализа. Смотрите факт 3 на скриншоте выше. Если рассмотреть всю ссылку страницы первого места, там вообще использован ни о чём не говорящий числовой хеш.


                      Однако никто не отменяет того, что заказчик сайта может решить, что важно внимание мелочам. И тут лучше знать как делать ЧПУ, чем не знать.

                      Ну что же, это грустно. Я помню, что 12 лет назад команда, в которой я работал, много сил и времени потратила, чтобы создать коробочный продукт под Joomla. Этот продукт позволял управлять ссылками на сайте, и тем какой вид будет у ЧПУ. Помню это было интересной работой для тех дремучих лет. Да и продукт раскупался как пирожки.

                      Впрочем технологий не становится меньше. Скорее наоборот и в геометрической прогрессии. Скучать не приходится, даже если что-то устарело.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.