fieldText('mtitle', _t('@seo', 'Title')); $this->fieldTextarea('mkeywords', _t('@seo', 'Keywords')); $this->fieldTextarea('mdescription', _t('@seo', 'Description')); } /** * Setup template settings key & group * @param string $key * @param string $group * @return static */ public function setupSettings(string $key, string $group = '') { $this->settingsKey = $key; $this->settingsGroup = $group; return $this; } /** * Get template settings key * @return string */ public function getSettingsKey() { return $this->settingsKey; } /** * Get template settings group * @return string */ public function getSettingsGroup() { return $this->settingsGroup; } /** * Get template settings config key * @return string */ public function getSettingsConfigKey(): string { if ($this->settingsKey && $this->settingsGroup) { return $this->settingsGroup . '_meta_' . $this->settingsKey; } return ''; } /** * Fill template * @param array $data */ public function fill(array $data = []) { $this->data = []; $languages = Lang::getLanguages(); # fields foreach ($this->fields as $field => $settings) { if (array_key_exists($field, $data)) { $this->data[$field] = (is_array($data[$field]) ? $data[$field] : array_fill_keys($languages, $data[$field]) ); } else { $this->data[$field] = array_fill_keys($languages, ''); } } # social $this->data[SocialProvider::SOCIAL_KEY] = ($this->social ? $data[SocialProvider::SOCIAL_KEY] ?? [] : []); $this->data[SocialProvider::SOCIALTEXT_KEY] = ($this->social ? $data[SocialProvider::SOCIALTEXT_KEY] ?? [] : []); # robots foreach (['noindex' => 'index', 'nofollow' => 'follow'] as $key => $property) { $this->data[$key] = ! empty($data[$key]); if ($this->data[$key]) { $this->$property = false; if ($key === 'noindex') { $this->indexReason = 'seo template settings noindex'; } } } } /** * Load settings & fill template * @param bool $fill * @return array */ public function load($fill = true) { if (! $this->data) { $key = $this->getSettingsConfigKey(); $settings = []; if ($key) { $settings = config::get($key, [], TYPE_ARRAY); } if ($fill) { $this->fill($settings); } else { return $settings; } } return $this->data; } /** * Save settings * @param array $settings * @return bool */ public function save(array $settings) { $key = $this->getSettingsConfigKey(); if (! $key) { return false; } if (bff::isTest()) { config::temp($key, $settings); } else { config::save($key, json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT)); } return true; } /** * Field placeholders extra settings * @param string $field field key * @param array $skip placeholders to skip [placeholder, ...] * @param array $replace placeholders to replace [placeholder => text, ...] * @return static */ public function fieldPlaceholders(string $field, array $skip = [], array $replace = []) { $this->fieldsPlaceholders[$field] = [ 'skip' => $skip, 'replace' => $replace, ]; return $this; } /** * Apply template to page data & return meta * @param array $page page data * @param bool|array $landing active landing page or false * @param bool $general use general template * @param array $opts [divider, lang, metaFields] * @return array meta data */ public function apply(array &$page, $landing, bool $general, array $opts = []): array { $meta = []; $metaFields = $opts['metaFields'] ?? ['mtitle', 'mkeywords', 'mdescription']; $lang = $opts['lang'] ?? Lang::current(); $seo = SEO::i(); $placeholders = $this->params; $generalReplace = function ($replace, $subject) { return str_replace( ($replace !== '' ? ['{meta-base}',' {meta-base}'] : [' {meta-base}','{meta-base}']), $replace, $subject ); }; # page data + template $inherit = $this->inheritance && ($landing === false || !empty($landing['joined'])); $settings = $this->load(); $fieldsExtra = array_keys($settings); if ($inherit) { foreach ($metaFields as $field) { if (array_key_exists($field, $page)) { $meta[$field] = $page[$field]; } } # page data + general template if ($general) { foreach ($settings as $field => &$v) { if (empty($v[$lang])) { continue; } if (in_array($field, $metaFields)) { $replace = $meta[$field] ?? ''; $meta[$field] = $generalReplace($replace, $v[$lang]); continue; } if (array_key_exists($field, $page)) { # extra fields data if (is_array($page[$field])) { foreach ($page[$field] as &$vv) { $vv = $generalReplace($vv, $v[$lang]); } unset($vv); } else { $page[$field] = $generalReplace($page[$field], $v[$lang]); } } } unset($v); } else { # only seo meta data if (empty($meta) && empty($fieldsExtra)) { return []; } } } else { # page template foreach ($settings as $field => &$v) { if (in_array($field, $metaFields)) { # use landing page data if ($landing !== false && !empty($landing[$field])) { $page[$field] = $meta[$field] = $landing[$field]; continue; } if (isset($v[$lang]) && $v[$lang] !== '') { $replace = $page[$field] ?? ''; $page[$field] = $meta[$field] = $generalReplace($replace, $v[$lang]); } } else { # landing page data if ($landing !== false) { continue; } if (! empty($v[$lang])) { # textfield + translation [ru => text, ...] $page[$field] = $v[$lang]; } else { $page[$field] = ($page[$field] ?? ''); } } } unset($v); } # page extra fields + placeholders if (! empty($page)) { $prepare = $this->fieldsPlaceholders; foreach ($fieldsExtra as $field) { if (! isset($prepare[$field]) && ! in_array($field, $prepare)) { $prepare[] = $field; } } if (! empty($prepare)) { foreach ($prepare as $k => $v) { if (is_string($v)) { if (isset($page[$v])) { $seo->metaTextPrepare($page[$v], $placeholders, $general, $opts); } continue; } if (is_array($v) && ! empty($v)) { $replace = $placeholders; $do = false; if (! empty($v['replace']) && is_array($v['replace'])) { foreach ($v['replace'] as $kk => $vv) { if ($vv instanceof \Closure) { $replace[$kk] = $vv; } else { $replace[$kk] = $replace[$vv] ?? $vv; } } $do = true; } if (! empty($v['skip']) && is_array($v['skip'])) { foreach ($v['skip'] as $vv) { $replace[$vv] = ''; if (in_array($vv, ['city', 'region', 'country'])) { $replace[$vv . '.in'] = ''; $replace[$vv . '.key'] = ''; } } $do = true; } if ($do) { $seo->metaTextPrepare($page[$k], $replace, $general, $opts); } } } } # Reset fields placeholders after apply $this->fieldsPlaceholders = []; } # seo meta + placeholders if (! empty($meta)) { $meta = $seo->metaTextPrepare($meta, $placeholders, $general, $opts); } # social if ($this->social) { foreach ([SocialProvider::SOCIAL_KEY, SocialProvider::SOCIALTEXT_KEY] as $socialJsonKey) { if (isset($page[$socialJsonKey])) { $social = &$page[$socialJsonKey]; if (is_string($social)) { $social = json_decode($social, true); } if (empty($social) || !is_array($social)) { $social = []; } unset($social); } } foreach ($this->socialProviders() as $social) { $meta = array_merge($meta, $social->getMeta($this, $page, $lang, $landing, $opts)); } } return $meta; } /** * Set template title * @param string $title * @return static */ public function title(string $title) { $this->title = $title; return $this; } /** * Add placeholder * @param string $key * @param string $title * @param array $settings [title, insert, first] * @return static */ public function placeholder(string $key, string $title, array $settings = []) { $settings['title'] = $settings['title'] ?? $settings['t'] ?? $title; $placeholder = array_merge($this->placeholders[$key] ?? [], $settings); if (! empty($settings['first'])) { $this->placeholders = array_merge([$key => $placeholder], $this->placeholders); } else { $this->placeholders[$key] = $placeholder; } return $this; } /** * Add placeholders * @param array $placeholders * @return static */ public function placeholders(array $placeholders) { foreach ($placeholders as $key => $settings) { if (! is_array($settings)) { $settings = ['title' => $settings]; } if (is_string($key) || is_int($key)) { $this->placeholder($key, '', $settings); } } return $this; } /** * Remove placeholder * @param string|string[] $key * @return static */ public function withoutPlaceholder($key) { if (is_array($key)) { foreach ($key as $k) { unset($this->placeholders[$k]); } } else { unset($this->placeholders[$key]); } return $this; } /** * Set placeholders values * @param string|array $key placeholder key * @param mixed $value * @return static */ public function with($key, $value = null) { if (! is_array($key)) { $key = [$key => $value]; } foreach ($key as $k => $v) { $this->params[$k] = $v; } return $this; } /** * Get placeholders with values to replace * @param string|null $key * @param mixed $default * @return string|array */ public function param(?string $key = null, $default = '') { if (! is_null($key)) { return $this->params[$key] ?? $default; } return $this->params; } /** * Add field * @param string $key * @param string $title * @param string $type * @param array $settings * @return static */ public function field(string $key, string $title, string $type, array $settings = []) { $settings['title'] = $title; $settings['type'] = $type; $this->fields[$key] = array_merge($this->fields[$key] ?? [], $settings); return $this; } /** * Add text field * @param string $key * @param string $title * @param array $settings * @return static */ public function fieldText(string $key, string $title, array $settings = []) { return $this->field($key, $title, self::FIELD_TEXT, $settings); } /** * Add textarea field * @param string $key * @param string $title * @param array $settings * @return static */ public function fieldTextarea(string $key, string $title, array $settings = []) { return $this->field($key, $title, self::FIELD_TEXTAREA, $settings); } /** * Add wysiwyg field * @param string $key * @param string $title * @param array $settings * @return static */ public function fieldWysiwyg(string $key, string $title, array $settings = []) { return $this->field($key, $title, self::FIELD_WYSIWYG, $settings); } /** * Add fields * @param array $fields * @return static */ public function fields(array $fields) { foreach ($fields as $key => $field) { if (! is_string($key) || ! is_array($field)) { continue; } $this->field( $key, $field['title'] ?? $field['t'] ?? '', $field['type'] ?? self::FIELD_TEXT, $field ); } return $this; } /** * Remove field * @param string $key * @return static */ public function withoutField(string $key) { unset($this->fields[$key]); return $this; } /** * Has list block on page * @param bool $enabled * @return static */ public function list($enabled = true) { $this->list = $enabled; if ($enabled) { $this->placeholder('page', _t('@seo', 'List page')); } else { $this->withoutPlaceholder('page'); } return $this; } /** * Has social meta on page * @param bool $enabled * @return static */ public function social($enabled = true) { $this->social = $enabled; return $this; } /** * Social meta providers * @return SocialProvider[] */ public function socialProviders(): array { $providers = []; foreach ( bff::filter('seo.template.social.providers', [ \bff\modules\seo\social\Facebook::class, ]) as $class ) { if (! class_exists($class)) { continue; } $provider = new $class(); if ($provider instanceof SocialProvider) { $providers[$provider->providerKey()] = $provider; } } return $providers; } /** * Robots: index/noindex page * @param bool $index * @param string $reason * @return static */ public function index($index = true, $reason = '') { $this->index = $index; $this->indexReason = $reason; return $this; } /** * Robots: follow/nofollow page * @param bool $follow * @return static */ public function follow($follow = true) { $this->follow = $follow; return $this; } /** * Set canonical url * @param string $url * @param array $query * @return static */ public function canonicalUrl(string $url, array $query = []) { if (! $this->canonical) { $this->canonical = new Canonical($this, $url, $query); } else { $this->canonical->setUrl($url, $query); } return $this; } /** * Has H1 field * @param bool $enabled * @param string|null $title * @return static */ public function titleH1($enabled = true, ?string $title = null) { if ($enabled) { $this->fieldText('titleh1', $title ?: _t('@seo', 'Title H1')); } else { $this->withoutField('titleh1'); } return $this; } /** * Has breadcrumb field * @param bool $enabled * @return static */ public function breadcrumb($enabled = true) { if ($enabled) { $this->fieldText('breadcrumb', _t('@seo', 'Breadcrumb')); } else { $this->withoutField('breadcrumb'); } return $this; } /** * Has seotext field * @param bool $enabled * @return static */ public function seotext($enabled = true) { if ($enabled) { $this->fieldWysiwyg('seotext', _t('@seo', 'SEO Text')); } else { $this->withoutField('seotext'); } return $this; } /** * Has settings inheritance * @param bool $enabled * @return static */ public function inheritance($enabled = true) { $this->inheritance = $enabled; return $this; } /** * Has landing page link settings * @param bool $enabled * @param array $opts * @return static */ public function withLandingPageLink($enabled = true, array $opts = []) { $this->landingpageLink = $enabled; $this->landingpageOptions = array_merge([ 'id_field' => 'landing_id', 'url_field' => 'landing_url', ], $opts); return $this; } /** * Create template from array (2x format fallback) * @param string $group * @param string $key * @param array $settings * @param array $placeholders * @return static */ public static function fromArray(string $group, string $key, array $settings, array $placeholders = []) { $template = new static(); $template->setupSettings($key, $group); foreach ($settings as $key => $data) { switch ($key) { case 't': case 'title': { $template->title($data); } break; case 'macros': { $template->placeholders($data); } break; case 'fields': { if (is_array($data)) { $template->fields($data); } } break; case 'inherit': { $template->inheritance(boolval($data)); } break; case 'list': { $template->list(boolval($data)); } break; case 'landing': { $template->withLandingPageLink( (is_bool($data) ? $data : true), (is_array($data) ? $data : []) ); } break; } } if ($placeholders) { $template->placeholders($placeholders); } return $template; } /** * Convert to array (2x format fallback) * @return array */ public function toArray() { return [ 'title' => $this->title, 'macros' => $this->placeholders, 'fields' => $this->fields, 'inherit' => $this->inheritance, 'landing' => $this->landingpageLink, 'list' => $this->list, ]; } }