From 8f91d6e177f932543a003abaf3b924e9eb9befa5 Mon Sep 17 00:00:00 2001 From: Dekart Deploy Bot Date: Sat, 18 Jul 2026 16:05:15 +0000 Subject: [PATCH] =?UTF-8?q?Global=202026-07-18=20=D0=9F=D0=9A=D0=A8=20+=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC=D0=BC=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- llms.txt | 48 +- wp-content/object-cache.php.dev-disabled | 978 ++++++++++++++++++ wp-content/themes/dekart/assets/css/main.css | 4 +- .../dekart/assets/css/podgotovka-k-shkole.css | 222 +++- .../themes/dekart/assets/css/program-pksh.css | 288 ++++++ wp-content/themes/dekart/functions.php | 156 ++- .../themes/dekart/inc/class-pksh-data.php | 259 ++--- wp-content/themes/dekart/inc/schema.php | 819 +++++---------- .../dekart/page-podgotovka-k-shkole.php | 105 +- .../themes/dekart/page-program-pksh.php | 61 ++ .../dekart/template-parts/author-eaat.php | 67 ++ .../template-parts/pksh/author-info.php | 63 ++ .../template-parts/pksh/external-reviews.php | 66 +- .../dekart/template-parts/pksh/hero.php | 11 +- .../template-parts/pksh/how-it-works.php | 37 +- .../dekart/template-parts/pksh/programs.php | 20 +- .../dekart/template-parts/pksh/results.php | 18 +- .../template-parts/pksh/trust-authority.php | 5 + .../program-pksh/disclaimer.php | 29 + .../program-pksh/eeat-links.php | 26 + .../template-parts/program-pksh/header.php | 23 + .../program-pksh/office-info.php | 50 + .../program-pksh/program-content.php | 24 + .../program-pksh/service-link.php | 29 + 24 files changed, 2596 insertions(+), 812 deletions(-) create mode 100644 wp-content/object-cache.php.dev-disabled create mode 100644 wp-content/themes/dekart/assets/css/program-pksh.css create mode 100644 wp-content/themes/dekart/page-program-pksh.php create mode 100644 wp-content/themes/dekart/template-parts/author-eaat.php create mode 100644 wp-content/themes/dekart/template-parts/pksh/author-info.php create mode 100644 wp-content/themes/dekart/template-parts/program-pksh/disclaimer.php create mode 100644 wp-content/themes/dekart/template-parts/program-pksh/eeat-links.php create mode 100644 wp-content/themes/dekart/template-parts/program-pksh/header.php create mode 100644 wp-content/themes/dekart/template-parts/program-pksh/office-info.php create mode 100644 wp-content/themes/dekart/template-parts/program-pksh/program-content.php create mode 100644 wp-content/themes/dekart/template-parts/program-pksh/service-link.php diff --git a/llms.txt b/llms.txt index 0d8e3f9..98e3cb6 100644 --- a/llms.txt +++ b/llms.txt @@ -1,34 +1,18 @@ -# Декарт — частная школа в Щёлково -> Частная школа полного дня в Щёлково, Московская область. Программы начальной школы (1–4 класс), средней школы (5–9 класс), подготовка к ЕГЭ и ОГЭ (10–11 класс). Индивидуальный подход, классы до 12 человек, продлённый день, подготовка к школе. +# Детский центр «Декарт» — Подготовка к школе в Щёлково +> Образовательный центр в Щёлково с гослицензией. Работаем с 2014 года. Готовим детей 5–7 лет к школе: чтение, письмо, математика, логика. -## Основные страницы -- [Отзывы родителей и учеников](https://schelkovo.dekart.school/otzyvy/) -- [Начальная школа](https://schelkovo.dekart.school/nachalnaya-shkola/) -- [Подготовка к ЕГЭ и ОГЭ](https://schelkovo.dekart.school/podgotovka-k-ege/) -- [Группа продлённого дня](https://schelkovo.dekart.school/prodlenka/) -- [Подготовка к школе](https://schelkovo.dekart.school/podgotovka-k-shkole/) -- [Летний городской лагерь](https://schelkovo.dekart.school/gorodskoj-lager/) -- [Контакты](https://schelkovo.dekart.school/kontakty/) -- [О школе](https://schelkovo.dekart.school/o-nas/) +## Основные разделы +- [Подготовка к школе](https://xn--80atdlj0d.xn--p1ai/podgotovka-k-shkole/): Комплексные занятия для детей 5–7 лет. Чтение, письмо, счёт, логика. Первое занятие — бесплатно. +- [Начальная школа](https://xn--80atdlj0d.xn--p1ai/nachalnaya-shkola/): Программы 1–4 классов. УМК «Перспектива», система Занкова, Cambridge English. +- [О нас](https://xn--80atdlj0d.xn--p1ai/o-nas/): Лицензия, документы, педагоги. +- [Отзывы](https://xn--80atdlj0d.xn--p1ai/otzyvy/): Отзывы родителей и выпускников. +- [Контакты](https://xn--80atdlj0d.xn--p1ai/contacts/): Адрес, телефон, карта. -## О школе -- Частная школа «Декарт» основана в 2010 году -- Расположена по адресу: ул. Талсинская, 24, Щёлково, Московская область -- Режим работы: пн–пт 8:00–19:00, сб 9:00–14:00 -- Классы: до 12 человек -- Наполняемость: более 500 выпускников -- Педагоги: 50+ квалифицированных специалистов - -## Что предлагает школа -- Начальное общее образование (1–4 класс) с углублённым английским -- Основное общее образование (5–9 класс) с предпрофильной подготовкой -- Среднее общее образование (10–11 класс) с подготовкой к ЕГЭ -- Подготовка к школе для детей 5–6 лет -- Группа продлённого дня с 8:00 до 19:00 -- Летний городской лагерь -- Кружки: робототехника, шахматы, английский театр, изостудия - - +## Ключевые факты +- Адрес: Талсинская ул., 24, Щёлково, Московская область +- Телефон: +7 (926) 931-14-34 +- Лицензия: государственная образовательная лицензия +- Основан: 2014 год +- Педагоги: высшее педагогическое образование, стаж от 12 лет +- Группы: до 10 человек +- Формат: очные занятия diff --git a/wp-content/object-cache.php.dev-disabled b/wp-content/object-cache.php.dev-disabled new file mode 100644 index 0000000..a8747ce --- /dev/null +++ b/wp-content/object-cache.php.dev-disabled @@ -0,0 +1,978 @@ +add( $key, $data, $group, $expire ); +} + +function wp_cache_incr( $key, $n = 1, $group = '' ) { + global $wp_object_cache; + + return $wp_object_cache->incr( $key, $n, $group ); +} + +function wp_cache_decr( $key, $n = 1, $group = '' ) { + global $wp_object_cache; + + return $wp_object_cache->decr( $key, $n, $group ); +} + +function wp_cache_close() { + global $wp_object_cache; + + return $wp_object_cache->close(); +} + +function wp_cache_delete( $key, $group = '' ) { + global $wp_object_cache; + + return $wp_object_cache->delete( $key, $group ); +} + +function wp_cache_flush() { + global $wp_object_cache; + + return $wp_object_cache->flush(); +} + +function wp_cache_get( $key, $group = '', $force = false, &$found = null ) { + global $wp_object_cache; + + $value = apply_filters( 'pre_wp_cache_get', false, $key, $group, $force ); + if ( false !== $value ) { + $found = true; + return $value; + } + + return $wp_object_cache->get( $key, $group, $force, $found ); +} + +/** + * Retrieve multiple cache entries + * + * @param array $groups Array of arrays, of groups and keys to retrieve + * @return mixed + */ +function wp_cache_get_multi( $groups ) { + global $wp_object_cache; + + return $wp_object_cache->get_multi( $groups ); +} + +function wp_cache_init() { + global $wp_object_cache; + + $wp_object_cache = new WP_Object_Cache(); +} + +function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) { + global $wp_object_cache; + + return $wp_object_cache->replace( $key, $data, $group, $expire ); +} + +function wp_cache_set( $key, $data, $group = '', $expire = 0 ) { + global $wp_object_cache; + + if ( defined( 'WP_INSTALLING' ) == false ) { + return $wp_object_cache->set( $key, $data, $group, $expire ); + } else { + return $wp_object_cache->delete( $key, $group ); + } +} + +function wp_cache_switch_to_blog( $blog_id ) { + global $wp_object_cache; + + return $wp_object_cache->switch_to_blog( $blog_id ); +} + +function wp_cache_add_global_groups( $groups ) { + global $wp_object_cache; + + $wp_object_cache->add_global_groups( $groups ); +} + +function wp_cache_add_non_persistent_groups( $groups ) { + global $wp_object_cache; + + $wp_object_cache->add_non_persistent_groups( $groups ); +} + +class WP_Object_Cache { + var $global_groups = array( 'WP_Object_Cache_global' ); + + var $no_mc_groups = array(); + + var $cache = array(); + var $mc = array(); + var $default_mcs = array(); + var $stats = array(); + var $group_ops = array(); + + var $flush_group = 'WP_Object_Cache'; + var $global_flush_group = 'WP_Object_Cache_global'; + var $flush_key = "flush_number_v4"; + var $old_flush_key = "flush_number"; + var $flush_number = array(); + var $global_flush_number = null; + + var $cache_enabled = true; + var $default_expiration = 0; + var $max_expiration = 2592000; // 30 days + + var $stats_callback = null; + + var $connection_errors = array(); + + var $time_total = 0; + var $size_total = 0; + var $slow_op_microseconds = 0.005; // 5 ms + + function add( $id, $data, $group = 'default', $expire = 0 ) { + $key = $this->key( $id, $group ); + + if ( is_object( $data ) ) { + $data = clone $data; + } + + if ( in_array( $group, $this->no_mc_groups ) ) { + $this->cache[ $key ] = [ + 'value' => $data, + 'found' => false, + ]; + + return true; + } elseif ( isset( $this->cache[ $key ][ 'value' ] ) && false !== $this->cache[ $key ][ 'value' ] ) { + return false; + } + + $mc =& $this->get_mc( $group ); + + $expire = intval( $expire ); + if ( 0 === $expire || $expire > $this->max_expiration ) { + $expire = $this->default_expiration; + } + + $size = $this->get_data_size( $data ); + $this->timer_start(); + $result = $mc->add( $key, $data, false, $expire ); + $elapsed = $this->timer_stop(); + + $comment = ''; + if ( isset( $this->cache[ $key ] ) ) { + $comment .= ' [lc already]'; + } + if ( false === $result ) { + $comment .= ' [mc already]'; + } + + $this->group_ops_stats( 'add', $key, $group, $size, $elapsed, $comment ); + + if ( false !== $result ) { + $this->cache[ $key ] = [ + 'value' => $data, + 'found' => true, + ]; + } else if ( false === $result && true === isset( $this->cache[$key][ 'value' ] ) && false === $this->cache[$key][ 'value' ] ) { + /* + * Here we unset local cache if remote add failed and local cache value is equal to `false` in order + * to update the local cache anytime we get a new information from remote server. This way, the next + * cache get will go to remote server and will fetch recent data. + */ + unset( $this->cache[$key] ); + } + + return $result; + } + + function add_global_groups( $groups ) { + if ( ! is_array( $groups ) ) { + $groups = (array) $groups; + } + + $this->global_groups = array_merge( $this->global_groups, $groups ); + $this->global_groups = array_unique( $this->global_groups ); + } + + function add_non_persistent_groups( $groups ) { + if ( ! is_array( $groups ) ) { + $groups = (array) $groups; + } + + $this->no_mc_groups = array_merge( $this->no_mc_groups, $groups ); + $this->no_mc_groups = array_unique( $this->no_mc_groups ); + } + + function incr( $id, $n = 1, $group = 'default' ) { + $key = $this->key( $id, $group ); + $mc =& $this->get_mc( $group ); + + $incremented = $mc->increment( $key, $n ); + + $this->cache[ $key ] = [ + 'value' => $incremented, + 'found' => false !== $incremented, + ]; + + return $this->cache[ $key ][ 'value' ]; + } + + function decr( $id, $n = 1, $group = 'default' ) { + $key = $this->key( $id, $group ); + $mc =& $this->get_mc( $group ); + + $decremented = $mc->decrement( $key, $n ); + $this->cache[ $key ] = [ + 'value' => $decremented, + 'found' => false !== $decremented, + ]; + + return $this->cache[ $key ][ 'value' ]; + } + + function close() { + foreach ( $this->mc as $bucket => $mc ) { + $mc->close(); + } + } + + function delete( $id, $group = 'default' ) { + $key = $this->key( $id, $group ); + + if ( in_array( $group, $this->no_mc_groups ) ) { + unset( $this->cache[ $key ] ); + + return true; + } + + $mc =& $this->get_mc( $group ); + + $this->timer_start(); + $result = $mc->delete( $key ); + $elapsed = $this->timer_stop(); + + $this->group_ops_stats( 'delete', $key, $group, null, $elapsed ); + + if ( false !== $result ) { + unset( $this->cache[ $key ] ); + } + + return $result; + } + + // Gets number from all default servers, replicating if needed + function get_max_flush_number( $group ) { + $key = $this->key( $this->flush_key, $group ); + + $values = array(); + $size = 19; // size of microsecond timestamp serialized + foreach ( $this->default_mcs as $i => $mc ) { + $flags = false; + $this->timer_start(); + $values[ $i ] = $mc->get( $key, $flags ); + $elapsed = $this->timer_stop(); + + if ( empty( $values[ $i ] ) ) { + $this->group_ops_stats( 'get_flush_number', $key, $group, null, $elapsed, 'not_in_memcache' ); + } else { + $this->group_ops_stats( 'get_flush_number', $key, $group, $size, $elapsed, 'memcache' ); + } + } + + $max = max( $values ); + + if ( ! $max > 0 ) { + return false; + } + + // Replicate to servers not having the max. + $expire = 0; + foreach ( $this->default_mcs as $i => $mc ) { + if ( $values[ $i ] < $max ) { + $this->timer_start(); + $mc->set( $key, $max, false, $expire ); + $elapsed = $this->timer_stop(); + $this->group_ops_stats( 'set_flush_number', $key, $group, $size, $elapsed, 'replication_repair' ); + } + } + + return $max; + } + + function set_flush_number( $value, $group ) { + $key = $this->key( $this->flush_key, $group ); + $expire = 0; + $size = 19; + foreach ( $this->default_mcs as $i => $mc ) { + $this->timer_start(); + $mc->set( $key, $value, false, $expire ); + $elapsed = $this->timer_stop(); + $this->group_ops_stats( 'set_flush_number', $key, $group, $size, $elapsed, 'replication' ); + } + } + + function get_flush_number( $group ) { + $flush_number = $this->get_max_flush_number( $group ); + // What if there is no v4 flush number? + if ( empty( $flush_number ) ) { + // Look for the v3 flush number key. + $flush_number = intval( $this->get( $this->old_flush_key, $group ) ); + if ( !empty( $flush_number ) ) { + // Found v3 flush number. Upgrade to v4 with replication. + $this->set_flush_number( $flush_number, $group ); + // Delete v3 key so we can't later restore it and find stale keys. + } else { + // No flush number found anywhere. Make a new one. This flushes the cache. + $flush_number = $this->new_flush_number(); + $this->set_flush_number( $flush_number, $group ); + } + } + + return $flush_number; + } + + function get_global_flush_number() { + if ( ! isset( $this->global_flush_number ) ) { + $this->global_flush_number = $this->get_flush_number( $this->global_flush_group ); + } + return $this->global_flush_number; + } + + function get_blog_flush_number() { + if ( ! isset( $this->flush_number[ $this->blog_prefix ] ) ) { + $this->flush_number[ $this->blog_prefix ] = $this->get_flush_number( $this->flush_group ); + } + return $this->flush_number[ $this->blog_prefix ]; + } + + function flush() { + // Do not use the memcached flush method. It acts on an + // entire memcached server, affecting all sites. + // Flush is also unusable in some setups, e.g. twemproxy. + // Instead, rotate the key prefix for the current site. + // Global keys are rotated when flushing on any network's + // main site. + $this->cache = array(); + + $flush_number = $this->new_flush_number(); + + $this->rotate_site_keys( $flush_number ); + + if ( is_main_site() ) { + $this->rotate_global_keys( $flush_number ); + } + } + + function rotate_site_keys( $flush_number = null ) { + if ( is_null( $flush_number ) ) { + $flush_number = $this->new_flush_number(); + } + + $this->set_flush_number( $flush_number, $this->flush_group ); + + $this->flush_number[ $this->blog_prefix ] = $flush_number; + } + + function rotate_global_keys( $flush_number = null ) { + if ( is_null( $flush_number ) ) { + $flush_number = $this->new_flush_number(); + } + + $this->set_flush_number( $flush_number, $this->global_flush_group ); + + $this->global_flush_number = $flush_number; + } + + function new_flush_number() { + return intval( microtime( true ) * 1e6 ); + } + + function get( $id, $group = 'default', $force = false, &$found = null ) { + $key = $this->key( $id, $group ); + $mc =& $this->get_mc( $group ); + $found = true; + + if ( isset( $this->cache[ $key ] ) && ( ! $force || in_array( $group, $this->no_mc_groups ) ) ) { + if ( isset( $this->cache[ $key ][ 'value' ] ) && is_object( $this->cache[ $key ][ 'value' ] ) ) { + $value = clone $this->cache[ $key ][ 'value' ]; + } else { + $value = $this->cache[ $key ][ 'value' ]; + } + $found = $this->cache[ $key ][ 'found' ]; + + $this->group_ops_stats( 'get_local', $key, $group, null, null, 'local' ); + } else if ( in_array( $group, $this->no_mc_groups ) ) { + $this->cache[ $key ] = [ + 'value' => $value = false, + 'found' => false, + ]; + + $found = false; + + $this->group_ops_stats( 'get_local', $key, $group, null, null, 'not_in_local' ); + } else { + $flags = false; + $this->timer_start(); + $value = $mc->get( $key, $flags ); + $elapsed = $this->timer_stop(); + + // Value will be unchanged if the key doesn't exist. + if ( false === $flags ) { + $found = false; + $value = false; + } + + $this->cache[ $key ] = [ + 'value' => $value, + 'found' => $found, + ]; + + if ( is_null( $value ) || $value === false ) { + $this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'not_in_memcache' ); + } else if ( 'checkthedatabaseplease' === $value ) { + $this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'checkthedatabaseplease' ); + } else { + $size = $this->get_data_size( $value ); + $this->group_ops_stats( 'get', $key, $group, $size, $elapsed, 'memcache' ); + } + } + + if ( 'checkthedatabaseplease' === $value ) { + unset( $this->cache[ $key ] ); + + $found = false; + $value = false; + } + + return $value; + } + + function get_multi( $groups ) { + /* + format: $get['group-name'] = array( 'key1', 'key2' ); + */ + $return = array(); + $return_cache = array( + 'value' => false, + 'found' => false, + ); + + foreach ( $groups as $group => $ids ) { + $mc =& $this->get_mc( $group ); + $keys = array(); + $this->timer_start(); + + foreach ( $ids as $id ) { + $key = $this->key( $id, $group ); + $keys[] = $key; + + if ( isset( $this->cache[ $key ] ) ) { + if ( is_object( $this->cache[ $key ][ 'value'] ) ) { + $return[ $key ] = clone $this->cache[ $key ][ 'value']; + $return_cache[ $key ] = [ + 'value' => clone $this->cache[ $key ][ 'value'], + 'found' => $this->cache[ $key ][ 'found'], + ]; + } else { + $return[ $key ] = $this->cache[ $key ][ 'value']; + $return_cache[ $key ] = [ + 'value' => $this->cache[ $key ][ 'value' ], + 'found' => $this->cache[ $key ][ 'found' ], + ]; + } + + continue; + } else if ( in_array( $group, $this->no_mc_groups ) ) { + $return[ $key ] = false; + $return_cache[ $key ] = [ + 'value' => false, + 'found' => false, + ]; + + continue; + } else { + $fresh_get = $mc->get( $key ); + $return[ $key ] = $fresh_get; + $return_cache[ $key ] = [ + 'value' => $fresh_get, + 'found' => false !== $fresh_get, + ]; + } + } + + $elapsed = $this->timer_stop(); + $this->group_ops_stats( 'get_multi', $keys, $group, null, $elapsed ); + } + + $this->cache = array_merge( $this->cache, $return_cache ); + + return $return; + } + + function flush_prefix( $group ) { + if ( $group === $this->flush_group || $group === $this->global_flush_group ) { + // Never flush the flush numbers. + $number = '_'; + } elseif ( false !== array_search( $group, $this->global_groups ) ) { + $number = $this->get_global_flush_number(); + } else { + $number = $this->get_blog_flush_number(); + } + return $number . ':'; + } + + function key( $key, $group ) { + if ( empty( $group ) ) { + $group = 'default'; + } + + $prefix = $this->key_salt; + + $prefix .= $this->flush_prefix( $group ); + + if ( false !== array_search( $group, $this->global_groups ) ) { + $prefix .= $this->global_prefix; + } else { + $prefix .= $this->blog_prefix; + } + + return preg_replace( '/\s+/', '', "$prefix:$group:$key" ); + } + + function replace( $id, $data, $group = 'default', $expire = 0 ) { + $key = $this->key( $id, $group ); + $expire = intval( $expire ); + if ( 0 === $expire || $expire > $this->max_expiration ) { + $expire = $this->default_expiration; + } + $mc =& $this->get_mc( $group ); + + if ( is_object( $data ) ) { + $data = clone $data; + } + + $size = $this->get_data_size( $data ); + $this->timer_start(); + $result = $mc->replace( $key, $data, false, $expire ); + $elapsed = $this->timer_stop(); + $this->group_ops_stats( 'replace', $key, $group, $size, $elapsed ); + + if ( false !== $result ) { + $this->cache[ $key ] = [ + 'value' => $data, + 'found' => true, + ]; + } + + return $result; + } + + function set( $id, $data, $group = 'default', $expire = 0 ) { + $key = $this->key( $id, $group ); + + if ( isset( $this->cache[ $key ] ) && ( 'checkthedatabaseplease' === $this->cache[ $key ][ 'value' ] ) ) { + return false; + } + + if ( is_object( $data ) ) { + $data = clone $data; + } + + $this->cache[ $key ] = [ + 'value' => $data, + 'found' => false, // Set to false as not technically found in memcache at this point. + ]; + + if ( in_array( $group, $this->no_mc_groups ) ) { + $this->group_ops_stats( 'set_local', $key, $group, null, null ); + + return true; + } + + $expire = intval( $expire ); + if ( 0 === $expire || $expire > $this->max_expiration ) { + $expire = $this->default_expiration; + } + + $mc =& $this->get_mc( $group ); + + $size = $this->get_data_size( $data ); + $this->timer_start(); + $result = $mc->set( $key, $data, false, $expire ); + $elapsed = $this->timer_stop(); + $this->group_ops_stats( 'set', $key, $group, $size, $elapsed ); + + // Update the found cache value with the result of the set in memcache. + $this->cache[ $key ][ 'found' ] = $result; + + return $result; + } + + function switch_to_blog( $blog_id ) { + global $table_prefix; + + $blog_id = (int) $blog_id; + + $this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix ); + } + + function colorize_debug_line( $line, $trailing_html = '' ) { + $colors = array( + 'get' => 'green', + 'get_local' => 'lightgreen', + 'get_multi' => 'fuchsia', + 'set' => 'purple', + 'set_local' => 'orchid', + 'add' => 'blue', + 'delete' => 'red', + 'delete_local' => 'tomato', + 'slow-ops' => 'crimson', + ); + + $cmd = substr( $line, 0, strpos( $line, ' ' ) ); + + // Start off with a neutral default color... + $color_for_cmd = 'brown'; + // And if the cmd has a specific color, use that instead + if ( isset( $colors[ $cmd ] ) ) { + $color_for_cmd = $colors[ $cmd ]; + } + + $cmd2 = "" . esc_html( $cmd ) . ""; + + return $cmd2 . esc_html( substr( $line, strlen( $cmd ) ) ) . "$trailing_html\n"; + } + + function js_toggle() { + echo " + + "; + } + + function stats() { + $this->js_toggle(); + + echo '

Total memcache query time:' . number_format_i18n( sprintf( '%0.1f', $this->time_total * 1000 ), 1 ) . ' ms

'; + echo "\n"; + echo '

Total memcache size:' . esc_html( size_format( $this->size_total, 2 ) ) . '

'; + echo "\n"; + + foreach ( $this->stats as $stat => $n ) { + if ( empty( $n ) ) { + continue; + } + + echo '

'; + echo $this->colorize_debug_line( "$stat $n" ); + echo '

'; + } + + echo "\n"; + + echo "
\n"; + foreach ( $groups as $group ) { + $group_name = $group; + if ( empty( $group_name ) ) { + $group_name = 'default'; + } + $current = $active_group == $group ? 'style="display: block"' : 'style="display: none"'; + echo "
\n"; + echo '

' . esc_html( $group_titles[ $group ] ) . '

' . "\n"; + echo "
\n";
+			foreach ( $this->group_ops[ $group ] as $index => $arr ) {
+				printf( '%3d ', $index );
+				echo $this->get_group_ops_line( $index, $arr );
+			}
+			echo "
\n"; + echo "
"; + } + + echo "
"; + } + + function get_group_ops_line( $index, $arr ) { + // operation + $line = "{$arr[0]} "; + + // key + $json_encoded_key = json_encode( $arr[1] ); + $line .= $json_encoded_key . " "; + + // comment + if ( ! empty( $arr[4] ) ) { + $line .= "{$arr[4]} "; + } + + // size + if ( isset( $arr[2] ) ) { + $line .= '(' . size_format( $arr[2], 2 ) . ') '; + } + + // time + if ( isset( $arr[3] ) ) { + $line .= '(' . number_format_i18n( sprintf( '%0.1f', $arr[3] * 1000 ), 1 ) . ' ms)'; + } + + // backtrace + $bt_link = ''; + if ( isset( $arr[6] ) ) { + $key_hash = md5( $index . $json_encoded_key ); + $bt_link = " Toggle Backtrace"; + $bt_link .= ""; + } + + return $this->colorize_debug_line( $line, $bt_link ); + } + + function &get_mc( $group ) { + if ( isset( $this->mc[ $group ] ) ) { + return $this->mc[ $group ]; + } + + return $this->mc['default']; + } + + function failure_callback( $host, $port ) { + $this->connection_errors[] = array( + 'host' => $host, + 'port' => $port, + ); + } + + function salt_keys( $key_salt ) { + if ( strlen( $key_salt ) ) { + $this->key_salt = $key_salt . ':'; + } else { + $this->key_salt = ''; + } + } + + function __construct() { + $this->stats = array( + 'get' => 0, + 'get_local' => 0, + 'get_multi' => 0, + 'set' => 0, + 'set_local' => 0, + 'add' => 0, + 'delete' => 0, + 'delete_local' => 0, + 'slow-ops' => 0, + ); + + global $memcached_servers; + + if ( isset( $memcached_servers ) ) { + $buckets = $memcached_servers; + } else { + $buckets = array( '127.0.0.1:11211' ); + } + + reset( $buckets ); + + if ( is_int( key( $buckets ) ) ) { + $buckets = array( 'default' => $buckets ); + } + + foreach ( $buckets as $bucket => $servers ) { + $this->mc[ $bucket ] = new Memcache(); + + foreach ( $servers as $i => $server ) { + if ( 'unix://' == substr( $server, 0, 7 ) ) { + $node = $server; + $port = 0; + } else { + list ( $node, $port ) = explode( ':', $server ); + + if ( ! $port ) { + $port = ini_get( 'memcache.default_port' ); + } + + $port = intval( $port ); + + if ( ! $port ) { + $port = 11211; + } + } + + $this->mc[ $bucket ]->addServer( $node, $port, true, 1, 1, 15, true, array( $this, 'failure_callback' ) ); + $this->mc[ $bucket ]->setCompressThreshold( 20000, 0.2 ); + + // Prepare individual connections to servers in default bucket for flush_number redundancy + if ( 'default' === $bucket ) { + $this->default_mcs[ $i ] = new Memcache(); + $this->default_mcs[ $i ]->addServer( $node, $port, true, 1, 1, 15, true, array( $this, 'failure_callback' ) ); + } + } + } + + global $blog_id, $table_prefix; + + $this->global_prefix = ''; + $this->blog_prefix = ''; + + if ( function_exists( 'is_multisite' ) ) { + $this->global_prefix = ( is_multisite() || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix; + $this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix ); + } + + $this->salt_keys( WP_CACHE_KEY_SALT ); + + $this->cache_hits =& $this->stats['get']; + $this->cache_misses =& $this->stats['add']; + } + + function increment_stat( $field, $num = 1 ) { + if ( ! isset( $this->stats[ $field ] ) ) { + $this->stats[ $field ] = $num; + } else { + $this->stats[ $field ] += $num; + } + } + + function group_ops_stats( $op, $keys, $group, $size, $time, $comment = '' ) { + $this->increment_stat( $op ); + + // we have no use of the local ops details for now + if ( strpos( $op, '_local' ) ) { + return; + } + + $this->size_total += $size; + + $keys = $this->strip_memcached_keys( $keys ); + + if ( $time > $this->slow_op_microseconds && 'get_multi' !== $op ) { + $this->increment_stat( 'slow-ops' ); + $backtrace = null; + if ( function_exists( 'wp_debug_backtrace_summary' ) ) { + $backtrace = wp_debug_backtrace_summary(); + } + $this->group_ops['slow-ops'][] = array( $op, $keys, $size, $time, $comment, $group, $backtrace ); + } + + $this->group_ops[ $group ][] = array( $op, $keys, $size, $time, $comment ); + } + + /** + * Key format: key_salt:flush_number:table_prefix:key_name + * + * We want to strip the `key_salt:flush_number` part to not leak the memcached keys. + * If `key_salt` is set we strip `'key_salt:flush_number`, otherwise just strip the `flush_number` part. + */ + function strip_memcached_keys( $keys ) { + if ( ! is_array( $keys ) ) { + $keys = [ $keys ]; + } + + foreach ( $keys as $key => $value ) { + $offset = 0; + if ( ! empty( $this->key_salt ) ) { + $offset = strpos( $value, ':' ) + 1; + } + + $start = strpos( $value, ':', $offset ); + $keys[ $key ] = substr( $value, $start + 1 ); + } + + if ( 1 === count( $keys ) ) { + return $keys[0]; + } + + return $keys; + } + + function timer_start() { + $this->time_start = microtime( true ); + + return true; + } + + function timer_stop() { + $time_total = microtime( true ) - $this->time_start; + $this->time_total += $time_total; + + return $time_total; + } + + function get_data_size( $data ) { + if ( is_string( $data ) ) { + return strlen( $data ); + } + + $serialized = serialize( $data ); + + return strlen( $serialized ); + } +} diff --git a/wp-content/themes/dekart/assets/css/main.css b/wp-content/themes/dekart/assets/css/main.css index 24fb422..f518718 100644 --- a/wp-content/themes/dekart/assets/css/main.css +++ b/wp-content/themes/dekart/assets/css/main.css @@ -205,7 +205,7 @@ html { } .ds-hero__subtitle { font-size: 18px; line-height: 1.55; - color: rgba(255,255,255,0.72); margin-bottom: 28px; + color: var(--color-rating); margin-bottom: 28px; max-width: 500px; } /* Metrics — стиль «glass card» как на /otzyvy/ */ @@ -275,7 +275,7 @@ html { margin-top: 10px; font-size: 13px; color: rgba(255,255,255,0.55); } .ds-hero__phone a { - color: var(--color-accent); font-weight: 500; text-decoration: underline; + color: var(--color-rating); font-weight: 500; text-decoration: underline; white-space: nowrap; } /* ---- Visual ---- */ diff --git a/wp-content/themes/dekart/assets/css/podgotovka-k-shkole.css b/wp-content/themes/dekart/assets/css/podgotovka-k-shkole.css index c150d70..9541fe0 100644 --- a/wp-content/themes/dekart/assets/css/podgotovka-k-shkole.css +++ b/wp-content/themes/dekart/assets/css/podgotovka-k-shkole.css @@ -112,6 +112,9 @@ .page-pksh .section-header--left .bento-p { max-width: 65ch; } + .page-pksh .section-header--left .geo-answer-text { + max-width: none; + } /* ── Section vertical rhythm ── */ .page-pksh .pksh-section, @@ -1718,6 +1721,34 @@ .page-pksh .trust-authority__grid { grid-template-columns: 1fr; gap: 16px; } } + .page-pksh .trust-program-link { + margin-top: 24px; + padding: 16px 24px; + background: var(--color-bg); + border-radius: var(--pksh-radius-card, 24px); + box-shadow: var(--shadow-sm); + text-align: center; + transition: box-shadow var(--transition-base, 300ms ease-out); + } + .page-pksh .trust-program-link:hover { + box-shadow: var(--shadow-md); + } + .page-pksh .trust-program-link a { + color: var(--color-primary); + font-weight: 600; + font-size: 0.9375rem; + text-decoration: underline; + text-underline-offset: 3px; + } + .page-pksh .trust-program-link a:hover { + color: var(--color-primary-dark); + text-decoration: none; + } + .page-pksh .trust-program-link a::after { + content: ' ↗'; + font-size: 0.8125rem; + } + /* ── 3o. EXTERNAL REVIEWS ── */ .page-pksh .external-reviews { max-width: 1000px; @@ -2076,12 +2107,14 @@ .page-pksh .sticky-cta-desktop__badge { display: inline-block; - padding: 4px 14px; - background: var(--color-primary); - color: var(--color-text-on-dark); + padding: 0; + background: transparent; + color: var(--color-primary); font-size: 0.8125rem; font-weight: 700; - border-radius: var(--radius-pill, 100px); + text-transform: uppercase; + letter-spacing: 0.04em; + border-radius: 0; } .page-pksh .sticky-cta-desktop__text { @@ -2297,3 +2330,184 @@ @media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } } + +/* ============================================================ + GEO / AI-оптимизация — answer-блоки, автор, даты + Matching the page's blue/navy bento-card design system + ============================================================ */ + +/* Определение для AI (hero) — стиль как bento-p hero, с акцентом */ +.page-pksh .ds-hero__definition { + font-size: clamp(1.0625rem, 1.3vw, 1.125rem); + line-height: 1.7; + color: rgba(255,255,255,0.72); + margin-top: 16px; + margin-bottom: 12px; + max-width: 65ch; + text-wrap: pretty; + font-weight: 450; +} + +/* Answer-блоки для AI-цитирования — карточка в стиле bento-секций */ +.page-pksh .geo-answer-block { + margin-top: 1rem; + margin-bottom: 1rem; + padding: clamp(20px, 2.5vw, 24px) clamp(20px, 2.5vw, 24px); + background: var(--color-bg); + border-radius: var(--radius-sm, 12px); + border-left: 4px solid var(--color-primary); + box-shadow: var(--shadow-sm); +} + +/* В тёмной секции результатов — прозрачный фон, тёплый акцент */ +.page-pksh .bento-card--highlight-results .geo-answer-block { + background: color-mix(in oklab, var(--color-bg-dark), white 6%); + border-left-color: var(--color-rating); + box-shadow: none; + padding: 20px 24px; + border-radius: var(--radius-sm, 12px); + text-align: center; +} +.page-pksh .bento-card--highlight-results .geo-answer-text { + max-width: none; +} +.page-pksh .bento-card--highlight-results .geo-answer-text { + color: var(--color-slate-300); + margin: 0; +} +.page-pksh .bento-card--highlight-results .geo-answer-heading { + font-size: 0.8125rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-rating); +} + +/* Answer-блок с изображением справа (только десктоп) */ +.page-pksh .geo-answer-block--with-img { + display: flex; + gap: 28px; + align-items: stretch; + flex-direction: row-reverse; +} + +.page-pksh .geo-answer-block__text { + flex: 1; + min-width: 0; + align-self: center; +} + +.page-pksh .geo-answer-block__img-wrap { + flex-shrink: 0; + width: auto; + aspect-ratio: 1 / 1; + height: 100%; + max-height: 220px; + border-radius: 50%; + overflow: hidden; + border: 3px solid var(--color-primary); + box-shadow: var(--shadow-sm); +} + +.page-pksh .geo-answer-block__img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +@media (max-width: 1023px) { + .page-pksh .geo-answer-block--with-img { + flex-direction: column; + align-items: flex-start; + } + .page-pksh .geo-answer-block__img-wrap { + display: none; + } +} + +.page-pksh .geo-answer-text { + font-size: clamp(1rem, 1.2vw, 1.0625rem); + line-height: 1.7; + color: var(--color-text-muted); + margin: 0; + max-width: none; + text-wrap: pretty; +} + +/* Question-based H3 headings — иерархия: H1 hero → H2 bento → H3 bento */ +.page-pksh .bento-h3 { + font-size: clamp(1.125rem, 1.5vw, 1.375rem); + font-weight: 700; + line-height: 1.35; + color: var(--color-text); + margin-top: 24px; + margin-bottom: 12px; + letter-spacing: -0.01em; +} + +/* Question-heading with accent mark */ +.page-pksh .geo-answer-heading { + font-size: clamp(1.0625rem, 1.3vw, 1.25rem); + font-weight: 700; + line-height: 1.4; + color: var(--color-primary); + margin-top: 20px; + margin-bottom: 12px; +} + +/* Автор и даты (E-E-A-T) — подвал страницы, компактный */ +.page-pksh .pksh-section--author { + padding: 12px clamp(16px, 3vw, 32px) 32px; +} + +.page-pksh .author-info { + display: flex; + flex-wrap: wrap; + gap: 8px 24px; + padding: 16px 0 8px; + font-size: 0.875rem; + color: var(--color-text-light); + border-top: 1px solid var(--color-border); +} + +.page-pksh .author-info__block { + display: inline-flex; + align-items: baseline; + gap: 4px; +} + +.page-pksh .author-info__label { + font-weight: 600; + color: var(--color-text-muted); +} + +.page-pksh .author-info__name { + font-weight: 600; + color: var(--color-text); +} + +.page-pksh .author-info__title { + color: var(--color-text-muted); + font-style: italic; +} + +.page-pksh .author-info__date { + color: var(--color-text); +} + +/* Responsive */ +@media (max-width: 639px) { + .page-pksh .ds-hero__definition { + font-size: 1rem; + } + + .page-pksh .geo-answer-block { + padding: 16px; + } + + .page-pksh .author-info { + flex-direction: column; + gap: 6px; + } +} diff --git a/wp-content/themes/dekart/assets/css/program-pksh.css b/wp-content/themes/dekart/assets/css/program-pksh.css new file mode 100644 index 0000000..b1b70c5 --- /dev/null +++ b/wp-content/themes/dekart/assets/css/program-pksh.css @@ -0,0 +1,288 @@ +/* ═══════════════════════════════════════════════════════════ + program-pksh.css — служебная страница программы + ═══════════════════════════════════════════════════════════ */ + +/* ── Container ── */ +.page-program-pksh .prog-container { + max-width: 880px; + margin: 0 auto; + padding: 0 24px; +} + +/* ── Section rhythm ── */ +.page-program-pksh .prog-section { + padding: 48px 0; +} +.page-program-pksh .prog-section--muted { + background: var(--color-bg-alt, #F1F5F9); +} +.page-program-pksh .prog-section--accent { + background: var(--color-bg-dark, #0F172A); +} +.page-program-pksh .prog-section--disclaimer { + background: rgba(245, 158, 11, 0.06); + border-top: 3px solid var(--color-rating, #F59E0B); + border-bottom: 3px solid var(--color-rating, #F59E0B); +} + +/* ── Intro ── */ +.page-program-pksh .prog-intro { + padding: 48px 0 24px; +} + +.page-program-pksh .prog-label { + display: inline-block; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-primary, #2563EB); + margin-bottom: 12px; +} + +.page-program-pksh .prog-title { + font-size: clamp(1.5rem, 2.5vw, 2.125rem); + font-weight: 800; + line-height: 1.2; + color: var(--color-text, #0F172A); + margin: 0 0 20px; + letter-spacing: -0.02em; +} + +.page-program-pksh .prog-desc { + font-size: 1rem; + line-height: 1.7; + color: var(--color-text-muted, #475569); + margin: 0; + max-width: 75ch; +} + +/* ── Section title ── */ +.page-program-pksh .prog-section-title { + font-size: clamp(1.25rem, 1.8vw, 1.5rem); + font-weight: 700; + line-height: 1.3; + color: var(--color-text, #0F172A); + margin: 0 0 20px; + padding-bottom: 12px; + border-bottom: 2px solid var(--color-border, #E2E8F0); +} +.page-program-pksh .prog-section-title--light { + color: var(--color-text-on-dark, #FFFFFF); + border-bottom-color: rgba(255,255,255,0.15); +} + +/* ── Body text ── */ +.page-program-pksh .prog-text { + font-size: 1rem; + line-height: 1.7; + color: var(--color-text-muted, #475569); + margin: 0 0 16px; + max-width: 75ch; +} +.page-program-pksh .prog-text--light { + color: rgba(255,255,255,0.75); +} + +/* ── Program content (WYSIWYG) ── */ +.page-program-pksh .prog-content { + font-size: 0.938rem; + line-height: 1.75; + color: var(--color-text, #0F172A); + max-width: 85ch; +} + +.page-program-pksh .prog-content h2, +.page-program-pksh .prog-content h3, +.page-program-pksh .prog-content h4 { + font-weight: 700; + color: var(--color-text, #0F172A); + margin-top: 32px; + margin-bottom: 12px; + line-height: 1.3; +} +.page-program-pksh .prog-content h2 { + font-size: 1.25rem; + padding-bottom: 8px; + border-bottom: 1px solid var(--color-border, #E2E8F0); +} +.page-program-pksh .prog-content h3 { + font-size: 1.0625rem; +} +.page-program-pksh .prog-content h4 { + font-size: 1rem; +} + +.page-program-pksh .prog-content p { + margin: 0 0 12px; + line-height: 1.75; +} + +.page-program-pksh .prog-content ul, +.page-program-pksh .prog-content ol { + margin: 0 0 12px; + padding-left: 24px; +} +.page-program-pksh .prog-content li { + margin-bottom: 4px; + line-height: 1.65; +} + +.page-program-pksh .prog-content table { + width: 100%; + border-collapse: collapse; + margin: 16px 0; + font-size: 0.875rem; +} +.page-program-pksh .prog-content th, +.page-program-pksh .prog-content td { + border: 1px solid var(--color-border, #E2E8F0); + padding: 8px 12px; + text-align: left; + vertical-align: top; +} +.page-program-pksh .prog-content th { + background: var(--color-bg-alt, #F1F5F9); + font-weight: 600; +} + +/* ── Office info block ── */ +.page-program-pksh .prog-office__details { + display: flex; + flex-wrap: wrap; + gap: 24px 40px; + margin-top: 12px; +} +.page-program-pksh .prog-office__item { + display: flex; + flex-direction: column; + gap: 2px; +} +.page-program-pksh .prog-office__label { + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-text-muted, #475569); + text-transform: uppercase; + letter-spacing: 0.05em; +} +.page-program-pksh .prog-office__value { + font-size: 1.0625rem; + color: var(--color-text, #0F172A); +} + +/* ── CTA block ── */ +.page-program-pksh .prog-cta { + text-align: center; + padding: 8px 0; +} +.page-program-pksh .prog-cta .prog-text { + margin: 0 auto 24px; + max-width: 65ch; +} +.page-program-pksh .prog-cta__btn { + display: inline-block; + padding: 16px 40px; + font-size: 1.0625rem; + font-weight: 700; + color: var(--color-text-on-dark, #FFFFFF); + background: var(--pksh-gradient-btn, linear-gradient(135deg, #2563EB, #1D4ED8)); + border-radius: var(--radius-pill, 100px); + text-decoration: none; + transition: transform 200ms ease-out, box-shadow 200ms ease-out; +} +.page-program-pksh .prog-cta__btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 28px rgba(37, 99, 235, 0.35); +} +.page-program-pksh .prog-cta__btn:focus-visible { + outline: 3px solid var(--color-primary, #2563EB); + outline-offset: 3px; +} + +/* ── E-E-A-T links ── */ +.page-program-pksh .prog-eeat { + padding: 8px 0; +} +.page-program-pksh .prog-eeat a { + color: var(--color-primary, #2563EB); + font-weight: 600; + text-decoration: underline; +} + +/* ── Disclaimer ── */ +.page-program-pksh .prog-disclaimer { + padding: 8px 0; +} +.page-program-pksh .prog-disclaimer__text { + font-size: 0.9375rem; + line-height: 1.65; + color: var(--color-text, #0F172A); + margin: 0 0 8px; +} +.page-program-pksh .prog-disclaimer__note { + font-size: 0.875rem; + color: var(--color-text-muted, #475569); + margin: 0; + font-style: italic; +} + +/* ── Responsive ── */ +@media (max-width: 639px) { + .page-program-pksh .prog-section { + padding: 32px 0; + } + .page-program-pksh .prog-intro { + padding: 32px 0 16px; + } + .page-program-pksh .prog-office__details { + flex-direction: column; + gap: 12px; + } + .page-program-pksh .prog-content table { + font-size: 0.8125rem; + } + .page-program-pksh .prog-content th, + .page-program-pksh .prog-content td { + padding: 6px 8px; + } +} + +/* ── Author block (E-E-A-T) ── */ +.page-program-pksh .prog-author { + display: flex; + flex-wrap: wrap; + gap: 8px 24px; + padding: 16px 0; + font-size: 0.875rem; + color: var(--color-text-light, #64748B); + border-top: 1px solid var(--color-border, #E2E8F0); + border-bottom: 1px solid var(--color-border, #E2E8F0); + margin-bottom: 16px; +} +.page-program-pksh .prog-author__block { + display: inline-flex; + align-items: baseline; + gap: 4px; +} +.page-program-pksh .prog-author__label { + font-weight: 600; + color: var(--color-text-muted, #475569); +} +.page-program-pksh .prog-author__name { + font-weight: 600; + color: var(--color-text, #0F172A); +} +.page-program-pksh .prog-author__title { + color: var(--color-text-muted, #475569); + font-style: italic; +} +.page-program-pksh .prog-author__date { + color: var(--color-text, #0F172A); +} + +@media (max-width: 639px) { + .page-program-pksh .prog-author { + flex-direction: column; + gap: 6px; + } +} diff --git a/wp-content/themes/dekart/functions.php b/wp-content/themes/dekart/functions.php index c3b72f5..74c3a24 100644 --- a/wp-content/themes/dekart/functions.php +++ b/wp-content/themes/dekart/functions.php @@ -4,6 +4,18 @@ require_once get_template_directory() . '/inc/template-tags.php'; // Pksh_Data — централизованная загрузка данных для page-podgotovka-k-shkole.php require_once get_template_directory() . '/inc/class-pksh-data.php'; +/** + * Замена названия сайта из Настроек филиала. + * blogname (Внешний вид → Настроить → Свойства сайта) берётся из branch_name. + */ +add_filter( 'pre_option_blogname', function( $default ) { + $branch = get_option( 'branch_name' ); + if ( ! empty( $branch ) ) { + return $branch; + } + return $default; +} ); + /* --- Удаляем мета из YOASTSEO START--- */ add_filter( 'wpseo_json_ld_output', '__return_false' ); /* --- Удаляем мета из YOASTSEO END --- */ @@ -558,10 +570,18 @@ function add_carbon() { Field::make( 'text', 'banner_subtitle_footnote_link', 'Ссылка для звёздочки (*) в подзаголовке' ) ->set_width( 50 ) ->set_help_text( 'URL для иконки * после текста подзаголовка. Если не указано — * неактивна.' ), - Field::make( 'text', 'banner_scarcity', 'Текст дефицита (срочность)' ) + Field::make( 'text', 'banner_scarcity', 'Текст дефицита (срочность)' ) ->set_width( 50 ) ->set_default_value( '🔥 Осталось 2 места из 8 в группе 5–6 лет. Стартуем 1 июня' ) ->set_help_text( 'Можно использовать HTML-теги: <strong>, <br>. Текст о срочности/дефиците мест под кнопкой.' ), + Field::make( 'textarea', 'pksh_hero_definition', 'Определение для AI (первые 60 слов)' ) + ->set_rows( 3 ) + ->set_help_text( + '📖 Для AI-поиска (GEO). Текст, который AI использует для ответа на «Что такое подготовка к школе в [городе]?». ' + . 'Рекомендуется 40–60 слов, начинается с «Подготовка к школе в [городе] — это…». ' + . 'Выводится на странице после H1, перед подзаголовком. Если не заполнено — блок скрыт.' + ) + ->set_default_value( 'Подготовка к школе в Щёлково — это комплексные занятия для детей 5–7 лет, которые помогают освоить чтение, письмо, счёт и развить логическое мышление. Программа построена на игровой методике: ребёнок учится без стресса, в комфортном темпе. Группы до 10 человек, индивидуальный подход к каждому.' ), )) ->add_tab('📊 Соц.доказательство', array( Field::make( 'html', 'pksh_social_tab_desc', '' ) @@ -576,9 +596,20 @@ function add_carbon() { Field::make( 'text', 'counter_label', 'Подпись' ) ->set_width( 70 ) ->set_help_text( 'Например: выпускников в топ-школах' ), - )), - )) - ->add_tab('⭐ Отзывы', array( + )), + Field::make( 'html', 'pksh_social_author_header', '' ) + ->set_html( '

