'',
'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 [];
}
}