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"> +
+ Бесплатно +

Записаться на бесплатную диагностику

+

Перезвоним за 15 минут · Без обязательств

+
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
- +
- <?php echo esc_attr($avatar_alt); ?> +
@@ -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++; ?>
  1. - +

    @@ -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; } ?>
    - <?php echo esc_attr($c_alt ?: 'Диплом или сертификат — ' . $full_name); ?> +
    @@ -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