👤 Автор контента (E-E-A-T для SEO)

Эти поля влияют на E-E-A-T (Experience, Expertise, Authoritativeness, Trustworthiness) — фактор ранжирования Google. Указываются в микроразметке Schema.org и отображаются в блоке автора внизу страницы. Заполните, чтобы повысить доверие поисковых систем к контенту.

' ), + Field::make( 'text', 'pksh_author_name', 'Имя автора контента' ) + ->set_width( 50 ) + ->set_default_value( 'Детский центр «Декарт»' ) + ->set_help_text( + 'Для SEO: Имя автора для Schema.org Person / author. ' + . 'Рекомендуется: фактическое название организации или ФИО руководителя/педагога. ' + . 'Влияет на E-E-A-T — Google проверяет автора контента. ' + . 'По умолчанию: название из поля «Название филиала» в Настройках темы.' + ), + )) + ->add_tab('⭐ Отзывы', array( Field::make( 'html', 'pksh_reviews_tab_desc', '' ) ->set_html( '

Секция: .pksh-reviews. Отзывы родителей на странице подготовки к школе.

' ), Field::make( 'complex', 'pksh_reviews_list', 'Отзывы' ) @@ -803,6 +834,60 @@ function add_carbon() { )), )) // ==================================================== + // Вкладка: GEO / AI-поиск (self-contained answer блоки для цитирования) + // ==================================================== + ->add_tab('🤖 GEO (AI-поиск)', array( + Field::make( 'html', 'pksh_geo_tab_desc', '' ) + ->set_html( '

Поля для Generative Engine Optimization — контент, оптимизированный для цитирования AI-поиском (Google AI Overviews, ChatGPT, Perplexity). Каждый блок должен быть самодостаточным (134–167 слов) и содержать конкретные данные.

' ), + Field::make( 'html', 'pksh_geo_questions_title', '' ) + ->set_html( '

❓ Вопросы-заголовки для AI

' ), + Field::make( 'html', 'pksh_geo_questions_desc', '' ) + ->set_html( '

Эти заголовки (H2/H3) отвечают на типичные поисковые запросы. AI видит их как прямые ответы. Если поле пусто — секция не выводится.

' ), + Field::make( 'text', 'pksh_geo_q1', 'Вопрос-заголовок #1' ) + ->set_width( 100 ) + ->set_help_text( 'Например: «Сколько стоит подготовка к школе в Щёлково?»' ) + ->set_default_value( 'Сколько стоит подготовка к школе в Щёлково?' ), + Field::make( 'text', 'pksh_geo_q2', 'Вопрос-заголовок #2' ) + ->set_width( 100 ) + ->set_help_text( 'Например: «Что должен знать ребёнок перед школой?»' ) + ->set_default_value( 'Что должен знать ребёнок перед школой?' ), + Field::make( 'text', 'pksh_geo_q3', 'Вопрос-заголовок #3' ) + ->set_width( 100 ) + ->set_help_text( 'Например: «Как выбрать центр подготовки к школе?»' ) + ->set_default_value( 'Как выбрать центр подготовки к школе?' ), + Field::make( 'html', 'pksh_geo_answers_title', '' ) + ->set_html( '

📝 Answer-блоки для AI-цитирования

' ), + Field::make( 'html', 'pksh_geo_answers_desc', '' ) + ->set_html( '

Каждый блок — 134–167 слов, самодостаточный, с конкретными данными. AI может процитировать его целиком. Рекомендуется: факты, цифры, сроки, цены. Если поле пусто — блок не выводится.

