diff --git a/wp-content/themes/dekart/assets/css/main.css b/wp-content/themes/dekart/assets/css/main.css
index fc2702d..da91be3 100644
--- a/wp-content/themes/dekart/assets/css/main.css
+++ b/wp-content/themes/dekart/assets/css/main.css
@@ -5019,6 +5019,35 @@ footer a:hover,
.form-wrapper .b24-form-container {
width: 100%;
}
+.form-wrapper__offer {
+ text-align: center;
+ margin-bottom: 20px;
+}
+.form-wrapper__badge {
+ display: inline-block;
+ padding: 4px 12px;
+ margin-bottom: 8px;
+ border-radius: 999px;
+ background: var(--main-yellow, #FFD43E);
+ color: #1F2937;
+ font-weight: 700;
+ font-size: 0.75rem;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+.form-wrapper__title {
+ margin: 0 0 6px;
+ font-size: 1.25rem;
+ line-height: 1.3;
+ font-weight: 700;
+ color: #1F2937;
+}
+.form-wrapper__hint {
+ margin: 0;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ color: #64748B;
+}
.form-wrapper .b24-form {
border-radius: 12px;
overflow: hidden;
diff --git a/wp-content/themes/dekart/assets/css/single-teacher.css b/wp-content/themes/dekart/assets/css/single-teacher.css
index ae9564d..63b773a 100644
--- a/wp-content/themes/dekart/assets/css/single-teacher.css
+++ b/wp-content/themes/dekart/assets/css/single-teacher.css
@@ -459,33 +459,6 @@
color: var(--color-text-muted, #475569);
margin-bottom: 20px;
}
-.teacher-single__subject-list {
- list-style: none;
- margin: 0;
- padding: 0;
- display: flex;
- flex-direction: column;
- gap: 14px;
-}
-.teacher-single__subject-list li {
- padding: 14px 16px;
- border-radius: 14px;
- background: var(--color-bg, #FFFFFF);
- border: 1px solid var(--color-border, #E2E8F0);
-}
-.teacher-single__subject-list strong {
- display: block;
- margin-bottom: 4px;
-}
-.teacher-single__subject-list span {
- display: inline-block;
- margin: 2px 6px 0 0;
- padding: 4px 10px;
- border-radius: var(--radius-pill, 100px);
- background: var(--color-primary-light, #EFF6FF);
- color: var(--color-primary, #2563EB);
- font-size: 0.8125rem;
-}
.teacher-single__cert-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
@@ -727,6 +700,12 @@
.teacher-single__timeline-item {
padding-bottom: 16px;
}
+ .teacher-single__hero .ds-hero__inner {
+ gap: 28px;
+ }
+ .teacher-single__hero-facts {
+ margin: var(--space-4, 24px) 0;
+ }
}
@media (prefers-reduced-motion: reduce) {
@@ -791,10 +770,9 @@
box-shadow: var(--shadow-md, 0 8px 24px rgba(15, 23, 42, 0.1));
}
-/* Микро-плитки: радиус 16 (иконки, серты, subject-list, FAQ, trust-shield) */
+/* Микро-плитки: радиус 16 (иконки, серты, FAQ, trust-shield) */
.teacher-single__segment-icon,
.teacher-single__cert,
-.teacher-single__subject-list li,
.teacher-single__trust-shield,
.teacher-single__faq-item {
border-radius: 16px;
@@ -1170,5 +1148,70 @@
flex-direction: column;
align-items: flex-start;
}
+ .teacher-single__course-link {
+ padding: 8px 0;
+ min-height: 44px;
+ line-height: 1.4;
+ }
+}
+
+/* ── Соц-доказательство (после программ) ── */
+.teacher-single__social-proof {
+ padding: 24px 0;
+}
+.teacher-single__social-proof-inner {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ gap: 20px 48px;
+ padding: 20px 28px;
+ border: 1px solid var(--color-border, rgba(15, 23, 42, 0.08));
+ border-radius: 20px;
+ background: rgba(15, 23, 42, 0.03);
+}
+.teacher-single__sp-item {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 6px 10px;
+ max-width: 420px;
+}
+.teacher-single__sp-stars {
+ display: inline-flex;
+ gap: 2px;
+ color: rgba(15, 23, 42, 0.25);
+}
+.teacher-single__sp-stars svg.is-filled {
+ color: #F59E0B;
+}
+.teacher-single__sp-num {
+ font-family: var(--font-display, inherit);
+ font-size: 1.375rem;
+ font-weight: 700;
+ line-height: 1.2;
+ color: var(--color-text, #0F172A);
+}
+.teacher-single__sp-text {
+ font-size: 0.875rem;
+ line-height: 1.4;
+ color: var(--color-text-muted, #64748B);
+}
+.teacher-single__sp-link {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--color-primary, #006DB7);
+ text-decoration: none;
+}
+.teacher-single__sp-link:hover {
+ text-decoration: underline;
+}
+@media (max-width: 639px) {
+ .teacher-single__social-proof-inner {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+ padding: 18px 20px;
+ }
}
diff --git a/wp-content/themes/dekart/assets/js/single-teacher.js b/wp-content/themes/dekart/assets/js/single-teacher.js
index 10c5e9e..8a22c2d 100644
--- a/wp-content/themes/dekart/assets/js/single-teacher.js
+++ b/wp-content/themes/dekart/assets/js/single-teacher.js
@@ -25,13 +25,20 @@
});
}
- // ── Sticky CTA (mobile): показать после hero ──
+ // ── Sticky CTA (mobile): показать после hero, скрыть у финального CTA ──
var sticky = document.querySelector('.teacher-single__sticky');
if (sticky) {
var hero = document.querySelector('.teacher-single__hero');
+ var finalCta = document.querySelector('.teacher-single__final');
if (hero) {
var showSticky = function () {
- sticky.hidden = window.scrollY < hero.offsetHeight - 80;
+ var afterHero = window.scrollY >= hero.offsetHeight - 80;
+ var finalVisible = false;
+ if (finalCta) {
+ var fr = finalCta.getBoundingClientRect();
+ finalVisible = fr.top < window.innerHeight && fr.bottom > 0;
+ }
+ sticky.hidden = !afterHero || finalVisible;
};
showSticky();
var ticking = false;
diff --git a/wp-content/themes/dekart/footer.php b/wp-content/themes/dekart/footer.php
index 9dbfdc5..3946fb0 100644
--- a/wp-content/themes/dekart/footer.php
+++ b/wp-content/themes/dekart/footer.php
@@ -96,6 +96,11 @@ if (! empty($b24_raw)) {
alt="Закрыть"
loading="lazy">
+
diff --git a/wp-content/themes/dekart/functions.php b/wp-content/themes/dekart/functions.php
index 022dd7a..ff8d4bf 100644
--- a/wp-content/themes/dekart/functions.php
+++ b/wp-content/themes/dekart/functions.php
@@ -3253,6 +3253,43 @@ function dekart_teacher_review_count(int $teacher_id): int
return isset($map[ $teacher_id ]) ? (int) $map[ $teacher_id ] : 0;
}
+/**
+ * Средняя оценка преподавателя по реальным отзывам CPT (поле review_star).
+ * Один SQL-агрегат вместо перебора всех отзывов carbon_get_post_meta на каждый id
+ * (500+ отзывов = 500 запросов, object-cache может быть отключён).
+ * Возвращает 0.0, если валидных оценок нет.
+ */
+function dekart_teacher_review_avg(int $teacher_id): float
+{
+ global $wpdb;
+
+ static $cache = array();
+ if (isset($cache[ $teacher_id ])) {
+ return $cache[ $teacher_id ];
+ }
+
+ $sql = $wpdb->prepare(
+ "SELECT AVG(CAST(pm.meta_value AS DECIMAL(10, 2)))
+ FROM {$wpdb->postmeta} pm
+ INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
+ INNER JOIN {$wpdb->postmeta} pmt
+ ON pmt.post_id = pm.post_id
+ AND pmt.meta_key = %s
+ AND pmt.meta_value = %s
+ WHERE pm.meta_key = %s
+ AND pm.meta_value REGEXP '^[1-5]$'
+ AND p.post_type = 'reviews'
+ AND p.post_status = 'publish'",
+ '_review_teacher|||0|id',
+ (string) $teacher_id,
+ '_review_star'
+ );
+
+ $cache[ $teacher_id ] = (float) $wpdb->get_var($sql);
+
+ return $cache[ $teacher_id ];
+}
+
/**
* OG-изображение 1200×630 для страницы «Преподаватели».
* Глобальный дефолт Yoast (banner-dekart-small.webp 355×250) даёт битый сниппет.
diff --git a/wp-content/themes/dekart/single-teacher.php b/wp-content/themes/dekart/single-teacher.php
index f89871c..d97f294 100644
--- a/wp-content/themes/dekart/single-teacher.php
+++ b/wp-content/themes/dekart/single-teacher.php
@@ -1,15 +1,9 @@
' . "\n";
+ }
+ }
+}, 1);
+
get_header();
$tid = (int) get_the_ID();
@@ -28,10 +36,28 @@ $branch_phone = get_option('branch_phone') ?: carbon_get_theme_option('site_pho
$branch_city = get_option('branch_city') ?: carbon_get_theme_option('site_city', 'Щёлково');
$branch_city_prep = get_option('branch_city_prep', 'в Щёлково') ?: 'в Щёлково';
$branch_name = get_option('branch_name') ?: carbon_get_theme_option('site_name', 'Декарт');
-$phone_clean = $branch_phone ? preg_replace('/[\s\-\–\(\)]/', '', $branch_phone) : '';
-$wa_on = (bool) get_option('branch_messenger_whatsapp_on', 1);
-$wa_url = $wa_on ? get_option('branch_messenger_whatsapp') : '';
-$wa_url = $wa_url ?: ( $phone_clean ? 'https://wa.me/' . $phone_clean : '' );
+// Тел.: цифры и «+». WhatsApp: 8-XXXXXXXXXX → 7-XXXXXXXXXX, без номера — без ссылки.
+$phone_clean = $branch_phone ? preg_replace('/[^0-9+]/', '', $branch_phone) : '';
+$wa_on = (bool) get_option('branch_messenger_whatsapp_on', 1);
+$wa_number = '';
+if ($wa_on) {
+ $wa_number = trim((string) get_option('branch_messenger_whatsapp'));
+ if (false !== strpos($wa_number, 'wa.me/')) {
+ $wa_number = substr($wa_number, strpos($wa_number, 'wa.me/') + 6);
+ }
+ $wa_number = preg_replace('/[^0-9]/', '', $wa_number);
+ if ('' !== $wa_number && '8' === $wa_number[0] && 11 === strlen($wa_number)) {
+ $wa_number = '7' . substr($wa_number, 1);
+ }
+}
+// Фолбэк: телефон филиала.
+if ('' === $wa_number && $phone_clean) {
+ $wa_number = preg_replace('/[^0-9]/', '', $phone_clean);
+ if ('8' === $wa_number[0] && 11 === strlen($wa_number)) {
+ $wa_number = '7' . substr($wa_number, 1);
+ }
+}
+$wa_url = ('' !== $wa_number) ? 'https://wa.me/' . $wa_number : '';
// ─── Профиль преподавателя (CF) ───
$direction = carbon_get_post_meta($tid, 'teacher_direction');
@@ -45,11 +71,24 @@ if ($start_year && absint($start_year) > 1900) {
$tenure = max(0, (int) current_time('Y') - (int) $start_year);
}
$avatar_id = carbon_get_post_meta($tid, 'teacher_avatar');
-$avatar_url = $avatar_id ? wp_get_attachment_url($avatar_id) : '';
+$avatar_url = $avatar_id ? wp_get_attachment_url($avatar_id) : ''; // Schema Person.image
$avatar_alt = $avatar_id ? get_post_meta($avatar_id, '_wp_attachment_image_alt', true) : '';
if (empty($avatar_alt)) {
$avatar_alt = 'Преподаватель ' . $full_name . ' — школа «' . $branch_name . '» ' . $branch_city;
}
+// image() = проверка наличия + готовый HTML, без отдельного url-запроса.
+$avatar_img = $avatar_id ? wp_get_attachment_image(
+ (int) $avatar_id,
+ array( 600, 600 ),
+ false,
+ array(
+ 'class' => 'ds-hero__photo',
+ 'alt' => $avatar_alt,
+ 'loading' => 'eager',
+ 'decoding' => 'async',
+ 'fetchpriority' => 'high', // LCP-изображение
+ )
+) : '';
$avg_score = carbon_get_post_meta($tid, 'teacher_avg_score');
$seats_left = carbon_get_post_meta($tid, 'teacher_seats_left');
@@ -58,8 +97,6 @@ $avg_label = carbon_get_post_meta($tid, 'teacher_avg_label');
if (empty($avg_label)) {
$avg_label = 'Средний балл учеников';
}
-$rating_label = 'Рейтинг преподавателя';
-$group_label = 'человек в мини-группе — внимание как на репетиторе';
$exam_label = carbon_get_post_meta($tid, 'teacher_exam_label') ?: 'нового набора';
$cta_primary = carbon_get_post_meta($tid, 'teacher_cta_primary') ?: 'Записаться на диагностику';
$sched_label = carbon_get_post_meta($tid, 'teacher_schedule_label') ?: 'Посмотреть расписание групп';
@@ -68,7 +105,7 @@ $risk_note = carbon_get_post_meta($tid, 'teacher_risk_note');
if (empty($risk_note)) {
$risk_note = 'Перезвоним за 15 минут · Без обязательств';
}
-$school_num = carbon_get_post_meta($tid, 'teacher_school_num') ?: '100%';
+$school_num = carbon_get_post_meta($tid, 'teacher_school_num');
$school_label = carbon_get_post_meta($tid, 'teacher_school_label');
if (empty($school_label)) {
$school_label = 'учеников продолжают обучение на следующий учебный год';
@@ -83,7 +120,7 @@ $about_text = carbon_get_post_meta($tid, 'teacher_about');
$education = carbon_get_post_meta($tid, 'teacher_education');
$add_education = carbon_get_post_meta($tid, 'teacher_add_education');
$video_html = carbon_get_post_meta($tid, 'teacher_video');
-// Если в поле ссылка (не iframe) — собираем iframe VK/YouTube автоматически. Пусто → блок не выводится.
+// Ссылка VK/YouTube → iframe. Пусто → блок скрыт.
if ($video_html && 0 !== strpos(ltrim($video_html), '<')) {
$video_url = trim($video_html);
if (preg_match('~vk\.com/video(-?\d+)_(\d+)~', $video_url, $vm)) {
@@ -97,10 +134,10 @@ if (empty($license)) {
$license = 'Частная школа «' . $branch_name . '» работает по лицензии на образовательную деятельность (бессрочная). Копия лицензии и реквизиты — по запросу у администратора.';
}
-// Отзывы (счётчик реальных, как везде на сайте)
+// Счётчик реальных отзывов.
$review_count = function_exists('dekart_teacher_review_count') ? dekart_teacher_review_count($tid) : 0;
-// Подзаголовок hero: редактируется в админке (вкладка Hero). Пусто — авто-сборка.
+// Подзаголовок hero — из админки (вкладка Hero); пусто — авто-сборка.
$hero_subtitle = trim((string) carbon_get_post_meta($tid, 'teacher_hero_subtitle'));
if ('' === $hero_subtitle) {
$hero_subtitle = ( $subj ? $subj : 'Обучение' ) . ' — мини-группы ' . $group_size . ' чел. · ' . $exam_label . ' · школа «' . $branch_name . '» ' . $branch_city_prep;
@@ -160,17 +197,20 @@ foreach ($legacy_questions as $lq) {
}
// ─── Дни до экзамена (полоса дефицита) ───
+// «Сегодня» = 0, «завтра» = 1; прошедшая дата → блок скрыт.
$days_left = '';
if ($exam_date && preg_match('/^\d{4}-\d{2}-\d{2}$/', $exam_date)) {
- $diff = (int) (( strtotime($exam_date) - current_time('timestamp') ) / DAY_IN_SECONDS);
+ $tz = wp_timezone();
+ $exam_ts = (int) ( new DateTime($exam_date, $tz) )->format('U');
+ $today_ts = (int) ( new DateTime('now', $tz) )->setTime(0, 0)->format('U');
+ $diff = (int) round( ( $exam_ts - $today_ts ) / DAY_IN_SECONDS );
if ($diff >= 0) {
$days_left = (string) $diff;
}
}
-// ─── Микроразметка (Schema.org JSON-LD, единый @graph) ───
-// Данные — только из CF преподавателя и настроек филиала (branch_*/site_*).
-// Пустые свойства в схему не выводятся (Google ругается на пустые обязательные поля).
+// ─── Микроразметка (единый @graph) ───
+// Пустые свойства не выводим (Google ругается на пустые обязательные поля).
$teachers_page = get_page_by_path('our-teachers');
$page_url = get_permalink($tid);
@@ -205,7 +245,7 @@ if ($branch_email) {
$org['email'] = $branch_email;
}
-// Адрес филиала — из опций (branch_*), фолбэк — CF theme options (site_*)
+// Адрес филиала: branch_* → фолбэк site_*.
$org_addr = array();
$org_street = get_option('branch_address') ?: carbon_get_theme_option('site_address');
$org_city = get_option('branch_city') ?: carbon_get_theme_option('site_city');
@@ -220,7 +260,7 @@ if (! empty($org_addr)) {
$org['address'] = $org_addr;
}
-// Лицензия (hasCredential) — из настроек филиала
+// Лицензия — из настроек филиала.
$org_license = get_option('branch_license') ?: carbon_get_theme_option('site_license');
if (! empty($org_license)) {
$org['hasCredential'] = array(
@@ -230,8 +270,7 @@ if (! empty($org_license)) {
}
// ── Person (преподаватель) ──
-// Каталог программ филиала (единый для всего сайта): название → ссылка.
-// Цена берётся из этого каталога только для микроразметки Course.offers (не выводится на странице).
+// Каталог программ филиала: название → url/price (цена — только для Course.offers в микроразметке).
$branch_programs_by_name = array();
foreach ((array) get_option('branch_programs', array()) as $bp) {
$pn = trim((string) ( $bp['name'] ?? '' ));
@@ -243,9 +282,7 @@ foreach ((array) get_option('branch_programs', array()) as $bp) {
}
}
-// Предметы: title → name курса (выбор из каталога филиала),
-// teacher_subjects_topics → чипы. URL — из каталога филиала (fallback: legacy url преподавателя);
-// цена — из того же каталога, но используется только микроразметкой Course.offers.
+// Предметы: title → курс; topics → чипы. URL/цена — из каталога филиала (fallback — CF преподавателя).
$teacher_subjects = (array) carbon_get_post_meta($tid, 'teacher_subjects');
$subject_items = array(); // [title => ['name', 'url', 'price', 'topics[]']] без дубликатов
foreach ($teacher_subjects as $ts) {
@@ -283,11 +320,13 @@ $geo_walk = get_option('branch_logistics_walk');
$geo_parking = get_option('branch_logistics_parking');
$geo_schools = get_option('branch_logistics_schools');
-// ─── Результаты учеников (CF; пусто — сборка из метрик преподавателя) ───
-$results = (array) carbon_get_post_meta($tid, 'teacher_results');
-if (empty($results) || ! is_array($results)) {
- $results = array();
- if ('' !== $school_num) { $results[] = array( 'result_title' => $school_num, 'result_text' => $school_label ); }
+// ─── Результаты учеников: CF или авто-сборка ───
+$cf_results = carbon_get_post_meta($tid, 'teacher_results');
+$results = array();
+if (is_array($cf_results) && ! empty($cf_results)) {
+ $results = array_values($cf_results);
+} else {
+ if (! empty($school_num)) { $results[] = array( 'result_title' => (string) $school_num, 'result_text' => $school_label ); }
if ($avg_score) { $results[] = array( 'result_title' => $avg_score, 'result_text' => $avg_label ); }
if ($tenure) { $results[] = array( 'result_title' => $tenure . ' лет', 'result_text' => 'педагогический стаж преподавателя' ); }
if ($review_count > 0) { $results[] = array( 'result_title' => (string) $review_count, 'result_text' => 'реальных отзывов родителей о преподавателе' ); }
@@ -317,69 +356,23 @@ if ('' !== $diploma) {
$person['alumniOf'] = array('@type' => 'CollegeOrUniversity', 'name' => $diploma);
}
-// AggregateRating — только реальные данные. Размещается на Course, а не на Person
-// (свойство aggregateRating невалидно для типа Person в schema.org).
-$course_rating = array();
-if ($rating_num > 0 && $review_count > 0) {
- $course_rating = array(
- '@type' => 'AggregateRating',
- 'ratingValue' => number_format($rating_num, 1),
- 'bestRating' => '5',
- 'ratingCount' => $review_count,
- );
-}
+// ── Рейтинг преподавателя ──
+// Среднее реальных отзывов (SQL-агрегат); CF teacher_rating — фолбэк, когда отзывов нет.
+$sp_avg_num = function_exists('dekart_teacher_review_avg')
+ ? (float) dekart_teacher_review_avg($tid)
+ : 0.0;
+$sp_avg = $sp_avg_num > 0 ? number_format($sp_avg_num, 1, '.', '') : '';
+$rating_num = $sp_avg_num > 0 ? $sp_avg_num : ( is_numeric($rating) ? (float) $rating : 0.0 );
-// Review — реальные отзывы из CPT «reviews», связанные с преподавателем. Не выдумываем.
-// Размещаются на Course (свойство review невалидно для Person).
-$review_pool = get_posts(array(
- 'post_type' => 'reviews',
- 'post_status' => 'publish',
- 'posts_per_page' => -1,
- 'fields' => 'ids',
- 'no_found_rows' => true,
-));
-$reviews = array();
-foreach ($review_pool as $rid) {
- $link = carbon_get_post_meta((int) $rid, 'review_teacher');
- if (! is_array($link) || empty($link[0]['id']) || (int) $link[0]['id'] !== $tid) {
- continue;
- }
- $r_author = trim((string) get_the_title((int) $rid));
- $r_body = trim((string) carbon_get_post_meta((int) $rid, 'review_text'));
- $r_star = (int) ( carbon_get_post_meta((int) $rid, 'review_star') ?: 5 );
+// AggregateRating не размечаем: невалиден для Person в schema.org, Google не даёт
+// по нему rich results. Средний балл — только UI; в Schema — отдельные Review с reviewRating.
- $review = array('@type' => 'Review');
- if ('' !== $r_author) {
- $review['author'] = array('@type' => 'Person', 'name' => $r_author);
- }
- $pub = get_the_date('c', (int) $rid);
- if ($pub) {
- $review['datePublished'] = $pub;
- }
- if ($r_star > 0) {
- $review['reviewRating'] = array(
- '@type' => 'Rating',
- 'ratingValue' => (string) $r_star,
- 'bestRating' => '5',
- );
- }
- if ('' !== $r_body) {
- $review['reviewBody'] = $r_body;
- }
- $reviews[] = $review;
- if (count($reviews) >= 3) {
- break;
- }
-}
-if (! empty($reviews)) {
- // $reviews переносятся на каждый Course ниже (в блоке сборки Course).
-}
+// Review не размечаем: Google не поддерживает Person среди itemReviewed для review
+// rich results. Отзывы остаются в видимом HTML (reviews-link) — там их место.
-// ── Course — по каждому предмету из CF (комбинация предмет + формат мини-группы) ──
-// Валидная структура: Course → hasCourseInstance (CourseInstance) с courseMode/instructor;
-// aggregateRating и review живут на Course (не на Person).
-// Person и Org объявляются один раз (@id), в курсах — только ссылки {"@id": ...}.
-$build_course = function ( $name, $cid, $availability = null, $description = '', $price = '' ) use ( $page_url, $org_id, $person_id, $group_size, $full_name, $course_rating, $reviews, $branch_price ) {
+// ── Course — по каждому предмету (предмет + мини-группа) ──
+// Person и Org объявляются один раз (@id); в курсах — ссылки.
+$build_course = function ( $name, $cid, $availability = null, $description = '', $price = '' ) use ( $page_url, $org_id, $person_id, $group_size, $full_name, $branch_price ) {
$course = array(
'@type' => 'Course',
'@id' => $page_url . '#course-' . $cid,
@@ -398,28 +391,30 @@ $build_course = function ( $name, $cid, $availability = null, $description = '',
'category' => 'Мини-группа 2–' . $group_size . ' человек',
),
);
- // Цена (только микроразметка Course.offers): из каталога программы (per-program) → fallback branch_price.
+ // Цена — только микроразметка: из каталога программы → фолбэк branch_price;
+ // Schema требует число — извлекаем из строки вида «от 1 290 ₽».
$offer_price = ( '' !== (string) $price ) ? $price : $branch_price;
- if ('' !== (string) $offer_price) {
- $course['offers']['price'] = $offer_price;
+ $offer_price_norm = preg_replace('/\s+/u', '', (string) $offer_price);
+ if (preg_match('/\d+(?:[.,]\d+)?/', $offer_price_norm, $pm)) {
+ $course['offers']['price'] = str_replace(',', '.', $pm[0]);
$course['offers']['priceCurrency'] = 'RUB';
}
if (null !== $availability) {
$course['offers']['availability'] = $availability;
}
- if (! empty($course_rating)) {
- $course['aggregateRating'] = $course_rating;
- }
- if (! empty($reviews)) {
- $course['review'] = $reviews;
- }
return $course;
};
$courses = array();
if (! empty($subject_names)) {
foreach ($subject_names as $i => $cn) {
- $availability = ( $seats_left && (int) $seats_left > 0 ) ? 'https://schema.org/LimitedAvailability' : 'https://schema.org/InStock';
+ // Availability: >0 → Limited, =0 → SoldOut, пусто → не указываем.
+ $availability = null;
+ if ('' !== (string) $seats_left) {
+ $availability = ( (int) $seats_left > 0 )
+ ? 'https://schema.org/LimitedAvailability'
+ : 'https://schema.org/SoldOut';
+ }
$courses[] = $build_course($cn, $i + 1, $availability, '', $subject_items[$cn]['price']);
}
} else {
@@ -480,7 +475,6 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
/
-
Преподаватели
@@ -543,10 +537,10 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
-
+
-
; ?>)
+
@@ -558,9 +552,8 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
'BreadcrumbList', 'itemListElement' => $bread
@@ -662,6 +654,44 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
+
+
+
+
+
+
+
+
+
+
+ рейтинг школы в Яндекс Картах
+
+ отзывов на внешних площадках
+
+
+
+
+
+
+
+
@@ -672,6 +702,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
Формат мини-группы закрывает три разные задачи — выберите свою.
+
$seg) :
if ($i >= 3) {
break;
@@ -681,9 +712,10 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
if (! $s_title) {
continue;
}
+ $seg_num++;
?>
-
+
@@ -743,15 +775,17 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
Как проходит первое занятие
+
$step) :
$st_title = $step['step_title'] ?? '';
$st_text = $step['step_text'] ?? '';
if (! $st_title) {
continue;
}
+ $step_num++;
?>
-
-
+
@@ -791,14 +825,24 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
Результаты учеников
- $tab) : ?>
-
- $tab) : ?>
-
+ $tab) :
+ $tab_id = sanitize_title($tab) . '-' . $ci;
+ if ('' === trim($tab_id, '-')) {
+ $tab_id = 'tab-' . $ci;
+ }
+ ?>
+
'BreadcrumbList', 'itemListElement' => $bread
Образование и опыт
-
-
-
@@ -859,18 +887,25 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
$c_alt ?: ('Диплом или сертификат — ' . $full_name),
+ 'loading' => 'lazy',
+ 'decoding' => 'async',
+ )
+ ) : '';
+ if (! $c_img) {
continue;
}
?>
-
+
@@ -899,7 +934,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
$tid ));
@@ -922,7 +957,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
@@ -994,10 +1029,10 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread