src/WebBundle/Controller/SeoController.php line 152

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace WebBundle\Controller;
  4. use DOMDocument;
  5. use DOMElement;
  6. use FlexApp\Service\AdsFilters\FilterAdsFilesDataService;
  7. use RuntimeException;
  8. use Symfony\Component\HttpFoundation\Request;
  9. use Symfony\Component\HttpFoundation\Response;
  10. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  11. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  12. use WebBundle\Entity\Vacancy;
  13. use WebBundle\Helper\App;
  14. use WebBundle\Helper\LocaleHelper;
  15. use WebBundle\Helper\StrHelper;
  16. use WebBundle\Helper\VacancyHelper;
  17. use WebBundle\Repository\ListCountryRepository;
  18. use WebBundle\Service\SeoService;
  19. use WebBundle\Service\SitemapGenerator;
  20. /**
  21.  * Контроллер для SEO-задач (sitemap, robots, html-sitemap и т.д.).
  22.  */
  23. class SeoController extends ExtendedController
  24. {
  25.     /** @required */
  26.     public SeoService $seoService;
  27.     /** @required */
  28.     public SitemapGenerator $sitemapGenerator;
  29.     private FilterAdsFilesDataService $filterAdsFilesDataService;
  30.     public function __construct(FilterAdsFilesDataService $filterAdsFilesDataService)
  31.     {
  32.         parent::__construct();
  33.         $this->filterAdsFilesDataService $filterAdsFilesDataService;
  34.     }
  35.     /**
  36.      * Главная страница для всех sitemap-файлов (sitemapindex).
  37.      * Генерирует ссылку на каждый sitemap для доступных локалей (один маршрут на локаль).
  38.      *
  39.      * @return Response XML-ответ с индексом sitemap-файлов.
  40.      */
  41.     public function indexAction(): Response
  42.     {
  43.         $siteMap = new DOMDocument('1.0''UTF-8');
  44.         $siteMap->formatOutput true;
  45.         $sitemapIndex $siteMap->createElementNS(
  46.             "http://www.sitemaps.org/schemas/sitemap/0.9",
  47.             'sitemapindex'
  48.         );
  49.         $sitemapIndex->setAttribute('xmlns''http://www.sitemaps.org/schemas/sitemap/0.9');
  50.         // Получаем список локалей/стран из сервиса
  51.         $localeCountryList $this->sitemapGenerator->generateLocaleCountryList();
  52.         foreach ($localeCountryList as $locale) {
  53.             $link $this->link('seo_sitemap', ['_locale' => $locale]);
  54.             $loc $siteMap->createElement('loc'$link);
  55.             $lastmod $siteMap->createElement('lastmod'date('c'));
  56.             $sitemap $siteMap->createElement('sitemap');
  57.             $sitemap->appendChild($loc);
  58.             $sitemap->appendChild($lastmod);
  59.             $sitemapIndex->appendChild($sitemap);
  60.         }
  61.         $siteMap->appendChild($sitemapIndex);
  62.         $response = new Response($siteMap->saveXML());
  63.         $response->headers->set('Content-Type''application/xml');
  64.         return $response;
  65.     }
  66.     /**
  67.      * Возвращает XML-содержимое sitemap для указанной локали.
  68.      *
  69.      * @param string $_locale Локаль, например, 'da-dk'.
  70.      *
  71.      * @return Response XML-ответ с содержимым sitemap.
  72.      */
  73.     public function siteMapXmlTEAction(string $_locale): Response
  74.     {
  75.         try {
  76.             $filePath $this->sitemapGenerator->generateSitemapSinglePath($_locale);
  77.             $content $this->filterAdsFilesDataService->getFileContents($filePath);
  78.         } catch (RuntimeException $e) {
  79.             throw new NotFoundHttpException(sprintf(
  80.                 'Sitemap файл не найден для локали "%s". Детали: %s',
  81.                 $_locale,
  82.                 $e->getMessage()
  83.             ));
  84.         }
  85.         return new Response($contentResponse::HTTP_OK, [
  86.             'Content-Type' => 'application/xml; charset=UTF-8',
  87.         ]);
  88.     }
  89.     /**
  90.      * Обрабатывает запросы к частичным Sitemap-файлам.
  91.      *
  92.      * Этот метод возвращает содержимое определённого файла Sitemap в зависимости от локали и номера части.
  93.      *
  94.      * @param string $_locale Локаль, например, 'da-dk'
  95.      * @param int $part Номер части Sitemap, например, 1
  96.      *
  97.      * @return Response Ответ с содержимым Sitemap-файла
  98.      */
  99.     public function sitemapPartAction(string $_localeint $part): Response
  100.     {
  101.         if ($part 1) {
  102.             throw new NotFoundHttpException('Номер части Sitemap должен быть положительным целым числом.');
  103.         }
  104.         try {
  105.             $filePath $this->sitemapGenerator->generateSitemapPath($_locale$part);
  106.             $content $this->filterAdsFilesDataService->getFileContents($filePath);
  107.         } catch (RuntimeException $e) {
  108.             throw new NotFoundHttpException(sprintf(
  109.                 'Файл Sitemap не найден: sitemap.%s_part%d.xml. Детали: %s',
  110.                 $_locale,
  111.                 $part,
  112.                 $e->getMessage()
  113.             ));
  114.         }
  115.         return new Response($contentResponse::HTTP_OK, [
  116.             'Content-Type' => 'application/xml; charset=UTF-8',
  117.         ]);
  118.     }
  119.     /**
  120.      * Генерирует файл robots.txt.
  121.      *
  122.      * @param Request $request HTTP-запрос.
  123.      *
  124.      * @return Response Ответ с содержимым robots.txt.
  125.      */
  126.     public function robotsTxtTEAction(Request $request): Response
  127.     {
  128.         if (App::getContainer()->getParameter('site') != 'tile.expert' && $request->get('debug') == null) {
  129.             $response $this->render(
  130.                 '@Web/Seo/robots.txt.twig',
  131.                 [
  132.                     'disallow' => false,
  133.                 ]
  134.             );
  135.         } else {
  136.             $list = [
  137.                 'disallow' => $this->getDisallowUrlPatterns(),
  138.             ];
  139.             foreach (LocaleHelper::getListCode() as $lc) {
  140.                 foreach (App::getCountryList() as $code => $country) {
  141.                     if ($code != 'en' && !in_array($lc$country['localesArr'])) {
  142.                         $list['full'][] = '/' $lc '-' $code '$';
  143.                         $list['full'][] = '/' $lc '-' $code '/';
  144.                     }
  145.                 }
  146.                 $list['full'][] = '/' $lc '-en';
  147.                 $list['full'][] = '/' $lc '_es-cn';
  148.                 $list['full'][] = '/' $lc '-es-cn';
  149.             }
  150.             $response $this->render(
  151.                 '@Web/Seo/robots.txt.twig',
  152.                 [
  153.                     'host' => 'tile.expert',
  154.                     'list' => $list,
  155.                     'locales' => LocaleHelper::getListCode(),
  156.                     'countries' => array_keys(App::getCountryList()),
  157.                     'allows' => $this->getAllowsUrlPatterns(),
  158.                 ]
  159.             );
  160.         }
  161.         $response->headers->set('Content-Type''text/plain');
  162.         return $response;
  163.     }
  164.     /**
  165.      * Генерирует HTML sitemap-индекс.
  166.      *
  167.      * @return Response Ответ с содержимым HTML sitemap.
  168.      */
  169.     public function sitemapHtmlIndex(): Response
  170.     {
  171.         $dom = new DOMDocument();
  172.         $head $dom->createElement('head');
  173.         $body $dom->createElement('body');
  174.         $dom->appendChild($head);
  175.         $dom->appendChild($body);
  176.         foreach (LocaleHelper::getListCodeAvailable() as $lc) {
  177.             foreach (App::getCountryList() as $code => $country) {
  178.                 if (
  179.                     empty($code)
  180.                     || ($code === 'en' && $lc === 'en')
  181.                     || ($code !== 'en' && !in_array($lc$country['localesArr']))
  182.                     || (!in_array($codeSitemapGenerator::COUNTRIES_EN) && $lc === 'en')
  183.                 ) {
  184.                     continue;
  185.                 }
  186.                 $code strtolower($code);
  187.                 if (in_array($codeSitemapGenerator::ONLY_NATIVE_LANG_AVAILABLE)) {
  188.                     $lang $code;
  189.                 } else {
  190.                     $lang $lc;
  191.                 }
  192.                 $l LocaleHelper::buildLocaleCountry($lc$lang$code);
  193.                 if ($l === 'en-fi') {
  194.                     continue;
  195.                 }
  196.                 $loc $dom->createElement('a'"sitemap.$l.html");
  197.                 $loc->setAttribute('href'$this->link('seo_sitemap_html', ['_locale' => $l]));
  198.                 $div $dom->createElement('div');
  199.                 $div->appendChild($loc);
  200.                 $body->appendChild($div);
  201.             }
  202.         }
  203.         $response = new Response($dom->saveHTML());
  204.         $response->headers->set('Content-Type''text/html');
  205.         return $response;
  206.     }
  207.     /**
  208.      * Генерирует HTML sitemap для указанной локали.
  209.      *
  210.      * @param string $_locale Локаль, например, 'da-dk'.
  211.      *
  212.      * @return Response Ответ с содержимым HTML sitemap.
  213.      */
  214.     public function sitemapHtmlTEAction(string $_locale): Response
  215.     {
  216.         $oTranslator App::getTranslator();
  217.         $locale explode('-'$_locale);
  218.         if (!empty($locale[1]) && in_array($locale[1], SitemapGenerator::ONLY_NATIVE_LANG_AVAILABLE)) {
  219.             if ($locale[0] !== $locale[1]) {
  220.                 $l $locale[1] . '-' $locale[1];
  221.                 return $this->redirect($this->link('seo_sitemap_html', ['_locale' => $l]));
  222.             }
  223.         }
  224.         $dom = new DOMDocument();
  225.         $head $dom->createElement('head');
  226.         $body $dom->createElement('body');
  227.         $dom->appendChild($head);
  228.         $dom->appendChild($body);
  229.         if (
  230.             !in_array($locale[0], App::getCountryList()[App::getCurCountry()]['localesArr']) ||
  231.             (!empty($locale[1]) && $locale[0] == 'en' && !in_array($locale[1], SitemapGenerator::COUNTRIES_EN))
  232.         ) {
  233.             throw $this->createNotFoundException('Not found sitemap');
  234.         }
  235.         // главная
  236.         $body $this->siteMapHtmlElem(
  237.             $dom,
  238.             $body,
  239.             $this->link('app_home', ['_locale' => $_locale]),
  240.             $oTranslator->trans('home_page')
  241.         );
  242.         // публикации
  243.         $blogs $this->seoService->getBlogs($locale[0], $locale[1] ?? null);
  244.         foreach ($blogs as $blog) {
  245.             $body $this->siteMapHtmlElem(
  246.                 $dom,
  247.                 $body,
  248.                 $this->link('app_publication_single', ['id' => $blog['url'], '_locale' => $_locale]),
  249.                 $blog['title']
  250.             );
  251.         }
  252.         // коллекции
  253.         $collections $this->seoService->getCollections();
  254.         foreach ($collections as $collection) {
  255.             if ($collection['f_url'] == 'testing-factory') {
  256.                 continue;
  257.             }
  258.             $body $this->siteMapHtmlElem(
  259.                 $dom,
  260.                 $body,
  261.                 $this->link(
  262.                     'app_collection',
  263.                     [
  264.                         'collectionUrl' => $collection['c_url'],
  265.                         'factoryUrl' => $collection['f_id'] == 12
  266.                             StrHelper::toLower($collection['f_url'])
  267.                             : $collection['f_url'],
  268.                         '_locale' => $_locale,
  269.                     ]
  270.                 ),
  271.                 $collection['c_name']
  272.             );
  273.         }
  274.         // фильтры
  275.         $filters $this->seoService->getFilters($locale[0]);
  276.         foreach ($filters as $filter) {
  277.             if (in_array($filter['url'], [
  278.                 'testing-factory',
  279.                 'cataloge',
  280.                 'kakelkatalog',
  281.                 'katalog',
  282.                 'catalogo',
  283.             ])) {
  284.                 continue;
  285.             }
  286.             $body $this->siteMapHtmlElem(
  287.                 $dom,
  288.                 $body,
  289.                 $this->link('app_catalog', ['key' => $filter['url'], '_locale' => $_locale]),
  290.                 $oTranslator->trans($filter['leftMenu'])
  291.             );
  292.         }
  293.         // статика
  294.         $statics $this->seoService->getStatics($locale[0]);
  295.         foreach ($statics as $static) {
  296.             if ($static['url'] === '_test') {
  297.                 continue;
  298.             }
  299.             $body $this->siteMapHtmlElem(
  300.                 $dom,
  301.                 $body,
  302.                 $this->link('app_page', ['url' => $static['url'], '_locale' => $_locale]),
  303.                 $static['title'][$locale[0]]
  304.             );
  305.         }
  306.         $response = new Response($dom->saveHTML());
  307.         $response->headers->set('Content-Type''text/html');
  308.         return $response;
  309.     }
  310.     /**
  311.      * Вспомогательный метод для HTML sitemap.
  312.      */
  313.     protected function siteMapHtmlElem(
  314.         DOMDocument $siteMap,
  315.         DOMElement  $urlSet,
  316.         string      $link,
  317.         string      $name null
  318.     ): DOMElement
  319.     {
  320.         $div $siteMap->createElement('div');
  321.         $a $siteMap->createElement('a'$name htmlspecialchars($name) : $link);
  322.         $a->setAttribute('href'$link);
  323.         $div->appendChild($a);
  324.         $urlSet->appendChild($div);
  325.         return $urlSet;
  326.     }
  327.     /**
  328.      * Генерирует индекс sitemap для вакансий.
  329.      *
  330.      * @return Response XML-ответ с индексом sitemap-файлов вакансий.
  331.      */
  332.     public function siteMapXmlJobIndexAction(): Response
  333.     {
  334.         $siteMap = new DOMDocument('1.0''UTF-8');
  335.         $siteMap->formatOutput true;
  336.         $sitemapIndex $siteMap->createElementNS(
  337.             "http://www.sitemaps.org/schemas/sitemap/0.9",
  338.             'sitemapindex'
  339.         );
  340.         $sitemapIndex->setAttribute('xmlns''http://www.sitemaps.org/schemas/sitemap/0.9');
  341.         $locales VacancyHelper::getAviableLocales();
  342.         foreach ($locales as $lc) {
  343.             foreach (App::getCountryList() as $code => $country) {
  344.                 if (
  345.                     empty($code) ||
  346.                     ($code == 'en' && $lc == 'en') ||
  347.                     ($code != 'en' && !in_array($lc$country['localesArr'])) ||
  348.                     (!in_array($codeSitemapGenerator::COUNTRIES_EN) && $lc == 'en')
  349.                 ) {
  350.                     continue;
  351.                 }
  352.                 $code strtolower($code);
  353.                 if (in_array($codeSitemapGenerator::ONLY_NATIVE_LANG_AVAILABLE)) {
  354.                     $lang $code;
  355.                 } else {
  356.                     $lang $lc;
  357.                 }
  358.                 $l LocaleHelper::buildLocaleCountry($lc$lang$code);
  359.                 $loc $siteMap->createElement('loc'$this->link('app_jobs_seo_sitemap', ['_locale' => $l]));
  360.                 $lastmod $siteMap->createElement('lastmod'date('c'));
  361.                 $sitemap $siteMap->createElement('sitemap');
  362.                 $sitemap->appendChild($loc);
  363.                 $sitemap->appendChild($lastmod);
  364.                 $sitemapIndex->appendChild($sitemap);
  365.             }
  366.         }
  367.         $siteMap->appendChild($sitemapIndex);
  368.         $response = new Response($siteMap->saveXML());
  369.         $response->headers->set('Content-Type''application/xml');
  370.         return $response;
  371.     }
  372.     /**
  373.      * Генерирует XML sitemap для вакансии указанной локали.
  374.      *
  375.      * @param string $_locale Локаль, например, 'da-dk'.
  376.      *
  377.      * @return Response XML-ответ с содержимым sitemap вакансий.
  378.      */
  379.     public function siteMapXmlJobAction(string $_locale): Response
  380.     {
  381.         $locale explode('-'$_locale);
  382.         if (!empty($locale[1]) && in_array($locale[1], SitemapGenerator::ONLY_NATIVE_LANG_AVAILABLE)) {
  383.             if ($locale[0] != $locale[1]) {
  384.                 $l $locale[1] . '-' $locale[1];
  385.                 return $this->redirect(
  386.                     $this->generateUrl('seo_sitemap', ['_locale' => $l]),
  387.                     UrlGeneratorInterface::ABSOLUTE_URL
  388.                 );
  389.             }
  390.         }
  391.         $siteMap = new DOMDocument('1.0''UTF-8');
  392.         $siteMap->formatOutput true;
  393.         $urlSet $siteMap->createElementNS("http://www.sitemaps.org/schemas/sitemap/0.9"'urlset');
  394.         $urlSet->setAttribute('xmlns:xsi''http://www.w3.org/2001/XMLSchema-instance');
  395.         $urlSet->setAttribute(
  396.             'xsi:schemaLocation',
  397.             'http://www.sitemaps.org/schemas/sitemap/0.9 '
  398.             'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd'
  399.         );
  400.         $code StrHelper::toUpper($locale[1] ?? $locale[0]);
  401.         if (
  402.             !in_array($locale[0], App::getCountryList()[$locale[1] ?? App::getCurCountry()]['localesArr'] ?? [])
  403.             || (!empty($locale[1]) && $locale[0] === 'en' && !in_array($locale[1], SitemapGenerator::COUNTRIES_EN))
  404.         ) {
  405.             throw $this->createNotFoundException('Not found site map');
  406.         }
  407.         /** @var ListCountryRepository $countryRepo */
  408.         $countryRepo $this->getDoctrine()->getRepository('WebBundle:ListCountry');
  409.         $country $countryRepo->getCountry($code);
  410.         $priority = ($country && explode('|'$country->getLocale())[0] === $locale[0])
  411.             ? '1.0'
  412.             '0.5';
  413.         $localeUp StrHelper::ucFirst($locale[0]);
  414.         $hide 'hide' $localeUp;
  415.         // Показать только активные вакансии
  416.         $items $this->getDoctrine()
  417.             ->getRepository(Vacancy::class)
  418.             ->findBy(
  419.                 [
  420.                     'status' => true,
  421.                     $hide => false,
  422.                 ],
  423.                 [
  424.                     'subject' => 'asc',
  425.                 ]
  426.             );
  427.         /** @var Vacancy $item */
  428.         foreach ($items as $item) {
  429.             $urlSet $this->siteMapXmlElem(
  430.                 $siteMap,
  431.                 $urlSet,
  432.                 $this->link('app_job_show', ['_locale' => $_locale'slug' => $item->getSubjectTranslit()]),
  433.                 $priority
  434.             );
  435.         }
  436.         // Неактивные вакансии с меньшим приоритетом
  437.         $priority '0.5';
  438.         $items $this->getDoctrine()
  439.             ->getRepository(Vacancy::class)
  440.             ->findBy(
  441.                 [
  442.                     'status' => false,
  443.                     $hide => false,
  444.                 ],
  445.                 [
  446.                     'subject' => 'asc',
  447.                 ]
  448.             );
  449.         foreach ($items as $item) {
  450.             $urlSet $this->siteMapXmlElem(
  451.                 $siteMap,
  452.                 $urlSet,
  453.                 $this->link('app_job_show', ['_locale' => $_locale'slug' => $item->getSubjectTranslit()]),
  454.                 $priority
  455.             );
  456.         }
  457.         $siteMap->appendChild($urlSet);
  458.         $response = new Response($siteMap->saveXML());
  459.         $response->headers->set('Content-Type''application/xml');
  460.         return $response;
  461.     }
  462.     /**
  463.      * Генерирует файл robots.txt для вакансий.
  464.      *
  465.      * @param Request $request HTTP-запрос.
  466.      *
  467.      * @return Response Ответ с содержимым robots.txt для вакансий.
  468.      */
  469.     public function robotsTxtJobsAction(Request $request): Response
  470.     {
  471.         if ((bool)$_SERVER['APP_DEBUG']) {
  472.             $response $this->render(
  473.                 '@Web/Seo/robots.txt.twig',
  474.                 [
  475.                     'host' => App::getContainer()->getParameter('jobs_subdomain') . '.' App::getContainer()->getParameter('site'),
  476.                     'list' => null,
  477.                     'disallow' => true,
  478.                 ]
  479.             );
  480.         } else {
  481.             $list = [
  482.                 'full' => [
  483.                     '/json/',
  484.                     '/ru/full/questionnaire/',
  485.                     '/ru/cv/modal',
  486.                     '/en-us/full/questionnaire/',
  487.                     '/en-us/cv/modal',
  488.                     '/interview/',
  489.                     '/tmp/_files/cv/',
  490.                     '/userdirs/vacancy/',
  491.                     '/ru/comments/create',
  492.                     '/ru/comment-form/',
  493.                     '/ru/comment-block/',
  494.                     '/en-us/comments/create',
  495.                     '/en-us/comment-form/',
  496.                     '/en-us/comment-block/',
  497.                 ],
  498.             ];
  499.             $response $this->render(
  500.                 '@Web/Seo/robots.txt.twig',
  501.                 [
  502.                     'host' => App::getContainer()->getParameter('jobs_subdomain') . '.' App::getContainer()->getParameter('site'),
  503.                     'list' => $list,
  504.                     'locales' => LocaleHelper::getListCode(),
  505.                     'countries' => array_keys(App::getCountryList()),
  506.                     'allows' => [],
  507.                 ]
  508.             );
  509.         }
  510.         $response->headers->set('Content-Type''text/plain');
  511.         return $response;
  512.     }
  513.     /**
  514.      * Вспомогательная логика для робота (Disallow и т.д.).
  515.      *
  516.      * @return array Список шаблонов URL для Disallow в robots.txt.
  517.      */
  518.     private function getDisallowUrlPatterns(): array
  519.     {
  520.         return [
  521.             '*/z_*',
  522.             '*/rasprodazha*',
  523.             '*/sale*',
  524.             '*/svendita*',
  525.             '*/docs/improving-site*',
  526.             '*/docs/payment-usca*',
  527.             '*/docs/payment-eact*',
  528.             '*/docs/payment-eu*',
  529.             '*/docs/payment-new*',
  530.             '*/catalogue/coll/*',
  531.             '*/catalogue/release-now*',
  532.             '*/catalogue/release-before-2017*',
  533.             '*/catalogue/release-2017*',
  534.             '*/catalogue/release-2018*',
  535.             '*/catalogue/release-2019*',
  536.             '*/catalogue/cevisama-2017*',
  537.             '*/catalogue/cevisama-2018*',
  538.             '*/catalogue/cevisama-2019*',
  539.             '*/catalogue/cevisama-2020*',
  540.             '*/catalogue/cersaie-2017*',
  541.             '*/catalogue/cersaie-2018*',
  542.             '*/catalogue/cersaie-2019*',
  543.             '*/catalogue/cersaie-2020*',
  544.             '*/catalogue/cersaie-2021*',
  545.             '*/catalogue/cersaie-2022*',
  546.             '*/catalogue/no-exhibitions-2020*',
  547.             '*/catalogue/no-exhibitions-2021*',
  548.             '*/catalogue/coverings-2018*',
  549.             '*/catalogue/coverings-2019*',
  550.             '*/catalogue/cataloge*',
  551.             '/blog/20-',
  552.             '*/rebajas*',
  553.             '*/destockage*',
  554.             '*/ausverkauf*',
  555.             '*/wyprzedaz*',
  556.             '*/opruiming-tegels*',
  557.             '*/alennusmyynti*',
  558.             '*/best-price*',
  559.             '*/miglior-prezzo*',
  560.             '*/mejor-precio*',
  561.             '*/meilleur-prix*',
  562.             '*/besten-preis*',
  563.             '*/najlepsza-cena*',
  564.             '*/beste-prijs*',
  565.             '*/paras-hinta*',
  566.             '*/basta-pris*',
  567.             '*/likvidaciya*',
  568.             '*/clearance*',
  569.             '*/liquidazione*',
  570.             '*/liquidacion*',
  571.             '*/liquidation*',
  572.             '*/liquidierung*',
  573.             '*/likwidacja*',
  574.             '*/opruiming*',
  575.             '*/poistomyynti*',
  576.             '*/top-20-mesyaca*',
  577.             '*/top-20-month*',
  578.             '*/kuukauden-top-20-lista*',
  579.             '*/manadens-topp-20*',
  580.             '*/manedens-20-mest-populære*',
  581.             '*/top-20-do-mes*',
  582.         ];
  583.     }
  584.     /**
  585.      * Вспомогательная логика для робота (Allow и т.д.).
  586.      *
  587.      * @return array Список шаблонов URL для Allow в robots.txt.
  588.      */
  589.     private function getAllowsUrlPatterns(): array
  590.     {
  591.         return [];
  592.     }
  593.     /**
  594.      * Генерирует абсолютную ссылку, заменяя & на &amp; для корректного отображения в XML.
  595.      *
  596.      * @param string $route Имя маршрута.
  597.      * @param array $params Параметры маршрута.
  598.      *
  599.      * @return string Абсолютная ссылка с заменёнными амперсандами.
  600.      */
  601.     public function link(string $route$params = []): string
  602.     {
  603.         return str_replace(
  604.             '&',
  605.             '&amp;',
  606.             $this->generateUrl($route$paramsUrlGeneratorInterface::ABSOLUTE_URL)
  607.         );
  608.     }
  609.     /**
  610.      * Вспомогательный метод для формирования <url> в sitemap Jobs.
  611.      *
  612.      * @param DOMDocument $siteMap DOM-документ sitemap.
  613.      * @param DOMElement $urlSet Элемент <urlset> в документе.
  614.      * @param string $link Абсолютная ссылка.
  615.      * @param string $priority Приоритет URL (по умолчанию '0.5').
  616.      * @param string $changefreq Частота изменений URL (по умолчанию 'daily').
  617.      * @param string $date Дата последнего изменения (по умолчанию текущая дата).
  618.      *
  619.      * @return DOMElement Обновлённый элемент <urlset>.
  620.      */
  621.     protected function siteMapXmlElem(
  622.         DOMDocument $siteMap,
  623.         DOMElement  $urlSet,
  624.         string      $link,
  625.         string      $priority '0.5',
  626.         string      $changefreq 'daily',
  627.         string      $date ''
  628.     ): DOMElement
  629.     {
  630.         $url $siteMap->createElement('url');
  631.         $loc $siteMap->createElement('loc'$link);
  632.         $url->appendChild($loc);
  633.         if (empty($date)) {
  634.             $date date('c');
  635.         }
  636.         $lastmod $siteMap->createElement('lastmod'$date);
  637.         $freq $siteMap->createElement('changefreq'$changefreq);
  638.         $prio $siteMap->createElement('priority'$priority);
  639.         $url->appendChild($lastmod);
  640.         $url->appendChild($freq);
  641.         $url->appendChild($prio);
  642.         $urlSet->appendChild($url);
  643.         return $urlSet;
  644.     }
  645. }