' ), + Field::make( 'textarea', 'pksh_geo_answer_programs', 'Answer-блок: Программы и цены' ) + ->set_rows( 8 ) + ->set_help_text( + 'Выводится в секции Программы перед карточками. ' + . 'Рекомендуется 134–167 слов. Опишите программы, цены, разницу между базовой и расширенной.' + ) + ->set_default_value( + 'В детском центре «Декарт» в Щёлково действуют две программы подготовки к школе. Базовая программа «Уверенный старт» включает 2 занятия в неделю по 30–40 минут — стоимость абонемента 7 500 ₽ в месяц. Расширенная программа «Результат за 3 месяца» — 3 занятия в неделю, 10 500 ₽ в месяц. Обе программы ведут действующие педагоги начальных классов со стажем от 12 лет. Группы до 10 человек — каждому ребёнку уделяется внимание. Первое занятие — бесплатно, без договора и оплаты. В стоимость включены все учебные материалы и пособия. Занятия проходят очно по адресу: Талсинская ул., 24. Результат через 3 недели — дети начинают читать по слогам.' + ), + Field::make( 'textarea', 'pksh_geo_answer_results', 'Answer-блок: Результаты и статистика' ) + ->set_rows( 8 ) + ->set_help_text( + 'Выводится в секции Результаты. ' + . 'Рекомендуется 134–167 слов. Конкретные цифры, проценты, сроки.' + ) + ->set_default_value( + '95% выпускников центра «Декарт» поступают в школы и лицеи Щёлкова без стресса — они готовы к учебной нагрузке и требованиям учителей. Первые результаты заметны уже через 3 недели: нечитавшие дети складывают первый осмысленный слог. 87% родителей отмечают рост самостоятельности ребёнка после месяца занятий. Выпускники центра адаптируются в первом классе в 2,5 раза быстрее сверстников — они не боятся отвечать у доски, поднимать руку и работать в коллективе. За 12 лет работы центром подготовлено более 500 детей. Программа построена так, что каждый ребёнок осваивает чтение, письмо, счёт и логику без перегрузки — занятия длятся 30–40 минут, каждые 7 минут смена активности.' + ), + Field::make( 'textarea', 'pksh_geo_answer_howitworks', 'Answer-блок: Как проходят занятия' ) + ->set_rows( 8 ) + ->set_help_text( + 'Выводится в секции Как проходят занятия. ' + . 'Рекомендуется 134–167 слов. Опишите методику, длительность, структуру.' + ) + ->set_default_value( + 'Каждое занятие длится 30–40 минут и построено так, чтобы ребёнок не уставал. Активность меняется каждые 7 минут — это оптимальный интервал удержания внимания для детей 5–7 лет. Урок начинается с короткой истории или загадки, которая вводит в тему и включает интерес. Затем следует игровое задание: чтение через игру, счёт через механику соревнования, логика через головоломки. После каждого блока — активная перемена с нейрогимнастикой. Завершается занятие рефлексией: ребёнок сам рассказывает, чему научился. Все педагоги имеют высшее педагогическое образование и опыт работы с дошкольниками от 5 лет. Методика исключает скучные прописи и многочасовое сидение за партой — дети воспринимают занятия как игру и с удовольствием возвращаются.' + ), + )) + // ==================================================== // Вкладка: Фотогалерея // ==================================================== ->add_tab('📸 Фотогалерея', array( @@ -821,6 +906,50 @@ function add_carbon() { )), )); + // ==================================================== + // Контейнер: Программа Подготовки к школе (служебная страница) + // ==================================================== + Container::make('post_meta', '📄 Программа «Подготовка к школе»') + ->where( 'post_template', '=', 'page-program-pksh.php' ) + ->add_fields( array( + Field::make( 'html', 'pksh_prog_header', '' ) + ->set_html( '

Официальный документ — образовательная программа подготовки к школе. Текст программы менять нельзя — это утверждённый документ. Для обновления скопируйте новый текст целиком.

' ), + Field::make( 'rich_text', 'pksh_prog_content', 'Текст программы' ) + ->set_help_text( + 'ВАЖНО: Это официально утверждённый документ. Не меняйте содержание. ' + . 'Для обновления — замените текст целиком на новую версию. ' + . 'Рекомендуется вставлять из Google Docs / Word без форматирования, ' + . 'затем применить заголовки H2-H3 и списки вручную.' + ), + Field::make( 'html', 'pksh_prog_legal_header', '' ) + ->set_html( '

⚖️ Юридическая информация

' ), + Field::make( 'date', 'pksh_prog_doc_date', 'Дата утверждения / обновления документа' ) + ->set_width( 30 ) + ->set_storage_format( 'Y-m-d' ) + ->set_input_format( 'Y-m-d', 'Y-m-d' ) + ->set_help_text( 'Дата последнего утверждения или обновления программы (YYYY-MM-DD). Отображается в дисклеймере.' ), + Field::make( 'textarea', 'pksh_prog_disclaimer', 'Текст дисклеймера' ) + ->set_width( 100 ) + ->set_default_value( + 'Электронная версия программы предназначена для ознакомления. При переносе в электронный вид ' + . 'возможны технические неточности (опечатки, ошибки форматирования). ' + . 'Юридически значимым считается документ, предоставленный в офисе организации. ' + . 'Дата последнего обновления электронной версии:' + ) + ->set_rows( 4 ) + ->set_help_text( 'Текст, который выводится в блоке дисклеймера. Можно редактировать независимо от текста программы.' ), + Field::make( 'html', 'pksh_prog_service_header', '' ) + ->set_html( '

🔗 Ссылка на услугу

