2026-09-15 single-teacher.php

This commit is contained in:
Dekart Deploy Bot 2026-09-15 17:33:30 +00:00
parent 91e8d18b43
commit 02e319935c

View File

@ -2,8 +2,7 @@
/**
* Личная страница преподавателя (Bento 2026).
*
* Данные из CF карточки преподавателя. Общие части: reviews-link,
* teachers-cards, contacts, author-eaat. Sticky-CTA на мобильном.
* Данные из CF карточки преподавателя.
*
* @package Dekart
*/
@ -12,7 +11,7 @@ if (! defined('ABSPATH')) {
exit;
}
// og:image:alt из featured image (паттерн page-camp.php)
// og:image:alt из featured image
add_action('wp_head', function () {
if (! is_singular('teacher')) {
return;
@ -36,7 +35,7 @@ $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', 'Декарт');
// Тел.: цифры и «+». WhatsApp: 8-XXXXXXXXXX → 7-XXXXXXXXXX, без номера — без ссылки.
// Нормализация телефона/WhatsApp
$phone_clean = $branch_phone ? preg_replace('/[^0-9+]/', '', $branch_phone) : '';
$wa_on = (bool) get_option('branch_messenger_whatsapp_on', 1);
$wa_number = '';
@ -50,7 +49,7 @@ if ($wa_on) {
$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)) {
@ -76,7 +75,7 @@ $avatar_alt = $avatar_id ? get_post_meta($avatar_id, '_wp_attachment_image_alt',
if (empty($avatar_alt)) {
$avatar_alt = 'Преподаватель ' . $full_name . ' — школа «' . $branch_name . '» ' . $branch_city;
}
// image() = проверка наличия + готовый HTML, без отдельного url-запроса.
// image(): проверка + вывод
$avatar_img = $avatar_id ? wp_get_attachment_image(
(int) $avatar_id,
array( 600, 600 ),
@ -120,7 +119,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');
// Ссылка VK/YouTube → iframe. Пусто → блок скрыт.
// 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)) {
@ -134,10 +133,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_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;
@ -197,7 +196,7 @@ foreach ($legacy_questions as $lq) {
}
// ─── Дни до экзамена (полоса дефицита) ───
// «Сегодня» = 0, «завтра» = 1; прошедшая дата → блок скрыт.
// Сегодня=0, завтра=1, прошлое=скрыто
$days_left = '';
if ($exam_date && preg_match('/^\d{4}-\d{2}-\d{2}$/', $exam_date)) {
$tz = wp_timezone();
@ -210,7 +209,7 @@ if ($exam_date && preg_match('/^\d{4}-\d{2}-\d{2}$/', $exam_date)) {
}
// ─── Микроразметка (единый @graph) ───
// Пустые свойства не выводим (Google ругается на пустые обязательные поля).
// Пустые свойства не выводим
$teachers_page = get_page_by_path('our-teachers');
$page_url = get_permalink($tid);
@ -245,7 +244,7 @@ if ($branch_email) {
$org['email'] = $branch_email;
}
// Адрес филиала: branch_* → фолбэк 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');
@ -260,7 +259,7 @@ if (! empty($org_addr)) {
$org['address'] = $org_addr;
}
// Лицензия — из настроек филиала.
// Лицензия филиала
$org_license = get_option('branch_license') ?: carbon_get_theme_option('site_license');
if (! empty($org_license)) {
$org['hasCredential'] = array(
@ -270,7 +269,7 @@ if (! empty($org_license)) {
}
// ── Person (преподаватель) ──
// Каталог программ филиала: название → url/price (цена — только для Course.offers в микроразметке).
// Каталог программ филиала
$branch_programs_by_name = array();
foreach ((array) get_option('branch_programs', array()) as $bp) {
$pn = trim((string) ( $bp['name'] ?? '' ));
@ -282,9 +281,9 @@ foreach ((array) get_option('branch_programs', array()) as $bp) {
}
}
// Предметы: title → курс; topics → чипы. URL/цена — из каталога филиала (fallback CF преподавателя).
// Предметы → курсы; fallback CF
$teacher_subjects = (array) carbon_get_post_meta($tid, 'teacher_subjects');
$subject_items = array(); // [title => ['name', 'url', 'price', 'topics[]']] без дубликатов
$subject_items = array(); // без дубликатов
foreach ($teacher_subjects as $ts) {
$sn = trim((string) ( $ts['teacher_subjects_title'] ?? '' ));
if ('' === $sn) {
@ -313,14 +312,14 @@ foreach ($teacher_subjects as $ts) {
}
$subject_names = array_keys($subject_items);
// ─── Общие данные из настроек филиала (не дублируются в CF преподавателя) ───
// ─── Данные филиала ───
$branch_price = get_option('branch_price');
$branch_geo_areas = get_option('branch_area_served');
$geo_walk = get_option('branch_logistics_walk');
$geo_parking = get_option('branch_logistics_parking');
$geo_schools = get_option('branch_logistics_schools');
// ─── Результаты учеников: CF или авто-сборка ───
// ─── Результаты: CF или авто ───
$cf_results = carbon_get_post_meta($tid, 'teacher_results');
$results = array();
if (is_array($cf_results) && ! empty($cf_results)) {
@ -357,21 +356,19 @@ if ('' !== $diploma) {
}
// ── Рейтинг преподавателя ──
// Среднее реальных отзывов (SQL-агрегат); CF teacher_rating — фолбэк, когда отзывов нет.
// Среднее отзывов (SQL); CF — фолбэк
$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 );
// AggregateRating не размечаем: невалиден для Person в schema.org, Google не даёт
// по нему rich results. Средний балл — только UI; в Schema — отдельные Review с reviewRating.
// AggregateRating невалиден для Person
// Review не размечаем: Google не поддерживает Person среди itemReviewed для review
// rich results. Отзывы остаются в видимом HTML (reviews-link) — там их место.
// Review: Person не поддержан Google
// ── Course — по каждому предмету (предмет + мини-группа) ──
// Person и Org объявляются один раз (@id); в курсах — ссылки.
// ── 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',
@ -391,8 +388,7 @@ $build_course = function ( $name, $cid, $availability = null, $description = '',
'category' => 'Мини-группа 2' . $group_size . ' человек',
),
);
// Цена — только микроразметка: из каталога программы → фолбэк branch_price;
// Schema требует число — извлекаем из строки вида «от 1 290 ₽».
// Цена — микроразметка; число из строки
$offer_price = ( '' !== (string) $price ) ? $price : $branch_price;
$offer_price_norm = preg_replace('/\s+/u', '', (string) $offer_price);
if (preg_match('/\d+(?:[.,]\d+)?/', $offer_price_norm, $pm)) {
@ -408,7 +404,7 @@ $build_course = function ( $name, $cid, $availability = null, $description = '',
$courses = array();
if (! empty($subject_names)) {
foreach ($subject_names as $i => $cn) {
// Availability: >0 → Limited, =0 → SoldOut, пусто → не указываем.
// >0 Limited, =0 SoldOut, пусто — нет
$availability = null;
if ('' !== (string) $seats_left) {
$availability = ( (int) $seats_left > 0 )
@ -461,7 +457,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php get_template_part('template-parts/svg-sprite'); ?>
<?php
// ─────────────────────────── HERO ───────────────────────────
// ─── HERO ───
?>
<section class="section-bg ds-hero teacher-single__hero" data-bg="hero" aria-labelledby="teacher-single-title">
<div class="ds-container">
@ -552,8 +548,8 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
</section>
<?php
// ─────────────────── ЦИФРЫ ───────────────────
// Карточки — только из CF complex teacher_metrics; пусто — секция скрыта.
// ─── ЦИФРЫ ───
// Только CF teacher_metrics; пусто — скрыта
$metrics = array();
$metric_cards = (array) carbon_get_post_meta($tid, 'teacher_metrics');
if (! empty($metric_cards) && is_array($metric_cards)) {
@ -585,7 +581,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php endif; ?>
<?php
// ─────────────────── ПОЛОСА ДЕФИЦИТА ───────────────────
// ─── ПОЛОСА ДЕФИЦИТА ───
if ($days_left !== '' || ( $seats_left && (int) $seats_left > 0 )) :
?>
<section class="teacher-single__scarcity section-bg reveal" data-bg="teacher-scarcity" aria-label="Осталось мест">
@ -610,8 +606,8 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php endif; ?>
<?php
// ─────────────────── ОБРАЗОВАНИЕ / СЕРТИФИКАТЫ ───────────────────
// Направления здесь не дублируем — они уже показаны ниже в секции «Программы и направления».
// ─── ОБРАЗОВАНИЕ / СЕРТИФИКАТЫ ───
// Направления — в секции программ
$certificates = (array) carbon_get_post_meta($tid, 'certificates');
if ($education || $add_education || ! empty($certificates)) :
?>
@ -639,7 +635,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php foreach ($certificates as $cert) :
$c_id = $cert['certificates_foto'] ?? '';
$c_alt = $c_id ? get_post_meta((int) $c_id, '_wp_attachment_image_alt', true) : '';
// image() = проверка наличия + вывод, без отдельного url-запроса.
// image(): проверка + вывод
$c_img = $c_id ? wp_get_attachment_image(
(int) $c_id,
'medium',
@ -664,10 +660,48 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
</section>
<?php endif; ?>
<?php
// ─── О СЕБЕ / ПЕРВОЕ ЗАНЯТИЕ ───
if ($about_text) :
?>
<section class="teacher-single__story section-bg reveal" data-bg="teacher-story" aria-labelledby="teacher-story-title">
<div class="container">
<div class="teacher-single__story-grid">
<div class="teacher-single__story-about">
<h2 id="teacher-story-title" class="bento-h2">О себе</h2>
<div class="teacher-single__story-text"><?php echo wp_kses_post(wpautop($about_text)); ?></div>
</div>
<div class="teacher-single__story-steps">
<h2 class="bento-h2">Как проходит первое занятие</h2>
<ol class="teacher-single__timeline">
<?php $step_num = 0; ?>
<?php foreach ($steps as $i => $step) :
$st_title = $step['step_title'] ?? '';
$st_text = $step['step_text'] ?? '';
if (! $st_title) {
continue;
}
$step_num++;
?>
<li class="teacher-single__timeline-item">
<span class="teacher-single__timeline-num" aria-hidden="true"><?php echo esc_html($step_num); ?></span>
<div class="teacher-single__timeline-content">
<h3><?php echo esc_html($st_title); ?></h3>
<?php if ($st_text) : ?><p><?php echo esc_html($st_text); ?></p><?php endif; ?>
</div>
</li>
<?php endforeach; ?>
</ol>
</div>
</div>
</div>
</section>
<?php endif; ?>
<?php if (! empty($subject_items)) : ?>
<?php
// ─────────────────── ПРОГРАММЫ (H2-секция на курс) ───────────────────
// Якорь #course-N совпадает с @id в микроразметке. Цена на странице не выводится.
// ─── ПРОГРАММЫ ───
// Якорь #course-N = @id
?>
<section class="teacher-single__courses section-bg reveal" data-bg="teacher-courses" aria-labelledby="teacher-courses-title">
<div class="container">
@ -710,8 +744,8 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php endif; ?>
<?php
// ─────────────────── СОЦИАЛЬНОЕ ДОКАЗАТЕЛЬСТВО ───────────────────
// Рейтинг преподавателя — $sp_avg (рассчитан выше), школы — из опций филиала.
// ─── СОЦИАЛЬНОЕ ДОКАЗАТЕЛЬСТВО ───
// Рейтинг: $sp_avg; школа — опции
$branch_avg = (string) get_option('branch_avg_rating', '');
$branch_cnt = (string) get_option('branch_ext_review_count', '');
if ('' !== $sp_avg || $branch_avg || $branch_cnt) :
@ -748,7 +782,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php endif; ?>
<?php
// ─────────────────── СЕГМЕНТЫ «ДЛЯ КОГО» ───────────────────
// ─── СЕГМЕНТЫ ───
?>
<section class="teacher-single__segments section-bg reveal" data-bg="teacher-segments" aria-labelledby="teacher-segments-title">
<div class="container">
@ -790,7 +824,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
</section>
<?php
// ─────────────────── ИЗМЕРИМЫЕ РЕЗУЛЬТАТЫ (отдельно от «Кому подойдут») ───────────────────
// ─── РЕЗУЛЬТАТЫ ───
if (! empty($results)) :
?>
<section class="teacher-single__results section-bg reveal" data-bg="teacher-results" aria-labelledby="teacher-results-title">
@ -816,44 +850,6 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
</section>
<?php endif; ?>
<?php
// ─────────────────── ЛИЧНАЯ ИСТОРИЯ + КАК ПРОХОДИТ ЗАНЯТИЕ ───────────────────
if ($about_text) :
?>
<section class="teacher-single__story section-bg reveal" data-bg="teacher-story" aria-labelledby="teacher-story-title">
<div class="container">
<div class="teacher-single__story-grid">
<div class="teacher-single__story-about">
<h2 id="teacher-story-title" class="bento-h2">О себе</h2>
<div class="teacher-single__story-text"><?php echo wp_kses_post(wpautop($about_text)); ?></div>
</div>
<div class="teacher-single__story-steps">
<h2 class="bento-h2">Как проходит первое занятие</h2>
<ol class="teacher-single__timeline">
<?php $step_num = 0; ?>
<?php foreach ($steps as $i => $step) :
$st_title = $step['step_title'] ?? '';
$st_text = $step['step_text'] ?? '';
if (! $st_title) {
continue;
}
$step_num++;
?>
<li class="teacher-single__timeline-item">
<span class="teacher-single__timeline-num" aria-hidden="true"><?php echo esc_html($step_num); ?></span>
<div class="teacher-single__timeline-content">
<h3><?php echo esc_html($st_title); ?></h3>
<?php if ($st_text) : ?><p><?php echo esc_html($st_text); ?></p><?php endif; ?>
</div>
</li>
<?php endforeach; ?>
</ol>
</div>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($video_html) : ?>
<section class="teacher-single__video section-bg reveal" data-bg="teacher-video" aria-labelledby="teacher-video-title">
<div class="container">
@ -867,7 +863,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php endif; ?>
<?php
// ─────────────────── КЕЙСЫ (ТАБЫ 9/11) ───────────────────
// ─── КЕЙСЫ ───
$case_tabs = array();
if (! empty($cases) && is_array($cases)) {
$case_tabs = array_values(array_unique(array_filter(array_map(function ($c) {
@ -917,7 +913,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php endif; ?>
<?php
// ─────────────────── FAQ (формат + общие) ───────────────────
// ─── FAQ ───
if (! empty($faq_items)) :
?>
<section class="teacher-single__faq section-bg reveal" data-bg="teacher-faq" aria-labelledby="teacher-faq-title">
@ -936,7 +932,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php endif; ?>
<?php
// ─── ОТЗЫВЫ О ПРЕПОДАВАТЕЛЕ (part reviews-link, как на главной) ───
// ─── ОТЗЫВЫ ───
$show_social_reviews = carbon_get_post_meta($tid, 'teacher_show_social_reviews');
if ('no' !== (string) $show_social_reviews) {
get_template_part('template-parts/reviews-link', null, array( 'teacher' => $tid ));
@ -944,7 +940,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
?>
<?php
// ─────────────────── ДОВЕРИЕ / ЛИЦЕНЗИЯ ───────────────────
// ─── ДОВЕРИЕ / ЛИЦЕНЗИЯ ───
?>
<section class="teacher-single__trust section-bg reveal" data-bg="teacher-trust" aria-labelledby="teacher-trust-title">
<div class="container">
@ -959,12 +955,12 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
</section>
<?php
// ─── ДРУГИЕ ПРЕПОДАВАТЕЛИ (общий блок сайта) ───
// ─── ДРУГИЕ ПРЕПОДАВАТЕЛИ ───
get_template_part('template-parts/teachers-cards');
?>
<?php
// ─────────────────── ГЕО-ПОКРЫТИЕ (из настроек филиала) ───────────────────
// ─── ГЕО ───
$geo_items = array();
if ('' !== (string) $branch_geo_areas) {
foreach (array_map('trim', explode(',', $branch_geo_areas)) as $ga) {
@ -1008,7 +1004,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
<?php endif; ?>
<?php
// ─────────────────── ФИНАЛЬНЫЙ CTA ───────────────────
// ─── ФИНАЛЬНЫЙ CTA ───
?>
<section class="teacher-single__final section-bg reveal" data-bg="teacher-final" aria-labelledby="teacher-final-title">
<div class="container">
@ -1034,7 +1030,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
// ─── КОНТАКТЫ / АДРЕС ───
get_template_part('template-parts/contacts');
// ─── ЭКСПЕРТНОСТЬ (E-E-A-T): автор — преподаватель, под картой ───
// ─── ЭКСПЕРТНОСТЬ ───
$teacher_creds = trim(implode(', ', array_filter(array(
$direction ?: '',
($tenure ? 'педагогический стаж ' . $tenure . ' лет' : ''),
@ -1046,7 +1042,7 @@ $schema_graph[] = array('@type' => 'BreadcrumbList', 'itemListElement' => $bread
?>
<?php
// ─────────────────── STICKY CTA (мобильный) ───────────────────
// ─── STICKY CTA ───
?>
<div class="teacher-single__sticky" hidden>
<div class="teacher-single__sticky-inner">