'', 'mkeywords' => '', 'mdescription' => '', ]; protected $metaKeys = [ 'mtitle', 'mkeywords', 'mdescription', ]; /** @var bool|array Request landing page */ private $landingPage = false; public function init() { parent::init(); $this->module_title = 'SEO'; } public function onNewRequest($request) { parent::onNewRequest($request); $this->robotsIndex = true; $this->robotsFollow = true; $this->metaData = [ 'mtitle' => '', 'mkeywords' => '', 'mdescription' => '', ]; $this->landingPage = false; } /** * Индексирование страницы роботом * @param bool $index true - разрешить, false - запретить, null - текущее значение * @return bool */ public function robotsIndex($index = true) { if (is_null($index)) { return $this->robotsIndex; } return ($this->robotsIndex = !empty($index)); } /** * Переход на страницу роботом * @param bool $follow true - разрешить, false - запретить, null - текущее значение * @return bool|void */ public function robotsFollow($follow = true) { if (is_null($follow)) { return $this->robotsFollow; } $this->robotsFollow = !empty($follow); } /** * Проверка URL текущего запроса на обязательное наличие/отсутствие завершающего слеша * @param bool $required true - URL должен обязательно завершаться слешем, * false - URL должен обязательно быть без завершающего слеша * @param array $opts ['status' - статус редиректа, 'throw' - Response исключение] * @return \bff\http\Response|false * @throws \bff\exception\ResponseException */ public function urlCorrectionEndSlash($required = true, array $opts = []) { $url = parse_url($this->app->request()->uri()); if (!empty($url['path'])) { $path = $url['path']; $last = mb_substr($path, -1); if ($required) { # URL должен обязательно завершаться слешем if ($last !== '/') { $path .= '/'; } else { # исправляем множественные завершающие слешы $path = rtrim($path, '/') . '/'; } } else { # URL должен обязательно быть без завершающего слеша if ($last === '/') { $path = rtrim($path, '/'); } } if ($path !== $url['path']) { $url = Url::to($path, ['lang' => false, 'query' => (isset($url['query']) ? '?' . $url['query'] : '')]); $redirect = Response::redirect($url, $opts['status'] ?? 301); if ($options['throw'] ?? true) { $redirect->throw(); } else { return $redirect; } } } return false; } /** * Вывод мета данных * @param array $options дополнительные настройки * @return string */ public function metaRender(array $options = []) { # дозаполняем мета-данные foreach ($this->metaKeys as $k) { if (isset($options[$k])) { $this->metaSet($k, $options[$k]); } } $view =& $this->metaData; # чистим незаполненные мета-данные foreach ($this->metaKeys as $k) { if (empty($view[$k])) { unset($view[$k]); } } if (! array_key_exists('language', $view)) { $view['language'] = ''; } if (! array_key_exists('robots', $view)) { $view['robots'] = ''; } if (!empty($options['csrf-token']) && ! $this->request->isRobot() && User::logined() && !isset($view['csrf-token'])) { $view['csrf-token'] = ''; } if (!empty($options['content-type']) && !isset($view['content-type'])) { $view = ['content-type' => ''] + $view; } # Disable convering prices on mobile devices into urls $view['format-detection'] = ''; $view = $this->app->filter('seo.meta.render', $view, $options); return join("\r\n", $view) . "\r\n"; } /** * Сброс автоматических мета данных * @param array $keys ключи * @return void */ public function metaReset(array $keys = []) { if (empty($keys)) { $keys[] = join('_', ['site', 'meta', 'main']); } foreach ($keys as $key) { foreach (config::prefixed($key) as $k => $v) { if (mb_stripos($k, '-') === 0) { config::save($key . $k, ''); } } } foreach ($this->metaData as &$v) { $v = ''; } unset($v); } /** * Установка мета данных * @param string $type тип данных * @param string|array $content * @param string|null $lang * @return array|string */ public function metaSet(string $type, $content, ?string $lang = null) { switch ($type) { case 'mtitle': { $limit = $this->config('seo.meta.limit.mtitle', 0, TYPE_UINT); $meta = '' . mb_substr(trim(strval($content)), 0, ($limit > 0 ? $limit : 1000)) . ''; $this->metaData[$type] = $meta; return $meta; } case 'mkeywords': { $limit = $this->config('seo.meta.limit.mkeywords', 0, TYPE_UINT); $content = mb_substr(htmlspecialchars(trim(strval($content)), ENT_QUOTES, 'UTF-8', false), 0, ($limit > 0 ? $limit : 250)); if ($content === '') { return []; } $meta = [ 'name' => 'keywords', 'lang' => $lang ?: $this->locale->current(), 'content' => $content, 'tag' => 'meta', ]; $meta['html'] = ''; $this->metaData[$type] = $meta['html']; return $meta; } case 'mdescription': { $limit = $this->config('seo.meta.limit.mdescription', 0, TYPE_UINT); $content = mb_substr(trim(strval($content)), 0, ($limit > 0 ? $limit : 300)); if ($content === '') { return []; } $meta = [ 'name' => 'description', 'lang' => $lang ?: $this->locale->current(), 'content' => $content, 'tag' => 'meta', ]; $meta['html'] = ''; $this->metaData[$type] = $meta['html']; return $meta; } default: { if (is_string($content)) { $this->metaData[$type] = trim($content); } elseif (is_array($content) && array_key_exists('html', $content)) { if (is_string($content['html'])) { $this->metaData[$type] = $content['html']; } } return $content; } } } /** * Get seo template * @param string $group template group (controller) * @param string $key template key * @param array $with * @return Template|null */ public function getTemplate(string $group, string $key, array $with = []) { # Search for page with required seo template $theme = $this->app->theme(); foreach ($theme->getPages() as $pageKey => $page) { if ( $this->app->guessControllerByClassName($page['class']) === $group && ($page = $theme->initPage($pageKey)) && $page->hasSeoSettings($group, $key) ) { return $page->seoTemplate(true, $with); } } # Get controller seo template (2x fallback) try { $controller = $this->app->resolveController($group); if ($controller && method_exists($controller, 'seoTemplates')) { $templates = $controller->seoTemplates(); if (! empty($templates['pages'][$key])) { return Template::fromArray($group, $key, $templates['pages'][$key], $templates['macros'] ?? []); } } } catch (Throwable $e) { return null; } # Unknown seo template return null; } /** * Get group seo templates * @param string $group template group (controller) * @return Template[] */ public function getTemplates(string $group) { $templates = []; $theme = $this->app->theme(); foreach ($theme->getPages() as $pageKey => $page) { if ( $this->app->guessControllerByClassName($page['class']) === $group && ($page = $theme->initPage($pageKey)) && $page->hasSeoSettings($group) ) { $templates[] = $page->seoTemplate(true); } } # Get controller seo template (2x fallback) if (empty($templates)) { try { $controller = $this->app->resolveController($group); if ($controller && method_exists($controller, 'seoTemplates')) { $settings = $controller->seoTemplates(); if (! empty($settings['pages'])) { foreach ($settings['pages'] as $key => $template) { $templates[] = Template::fromArray($group, $key, $template, $settings['macros'] ?? []); } } } } catch (Throwable $e) { // unknown controller } } return $templates; } public function seoTemplates() { $templates = [ 'pages' => [], 'groups' => [], 'macros' => [], ]; $templates['pages']['landingpage'] = (new Template()) ->breadcrumb() ->titleH1() ->seotext() ->fields(config::sys('seo.landing.pages.fields', [], TYPE_ARRAY)) ->toArray(); return $templates; } /** * Apply seo template to page * @param Template $template template settings * @param array $page @ref page data to apply to * @param array $opts options [landing-skip, divider, lang, general_template_key] * @return bool */ public function applyTemplate(Template $template, array &$page = [], array $opts = []) { $lang = $opts['lang'] ?? $this->locale->current(); $generalTemplateUsageKey = $opts['general_template_key'] ?? 'mtemplate'; # apply landing page settings $landing = $this->landingPage(); if (! empty($opts['landing-skip'])) { $landing = false; } if ($landing !== false) { $landingFields = $this->landingPagesFields(); foreach ($landingFields as $k => &$v) { # extra fields if (isset($landing[$k]) && !empty($landing[$k])) { $page[$k] = $landing[$k]; } } unset($v); foreach ($this->metaKeys as $k) { # meta fields if (isset($landing[$k]) && !empty($landing[$k])) { $page[$k] = $landing[$k]; } } # landing page is always "robots=index" $template->index(true, 'landing page'); } # list if ($template->list) { # mark pages 2+ as "robots=noindex" $pageId = $template->param('page'); if ( ! empty($pageId) && is_numeric($pageId) && $pageId > 1 && ! $this->config('seo.pages.index', true, TYPE_BOOL) ) { $template->index(false, sprintf('list pagination, page %s', $pageId)); } # mark empty lists as "robots=noindex" $total = $template->param('total', null); if ( ! is_null($total) && empty($total) && ! $this->config('seo.lists.empty.index', true, TYPE_BOOL) ) { $template->index(false, 'empty list'); } } # apply template to page data & build final meta tags (title, keywords, description) $general = (isset($page[$generalTemplateUsageKey]) ? !empty($page[$generalTemplateUsageKey]) : ($landing !== false ? false : true)); $meta = $template->apply($page, $landing, $general, array_merge($opts, [ 'metaFields' => $this->metaKeys, 'lang' => $lang, ])); # language $meta['language'] = ''; # canonical if ($template->canonical) { foreach ($template->canonical->getMeta($lang, $landing) as $k => $v) { $meta[$k] = $v['html']; } } # hidden language if (in_array($lang, $this->locale->getHiddenLocalesList(), true)) { $template->index(false, sprintf('hidden language %s', $lang)); } # robots $meta['robots'] = ''; # set foreach ($meta as $k => $v) { $this->metaSet($k, $v); } return true; } /** * Устанавливаем социальные meta-теги Open Graph * @url https://developers.facebook.com/docs/opengraph/howtos/maximizing-distribution-media-content * @param string $title заголовок * @param string $description описание * @param string|array $image изображение (или несколько) * @param string $url каноническая ссылка на страницу * @param string $siteName название сайта * @param array $opts * @return array */ public function setSocialMetaOG($title, $description, $image, $url, $siteName, array $opts = []) { $opts = $this->defaults($opts, [ 'lang' => $this->locale->current(), 'fb_app_id' => '', # ID facebook приложения ]); # результат $return = []; # title, description, url, site_name if (empty($siteName)) { $siteName = Site::title('seo.opengraph.sitename'); } if (strpos($url, '{site') !== false) { $url = Url::dynamic($url, [], ['lang' => $opts['lang']]); } foreach (['title' => $title, 'description' => $description, 'url' => $url, 'site_name' => $siteName] as $k => $v) { $data = htmlspecialchars(trim(strval($v)), ENT_QUOTES, 'UTF-8', false); if (!empty($data)) { $return['og:' . $k] = ['content' => $data, 'html' => '']; } } # image if (!empty($image)) { $requestScheme = $this->request->scheme() . ':'; if (is_string($image)) { if ($image[0] == '/') { $image = $requestScheme . $image; } $image = htmlspecialchars($image, ENT_QUOTES, 'UTF-8'); $return['og:image'] = ['content' => $image, 'html' => '']; } else { if (is_array($image)) { $i = 1; foreach ($image as &$v) { if (!empty($v)) { if ($v[0] == '/') { $v = $requestScheme . $v; } $return['og:image' . $i] = ['content' => htmlspecialchars($v, ENT_QUOTES, 'UTF-8')]; $return['og:image' . $i]['html'] = ''; if (mb_stripos($v, Url::to('/files/', ['lang' => false])) === 0) { $v = str_replace(Url::to('/', ['lang' => false]), PATH_PUBLIC, $v); if (!file_exists($v)) { continue; } } $size = getimagesize($v); if ($size !== false) { $return['og:image' . $i . ':width'] = ['content' => $size[0], 'html' => '']; $return['og:image' . $i . ':height'] = ['content' => $size[1], 'html' => '']; } $i++; } } unset($v); } } } # locale $locale = $this->locale->getLanguageSettings($opts['lang'], 'locale'); if (!empty($locale)) { $return['og:locale'] = ['content' => $locale, 'html' => '']; } # fb_app_id if (!empty($opts['fb_app_id'])) { $return['fb:app_id'] = ['content' => $opts['fb_app_id'], 'html' => '']; } # type $return['og:type'] = ['content' => 'website', 'html' => '']; # return => _metaData foreach ($return as $k => $v) { $this->metaData[$k] = $v['html']; } return $return; } /** * Подстановка макросов в мета-текст * @param string|array $text @ref текст * @param array $placeholders @ref данные для подстановки вместо макросов * @param bool $general используется базовый seo-шаблон * @param array $opts доп. параметры [divider] * @return string|array */ public function metaTextPrepare(&$text, array &$placeholders = [], $general = false, array $opts = []) { if (empty($text)) { return $text; } # подготавливаем макросы для замены $placeholders['site.title'] = Site::title('seo.template.macros'); $replace = []; # разделитель пустых макросов if (! empty($opts['divider'])) { # создадим замены для пустых макросов $this->metaTextPrepareEmptyGroup($opts['divider'], $text, $replace, $placeholders); $replace[$opts['divider']] = ''; } $replaceCallable = []; foreach ($placeholders as $key => $value) { if ($key == 'page' && is_numeric($value)) { $value = ($value > 1 ? _t('pgn', ' - page [page]', ['page' => $value]) : ''); $replace[' {' . $key . '}'] = $replace['{' . $key . '}'] = $value; } else { if ($value == '') { foreach ([' ', ' - ', ' | ', ', ', ': ', '/', '/ '] as $prefix) { $replace[$prefix . '{' . $key . '}'] = $value; } } elseif ($value instanceof Closure) { $replaceCallable['{' . $key . '}'] = $value; continue; } $replace['{' . $key . '}'] = $value; } } if ($this->app->hooksAdded('seo.meta.text.prepare')) { $text = $this->app->filter('seo.meta.text.prepare', $text, ['replace' => &$replace, 'macrosData' => &$placeholders]); } if (is_string($text)) { $text = strtr($text, $replace); foreach ($replaceCallable as $placeholder => $callback) { $text = call_user_func($callback, 0, $general, $text, $placeholder); // index, generalTemplate, text, placeholder } } else { foreach ($text as $index => &$string) { if (is_string($string)) { $string = strtr($string, $replace); foreach ($replaceCallable as $placeholder => $callback) { $string = call_user_func($callback, $index, $general, $string, $placeholder); // index, generalTemplate, text, placeholder } } } } return $text; } /** * Удаление пустых макросов * @param string $divider разделитель пустых макросов * @param string|array $text @ref текст * @param array $replace @ref данные для замены * @param array $placeholders @ref данные для подстановки вместо макросов */ protected function metaTextPrepareEmptyGroup($divider, &$text, &$replace, &$placeholders) { if (empty($divider)) { return; } if (empty($text)) { return; } if (is_array($text)) { foreach ($text as &$string) { $this->metaTextPrepareEmptyGroup($divider, $string, $replace, $placeholders); } return; } if (mb_strpos($text, $divider) === false) { return; } # разбиваем строку на части $parts = explode($divider, $text); foreach ($parts as $k => $v) { preg_match_all('/\{([\w:\-\.]+)\}/', $v, $m); if (!empty($m[1])) { $filled = true; foreach ($m[1] as $kk => $vv) { if (empty($placeholders[$vv])) { $filled = false; # если один из макросов внутри части - пустой break; } } if ($filled) { continue; } $replace[($k ? $divider : '') . $v] = ''; # заменяем всю часть на пустую строку } } } /** * Формирование посадочной страницы * @param string|null $uri URI текущего запроса или null - вернуть данные о текущей посадочной странице * @param \bff\http\Request|null $request * @return mixed */ public function landingPage($uri = null, $request = null) { # Посадочные страницы не используются (выключены) if (! $this->landingPagesEnabled()) { return false; } if (! is_string($uri)) { # Посадочная страница не была объявлена if (empty($this->landingPage)) { return false; } return $this->landingPage; } $request = $request ?? $this->app->request(); # URL decode if (mb_strpos($uri, '%') !== false && (urldecode($uri) !== $uri)) { $uri = urldecode($uri); } # Выполняем поиск посадочной страницы по URI текущего запроса $segments = Url::segments(); $this->landingPage = $this->model->landingpageDataByRequest( $request->urlVariations($uri, $segments) ); # Дополняем / убираем завершающий "/" if (empty($this->landingPage) && !empty($uri) && mb_stripos($uri, '?') === false) { $uri = (mb_substr($uri, -1) === '/' ? mb_substr($uri, 0, -1) : $uri . '/'); $this->landingPage = $this->model->landingpageDataByRequest( $request->urlVariations($uri, $segments) ); } # Нет совпадений if (empty($this->landingPage)) { $this->landingPage = false; return false; } # Формируем итоговый URL $landingURL = $this->landingPage['landing_uri']; if ($this->landingPage['is_relative']) { # Конвертируем relative => dynamic # Preserve subdomains: sub1.sub2.SITEHOST => [sub1,sub2] $subdomains = trim(str_replace(SITEHOST, '', $request->host()), '.'); if (! empty($subdomains)) { $subdomains = explode('.', $subdomains); } $landingURL = Url::to($landingURL, [ 'dynamic' => true, 'subdomains' => $subdomains, 'segments' => false, # cut segments ]); $this->landingPage['is_relative'] = 0; } else { if (mb_stripos($landingURL, '//') === 0) { $landingURL = $request->scheme() . ':' . $landingURL; } } # отрезаем {query} if (($queryPosition = strpos($landingURL, '?')) !== false) { $landingURL = mb_substr($landingURL, 0, $queryPosition); } $this->landingPage['landing_uri_original'] = $this->landingPage['landing_uri']; $this->landingPage['landing_uri'] = $landingURL; return $this->landingPage['original_uri']; } /** * Включено ли использование посадочных страниц * @return bool */ public function landingPagesEnabled() { return $this->config('seo.landing.pages.enabled', true, TYPE_BOOL); } /** * Доп. поля для посадочных страниц * @return array */ public function landingPagesFields() { return ($this->seoTemplates()['pages']['landingpage']['fields']) ?? []; } /** * Включено ли использование редиректов * @return bool */ public function redirectsEnabled() { return $this->config('seo.redirects', true, TYPE_BOOL); } /** * Выполняем редирект * @param string $uri URI текущего запроса (без extra данных) * @param \bff\http\Request|null $request объект запроса * @return bool|array [to, status] */ public function redirectsProcess($uri, $request = null) { if (! $this->redirectsEnabled()) { return false; } $request = $request ?? $this->app->request(); # Формируем варианты URL $extra = Url::segments(); $set = $request->urlVariations($uri, $extra); # Выполняем поиск $redirect = $this->model->redirectsByRequest($set); if (empty($redirect)) { return false; } # Подготавливаем URL редиректа $scheme = $request->scheme(); $host = $request->host(); $extra = join('/', $extra); $to = $redirect['to_uri']; if (empty($to)) { return false; } if ($redirect['is_relative']) { if ($to[0] === '/') { if (isset($to[1])) { if ($to[1] !== '/') { # /{to} => http{s}://{host}/{extra}/{to} $to = $scheme . '://' . $host . ($redirect['add_extra'] && !empty($extra) ? '/' . $extra : '') . $to; } else { # //{to} => http{s}://{to} $to = $scheme . ':' . $to; } } else { # / => http{s}://{host}/{extra}/ $to = $scheme . '://' . $host . ($redirect['add_extra'] && !empty($extra) ? '/' . $extra . '/' : ''); } } elseif (mb_stripos($to, 'www.') === 0) { # www.{to} => http{s}://www.{to} $to = $scheme . '://' . $to; } } # Макросы if (mb_stripos($to, '{') !== false) { $to = strtr($to, [ Url::HOST_PLACEHOLDER => SITEHOST, '/{extra}' => (!empty($extra) ? '/' . $extra : ''), '{extra}' => $extra, ]); } # Подставляем Query $query = $request->query(); if ($redirect['add_query'] && !empty($query)) { $toQuery = mb_stripos($to, '?'); if ($toQuery === false) { $to = $to . '?' . $query; } else { $to = mb_substr($to, 0, $toQuery) . '?' . $query; } } # Итоговый URL $redirect['to'] = $to; # Статус редиректа $redirect['status'] = intval($redirect['status']); if ($redirect['status'] !== 301 && $redirect['status'] !== 302) { $redirect['status'] = 301; } return $redirect; } /** * Системные настройки: настройки ядра * @param array $extend @ref * @param string $tab */ public function settingsSystemCore(&$extend = [], $tab = 'def') { if (!isset($extend['settings'])) { $extend['settings'] = []; } $textSymbols = tpl::declension(10, _t('', 'symbol;symbols;symbols'), false); $extend['settings']['seo.meta.limit.mtitle'] = [ 'title' => _t('seo', 'Title Length Limit'), 'description' => _t('seo', 'Meta Title'), 'type' => TYPE_UNUM, 'input' => 'number', 'default' => 1000, 'options' => [ 'min' => ['value' => 0], 'tip' => $textSymbols, ], 'tab' => $tab, ]; $extend['settings']['seo.meta.limit.mkeywords'] = [ 'title' => _t('seo', 'Keywords Length Limit'), 'description' => _t('seo', 'Meta Keywords'), 'type' => TYPE_UNUM, 'input' => 'number', 'default' => 250, 'options' => [ 'min' => ['value' => 0], 'tip' => $textSymbols, ], 'tab' => $tab, ]; $extend['settings']['seo.meta.limit.mdescription'] = [ 'title' => _t('seo', 'Description Length Limit'), 'description' => _t('seo', 'Meta Description'), 'type' => TYPE_UNUM, 'input' => 'number', 'default' => 300, 'options' => [ 'min' => ['value' => 0], 'tip' => $textSymbols, ], 'tab' => $tab, ]; $extend['settings']['seo.landing.pages.enabled'] = [ 'title' => _t('seo', 'Landing Pages'), 'type' => TYPE_BOOL, 'input' => 'select', 'default' => false, 'options' => [ true => [ 'title' => _t('', 'Enabled'), ], false => [ 'title' => _t('', 'Disabled'), 'description' => _t('seo', 'Landing pages function disabled'), ], ], 'tab' => $tab, ]; $extend['settings']['seo.redirects'] = [ 'title' => _t('seo', 'Redirects'), 'type' => TYPE_BOOL, 'input' => 'select', 'default' => false, 'options' => [ true => [ 'title' => _t('', 'Enabled'), ], false => [ 'title' => _t('', 'Disabled'), 'description' => _t('seo', 'Redirects function disabled'), ], ], 'tab' => $tab, ]; $extend['settings']['seo.lists.empty.index'] = [ 'title' => _t('seo', 'Indexing Empty Lists'), 'type' => TYPE_BOOL, 'input' => 'select', 'default' => true, 'options' => [ true => [ 'title' => _t('', 'Enabled'), 'description' => _t('seo', 'For all list pages, regardless of the results, an index tag is set.'), ], false => [ 'title' => _t('', 'Disabled'), 'description' => _t('seo', 'All the empty list pages are tagged with no-index tag'), ], ], 'tab' => $tab, ]; $extend['settings']['seo.pages.index'] = [ 'title' => _t('seo', 'Pagination Indexing'), 'type' => TYPE_BOOL, 'input' => 'select', 'default' => true, 'options' => [ true => [ 'title' => _t('', 'Enabled'), 'description' => _t('seo', 'All the list pages are tagged with index tag'), ], false => [ 'title' => _t('', 'Disabled'), 'description' => _t('seo', 'For all pages of the list, starting with the second, the no-index tag will be set'), ], ], 'tab' => $tab, ]; $extend['settings']['seo.pages.canonical'] = [ 'title' => _t('seo', 'Canonical Pages Indexing'), 'type' => TYPE_BOOL, 'input' => 'select', 'default' => true, 'options' => [ true => [ 'title' => _t('', 'Enabled'), 'description' => _t('seo', 'For all pages starting from the second canonical page, a list page with page number will be indicated'), ], false => [ 'title' => _t('', 'Disabled'), 'description' => _t('seo', 'For all pages starting from the second canonical, the first page of the list will be indicated'), ], ], 'tab' => $tab, ]; $lang = $this->locale->getLanguages(true); if (count($lang) > 1) { $extend['settings']['site.sitemapXML.langs'] = [ 'title' => _t('seo', 'Multilingual Sitemap.xml'), 'type' => TYPE_UINT, 'input' => 'select', 'default' => 0, 'options' => [ static::SITEMAP_LANG_NONE => [ 'title' => _t('', 'Disabled'), ], static::SITEMAP_LANG_TRANSLATIBLE => [ 'title' => _t('', 'Pages with translation'), 'description' => _t('seo', 'For pages with the ability to translate'), ], static::SITEMAP_LANG_ALL => [ 'title' => _t('', 'All'), 'description' => _t('seo', 'For all pages'), ], ], 'tab' => $tab, ]; } $extend['options']['tabs'][$tab] = ['t' => _t('@', 'General')]; $extend['options']['tabs']['social'] = ['t' => _t('@seo', 'SEM'), 'ajax' => Url::admin('seo/settingsSystemSocialSettings')]; } /** * Формирование массива дополнительных языков * @param mixed ...$modes доступные режимы SITEMAP_LANG_* * @return array */ public function sitemapXMLAltLangs(...$modes) { do { $languages = $this->locale->getLanguages(true); if (count($languages) <= 1) { break; } if (empty($modes) || !is_array($modes)) { break; } $modes = array_map(function ($a) { return (int)$a; }, $modes); $mode = $this->config('site.sitemapXML.langs', static::SITEMAP_LANG_NONE, TYPE_UINT); if (!in_array($mode, $modes, true)) { break; } $def = $this->locale->getDefaultLanguage(); $k = array_search($def, $languages); unset($languages[$k]); return $languages; } while (false); return []; } }