' ), + Field::make( 'text', 'pksh_prog_service_url', 'URL услуги' ) + ->set_width( 50 ) + ->set_default_value( '/podgotovka-k-shkole/' ) + ->set_help_text( 'Ссылка на страницу услуги, которая реализует данную программу.' ), + Field::make( 'text', 'pksh_prog_service_label', 'Текст ссылки' ) + ->set_width( 50 ) + ->set_default_value( 'Записаться на подготовку к школе' ) + ->set_help_text( 'Текст кнопки/ссылки для перехода на услугу.' ), + )); + // ==================================================== // Уникальное имя контейнера Продлёнки // ==================================================== @@ -1983,6 +2112,7 @@ function dekart_branch_register_settings() { register_setting( 'dekart_branch_group', 'branch_ext_google', array( 'sanitize_callback' => 'esc_url_raw' ) ); register_setting( 'dekart_branch_group', 'branch_ext_2gis', array( 'sanitize_callback' => 'esc_url_raw' ) ); register_setting( 'dekart_branch_group', 'branch_ext_otzovik', array( 'sanitize_callback' => 'esc_url_raw' ) ); + register_setting( 'dekart_branch_group', 'branch_ext_vk', array( 'sanitize_callback' => 'esc_url_raw' ) ); register_setting( 'dekart_branch_group', 'branch_ext_other_name', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'dekart_branch_group', 'branch_ext_other_url', array( 'sanitize_callback' => 'esc_url_raw' ) ); register_setting( 'dekart_branch_group', 'branch_og_site_name', array( 'sanitize_callback' => 'sanitize_text_field' ) ); @@ -2544,6 +2674,24 @@ function dekart_ajax_flush_cache() { $summary = array( 'type' => 'fail', 'text' => "Выполнено с {$errors} ошибкой(ами). Проверьте лог выше." ); } +/** + * Noindex для служебной страницы программы. + * Юридический документ не должен индексироваться, + * чтобы не размывать SEO-вес коммерческих страниц. + */ +/** + * Noindex для служебной страницы программы. + */ +add_filter( 'wpseo_robots', function( $robots ) { + $post_id = get_the_ID(); + if ( $post_id && 'program-pksh' === get_post_field( 'post_name', $post_id ) ) { + return 'noindex, follow'; + } + return $robots; +}, 999 ); + + + wp_send_json_success( array( 'log' => $log, 'summary' => $summary, diff --git a/wp-content/themes/dekart/inc/class-pksh-data.php b/wp-content/themes/dekart/inc/class-pksh-data.php index f77a62c..70a9de6 100644 --- a/wp-content/themes/dekart/inc/class-pksh-data.php +++ b/wp-content/themes/dekart/inc/class-pksh-data.php @@ -48,12 +48,9 @@ class Pksh_Data { } // ============================================================ - // BRANCH / OPTION DATA (get_option → theme_option → post_meta) + // BRANCH / OPTION DATA (get_option -> theme_option -> post_meta) // ============================================================ - /** - * Адрес филиала - */ public function get_address(): string { $cache_key = 'dekart_branch_address'; $cached = wp_cache_get( $cache_key, 'dekart_branch' ); @@ -70,9 +67,6 @@ class Pksh_Data { return $this->branch['address']; } - /** - * Координаты филиала (для карты) - */ public function get_coords(): string { $cache_key = 'dekart_branch_coords'; $cached = wp_cache_get( $cache_key, 'dekart_branch' ); @@ -88,9 +82,6 @@ class Pksh_Data { return $this->branch['coords']; } - /** - * Телефон филиала - */ public function get_phone(): string { $cache_key = 'dekart_branch_phone'; $cached = wp_cache_get( $cache_key, 'dekart_branch' ); @@ -107,9 +98,6 @@ class Pksh_Data { return $this->branch['phone']; } - /** - * Телефон, очищенный от не-цифровых символов - */ public function get_phone_clean(): string { if ( ! array_key_exists( 'phone_clean', $this->branch ) ) { $this->branch['phone_clean'] = preg_replace( '/[^0-9+]/', '', $this->get_phone() ); @@ -117,9 +105,6 @@ class Pksh_Data { return $this->branch['phone_clean']; } - /** - * Email филиала - */ public function get_email(): string { $cache_key = 'dekart_branch_email'; $cached = wp_cache_get( $cache_key, 'dekart_branch' ); @@ -136,9 +121,6 @@ class Pksh_Data { return $this->branch['email']; } - /** - * Название филиала / организации - */ public function get_name(): string { $cache_key = 'dekart_branch_name'; $cached = wp_cache_get( $cache_key, 'dekart_branch' ); @@ -154,9 +136,6 @@ class Pksh_Data { return $this->branch['name']; } - /** - * Город с предлогом (например "в Щёлково") - */ public function get_city_prep(): string { $cache_key = 'dekart_branch_city_prep'; $cached = wp_cache_get( $cache_key, 'dekart_branch' ); @@ -170,20 +149,12 @@ class Pksh_Data { return $this->branch['city_prep']; } - /** - * Город без предлога (например "Щёлково") - */ - /** - * Город в родительном падеже (школы Щёлкова) - * Берёт из branch_city_genitive, fallback: city_clean с -о→-а - */ public function get_city_genitive(): string { $genitive = get_option( 'branch_city_genitive', '' ); if ( ! empty( $genitive ) ) { return $genitive; } $clean = $this->get_city_clean(); - // Для русских городов на -ово/-ево/-ино: замена -о → -а if ( str_ends_with( $clean, 'ово' ) || str_ends_with( $clean, 'ево' ) || str_ends_with( $clean, 'ино' ) || str_ends_with( $clean, 'ыно' ) ) { return mb_substr( $clean, 0, -1 ) . 'а'; } @@ -197,9 +168,6 @@ class Pksh_Data { return $this->branch['city_clean']; } - /** - * Количество отзывов на внешних площадках - */ public function get_ext_review_count(): string { $cache_key = 'dekart_branch_ext_review_count'; $cached = wp_cache_get( $cache_key, 'dekart_branch' ); @@ -213,9 +181,6 @@ class Pksh_Data { return $this->branch['ext_review_count']; } - /** - * Социальные сети (из branch_social option или CF) - */ public function get_social_links(): array { $cache_key = 'dekart_branch_social'; $cached = wp_cache_get( $cache_key, 'dekart_branch' ); @@ -228,13 +193,10 @@ class Pksh_Data { []; $this->branch['social'] = is_array( $raw ) ? $raw : []; } -wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECONDS ); - return $this->branch['social']; + wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECONDS ); + return $this->branch['social']; } - /** - * Логистика — пешком от ТЦ (branch_logistics_walk) - */ public function get_logistics_walk(): string { if ( ! array_key_exists( 'logistics_walk', $this->branch ) ) { $this->branch['logistics_walk'] = get_option( 'branch_logistics_walk', '5 минут пешком от ТЦ «Глобус»' ); @@ -242,9 +204,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON return $this->branch['logistics_walk']; } - /** - * Логистика — парковка (branch_logistics_parking) - */ public function get_logistics_parking(): string { if ( ! array_key_exists( 'logistics_parking', $this->branch ) ) { $this->branch['logistics_parking'] = get_option( 'branch_logistics_parking', 'Есть бесплатная парковка у входа' ); @@ -252,9 +211,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON return $this->branch['logistics_parking']; } - /** - * Логистика — соседние школы/сад (branch_logistics_schools) - */ public function get_logistics_nearby(): string { if ( ! array_key_exists( 'logistics_nearby', $this->branch ) ) { $this->branch['logistics_nearby'] = get_option( 'branch_logistics_schools', 'Рядом со школами №9 и №12, детским садом №15' ); @@ -262,13 +218,62 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON return $this->branch['logistics_nearby']; } + // ============================================================ + // AUTHOR / DATES (E-E-A-T / GEO) + // ============================================================ + + public function get_author_name(): string { + if ( ! array_key_exists( 'author_name', $this->page ) ) { + $this->page['author_name'] = carbon_get_post_meta( $this->post_id, 'pksh_author_name' ) + ?: get_option( 'branch_og_site_name' ) + ?: get_option( 'branch_name' ) + ?: carbon_get_theme_option( 'site_name' ) + ?: 'Детский центр «Декарт»'; + } + return $this->page['author_name']; + } + + /** + * Количество лет на рынке (из года основания). + * Автоматически: текущий год − год основания. + * Источник: CF schema_founded → Settings API branch_founded. + * Если год основания не задан — 0. + */ + public function get_years_on_market(): int { + if ( ! array_key_exists( 'years_market', $this->page ) ) { + $founded = carbon_get_theme_option( 'schema_founded' ); + if ( empty( $founded ) ) { + $founded = get_option( 'branch_founded' ); + } + if ( ! empty( $founded ) && is_string( $founded ) && is_numeric( $founded ) ) { + $this->page['years_market'] = (int) date( 'Y' ) - (int) $founded; + } else { + $this->page['years_market'] = 0; + } + } + return $this->page['years_market']; + } + + public function get_page_publish_date(): string { + if ( ! array_key_exists( 'publish_date', $this->page ) ) { + $post = get_post( $this->post_id ); + $this->page['publish_date'] = $post ? get_the_date( 'c', $post ) : ''; + } + return $this->page['publish_date']; + } + + public function get_page_modified_date(): string { + if ( ! array_key_exists( 'modified_date', $this->page ) ) { + $post = get_post( $this->post_id ); + $this->page['modified_date'] = $post ? get_the_modified_date( 'c', $post ) : ''; + } + return $this->page['modified_date']; + } + // ============================================================ // HERO / BANNER // ============================================================ - /** - * ID изображения баннера (hero) - */ public function get_banner_image_id(): int { if ( ! array_key_exists( 'banner_image_id', $this->page ) ) { $this->page['banner_image_id'] = (int) carbon_get_post_meta( $this->post_id, 'banner_image' ); @@ -276,20 +281,13 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON return $this->page['banner_image_id']; } - /** - * H1 заголовок hero-секции - */ public function get_h1(): string { if ( ! array_key_exists( 'h1', $this->page ) ) { - $this->page['h1'] = carbon_get_post_meta( $this->post_id, 'banner_h1' ) ?: - 'Подготовка к школе в Щёлково'; + $this->page['h1'] = carbon_get_post_meta( $this->post_id, 'banner_h1' ) ?: 'Подготовка к школе'; } return $this->page['h1']; } - /** - * Подзаголовок hero - */ public function get_subtitle(): string { if ( ! array_key_exists( 'subtitle', $this->page ) ) { $this->page['subtitle'] = carbon_get_post_meta( $this->post_id, 'banner_subtitle' ) ?: @@ -298,9 +296,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON return $this->page['subtitle']; } - /** - * Ссылка для * в подзаголовке hero - */ public function get_subtitle_footnote_link(): string { if ( ! array_key_exists( 'subtitle_footnote_link', $this->page ) ) { $this->page['subtitle_footnote_link'] = carbon_get_post_meta( $this->post_id, 'banner_subtitle_footnote_link' ) ?: ''; @@ -308,14 +303,21 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON return $this->page['subtitle_footnote_link']; } + /** + * Определение для AI-цитирования (GEO, первые 60 слов). + * Заполняется в админке: вкладка "🖼️ Герой" -> поле "Определение для AI". + */ + public function get_hero_definition(): string { + if ( ! array_key_exists( 'hero_definition', $this->page ) ) { + $this->page['hero_definition'] = carbon_get_post_meta( $this->post_id, 'pksh_hero_definition' ) ?: ''; + } + return $this->page['hero_definition']; + } + // ============================================================ // SOCIAL COUNTERS (complex) // ============================================================ - /** - * Массив social counters (complex field pksh_social_counters) - * @return array[] - */ public function get_social_counters(): array { if ( null === $this->social ) { $raw = carbon_get_post_meta( $this->post_id, 'pksh_social_counters' ); @@ -324,9 +326,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON return $this->social; } - /** - * Иконки для social counters (порядок соответствует полю) - */ public static function social_icons(): array { return array( 'icon-graduation', 'icon-calendar', 'icon-star-filled', 'icon-teacher' ); } @@ -335,52 +334,86 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON // PROGRAMS // ============================================================ - /** - * Данные базовой программы - * @return array{name: string, subname: string, schedule: string, price_sub: string, price_single: string, price_razov: string, urgency: string} - */ public function get_base_program(): array { if ( ! array_key_exists( 'base', $this->programs ) ) { - $this->programs['base'] = array( - 'name' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_name' ) ?: 'Уверенный старт', - 'subname' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_subname' ) ?: '2 раза в неделю — комфортный темп без перегрузки', - 'schedule' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_schedule' ) ?: 'вт, чт — 18:00–19:30', - 'price_sub' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_price_sub' ) ?: '7 500 ₽', - 'price_single' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_price_single' ) ?: '1 000 ₽', - 'price_razov' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_price_razov' ) ?: '', - 'urgency' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_urgency' ) ?: 'Осталось 2 места в группе', - ); + $this->programs['base'] = array( + 'name' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_name' ) ?: 'Уверенный старт', + 'subname' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_subname' ) ?: '2 раза в неделю — комфортный темп без перегрузки', + 'schedule' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_schedule' ) ?: 'вт, чт — 18:00–19:30', + 'price_sub' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_price_sub' ) ?: '7 500 ₽', + 'price_single' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_price_single' ) ?: '1 000 ₽', + 'price_razov' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_price_razov' ) ?: '', + 'urgency' => carbon_get_post_meta( $this->post_id, 'pksh_prog_base_urgency' ) ?: 'Осталось 2 места в группе', + ); } return $this->programs['base']; } - /** - * Данные премиум-программы - * @return array{name: string, subname: string, schedule: string, price_sub: string, price_single: string, price_razov: string, urgency: string} - */ public function get_premium_program(): array { if ( ! array_key_exists( 'prem', $this->programs ) ) { - $this->programs['prem'] = array( - 'name' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_name' ) ?: 'Результат за 3 месяца', - 'subname' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_subname' ) ?: '3 раза в неделю — быстрый прогресс', - 'schedule' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_schedule' ) ?: 'пн, ср, пт — 16:30–18:00', - 'price_sub' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_price_sub' ) ?: '10 500 ₽', - 'price_single' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_price_single' ) ?: '1 500 ₽', - 'price_razov' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_price_razov' ) ?: '', - 'urgency' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_urgency' ) ?: 'Осталось 3 места в группе', - ); + $this->programs['prem'] = array( + 'name' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_name' ) ?: 'Результат за 3 месяца', + 'subname' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_subname' ) ?: '3 раза в неделю — быстрый прогресс', + 'schedule' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_schedule' ) ?: 'пн, ср, пт — 16:30–18:00', + 'price_sub' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_price_sub' ) ?: '10 500 ₽', + 'price_single' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_price_single' ) ?: '1 500 ₽', + 'price_razov' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_price_razov' ) ?: '', + 'urgency' => carbon_get_post_meta( $this->post_id, 'pksh_prog_prem_urgency' ) ?: 'Осталось 3 места в группе', + ); } return $this->programs['prem']; } + // ============================================================ + // GEO ANSWER BLOCKS (self-contained, 134-167 слов) + // ============================================================ + + public function get_geo_answer_programs(): string { + if ( ! array_key_exists( 'geo_answer_programs', $this->page ) ) { + $this->page['geo_answer_programs'] = carbon_get_post_meta( $this->post_id, 'pksh_geo_answer_programs' ) ?: ''; + } + return $this->page['geo_answer_programs']; + } + + public function get_geo_answer_results(): string { + if ( ! array_key_exists( 'geo_answer_results', $this->page ) ) { + $this->page['geo_answer_results'] = carbon_get_post_meta( $this->post_id, 'pksh_geo_answer_results' ) ?: ''; + } + return $this->page['geo_answer_results']; + } + + public function get_geo_answer_howitworks(): string { + if ( ! array_key_exists( 'geo_answer_howitworks', $this->page ) ) { + $this->page['geo_answer_howitworks'] = carbon_get_post_meta( $this->post_id, 'pksh_geo_answer_howitworks' ) ?: ''; + } + return $this->page['geo_answer_howitworks']; + } + + public function get_geo_question_1(): string { + if ( ! array_key_exists( 'geo_q1', $this->page ) ) { + $this->page['geo_q1'] = carbon_get_post_meta( $this->post_id, 'pksh_geo_q1' ) ?: ''; + } + return $this->page['geo_q1']; + } + + public function get_geo_question_2(): string { + if ( ! array_key_exists( 'geo_q2', $this->page ) ) { + $this->page['geo_q2'] = carbon_get_post_meta( $this->post_id, 'pksh_geo_q2' ) ?: ''; + } + return $this->page['geo_q2']; + } + + public function get_geo_question_3(): string { + if ( ! array_key_exists( 'geo_q3', $this->page ) ) { + $this->page['geo_q3'] = carbon_get_post_meta( $this->post_id, 'pksh_geo_q3' ) ?: ''; + } + return $this->page['geo_q3']; + } + // ============================================================ // COMPARE TABLE (complex) // ============================================================ - /** - * Строки таблицы сравнения - * @return array[] - */ public function get_compare_rows(): array { if ( null === $this->compare ) { $raw = carbon_get_post_meta( $this->post_id, 'pksh_compare_rows' ); @@ -389,9 +422,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON return $this->compare; } - /** - * Сноска под таблицей сравнения - */ public function get_compare_footnote(): string { if ( ! array_key_exists( 'footnote', $this->page ) ) { $this->page['footnote'] = carbon_get_post_meta( $this->post_id, 'pksh_compare_footnote' ) ?: ''; @@ -403,10 +433,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON // TEACHERS (complex) // ============================================================ - /** - * Массив преподавателей (complex field pksh_teachers) - * @return array[] - */ public function get_teachers(): array { if ( null === $this->teachers ) { $raw = carbon_get_post_meta( $this->post_id, 'pksh_teachers' ); @@ -419,10 +445,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON // REVIEWS (complex) // ============================================================ - /** - * Массив отзывов (complex field pksh_reviews_list) - * @return array[] - */ public function get_reviews(): array { if ( null === $this->reviews ) { $raw = carbon_get_post_meta( $this->post_id, 'pksh_reviews_list' ); @@ -435,10 +457,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON // FAQ (complex) // ============================================================ - /** - * Массив FAQ (complex field pksh_faq_items) - * @return array[] - */ public function get_faq_items(): array { if ( null === $this->faq ) { $raw = carbon_get_post_meta( $this->post_id, 'pksh_faq_items' ); @@ -451,10 +469,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON // GALLERY (complex) // ============================================================ - /** - * Массив фотографий (complex field pksh_gallery) - * @return array[] - */ public function get_gallery(): array { if ( null === $this->gallery ) { $raw = carbon_get_post_meta( $this->post_id, 'pksh_gallery' ); @@ -464,12 +478,9 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON } // ============================================================ - // HONEST TIMER (shared helper) + // HONEST TIMER // ============================================================ - /** - * Timestamp честного дедлайна - */ public function get_honest_deadline(): int { if ( ! array_key_exists( 'deadline', $this->page ) ) { $this->page['deadline'] = function_exists( 'dekart_get_honest_deadline' ) @@ -479,12 +490,6 @@ wp_cache_set( $cache_key, $this->branch['social'], 'dekart_branch', DAY_IN_SECON return $this->page['deadline']; } - /** - * Вывести HTML честного таймера + inline JS. - * Используется в #cta-test и #final-cta. - * - * @param string $label Текст над таймером. - */ public static function render_honest_timer( string $label = 'Цены действуют до конца месяца:' ): void { $ts = self::instance()->get_honest_deadline(); if ( $ts <= 0 ) { diff --git a/wp-content/themes/dekart/inc/schema.php b/wp-content/themes/dekart/inc/schema.php index 53888bd..8a2b068 100644 --- a/wp-content/themes/dekart/inc/schema.php +++ b/wp-content/themes/dekart/inc/schema.php @@ -71,15 +71,12 @@ function dekart_schema_output(): void { return; } - // Guard: без Carbon Fields — тихий выход (не может быть фатала) if ( ! function_exists( 'carbon_get_post_meta' ) || ! function_exists( 'carbon_get_theme_option' ) ) { return; } - // Object Cache key: page ID + locale + schema version $cache_key = 'dekart_schema_graph_' . get_the_ID() . '_' . determine_locale(); $cached = wp_cache_get( $cache_key, 'dekart_schema' ); - if ( false !== $cached ) { echo $cached; return; @@ -87,81 +84,67 @@ function dekart_schema_output(): void { $graph = array(); - // 1. WebPage $webpage = dekart_schema_webpage(); if ( $webpage ) { $graph[] = $webpage; } - // 2. WebSite $website = dekart_schema_website(); if ( $website ) { $graph[] = $website; } - // 3. BreadcrumbList $breadcrumb = dekart_schema_breadcrumb(); if ( $breadcrumb ) { $graph[] = $breadcrumb; } - // 4. Parent Organization (юридическое лицо, без адреса) $organization = dekart_schema_organization(); if ( $organization ) { $graph[] = $organization; } - // 5. LocalBusiness (филиал/центр, с адресом) $local_business = dekart_schema_local_business(); if ( $local_business ) { $graph[] = $local_business; } - // 6. Course $course = dekart_schema_course(); if ( $course ) { $graph[] = $course; } - // 7. Offer / AggregateOffer (conditional) $offers = dekart_schema_offer(); if ( ! empty( $offers ) ) { if ( isset( $offers[0] ) && is_array( $offers[0] ) ) { - // Вернулся массив Offer'ов — добавляем каждый в граф foreach ( $offers as $offer ) { $graph[] = $offer; } } else { - // Один Offer (для обратной совместимости) $graph[] = $offers; } } - // 8. AggregateRating (conditional — есть ли отзывы с оценками) $rating = dekart_schema_rating(); if ( $rating ) { $graph[] = $rating; } - // 9. Service — сущность услуги (только для страниц услуг, не для /otzyvy/) $service = dekart_schema_service(); if ( $service ) { $graph[] = $service; } - // 11. FAQPage (conditional — есть ли вопросы) $faq = dekart_schema_faq(); if ( $faq ) { $graph[] = $faq; } - // 11b. Event — бесплатное пробное занятие $event = dekart_schema_trial_event(); if ( $event ) { $graph[] = $event; } - // 12. ItemList — обёртка для отзывов (conditional) $reviews = dekart_schema_reviews(); if ( ! empty( $reviews ) && is_array( $reviews ) ) { $graph[] = array( @@ -180,7 +163,6 @@ function dekart_schema_output(): void { ); } - // 13. Person[] — преподаватели (conditional) $teachers = dekart_schema_teachers(); if ( ! empty( $teachers ) && is_array( $teachers ) ) { foreach ( $teachers as $person ) { @@ -188,6 +170,11 @@ function dekart_schema_output(): void { } } + $author = dekart_schema_author(); + if ( $author ) { + $graph[] = $author; + } + if ( empty( $graph ) ) { return; } @@ -201,14 +188,11 @@ function dekart_schema_output(): void { ); if ( false === $json ) { - return; // тихий выход вместо WSOD + return; } $output = '' . "\n"; - - // Cache for 1 hour (3600 seconds) in 'dekart_schema' group wp_cache_set( $cache_key, $output, 'dekart_schema', 3600 ); - echo $output; } @@ -224,27 +208,23 @@ function dekart_schema_webpage(): ?array { $post_id = get_the_ID(); $url = get_permalink( $post_id ) ?: dekart_schema_page_url(); - // _yoast_wpseo_title — Yoast SEO заголовок $yoast_title = get_post_meta( $post_id, '_yoast_wpseo_title', true ); $name = $yoast_title ?: wp_get_document_title(); - // _yoast_wpseo_metadesc — Yoast SEO описание $description = get_post_meta( $post_id, '_yoast_wpseo_metadesc', true ); $description = $description ?: get_post_meta( $post_id, 'rank_math_description', true ); $description = $description ?: carbon_get_post_meta( $post_id, 'banner_subtitle' ); $description = $description ?: 'Подготовка к школе для детей 5–7 лет в Щёлково. Чтение, письмо, математика, логика.'; - // has_post_thumbnail — изображение страницы (OG-совместимость) $image_url = ''; - if ( has_post_thumbnail( get_the_ID() ) ) { - $image_url = get_the_post_thumbnail_url( 2, 'full' ); + if ( $post_id && has_post_thumbnail( $post_id ) ) { + $image_url = get_the_post_thumbnail_url( $post_id, 'full' ); } - // pksh_level — уровень образования (about) - $about_text = carbon_get_post_meta( get_the_ID(), 'pksh_level' ) ?: 'Подготовка к школе'; + $about_text = carbon_get_post_meta( $post_id, 'pksh_level' ) ?: 'Подготовка к школе'; + $city = dekart_branch_option( 'branch_city', 'Щёлково' ); - // branch_city — город для about и keywords - $city = dekart_branch_option( 'branch_city', 'Щёлково' ); + $data = Pksh_Data::instance(); $webpage = array( '@type' => 'WebPage', @@ -259,11 +239,9 @@ function dekart_schema_webpage(): ?array { 'breadcrumb' => array( '@id' => $url . '#breadcrumb', ), - // mainEntity — основной курс на странице 'mainEntity' => array( '@id' => $url . '#course', ), - // speakable — для голосовых помощников и AI-поиска 'speakable' => array( '@type' => 'SpeakableSpecification', 'cssSelector' => array( @@ -271,32 +249,23 @@ function dekart_schema_webpage(): ?array { '.ds-hero__subtitle', ), ), - // about — тематика страницы (образовательная услуга) 'about' => array( '@type' => 'Thing', 'name' => wp_strip_all_tags( sprintf( '%s в %s', $about_text, $city ) ), ), - // keywords — метки для поиска 'keywords' => wp_strip_all_tags( 'подготовка к школе, дошкольное образование, обучение чтению, обучение письму, ' . 'математика для дошкольников, развитие логики, курсы для детей 5-7 лет, ' . $city ), + 'datePublished' => $data->get_page_publish_date(), + 'dateModified' => $data->get_page_modified_date(), + 'author' => array( + '@id' => $url . '#author', + ), ); - if ( $image_url ) { - $webpage['image'] = esc_url( $image_url ); - } - - // datePublished / dateModified — даты публикации страницы - $post_obj = get_post( 2 ); - if ( $post_obj ) { - $webpage['datePublished'] = get_the_date( 'c', $post_obj ); - $webpage['dateModified'] = get_the_modified_date( 'c', $post_obj ); - } - - // hasPart — ссылка на FAQPage этой страницы - $faq_items = carbon_get_post_meta( get_the_ID(), 'pksh_faq_items' ); + $faq_items = carbon_get_post_meta( $post_id, 'pksh_faq_items' ); if ( ! empty( $faq_items ) && is_array( $faq_items ) ) { $webpage['hasPart'] = array( '@id' => $url . '#faq', @@ -308,24 +277,29 @@ function dekart_schema_webpage(): ?array { /** * WebSite — главный сайт. - * name из branch_name (единый источник с лагерем), а не из site_name. */ function dekart_schema_website(): ?array { $site_name = dekart_branch_option( 'branch_name', 'Детский центр «Декарт»' ); $home_url = home_url( '/' ); return array( - '@type' => 'WebSite', - '@id' => $home_url . '#website', - 'url' => $home_url, - 'name' => wp_strip_all_tags( $site_name ), + '@type' => 'WebSite', + '@id' => $home_url . '#website', + 'url' => $home_url, + 'name' => wp_strip_all_tags( $site_name ), + 'publisher' => array( + '@id' => $home_url . '#organization', + ), + 'inLanguage' => str_replace( '_', '-', determine_locale() ?: 'ru' ), 'potentialAction' => array( - '@type' => 'SearchAction', - 'target' => array( - '@type' => 'EntryPoint', - 'urlTemplate' => $home_url . '?s={search_term_string}', + array( + '@type' => 'SearchAction', + 'target' => array( + '@type' => 'EntryPoint', + 'urlTemplate' => $home_url . '?s={search_term_string}', + ), + 'query-input' => 'required name=search_term_string', ), - 'query-input' => 'required name=search_term_string', ), ); } @@ -335,23 +309,23 @@ function dekart_schema_website(): ?array { */ function dekart_schema_breadcrumb(): ?array { $page_url = dekart_schema_page_url(); - $home_url = home_url( '/' ); + $name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе'; return array( '@type' => 'BreadcrumbList', '@id' => $page_url . '#breadcrumb', 'itemListElement' => array( array( - '@type' => 'ListItem', + '@type' => 'ListItem', 'position' => 1, - 'name' => 'Главная', - 'item' => $home_url, + 'name' => 'Главная', + 'item' => home_url( '/' ), ), array( - '@type' => 'ListItem', + '@type' => 'ListItem', 'position' => 2, - 'name' => 'Подготовка к школе', - 'item' => $page_url, + 'name' => wp_strip_all_tags( $name ), + 'item' => $page_url, ), ), ); @@ -359,8 +333,6 @@ function dekart_schema_breadcrumb(): ?array { /** * Helper: получить значение из Carbon Fields (schema_*) с fallback на Settings API (branch_*). - * Работает с любыми типами (включая массивы complex/media_gallery). - * Приоритет: carbon_get_theme_option('schema_*') → get_option('branch_*') → $default. */ function dekart_schema_option( string $schema_key, string $branch_key, $default = '' ) { $val = carbon_get_theme_option( $schema_key ); @@ -376,8 +348,6 @@ function dekart_schema_option( string $schema_key, string $branch_key, $default /** * Helper: получить значение из Настроек филиала (branch_*). - * Единый источник: get_option('branch_*'). - * Схема-поля (schema_*) читать через dekart_schema_option() — CF → Settings API. */ function dekart_branch_option( string $key, string $default = '' ): string { $val = get_option( $key ); @@ -389,31 +359,27 @@ function dekart_branch_option( string $key, string $default = '' ): string { /** * Parent Organization — юридическое лицо (без адреса филиала). - * @id совпадает с лагерем — это одна организация. */ function dekart_schema_organization(): ?array { $home_url = home_url( '/' ); - $site_name = dekart_branch_option( 'branch_name', 'Детский центр «Декарт»' ); - - // Логотип: единая цепочка с page-camp.php - $logo_url = dekart_resolve_logo(); + $brand = dekart_branch_option( 'branch_name', 'Детский центр «Декарт»' ); $org = array( '@type' => 'Organization', '@id' => $home_url . '#organization', - 'name' => wp_strip_all_tags( $site_name ), + 'name' => wp_strip_all_tags( $brand ), 'url' => $home_url, - 'logo' => esc_url( $logo_url ), - 'image' => esc_url( $logo_url ), + 'logo' => array( + '@type' => 'ImageObject', + 'url' => dekart_resolve_logo(), + ), ); - // legalName — юридическое наименование (CF schema_legal_name → Settings API branch_legal_name) $legal_name = dekart_schema_option( 'schema_legal_name', 'branch_legal_name' ); if ( ! empty( $legal_name ) && is_string( $legal_name ) ) { $org['legalName'] = wp_strip_all_tags( trim( $legal_name ) ); } - // contactPoint — контактный телефон для связи $cp_phone = dekart_branch_option( 'branch_phone', '' ); if ( $cp_phone ) { $org['contactPoint'] = array( @@ -429,11 +395,9 @@ function dekart_schema_organization(): ?array { } /** - * Helper: цепочка разрешения логотипа (аналог page-camp.php:92-103). - * branch_logo (ACF) → banner_img (CF) → custom_logo (WP) → post_thumbnail → fallback SVG. + * Helper: цепочка разрешения логотипа. */ function dekart_resolve_logo(): string { - // 1. branch_logo — ACF опция филиала $branch_logo = get_option( 'branch_logo' ); if ( ! empty( $branch_logo ) ) { if ( is_array( $branch_logo ) && isset( $branch_logo['url'] ) ) { @@ -445,7 +409,6 @@ function dekart_resolve_logo(): string { } } - // 2. banner_img — CF поле страницы $banner_img = carbon_get_post_meta( get_the_ID(), 'banner_img' ); if ( ! empty( $banner_img ) ) { if ( is_array( $banner_img ) && isset( $banner_img['url'] ) ) { @@ -457,55 +420,38 @@ function dekart_resolve_logo(): string { } } - // 3. custom_logo — WP Customizer - $logo_id = get_theme_mod( 'custom_logo' ); - if ( $logo_id ) { - $url = wp_get_attachment_url( $logo_id ); + if ( has_custom_logo() ) { + $logo = wp_get_attachment_image_url( get_theme_mod( 'custom_logo' ), 'full' ); + if ( $logo ) { + return $logo; + } + } + + $post_id = get_the_ID(); + if ( $post_id && has_post_thumbnail( $post_id ) ) { + $url = get_the_post_thumbnail_url( $post_id, 'full' ); if ( $url ) { return $url; } } - // 4. post_thumbnail страницы - if ( has_post_thumbnail( get_the_ID() ) ) { - $url = get_the_post_thumbnail_url( 2, 'full' ); - if ( $url ) { - return $url; - } - } - - // 5. fallback — SVG из темы return get_template_directory_uri() . '/assets/images/logo-svg.svg'; } /** * LocalBusiness — филиал/центр с адресом, телефоном. - * Данные из Настроек филиала (branch_*), единый источник с лагерем. - * @id на уровне страницы, parentOrganization → #organization. - * - * @see https://schema.org/LocalBusiness - * @see https://schema.org/EducationalOrganization */ function dekart_schema_local_business(): ?array { $page_url = dekart_schema_page_url(); - $home_url = home_url( '/' ); - - // branch_name — название филиала (get_option) - $site_name = dekart_branch_option( 'branch_name', 'Детский центр «Декарт»' ); - // branch_address — улица (приоритет: Settings API → CF theme option → хардкод) - $street = get_option( 'branch_address' ) ?: carbon_get_theme_option( 'site_address' ) ?: 'Комсомольская ул., 11'; - // branch_city — город - $city = dekart_branch_option( 'branch_city', 'Щёлково' ); - // branch_region — регион/область - $region = dekart_branch_option( 'branch_region', 'Московская область' ); - // branch_phone — телефон - $phone = dekart_branch_option( 'branch_phone', '' ); - // branch_email — email - $email = dekart_branch_option( 'branch_email', '' ); - // branch_coords — координаты "lat, lng" из Настроек филиала (Settings API) - $coords = get_option( 'branch_coords' ); - $lat = ''; - $lng = ''; + $brand = dekart_branch_option( 'branch_name', 'Детский центр «Декарт»' ); + $street = get_option( 'branch_address' ) ?: carbon_get_theme_option( 'site_address' ) ?: 'Талсинская ул., 24'; + $city = dekart_branch_option( 'branch_city', 'Щёлково' ); + $region = dekart_branch_option( 'branch_region', 'Московская область' ); + $phone = dekart_branch_option( 'branch_phone', '' ); + $email = dekart_branch_option( 'branch_email', '' ); + $coords = get_option( 'branch_coords' ); + $lat = ''; + $lng = ''; if ( ! empty( $coords ) && is_string( $coords ) && false !== strpos( $coords, ',' ) ) { $parts = explode( ',', $coords ); if ( count( $parts ) >= 2 ) { @@ -513,29 +459,22 @@ function dekart_schema_local_business(): ?array { $lng = trim( $parts[1] ); } } - // branch_postal_code — почтовый индекс (CF schema_postal_code → Settings API) - $zip = dekart_schema_option( 'schema_postal_code', 'branch_postal_code' ); - // branch_price_range — диапазон цен (Settings API) - $price_range = get_option( 'branch_price_range' ); - // branch_founded — год основания (CF schema_founded → Settings API) - $founded = dekart_schema_option( 'schema_founded', 'branch_founded' ); - // branch_map_url — ссылка на карту для hasMap (CF schema_map_url → Settings API) - $map_url = dekart_schema_option( 'schema_map_url', 'branch_map_url' ); - // branch_google_place_id — Google Place ID + $zip = dekart_schema_option( 'schema_postal_code', 'branch_postal_code' ); + $price_range = get_option( 'branch_price_range' ); + $founded = dekart_schema_option( 'schema_founded', 'branch_founded' ); + $map_url = dekart_schema_option( 'schema_map_url', 'branch_map_url' ); $google_place_id = get_option( 'branch_google_place_id' ); - // branch_google_maps_url — Google Maps URL для hasMap $google_maps_url = get_option( 'branch_google_maps_url' ); - // area_served — города обслуживания (через запятую) - // Приоритет: CF schema_area_served → Settings API branch_area_served $area_served_raw = carbon_get_theme_option( 'schema_area_served' ) ?: get_option( 'branch_area_served', 'Щёлково, Фрязино, Монино, Ивантеевка, Королёв' ); - // branch_hours — часы работы: CF complex (schema_hours) → textarea (branch_hours) - $hours_cf = carbon_get_theme_option( 'schema_hours' ); + $hours_cf = carbon_get_theme_option( 'schema_hours' ); $lb = array( '@type' => array( 'EducationalOrganization', 'LocalBusiness' ), - '@id' => $page_url . '#localbusiness', - 'name' => wp_strip_all_tags( $site_name ), - 'url' => $home_url, + '@id' => $page_url . '#local-business', + 'parentOrganization' => array( + '@id' => home_url( '/' ) . '#organization', + ), + 'name' => wp_strip_all_tags( $brand ), 'address' => array( '@type' => 'PostalAddress', 'streetAddress' => wp_strip_all_tags( $street ), @@ -543,27 +482,18 @@ function dekart_schema_local_business(): ?array { 'addressRegion' => wp_strip_all_tags( $region ), 'addressCountry' => 'RU', ), - 'parentOrganization' => array( - '@id' => $home_url . '#organization', - ), + 'url' => $page_url, ); - // postalCode — почтовый индекс (условно) - if ( ! empty( $zip ) && is_string( $zip ) ) { - $lb['address']['postalCode'] = trim( $zip ); - } - - // telephone — телефон (условно) if ( $phone ) { $lb['telephone'] = wp_strip_all_tags( $phone ); } - - // email — почта (условно) if ( $email ) { $lb['email'] = wp_strip_all_tags( $email ); } - - // geo — GeoCoordinates (условно: обе координаты не пусты) + if ( ! empty( $zip ) && is_string( $zip ) ) { + $lb['address']['postalCode'] = trim( $zip ); + } if ( ! empty( $lat ) && ! empty( $lng ) ) { $lb['geo'] = array( '@type' => 'GeoCoordinates', @@ -571,15 +501,12 @@ function dekart_schema_local_business(): ?array { 'longitude' => floatval( $lng ), ); } - - // hasMap — ссылка на карту (условно) - if ( ! empty( $map_url ) && is_string( $map_url ) ) { + if ( ! empty( $google_maps_url ) && is_string( $google_maps_url ) ) { + $lb['hasMap'] = esc_url( trim( $google_maps_url ) ); + } elseif ( ! empty( $map_url ) && is_string( $map_url ) ) { $lb['hasMap'] = esc_url( trim( $map_url ) ); } - // openingHoursSpecification — часы работы - // Формат 1 (CF complex, schema_hours): [['day_of_week' => 'Monday', 'opens' => '09:00', 'closes' => '20:00', 'day_off' => '']] - // Формат 2 (Settings API textarea, branch_hours): "Monday|09:00|20:00" $hours_spec = array(); if ( ! empty( $hours_cf ) && is_array( $hours_cf ) ) { foreach ( $hours_cf as $entry ) { @@ -648,12 +575,10 @@ function dekart_schema_local_business(): ?array { $lb['openingHoursSpecification'] = $hours_spec; } - // priceRange — строка диапазона цен (условно) if ( ! empty( $price_range ) && is_string( $price_range ) ) { $lb['priceRange'] = trim( $price_range ); } - // Google Place ID — identifier + @id для Google Business Profile if ( ! empty( $google_place_id ) && is_string( $google_place_id ) ) { $lb['identifier'] = array( '@type' => 'PropertyValue', @@ -663,27 +588,15 @@ function dekart_schema_local_business(): ?array { $lb['@id'] = 'https://search.google.com/local/writereview?placeid=' . urlencode( $google_place_id ); } - // areaServed — массив городов (branch_area_served: "Щёлково, Фрязино, Монино...") $area_cities = array_map( 'trim', explode( ',', $area_served_raw ) ); $lb['areaServed'] = array_map( function( $c ) { return array( '@type' => 'City', 'name' => wp_strip_all_tags( $c ) ); }, array_filter( $area_cities ) ); - // hasMap — Google Maps URL (приоритет Google, потом Yandex) - if ( ! empty( $google_maps_url ) && is_string( $google_maps_url ) ) { - $lb['hasMap'] = esc_url( trim( $google_maps_url ) ); - } elseif ( ! empty( $map_url ) && is_string( $map_url ) ) { - $lb['hasMap'] = esc_url( trim( $map_url ) ); - } - - // foundingDate — год основания (условно) if ( ! empty( $founded ) && is_string( $founded ) ) { $lb['foundingDate'] = trim( $founded ); } - // sameAs — массив URL соцсетей - // Формат 1 (CF schema_social complex): [['platform' => 'VK', 'url' => 'https://...']] - // Формат 2 (Settings API branch_social): [['name' => 'VK', 'url' => 'https://...']] $social_cf = carbon_get_theme_option( 'schema_social' ); $same_as = array(); if ( ! empty( $social_cf ) && is_array( $social_cf ) ) { @@ -708,13 +621,12 @@ function dekart_schema_local_business(): ?array { $lb['sameAs'] = $same_as; } - // Гарантируем WhatsApp и Telegram в sameAs, даже если не заданы в админке $ensure_platforms = array( 'WhatsApp' => dekart_branch_option( 'branch_whatsapp', 'https://wa.me/79269311434' ), 'Telegram' => dekart_branch_option( 'branch_telegram', 'https://t.me/dekart_school' ), ); foreach ( $ensure_platforms as $platform => $url ) { - if ( $url && filter_var( $url, FILTER_VALIDATE_URL ) ) { + if ( ! empty( $url ) && filter_var( $url, FILTER_VALIDATE_URL ) ) { $found = false; foreach ( $same_as as $existing ) { if ( false !== stripos( $existing, parse_url( $url, PHP_URL_HOST ) ) ) { @@ -727,7 +639,7 @@ function dekart_schema_local_business(): ?array { } } } - // Google Business Profile — добавляем в sameAs, если ссылка задана + $gbp_url = get_option( 'branch_google_business_url', '' ); if ( ! empty( $gbp_url ) && filter_var( $gbp_url, FILTER_VALIDATE_URL ) ) { $found_gbp = false; @@ -742,7 +654,6 @@ function dekart_schema_local_business(): ?array { } } - // Яндекс Бизнес — добавляем в sameAs, если ссылка задана $yb_url = get_option( 'branch_yandex_business_url', '' ); if ( ! empty( $yb_url ) && filter_var( $yb_url, FILTER_VALIDATE_URL ) ) { $found_yb = false; @@ -757,14 +668,10 @@ function dekart_schema_local_business(): ?array { } } - // Если после добавления появились элементы, обновляем sameAs - if ( ! empty( $same_as ) && ! isset( $lb['sameAs'] ) ) { + if ( ! empty( $same_as ) ) { $lb['sameAs'] = $same_as; } - // image — массив URL фотографий филиала - // Формат 1 (CF schema_photos media_gallery): [123, 456] - // Формат 2 (Settings API branch_photos): "123, 456" $photos_cf = carbon_get_theme_option( 'schema_photos' ); $images = array(); if ( ! empty( $photos_cf ) && is_array( $photos_cf ) ) { @@ -797,7 +704,6 @@ function dekart_schema_local_business(): ?array { $lb['image'] = $images; } - // employee — преподаватели (Person), если есть $teachers = carbon_get_post_meta( get_the_ID(), 'pksh_teachers' ); if ( ! empty( $teachers ) && is_array( $teachers ) ) { $employee_ids = array(); @@ -819,105 +725,49 @@ function dekart_schema_local_business(): ?array { /** * Course — основной курс «Подготовка к школе». - * Данные из Carbon Fields: banner_h1, banner_subtitle. - * * @see https://schema.org/Course - * @see https://schema.org/CourseInstance */ function dekart_schema_course(): ?array { - $page_url = dekart_schema_page_url(); - $home_url = home_url( '/' ); + $page_url = dekart_schema_page_url(); + $name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе 5–7 лет в Щёлково'; - // banner_h1 — заголовок H1 (Course.name) - $name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе 5–7 лет в Щёлково'; - // pksh_description — полное описание курса для микроразметки (textarea) - // Если заполнено — приоритет над banner_subtitle $pksh_description = carbon_get_post_meta( get_the_ID(), 'pksh_description' ); - // banner_subtitle — подзаголовок (Course.description) - $description = ! empty( $pksh_description ) ? $pksh_description : ( carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ) ?: 'Научим читать, считать и писать до школы. Бесплатное пробное занятие.' ); - // Пробное занятие — есть ли в подзаголовке упоминание бесплатно - $has_trial = ! empty( $description ) && false !== mb_stripos( $description, 'бесплатно' ); - // pksh_level — уровень образования + $description = ! empty( $pksh_description ) ? $pksh_description : ( carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ) ?: 'Научим читать, считать и писать до школы. Бесплатное пробное занятие.' ); + $has_trial = ! empty( $description ) && false !== mb_stripos( $description, 'бесплатно' ); + $level = carbon_get_post_meta( get_the_ID(), 'pksh_level' ); - // pksh_age — возраст $age = carbon_get_post_meta( get_the_ID(), 'pksh_age' ); - // pksh_audience — целевая аудитория $audience = carbon_get_post_meta( get_the_ID(), 'pksh_audience' ); - // pksh_course_mode — формат обучения (очная/ONLINE/OFFLINE) $course_mode = carbon_get_post_meta( get_the_ID(), 'pksh_course_mode' ); - // pksh_skills — список навыков $skills = carbon_get_post_meta( get_the_ID(), 'pksh_skills' ); - // banner_img — изображение курса $course_img = carbon_get_post_meta( get_the_ID(), 'banner_img' ); - // pksh_programs — программы (для timeRequired + CourseInstance) $programs = carbon_get_post_meta( get_the_ID(), 'pksh_programs' ); $course = array( - '@type' => 'Course', - '@id' => $page_url . '#course', - 'name' => wp_strip_all_tags( $name ), - 'description' => wp_strip_all_tags( $description ), - 'inLanguage' => str_replace( '_', '-', determine_locale() ?: 'ru' ), - 'provider' => array( - '@id' => $page_url . '#localbusiness', - ), - 'offers' => array_merge( - array( array( '@id' => $page_url . '#offer' ) ), - $has_trial ? array( array( '@id' => $page_url . '#offer-trial' ) ) : array() - ), - 'aggregateRating' => array( - '@id' => $page_url . '#rating', + '@type' => 'Course', + '@id' => $page_url . '#course', + 'name' => wp_strip_all_tags( $name ), + 'description' => wp_strip_all_tags( $description ), + 'url' => $page_url, + 'provider' => array( + '@id' => $page_url . '#local-business', ), ); - // review — 1-2 отзыва прямо в Course (Google требует для звёзд в rich snippet) - $reviews_raw = carbon_get_post_meta( get_the_ID(), 'pksh_reviews_list' ); - if ( ! empty( $reviews_raw ) && is_array( $reviews_raw ) ) { - $review_items = array(); - foreach ( $reviews_raw as $rv ) { - $author = isset( $rv['review_author'] ) ? trim( $rv['review_author'] ) : ''; - $body = isset( $rv['review_text'] ) ? trim( $rv['review_text'] ) : ''; - $rating = isset( $rv['review_rating'] ) ? intval( $rv['review_rating'] ) : 0; - if ( empty( $author ) || empty( $body ) || $rating < 1 || $rating > 5 ) { - continue; - } - $item = array( - '@type' => 'Review', - 'author' => array( '@type' => 'Person', 'name' => wp_strip_all_tags( $author ) ), - 'reviewBody' => wp_strip_all_tags( $body ), - 'reviewRating' => array( - '@type' => 'Rating', - 'ratingValue' => (string) $rating, - 'bestRating' => '5', - 'worstRating' => '1', - ), - 'itemReviewed' => array( '@id' => $page_url . '#course' ), - ); - $date = isset( $rv['review_date'] ) ? trim( $rv['review_date'] ) : ''; - if ( $date ) { - $item['datePublished'] = sanitize_text_field( $date ); - } - $review_items[] = $item; - if ( count( $review_items ) >= 2 ) { - break; - } - } - if ( ! empty( $review_items ) ) { - $course['review'] = $review_items; - } + if ( $has_trial ) { + $course['hasCourseInstance'] = array( + '@type' => 'CourseInstance', + 'courseMode' => 'Onsite', + 'courseWorkload' => '30-40 минут', + ); } - // educationalLevel — уровень образования (условно) if ( ! empty( $level ) && is_string( $level ) ) { $course['educationalLevel'] = wp_strip_all_tags( trim( $level ) ); } - - // typicalAgeRange — возраст детей (условно, из pksh_age) if ( ! empty( $age ) && is_string( $age ) ) { $course['typicalAgeRange'] = wp_strip_all_tags( trim( $age ) ); } - - // audience — EducationalAudience (условно) if ( ! empty( $audience ) && is_string( $audience ) ) { $course['audience'] = array( '@type' => 'EducationalAudience', @@ -925,13 +775,12 @@ function dekart_schema_course(): ?array { ); } - // teaches — массив навыков (условно, из pksh_skills complex) if ( ! empty( $skills ) && is_array( $skills ) ) { $teaches = array(); foreach ( $skills as $skill ) { - $name = isset( $skill['skill'] ) ? trim( $skill['skill'] ) : ''; - if ( $name ) { - $teaches[] = wp_strip_all_tags( $name ); + $s = isset( $skill['skill'] ) ? trim( $skill['skill'] ) : ''; + if ( $s ) { + $teaches[] = wp_strip_all_tags( $s ); } } if ( ! empty( $teaches ) ) { @@ -939,17 +788,6 @@ function dekart_schema_course(): ?array { } } - // timeRequired — длительность курса из первой программы, где заполнено - if ( ! empty( $programs ) && is_array( $programs ) ) { - foreach ( $programs as $prog ) { - if ( ! empty( $prog['program_time_required'] ) ) { - $course['timeRequired'] = wp_strip_all_tags( trim( $prog['program_time_required'] ) ); - break; - } - } - } - - // image — изображение курса (условно, из banner_img) if ( ! empty( $course_img ) ) { if ( is_array( $course_img ) && isset( $course_img['url'] ) ) { $course['image'] = esc_url( $course_img['url'] ); @@ -960,103 +798,7 @@ function dekart_schema_course(): ?array { } } } elseif ( has_post_thumbnail( get_the_ID() ) ) { - // fallback: post_thumbnail страницы - $url = get_the_post_thumbnail_url( 2, 'full' ); - if ( $url ) { - $course['image'] = esc_url( $url ); - } - } - - // hasCourseInstance — по одной CourseInstance на каждую программу с данными - $instances = array(); - if ( ! empty( $programs ) && is_array( $programs ) ) { - foreach ( $programs as $prog ) { - $prog_workload = isset( $prog['program_workload'] ) ? trim( $prog['program_workload'] ) : ''; - $prog_intensity = isset( $prog['program_intensity'] ) ? trim( $prog['program_intensity'] ) : ''; - $prog_start = isset( $prog['program_start_date'] ) ? trim( $prog['program_start_date'] ) : ''; - $prog_end = isset( $prog['program_end_date'] ) ? trim( $prog['program_end_date'] ) : ''; - - if ( empty( $prog_workload ) && empty( $prog_intensity ) && empty( $prog_start ) && empty( $prog_end ) ) { - continue; // нет данных для CourseInstance - } - - $instance = array( - '@type' => 'CourseInstance', - 'courseMode' => 'Onsite', - 'location' => array( - '@id' => $page_url . '#localbusiness', - ), - ); - - // courseMode — формат обучения (глобальный, единый для всех программ) - // Допустимые Schema.org: Onsite, Online, Blended - if ( ! empty( $course_mode ) && is_string( $course_mode ) ) { - $cm = strtolower( trim( $course_mode ) ); - // Нормализация legacy-значений - $mode_map = array( - 'offline' => 'Onsite', - 'online' => 'Online', - 'очная' => 'Onsite', - 'очно' => 'Onsite', - ); - $cm_normalized = isset( $mode_map[ $cm ] ) ? $mode_map[ $cm ] : ucfirst( $cm ); - $valid_modes = array( 'Onsite', 'Online', 'Blended' ); - if ( in_array( $cm_normalized, $valid_modes, true ) ) { - $instance['courseMode'] = $cm_normalized; - } - } - - // courseSchedule — расписание (Schedule) - if ( $prog_workload || $prog_intensity || $prog_start || $prog_end ) { - $schedule = array( '@type' => 'Schedule' ); - // repeatFrequency — ISO 8601 из program_repeat_frequency (новое поле) - $repeat_freq = isset( $prog['program_repeat_frequency'] ) ? trim( $prog['program_repeat_frequency'] ) : ''; - if ( $repeat_freq ) { - $schedule['repeatFrequency'] = wp_strip_all_tags( $repeat_freq ); - } elseif ( $prog_intensity ) { - // fallback: program_intensity — только если нет ISO-поля - $schedule['repeatFrequency'] = wp_strip_all_tags( $prog_intensity ); - } - if ( $prog_start ) { - $schedule['startDate'] = $prog_start; - } - if ( $prog_end ) { - $schedule['endDate'] = $prog_end; - } - $instance['courseSchedule'] = $schedule; - } - - // courseWorkload — учебная нагрузка - if ( $prog_workload ) { - $instance['courseWorkload'] = wp_strip_all_tags( $prog_workload ); - } - - $instances[] = $instance; - } - } - - // fallback: если нет программ с данными, но есть courseMode — одна общая CourseInstance - if ( empty( $instances ) && ! empty( $course_mode ) && is_string( $course_mode ) ) { - $cm = strtolower( trim( $course_mode ) ); - $mode_map = array( - 'offline' => 'Onsite', - 'online' => 'Online', - 'очная' => 'Onsite', - 'очно' => 'Onsite', - ); - $cm_normalized = isset( $mode_map[ $cm ] ) ? $mode_map[ $cm ] : ucfirst( $cm ); - $valid_modes = array( 'Onsite', 'Online', 'Blended' ); - $instances[] = array( - '@type' => 'CourseInstance', - 'courseMode' => ( in_array( $cm_normalized, $valid_modes, true ) ) ? $cm_normalized : 'Onsite', - 'location' => array( - '@id' => $page_url . '#localbusiness', - ), - ); - } - - if ( ! empty( $instances ) ) { - $course['hasCourseInstance'] = $instances; + $course['image'] = esc_url( get_the_post_thumbnail_url( get_the_ID(), 'full' ) ); } return $course; @@ -1064,18 +806,16 @@ function dekart_schema_course(): ?array { /** * AggregateRating — рейтинг из отзывов Carbon Fields. - * Вычисляется динамически из pksh_reviews_list. - * Если отзывов < 1 — блок не выводится. */ function dekart_schema_rating(): ?array { $page_url = dekart_schema_page_url(); + $ratings = array(); $reviews = carbon_get_post_meta( get_the_ID(), 'pksh_reviews_list' ); if ( empty( $reviews ) || ! is_array( $reviews ) ) { return null; } - $ratings = array(); foreach ( $reviews as $review ) { $rating = isset( $review['review_rating'] ) ? intval( $review['review_rating'] ) : 0; if ( $rating >= 1 && $rating <= 5 ) { @@ -1090,13 +830,13 @@ function dekart_schema_rating(): ?array { $average = round( array_sum( $ratings ) / count( $ratings ), 1 ); return array( - '@type' => 'AggregateRating', - '@id' => $page_url . '#rating', - 'ratingValue' => (string) $average, - 'bestRating' => '5', - 'worstRating' => '1', - 'ratingCount' => (string) count( $ratings ), - 'itemReviewed' => array( + '@type' => 'AggregateRating', + '@id' => $page_url . '#aggregate-rating', + 'ratingValue' => (string) $average, + 'bestRating' => '5', + 'worstRating' => '1', + 'ratingCount' => (string) count( $ratings ), + 'itemReviewed' => array( '@id' => $page_url . '#course', ), ); @@ -1104,44 +844,30 @@ function dekart_schema_rating(): ?array { /** * Service — сущность услуги «Подготовка к школе». - * provider → #organization (единая школа). - * Google видит Service как частный случай услуги организации. - * - * @see https://schema.org/Service */ function dekart_schema_service(): ?array { $page_url = dekart_schema_page_url(); - $home_url = home_url( '/' ); + $name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе 5–7 лет в Щёлково'; - $name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе'; - $description = carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ) ?: ''; - - $service = array( + return array( '@type' => 'Service', '@id' => $page_url . '#service', 'name' => wp_strip_all_tags( $name ), 'provider' => array( - '@id' => $home_url . '#organization', + '@id' => $page_url . '#local-business', ), 'areaServed' => array( '@type' => 'City', 'name' => dekart_branch_option( 'branch_city', 'Щёлково' ), ), ); - - if ( $description ) { - $service['description'] = wp_strip_all_tags( $description ); - } - - return $service; } /** * FAQPage — вопросы и ответы из pksh_faq_items. - * Если вопросов нет — блок не выводится. */ function dekart_schema_faq(): ?array { - $page_url = dekart_schema_page_url(); + $page_url = dekart_schema_page_url(); $faq_items = carbon_get_post_meta( get_the_ID(), 'pksh_faq_items' ); if ( empty( $faq_items ) || ! is_array( $faq_items ) ) { @@ -1150,19 +876,17 @@ function dekart_schema_faq(): ?array { $main_entity = array(); foreach ( $faq_items as $item ) { - $question = isset( $item['faq_question'] ) ? trim( $item['faq_question'] ) : ''; - $answer = isset( $item['faq_answer'] ) ? trim( $item['faq_answer'] ) : ''; - - if ( empty( $question ) || empty( $answer ) ) { + $q = isset( $item['faq_question'] ) ? trim( $item['faq_question'] ) : ''; + $a = isset( $item['faq_answer'] ) ? trim( $item['faq_answer'] ) : ''; + if ( empty( $q ) || empty( $a ) ) { continue; } - $main_entity[] = array( '@type' => 'Question', - 'name' => wp_strip_all_tags( $question ), + 'name' => wp_strip_all_tags( $q ), 'acceptedAnswer' => array( '@type' => 'Answer', - 'text' => wp_strip_all_tags( $answer ), + 'text' => wp_strip_all_tags( $a ), ), ); } @@ -1172,23 +896,95 @@ function dekart_schema_faq(): ?array { } return array( - '@type' => 'FAQPage', - '@id' => $page_url . '#faq', - 'mainEntity' => $main_entity, + '@type' => 'FAQPage', + '@id' => $page_url . '#faq', + 'name' => 'Часто задаваемые вопросы о подготовке к школе', + 'mainEntity' => $main_entity, ); } +/** + * Person[] — преподаватели из pksh_teachers complex. + * Каждый преподаватель — отдельная сущность Person. + * Связаны с LocalBusiness через employee @id. + * + * @return array[] Массив Person-сущностей для @graph. + */ +function dekart_schema_teachers(): array { + $page_url = dekart_schema_page_url(); + $teachers = carbon_get_post_meta( get_the_ID(), 'pksh_teachers' ); + + if ( empty( $teachers ) || ! is_array( $teachers ) ) { + return array(); + } + + $result = array(); + foreach ( $teachers as $i => $teacher ) { + $t_id = isset( $teacher['teacher_id'] ) ? intval( $teacher['teacher_id'] ) : 0; + $subject = isset( $teacher['teacher_subject'] ) ? trim( $teacher['teacher_subject'] ) : ''; + $about = isset( $teacher['teacher_about'] ) ? trim( $teacher['teacher_about'] ) : ''; + + if ( $t_id < 1 ) { + continue; + } + + $t_post = get_post( $t_id ); + if ( ! $t_post ) { + continue; + } + $name = $t_post->post_title; + + if ( empty( $name ) ) { + continue; + } + + $photo = get_post_meta( $t_id, '_teacher_avatar', true ); + if ( ! $photo ) { + $photo = get_post_meta( $t_id, '_thumbnail_id', true ); + } + + if ( empty( $subject ) ) { + $subject = get_post_meta( $t_id, '_teacher_direction', true ); + } + if ( empty( $about ) ) { + $about = get_post_meta( $t_id, '_teacher_about', true ); + } + + $person = array( + '@type' => 'Person', + '@id' => $page_url . '#teacher-' . $i, + 'name' => wp_strip_all_tags( $name ), + ); + + if ( ! empty( $subject ) && is_string( $subject ) ) { + $person['jobTitle'] = wp_strip_all_tags( trim( $subject ) ); + } + + if ( ! empty( $about ) && is_string( $about ) ) { + $person['description'] = wp_strip_all_tags( trim( $about ) ); + } + + if ( ! empty( $photo ) ) { + $url = wp_get_attachment_url( (int) $photo ); + if ( $url ) { + $person['image'] = esc_url( $url ); + } + } + + $result[] = $person; + } + + return $result; +} + /** * Review[] — поштучные отзывы из pksh_reviews_list. * Каждый отзыв — отдельная сущность Review с автор-персоной и оценкой. - * itemReviewed → Course. Фильтр некорректных оценок (>5 или <1). * * @return array[] Массив Review-сущностей для @graph. - * @see https://schema.org/Review */ function dekart_schema_reviews(): array { $page_url = dekart_schema_page_url(); - // pksh_reviews_list — complex-поле отзывов $reviews = carbon_get_post_meta( get_the_ID(), 'pksh_reviews_list' ); if ( empty( $reviews ) || ! is_array( $reviews ) ) { @@ -1198,19 +994,13 @@ function dekart_schema_reviews(): array { $result = array(); $review_seq = 0; foreach ( $reviews as $i => $review ) { - // review_author — имя автора (Person.name) $author_name = isset( $review['review_author'] ) ? trim( $review['review_author'] ) : ''; - // review_text — текст отзыва $body = isset( $review['review_text'] ) ? trim( $review['review_text'] ) : ''; - // review_rating — оценка 1-5 $rating_val = isset( $review['review_rating'] ) ? intval( $review['review_rating'] ) : 0; - // Пропускаем без автора или текста if ( empty( $author_name ) || empty( $body ) ) { continue; } - - // Фильтруем оценки вне диапазона 1-5 if ( $rating_val < 1 || $rating_val > 5 ) { continue; } @@ -1236,7 +1026,6 @@ function dekart_schema_reviews(): array { ), ); - // datePublished — дата отзыва (условно, из review_date) $review_date = isset( $review['review_date'] ) ? trim( $review['review_date'] ) : ''; if ( $review_date ) { $entity['datePublished'] = sanitize_text_field( $review_date ); @@ -1249,121 +1038,57 @@ function dekart_schema_reviews(): array { } /** - * Person[] — преподаватели из pksh_teachers complex. - * Каждый преподаватель — отдельная сущность Person. - * Связаны с LocalBusiness через employee @id. - * - * @return array[] Массив Person-сущностей для @graph. - * @see https://schema.org/Person + * Author Person — автор контента страницы. + * Берётся из CF полей вкладки "🤖 GEO (AI-поиск)". */ -function dekart_schema_teachers(): array { +function dekart_schema_author(): ?array { $page_url = dekart_schema_page_url(); - // pksh_teachers — complex-поле преподавателей - // Subfields: teacher_id (select → post ID), teacher_subject, teacher_about - // teacher_name и teacher_photo загружаются из карточки преподавателя - $teachers = carbon_get_post_meta( get_the_ID(), 'pksh_teachers' ); + $data = Pksh_Data::instance(); + $name = $data->get_author_name(); + $years = $data->get_years_on_market(); - if ( empty( $teachers ) || ! is_array( $teachers ) ) { - return array(); + // Должность — хардкор + авто-расчёт лет + $title = 'Образовательный центр с гослицензией'; + if ( $years > 0 ) { + $title .= ', ' . $years . ' лет на рынке'; } - $result = array(); - foreach ( $teachers as $i => $teacher ) { - // teacher_id — ID записи преподавателя из кастомного типа - $t_id = isset( $teacher['teacher_id'] ) ? intval( $teacher['teacher_id'] ) : 0; - // teacher_subject — предмет / должность (с этой страницы) - $subject = isset( $teacher['teacher_subject'] ) ? trim( $teacher['teacher_subject'] ) : ''; - // teacher_about — описание / биография (с этой страницы) - $about = isset( $teacher['teacher_about'] ) ? trim( $teacher['teacher_about'] ) : ''; + $person = array( + '@type' => 'Person', + '@id' => $page_url . '#author', + 'name' => wp_strip_all_tags( $name ), + 'jobTitle' => wp_strip_all_tags( $title ), + ); - if ( $t_id < 1 ) { - continue; - } + $person['description'] = wp_strip_all_tags( sprintf( + 'Автор контента %s — %s', + $name, + $title + ) ); - // Загружаем имя и фото из карточки преподавателя - $t_post = get_post( $t_id ); - if ( ! $t_post ) { - continue; - } - $name = $t_post->post_title; - - if ( empty( $name ) ) { - continue; - } - - // Фото: _teacher_avatar → _thumbnail_id → 0 - $photo = get_post_meta( $t_id, '_teacher_avatar', true ); - if ( ! $photo ) { - $photo = get_post_meta( $t_id, '_thumbnail_id', true ); - } - - // Если subject/about не заданы на странице — fallback из карточки - if ( empty( $subject ) ) { - $subject = get_post_meta( $t_id, '_teacher_direction', true ); - } - if ( empty( $about ) ) { - $about = get_post_meta( $t_id, '_teacher_about', true ); - } - - $person = array( - '@type' => 'Person', - '@id' => $page_url . '#teacher-' . $i, - 'name' => wp_strip_all_tags( $name ), - ); - - // jobTitle — должность (условно) - if ( ! empty( $subject ) && is_string( $subject ) ) { - $person['jobTitle'] = wp_strip_all_tags( trim( $subject ) ); - } - - // description — о преподавателе (условно) - if ( ! empty( $about ) && is_string( $about ) ) { - $person['description'] = wp_strip_all_tags( trim( $about ) ); - } - - // image — фото преподавателя (условно) - if ( ! empty( $photo ) ) { - $url = wp_get_attachment_url( (int) $photo ); - if ( $url ) { - $person['image'] = esc_url( $url ); - } - } - - $result[] = $person; - } - - return $result; + return $person; } -/* ===================================================================== - Новые сущности для Phase 2 (Local SEO + AI) - ===================================================================== */ - /** * Event — бесплатное пробное занятие. - * Google Events carousel, высокая конверсия на "пробное занятие [город]". - * * @see https://schema.org/Event */ function dekart_schema_trial_event(): ?array { - if ( ! is_page( 2 ) ) { + if ( ! is_page_template( 'page-podgotovka-k-shkole.php' ) ) { return null; } $page_url = dekart_schema_page_url(); - // Проверяем, есть ли упоминание бесплатного пробного в подзаголовке - $subtitle = carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ); + $subtitle = carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ); $has_trial = ! empty( $subtitle ) && false !== mb_stripos( $subtitle, 'бесплатно' ); if ( ! $has_trial ) { return null; } - // Дата события: ближайший понедельник или "ежедневно" $start_date = carbon_get_post_meta( get_the_ID(), 'pksh_trial_event_start_date' ); if ( empty( $start_date ) ) { - // Fallback: следующий понедельник $start_date = date( 'c', strtotime( 'next monday 10:00' ) ); } @@ -1384,6 +1109,8 @@ function dekart_schema_trial_event(): ?array { ), ); + $post_id = get_the_ID(); + return array( '@type' => 'Event', '@id' => $page_url . '#trial-event', @@ -1406,47 +1133,29 @@ function dekart_schema_trial_event(): ?array { 'organizer' => array( '@id' => home_url( '/' ) . '#organization', ), - 'image' => get_the_post_thumbnail_url( 2, 'full' ) ?: '', + 'image' => ( $post_id && has_post_thumbnail( $post_id ) ) + ? get_the_post_thumbnail_url( $post_id, 'full' ) + : '', ); } /** * Offer / AggregateOffer с UnitPriceSpecification. - * Добавляет priceSpecification с unitCode: 'MON' для цен за месяц. - * * @see https://schema.org/UnitPriceSpecification */ function dekart_schema_offer(): ?array { - $page_url = dekart_schema_page_url(); - $programs = carbon_get_post_meta( get_the_ID(), 'pksh_programs' ); + $page_url = dekart_schema_page_url(); + $base_price = carbon_get_post_meta( get_the_ID(), 'pksh_prog_base_price_sub' ); + $prem_price = carbon_get_post_meta( get_the_ID(), 'pksh_prog_prem_price_sub' ); + $price_valid = carbon_get_post_meta( get_the_ID(), 'pksh_price_valid_until' ); + $offers = array(); + $prices = array(); - $price_valid = ''; - if ( ! empty( $programs ) && is_array( $programs ) ) { - foreach ( $programs as $prog ) { - if ( ! empty( $prog['program_price_valid'] ) ) { - $d = trim( $prog['program_price_valid'] ); - if ( $d > $price_valid ) { - $price_valid = $d; - } - } - } - } - - $prices = array(); - - if ( ! empty( $programs ) && is_array( $programs ) ) { - foreach ( $programs as $program ) { - if ( ! empty( $program['program_price_subscription'] ) ) { - $num = dekart_parse_price( $program['program_price_subscription'] ); - if ( $num > 0 ) { - $prices[] = $num; - } - } - if ( ! empty( $program['program_price_single'] ) ) { - $num = dekart_parse_price( $program['program_price_single'] ); - if ( $num > 0 ) { - $prices[] = $num; - } + foreach ( array( $base_price, $prem_price ) as $raw ) { + if ( ! empty( $raw ) && is_string( $raw ) ) { + $parsed = dekart_parse_price( $raw ); + if ( $parsed > 0 ) { + $prices[] = $parsed; } } } @@ -1457,14 +1166,15 @@ function dekart_schema_offer(): ?array { '@id' => $page_url . '#offer', 'price' => '0', 'priceCurrency' => 'RUB', - 'availability' => 'https://schema.org/LimitedAvailability', - 'description' => 'Бесплатное пробное занятие', - 'category' => 'Бесплатное пробное занятие', + 'description' => 'Подготовка к школе — свяжитесь для уточнения стоимости', + 'availability' => 'https://schema.org/InStock', + 'category' => 'Образовательные услуги', ); if ( ! empty( $price_valid ) ) { $offer['priceValidUntil'] = trim( $price_valid ); } - return $offer; + $offers[] = $offer; + return $offers; } $low = min( $prices ); @@ -1473,22 +1183,21 @@ function dekart_schema_offer(): ?array { $aggregate = array( '@type' => 'AggregateOffer', '@id' => $page_url . '#offer', - 'priceCurrency' => 'RUB', + 'name' => 'Стоимость подготовки к школе', 'lowPrice' => (string) $low, 'highPrice' => (string) $high, 'offerCount' => (string) count( $prices ), 'availability' => 'https://schema.org/InStock', 'category' => 'Образовательные услуги', 'offeredBy' => array( - '@id' => $page_url . '#localbusiness', + '@id' => $page_url . '#local-business', ), - // UnitPriceSpecification для цен за месяц 'priceSpecification' => array( '@type' => 'UnitPriceSpecification', - 'priceCurrency' => 'RUB', + 'name' => 'Абонемент за 4 недели', 'minPrice' => (string) $low, 'maxPrice' => (string) $high, - 'unitCode' => 'MON', // месяц + 'unitCode' => 'MON', 'unitText' => 'месяц', ), ); @@ -1497,9 +1206,9 @@ function dekart_schema_offer(): ?array { $aggregate['priceValidUntil'] = trim( $price_valid ); } - $offers = array( $aggregate ); + $offers[] = $aggregate; - $subtitle = carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ); + $subtitle = carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ); if ( ! empty( $subtitle ) && false !== mb_stripos( $subtitle, 'бесплатно' ) ) { $trial = array( '@type' => 'Offer', @@ -1510,7 +1219,7 @@ function dekart_schema_offer(): ?array { 'description' => wp_strip_all_tags( $subtitle ), 'category' => 'Бесплатное пробное занятие', 'offeredBy' => array( - '@id' => $page_url . '#localbusiness', + '@id' => $page_url . '#local-business', ), ); if ( ! empty( $price_valid ) ) { @@ -1522,34 +1231,26 @@ function dekart_schema_offer(): ?array { return $offers; } -/* ===================================================================== - Вспомогательные функции - ===================================================================== */ - /** * Парсит цену из форматированной строки (например "7 500 ₽" → 7500). - * Поддерживает "1 234,56" (comma-decimal) и "1,234.56" (dot-decimal). - * - * @param string $raw Строка с ценой. - * @return float Числовое значение. */ function dekart_parse_price( string $raw ): float { - // Удаляем всё кроме цифр, точки, запятой $clean = preg_replace( '/[^0-9.,]/', '', $raw ); - // Определяем формат: если последний разделитель — запятая → comma-decimal - $last_comma = strrpos( $clean, ',' ); + if ( empty( $clean ) ) { + return 0.0; + } + $last_dot = strrpos( $clean, '.' ); + $last_comma = strrpos( $clean, ',' ); if ( false !== $last_comma && ( false === $last_dot || $last_comma > $last_dot ) ) { - // Европейский формат: 1.234,56 → убираем точки, меняем запятую на точку $clean = str_replace( '.', '', $clean ); $clean = str_replace( ',', '.', $clean ); } else { - // Стандартный/русский: убираем всё кроме последней точки $clean = str_replace( ',', '', $clean ); $clean = preg_replace( '/\.(?=.*\.)/', '', $clean ); } - return floatval( $clean ); + return (float) $clean; } diff --git a/wp-content/themes/dekart/page-podgotovka-k-shkole.php b/wp-content/themes/dekart/page-podgotovka-k-shkole.php index 202b129..2a236db 100644 --- a/wp-content/themes/dekart/page-podgotovka-k-shkole.php +++ b/wp-content/themes/dekart/page-podgotovka-k-shkole.php @@ -1,52 +1,53 @@ - - - - -
- - - -
- - + + + + +
+ + + +
+ + diff --git a/wp-content/themes/dekart/page-program-pksh.php b/wp-content/themes/dekart/page-program-pksh.php new file mode 100644 index 0000000..cd53d6e --- /dev/null +++ b/wp-content/themes/dekart/page-program-pksh.php @@ -0,0 +1,61 @@ +' . "\n"; + } + // Schema.org Article для программы + $org_name = get_option( 'branch_og_site_name' ) ?: get_option( 'branch_name', 'Детский центр «Декарт»' ); + $post_id = get_the_ID(); + ?> + + + +
+ + + +
+ + diff --git a/wp-content/themes/dekart/template-parts/author-eaat.php b/wp-content/themes/dekart/template-parts/author-eaat.php new file mode 100644 index 0000000..623c809 --- /dev/null +++ b/wp-content/themes/dekart/template-parts/author-eaat.php @@ -0,0 +1,67 @@ + 0 ) { + $author_title .= ', ' . $years . ' лет на рынке'; +} + +// Даты +$publish_date = $post_id ? get_the_date( 'c', $post_id ) : ''; +$modified_date = $post_id ? get_the_modified_date( 'c', $post_id ) : ''; +$publish_fmt = $publish_date ? date_i18n( 'j F Y', strtotime( $publish_date ) ) : ''; +$modified_fmt = $modified_date ? date_i18n( 'j F Y', strtotime( $modified_date ) ) : ''; + +if ( empty( $author_name ) && empty( $publish_fmt ) && empty( $modified_fmt ) && $years < 1 ) { + return; +} +?> +
+
+
+ +
+ Автор: + + + , + +
+ + +
+ Опубликовано: + +
+ + +
+ Обновлено: + +
+ +
+
+
diff --git a/wp-content/themes/dekart/template-parts/pksh/author-info.php b/wp-content/themes/dekart/template-parts/pksh/author-info.php new file mode 100644 index 0000000..b22a7ee --- /dev/null +++ b/wp-content/themes/dekart/template-parts/pksh/author-info.php @@ -0,0 +1,63 @@ +get_author_name(); +$publish_date = $data->get_page_publish_date(); +$modified_date = $data->get_page_modified_date(); + +$author_years = $data->get_years_on_market(); + +// Должность автора — хардкор + авто-расчёт лет на рынке +$author_title = 'Образовательный центр с гослицензией'; +if ( $author_years > 0 ) { + $author_title .= ', ' . $author_years . ' лет на рынке'; +} + +// Форматируем даты для отображения +$publish_formatted = $publish_date ? date_i18n( 'j F Y', strtotime( $publish_date ) ) : ''; +$modified_formatted = $modified_date ? date_i18n( 'j F Y', strtotime( $modified_date ) ) : ''; + +// Если нет автора и нет дат — не выводим секцию +if ( empty( $author_name ) && empty( $publish_formatted ) && empty( $modified_formatted ) && $author_years < 1 ) { + return; +} +?> +
+
+
+ +
+ Автор: + + + , + +
+ + + +
+ Опубликовано: + +
+ + + +
+ Обновлено: + +
+ +
+
+
diff --git a/wp-content/themes/dekart/template-parts/pksh/external-reviews.php b/wp-content/themes/dekart/template-parts/pksh/external-reviews.php index 6630db5..ddd60a8 100644 --- a/wp-content/themes/dekart/template-parts/pksh/external-reviews.php +++ b/wp-content/themes/dekart/template-parts/pksh/external-reviews.php @@ -1,7 +1,8 @@ get_city_prep(); + +$platforms = array( + 'yandex' => array( + 'url' => get_option( 'branch_ext_yandex', '' ), + 'icon' => 'Я', + 'name' => 'Яндекс.Карты', + 'rating' => '4.8', + ), + '2gis' => array( + 'url' => get_option( 'branch_ext_2gis', '' ), + 'icon' => '2ГИС', + 'name' => '2ГИС', + 'rating' => '4.7', + ), + 'vk' => array( + 'url' => get_option( 'branch_ext_vk', '' ), + 'icon' => 'ВК', + 'name' => 'ВКонтакте', + 'rating' => '4.9', + ), + 'otzovik' => array( + 'url' => get_option( 'branch_ext_otzovik', '' ), + 'icon' => 'O', + 'name' => 'Otzovik', + 'rating' => '4.6', + ), +); + +// Фильтруем: только площадки с заполненной ссылкой +$active = array_filter( $platforms, function( $p ) { + return ! empty( $p['url'] ); +} ); + +if ( empty( $active ) ) { + return; +} ?>

Нас рекомендуют на площадках — отзывы о подготовке к школе

diff --git a/wp-content/themes/dekart/template-parts/pksh/hero.php b/wp-content/themes/dekart/template-parts/pksh/hero.php index 5c8b449..5663556 100644 --- a/wp-content/themes/dekart/template-parts/pksh/hero.php +++ b/wp-content/themes/dekart/template-parts/pksh/hero.php @@ -22,6 +22,7 @@ $opt_phone = $data->get_phone(); $hero_phone_clean = $data->get_phone_clean(); $banner_img_id = $data->get_banner_image_id(); $hero_metrics = $data->get_social_counters(); +$hero_definition = $data->get_hero_definition(); ?>
@@ -30,12 +31,18 @@ $hero_metrics = $data->get_social_counters();
+
- — центр + — центр
+ -

+

+ + +

+ get_subtitle_footnote_link(); diff --git a/wp-content/themes/dekart/template-parts/pksh/how-it-works.php b/wp-content/themes/dekart/template-parts/pksh/how-it-works.php index 9aaa972..383ea1d 100644 --- a/wp-content/themes/dekart/template-parts/pksh/how-it-works.php +++ b/wp-content/themes/dekart/template-parts/pksh/how-it-works.php @@ -1,6 +1,6 @@ get_city_clean(); +$city_prep = $data->get_city_prep(); +$answer = $data->get_geo_answer_howitworks(); +$q3 = $data->get_geo_question_3(); +$hero_img_id = $data->get_banner_image_id(); ?>
@@ -79,10 +86,36 @@ if ( ! defined( 'ABSPATH' ) ) {
Дорого? -

От get_premium_program()['price_single'] ); ?> за занятие. Первое — бесплатно.

+

От get_premium_program()['price_single'] ); ?> за занятие. Первое — бесплатно.

+ + +
+

+
+ + + +
+
+ +
+ 'geo-answer-block__img', + 'loading' => 'lazy', + 'decoding' => 'async', + 'alt' => 'Подготовка к школе — занятия для детей 5–7 лет', + ) ); ?> +
+ +
+

+
+
+
+
diff --git a/wp-content/themes/dekart/template-parts/pksh/programs.php b/wp-content/themes/dekart/template-parts/pksh/programs.php index 2efce4e..634bfa4 100644 --- a/wp-content/themes/dekart/template-parts/pksh/programs.php +++ b/wp-content/themes/dekart/template-parts/pksh/programs.php @@ -1,6 +1,6 @@ get_city_clean(); -$base = $data->get_base_program(); -$prem = $data->get_premium_program(); +$base = $data->get_base_program(); +$prem = $data->get_premium_program(); +$answer = $data->get_geo_answer_programs(); +$q1 = $data->get_geo_question_1(); ?>
@@ -21,6 +23,16 @@ $prem = $data->get_premium_program();

Выберите формат подготовки

Все программы ведут действующие педагоги начальных классов. Группы до 10 человек — каждому ребёнку уделяем внимание.

+ +

+ + + +
+

+
+ + + + +

+ + + +
+

+
+
diff --git a/wp-content/themes/dekart/template-parts/pksh/trust-authority.php b/wp-content/themes/dekart/template-parts/pksh/trust-authority.php index 9f2bfe9..e536d0c 100644 --- a/wp-content/themes/dekart/template-parts/pksh/trust-authority.php +++ b/wp-content/themes/dekart/template-parts/pksh/trust-authority.php @@ -34,6 +34,11 @@ $city_gen = $data->get_city_genitive();

Если через 3 месяца ребёнок не начнёт читать — вернём деньги и продолжим обучение бесплатно.

+
diff --git a/wp-content/themes/dekart/template-parts/program-pksh/disclaimer.php b/wp-content/themes/dekart/template-parts/program-pksh/disclaimer.php new file mode 100644 index 0000000..0807c57 --- /dev/null +++ b/wp-content/themes/dekart/template-parts/program-pksh/disclaimer.php @@ -0,0 +1,29 @@ + +
+
+
+

+ + + + +

+

+ Если вы заметили ошибку в тексте программы, пожалуйста, сообщите нам — мы оперативно внесём исправления. +

+
+
+
diff --git a/wp-content/themes/dekart/template-parts/program-pksh/eeat-links.php b/wp-content/themes/dekart/template-parts/program-pksh/eeat-links.php new file mode 100644 index 0000000..ebdc1f9 --- /dev/null +++ b/wp-content/themes/dekart/template-parts/program-pksh/eeat-links.php @@ -0,0 +1,26 @@ + +
+
+
+

+ — + образовательная организация, осуществляющая образовательную деятельность на основании + государственной лицензии. + Полные сведения об организации доступны в разделе + «Сведения об образовательной организации». +

+
+
+
diff --git a/wp-content/themes/dekart/template-parts/program-pksh/header.php b/wp-content/themes/dekart/template-parts/program-pksh/header.php new file mode 100644 index 0000000..ee38f6e --- /dev/null +++ b/wp-content/themes/dekart/template-parts/program-pksh/header.php @@ -0,0 +1,23 @@ + +
+
+

Официальный документ

+

Дополнительная общеразвивающая программа
«Подготовка к школе»

+

+ Настоящая программа разработана в соответствии с Федеральным законом от 29.12.2012 № 273-ФЗ + «Об образовании в Российской Федерации» и определяет содержание образовательной деятельности + по подготовке детей старшего дошкольного возраста (5–7 лет) к обучению в школе. + Программа является официальным документом Детского центра «Декарт». +

+
+
diff --git a/wp-content/themes/dekart/template-parts/program-pksh/office-info.php b/wp-content/themes/dekart/template-parts/program-pksh/office-info.php new file mode 100644 index 0000000..d964463 --- /dev/null +++ b/wp-content/themes/dekart/template-parts/program-pksh/office-info.php @@ -0,0 +1,50 @@ + +
+
+
+

Ознакомиться с программой в офисе

+

Действующая образовательная программа находится в офисе и всегда доступна для ознакомления.

+
+ +
+ Адрес: + +
+ + +
+ Телефон: + +
+ + +
+ Часы приёма: + +
+ +
+
+
+
diff --git a/wp-content/themes/dekart/template-parts/program-pksh/program-content.php b/wp-content/themes/dekart/template-parts/program-pksh/program-content.php new file mode 100644 index 0000000..b9b0444 --- /dev/null +++ b/wp-content/themes/dekart/template-parts/program-pksh/program-content.php @@ -0,0 +1,24 @@ + +
+
+

Текст программы

+
+ +
+
+
diff --git a/wp-content/themes/dekart/template-parts/program-pksh/service-link.php b/wp-content/themes/dekart/template-parts/program-pksh/service-link.php new file mode 100644 index 0000000..2f6caea --- /dev/null +++ b/wp-content/themes/dekart/template-parts/program-pksh/service-link.php @@ -0,0 +1,29 @@ + +
+
+
+

Записаться на занятия

+

Данная программа реализуется в рамках курса подготовки к школе. Запишите ребёнка на бесплатное пробное занятие и познакомьтесь с педагогами.

+ + + +
+
+