fix: SEO audit fixes for /otzyvy/ — Schema.org, PHP, JS, CSS, A11Y

Critical:
- Move GeoCoordinates from PostalAddress to Organization level
- Fix AggregateRating @id to point at #organization, not page URL

High:
- Replace Review.itemReviewed inline LocalBusiness with @id reference
- Change @type from array to just 'School'
- Remove foundingDate from reviews page (add comment for CF)
- Make telephone property conditional (only when non-empty)
- Add postalCode from branch_postal_code option
- Fix branch_city fallback from 'городе' to 'Щёлково'
- Replace inline onclick yaCounter with JS global handler
- Remove inline critical CSS (styles in file)
- Move price text to CF field reviews_intro_price_text

Medium:
- Add --color-bg and --accent CSS variables
- Fix .page-rewiews typo → .page-reviews
- Fix .contact-map display:flex→block
- Add prefers-reduced-motion support
- JS: var→let/const, scroll throttle via requestAnimationFrame
- JS: catch(e)→console.warn
- JS: data-target→data-target-value (avoid conflict)
- A11Y: FAQ aria-expanded/aria-controls, phone aria-label
- A11Y: modal role=dialog aria-modal=true
- A11Y: breadcrumb role=navigation aria-label
- Remove desktop-only class from breadcrumbs (mobile support)

Low:
- Add preconnect for unpkg.com (Phosphor Icons CDN)
- Add decoding=async to gallery images
This commit is contained in:
Dekart Deploy Bot 2026-06-27 12:31:12 +00:00
parent 6f5feb7521
commit c7bd6cef74
4 changed files with 4131 additions and 324 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,222 @@
/**
* reviews-page.js интерактив для page-reviews.php
* Анимации, фильтры, sticky CTA, FAQ, пагинация
*
* @package Dekart
*/
document.addEventListener('DOMContentLoaded', function () {
'use strict';
// ───────────────────────────────────────────────
// 1. АНИМАЦИЯ ПОЯВЛЕНИЯ (Intersection Observer)
// ───────────────────────────────────────────────
const revealElements = document.querySelectorAll('.reveal');
if (revealElements.length && 'IntersectionObserver' in window) {
const observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('reveal--visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
revealElements.forEach(function (el) {
observer.observe(el);
});
} else {
// Fallback: показываем всё сразу
revealElements.forEach(function (el) {
el.classList.add('reveal--visible');
});
}
// ───────────────────────────────────────────────
// 2. КНОПКИ "ЧИТАТЬ ЕЩЁ"
// ───────────────────────────────────────────────
document.querySelectorAll('.review-card__btn-more').forEach(function (btn) {
btn.addEventListener('click', function () {
const textBlock = this.parentNode.querySelector('.lw-card__text');
if (textBlock) {
textBlock.classList.toggle('review-card__text--expanded');
this.textContent = textBlock.classList.contains('review-card__text--expanded')
? 'Свернуть'
: 'Читать ещё';
}
});
});
// ───────────────────────────────────────────────
// 3. КНОПКА "ПОКАЗАТЬ ЕЩЁ" (десктоп)
// ───────────────────────────────────────────────
const loadMoreBtn = document.getElementById('reviews-loadmore');
if (loadMoreBtn) {
let nextBatch = 2;
const btn = loadMoreBtn.querySelector('.reviews-loadmore__btn');
if (btn) {
btn.addEventListener('click', function () {
const cards = document.querySelectorAll('.lw-card[data-batch="' + nextBatch + '"]');
if (cards.length) {
cards.forEach(function (card) {
card.style.display = 'flex';
});
nextBatch++;
// Если следующих батчей нет — прячем кнопку
const remaining = document.querySelectorAll('.lw-card[data-batch="' + nextBatch + '"]');
if (!remaining.length) {
loadMoreBtn.style.display = 'none';
}
}
});
}
}
// ───────────────────────────────────────────────
// 4. FAQ Accordion — keyboard + ARIA
// Глобальный click handler в add.js обрабатывает
// .box__accordion_label, синхронизируем aria-expanded
// ───────────────────────────────────────────────
document.querySelectorAll('.box__accordion_label').forEach(function (label) {
label.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.click();
}
});
// При клике (из add.js) синхронизируем aria-expanded
label.addEventListener('click', function () {
const content = document.getElementById(this.getAttribute('aria-controls'));
const isOpen = content && content.style.maxHeight && content.style.maxHeight !== '0px';
this.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
});
// ───────────────────────────────────────────────
// 5. СЧЁТЧИК (CountUp) для цифр доверия
// ───────────────────────────────────────────────
function animateCounter(el) {
const target = parseInt(el.getAttribute('data-target-value'), 10) ||
parseInt(el.getAttribute('data-target'), 10);
if (!target) return;
let current = 0;
const step = Math.ceil(target / 60); // 60 кадров ~ 1 сек
const timer = setInterval(function () {
current += step;
if (current >= target) {
current = target;
clearInterval(timer);
}
el.textContent = current.toLocaleString('ru-RU');
}, 16);
}
document.querySelectorAll('.counter-number').forEach(function (counter) {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
animateCounter(entry.target);
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.5 }
);
observer.observe(counter);
} else {
animateCounter(counter);
}
});
// ───────────────────────────────────────────────
// 6. STICKY CTA (мобильные) — троттлинг скролла
// ───────────────────────────────────────────────
const stickyCta = document.querySelector('.sticky-cta-mobile');
if (stickyCta) {
const heroSection = document.querySelector('.reviews-hero');
if (heroSection) {
const heroBottom = heroSection.offsetTop + heroSection.offsetHeight;
let ticking = false;
window.addEventListener('scroll', function () {
if (!ticking) {
window.requestAnimationFrame(function () {
if (window.scrollY > heroBottom) {
stickyCta.classList.add('sticky-cta-mobile--visible');
} else {
stickyCta.classList.remove('sticky-cta-mobile--visible');
}
ticking = false;
});
ticking = true;
}
});
}
}
// ───────────────────────────────────────────────
// 7. POPUP ФОРМЫ ОТЗЫВА (iframe)
// ───────────────────────────────────────────────
const formBtn = document.querySelector('.review-form-block__btn');
if (formBtn) {
formBtn.addEventListener('click', function () {
const popup = document.querySelector('.frame__popup');
const iframe = popup ? popup.querySelector('.frame_reviews') : null;
const frameUrl = popup ? popup.getAttribute('data-frame-url') : '';
if (popup && iframe && frameUrl) {
iframe.src = frameUrl;
popup.style.display = 'flex';
}
});
}
const closeBtn = document.querySelector('.btn-close-frame__popup');
if (closeBtn) {
closeBtn.addEventListener('click', function () {
const popup = this.parentNode.parentNode;
popup.style.display = 'none';
const iframe = popup.querySelector('.frame_reviews');
if (iframe) iframe.src = '';
});
}
// ───────────────────────────────────────────────
// 8. ЦЕЛИ ЯНДЕКС.МЕТРИКИ
// ───────────────────────────────────────────────
function reachGoal(name) {
if (typeof yaCounter99835482 !== 'undefined') {
try {
yaCounter99835482.reachGoal(name);
} catch (e) {
if (window.console) {
console.warn('Metrika reachGoal error:', e);
}
}
}
}
// Клики по всем CTA-кнопкам
document.querySelectorAll('.js-reviews-cta').forEach(function (btn) {
btn.addEventListener('click', function () {
const goal = this.getAttribute('data-goal') || 'cta_otzyvy_click';
reachGoal(goal);
});
});
// Клик по телефону
document.querySelectorAll('a[href^="tel:"]').forEach(function (link) {
link.addEventListener('click', function () {
reachGoal('phone_click_otzyvy');
});
});
// Клик по WhatsApp/Telegram
document.querySelectorAll('a[href*="wa.me"], a[href*="t.me"]').forEach(function (link) {
link.addEventListener('click', function () {
reachGoal('messenger_click_otzyvy');
});
});
});

View File

@ -240,35 +240,62 @@ function add_carbon() {
->add_fields( array( ->add_fields( array(
Field::make( 'image', 'img', 'Фотография:' ), Field::make( 'image', 'img', 'Фотография:' ),
)), )),
))
->add_tab('SEO и микроразметка', array(
Field::make( 'html', 'schema_branch_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Поля для <strong>JSON-LD микроразметки</strong> Schema.org филиала. Если поле пусто — свойство не добавляется в граф.</p>' ),
Field::make( 'text', 'schema_legal_name', 'Юридическое название (legalName)' )
->set_width( 50 )
->set_help_text( 'Полное юр. название организации. Например: "ИП Иванов Иван Иванович"' ),
Field::make( 'text', 'schema_postal_code', 'Почтовый индекс (postalCode)' )
->set_width( 50 )
->set_help_text( 'Индекс для address.postalCode в микроразметке.' ),
Field::make( 'text', 'schema_founded', 'Год основания (foundingDate)' )
->set_width( 50 )
->set_help_text( 'Год или полная дата: "2015" или "2015-09-01".' ),
Field::make( 'text', 'schema_map_url', 'Ссылка на карту (hasMap)' )
->set_width( 50 )
->set_help_text( 'Полная ссылка на Яндекс.Карты или Google Maps.' ),
Field::make( 'complex', 'schema_hours', 'Часы работы (openingHoursSpecification)' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Режим работы на каждый день недели.' )
->add_fields( array(
Field::make( 'select', 'day_of_week', 'День недели' )
->set_width( 30 )
->set_options( array(
'Monday' => 'Понедельник',
'Tuesday' => 'Вторник',
'Wednesday' => 'Среда',
'Thursday' => 'Четверг',
'Friday' => 'Пятница',
'Saturday' => 'Суббота',
'Sunday' => 'Воскресенье',
) ),
Field::make( 'time', 'opens', 'Открытие' )
->set_width( 25 ),
Field::make( 'time', 'closes', 'Закрытие' )
->set_width( 25 ),
Field::make( 'checkbox', 'day_off', 'Выходной' )
->set_width( 20 )
->set_help_text( 'Отметьте, если в этот день не работаем.' ),
) ),
Field::make( 'complex', 'schema_social', 'Социальные сети (sameAs)' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Ссылки на соцсети профилей для sameAs[].' )
->add_fields( array(
Field::make( 'text', 'platform', 'Платформа' )
->set_width( 30 )
->set_help_text( 'Например: VK, Telegram, YouTube' ),
Field::make( 'text', 'url', 'Ссылка' )
->set_width( 70 )
->set_help_text( 'Полный URL профиля.' ),
) ),
Field::make( 'media_gallery', 'schema_photos', 'Фотографии филиала (image[])' )
->set_help_text( 'Выберите фотографии интерьера/экстерьера для image[].' ),
)); ));
// ==================================================== // (пусто — старые поля banner_img_desktop, banner_img_mobile,
// Доп поля для всех страниц // banner_h1, banner_text, page_breadcrumbs удалены 31 мая 2026)
// ====================================================
Container::make('post_meta', 'Дополнительные поля')
->where('post_type', '=', 'page')
->where('post_id', '!=', 64)
->add_fields(array(
Field::make( 'image', 'banner_img_desktop', 'Изображение для баннера (десктопная версия)' )
->help_text( 'Размер изображения (желательно) 1295px на 454px' )
->set_width( 15 ),
Field::make( 'image', 'banner_img_mobile', 'Изображение для баннера (мобильная версия)' )
->help_text( 'Размер изображения (желательно) 450px на 225px' )
->set_width( 15 ),
Field::make( 'text', 'banner_h1', 'текст для H1' )
->set_width( 100 ),
Field::make( 'text', 'banner_text', 'Дополнительный текст под H1' )
->set_width( 100 ),
Field::make( 'complex', 'page_breadcrumbs', 'Хлебные крошки' )
->set_layout( 'tabbed-horizontal' )
->add_fields( array(
Field::make( 'text', 'breadcrumbs_name', 'Название крошки:' )
->set_width( 50 ),
Field::make( 'text', 'breadcrumbs_link', 'Ссылка крошки:' )
->set_width( 50 )
->set_default_value('#'),
)),
));
// ==================================================== // ====================================================
// Доп поля для категории Курсов // Доп поля для категории Курсов
@ -473,6 +500,212 @@ function add_carbon() {
)), )),
)); ));
// ====================================================
// Доп поля для страницы "Подготовка к школе (Bento 2026)"
// ====================================================
Container::make('post_meta', 'Настройки Подготовки к школе')
->where('post_template', '=', 'page-podgotovka-k-shkole.php')
->add_tab('🖼️ Герой (баннер)', array(
Field::make( 'html', 'pksh_hero_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.hero-pksh</strong>. Первый экран страницы подготовки к школе.</p>' ),
Field::make( 'image', 'banner_image', 'Фото баннера' )
->set_width( 50 )
->set_help_text( 'Рекомендуемый размер: 689×689 px, квадратное. Если не указано — используется фото по умолчанию.' ),
Field::make( 'text', 'banner_h1', 'Заголовок H1' )
->set_width( 50 )
->set_default_value( 'Ваш ребёнок готов к первому классу? Научим читать, считать и писать за 4 месяца — без слёз и стресса' )
->set_help_text( 'Главный заголовок на первом экране.' ),
Field::make( 'textarea', 'banner_subtitle', 'Подзаголовок' )
->set_width( 50 )
->set_default_value( 'Первое занятие — бесплатно. Результат через 3 недели — или вернём деньги' )
->set_help_text( 'Текст под заголовком. Опционально.' ),
Field::make( 'text', 'banner_scarcity', 'Текст дефицита (срочность)' )
->set_width( 50 )
->set_default_value( '🔥 Осталось <strong>2 места из 8</strong> в группе 56 лет. Стартуем <strong>1 июня</strong>' )
->set_help_text( 'Можно использовать HTML-теги: &lt;strong&gt;, &lt;br&gt;. Текст о срочности/дефиците мест под кнопкой.' ),
))
->add_tab('📊 Соц.доказательство', array(
Field::make( 'html', 'pksh_social_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.social-counters</strong>. Блок цифр социального доказательства (4 карточки). Иконки заданы в шаблоне, здесь только текст.</p>' ),
Field::make( 'complex', 'pksh_social_counters', 'Показатели' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Добавьте до 4 показателей. Порядок = порядок вывода. Иконки не редактируются.' )
->add_fields( array(
Field::make( 'text', 'counter_number', 'Число' )
->set_width( 30 )
->set_help_text( 'Например: 500+, 12, 4.8, 20+' ),
Field::make( 'text', 'counter_label', 'Подпись' )
->set_width( 70 )
->set_help_text( 'Например: выпускников в топ-школах' ),
)),
))
->add_tab('Битрикс24', array(
Field::make( 'textarea', 'pksh_bitrix_script', 'Скрипт формы Битрикс24' )
->set_help_text( 'Вставьте полный JS-код интеграции с CRM-формой Битрикс24. Выводится в блоке Final CTA на странице подготовки к школе.' ),
))
->add_tab('⭐ Отзывы', array(
Field::make( 'html', 'pksh_reviews_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.pksh-reviews</strong>. Отзывы родителей на странице подготовки к школе.</p>' ),
Field::make( 'complex', 'pksh_reviews_list', 'Отзывы' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Добавьте отзывы родителей. Показываются на странице подготовки к школе.' )
->add_fields( array(
Field::make( 'image', 'review_avatar', 'Аватар' )
->set_width( 15 )
->set_help_text( 'Опционально. Если не указано — генерируется из имени.' ),
Field::make( 'text', 'review_author', 'Имя автора' )
->set_width( 25 ),
Field::make( 'text', 'review_rating', 'Оценка (15)' )
->set_width( 10 )
->set_attribute( 'type', 'number' ),
Field::make( 'date', 'review_date', 'Дата отзыва' )
->set_width( 15 )
->set_help_text( 'Для Schema.org Review.datePublished.' ),
Field::make( 'textarea', 'review_text', 'Текст отзыва' )
->set_width( 35 ),
)),
))
->add_tab('❓ FAQ', array(
Field::make( 'html', 'pksh_faq_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>#pksh-faq</strong>. Вопросы и ответы в стиле лагеря, две колонки.</p>' ),
Field::make( 'complex', 'pksh_faq_items', 'Вопросы и ответы' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Добавьте вопросы и ответы. Показываются в секции FAQ на странице подготовки к школе.' )
->add_fields( array(
Field::make( 'textarea', 'faq_question', 'Вопрос' )
->set_width( 30 ),
Field::make( 'textarea', 'faq_answer', 'Ответ' )
->set_width( 70 ),
)),
))
->add_tab('👨‍🏫 Преподаватели', array(
Field::make( 'html', 'pksh_teachers_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>#teachers</strong>. Карточки преподавателей в стиле лагеря.</p>' ),
Field::make( 'complex', 'pksh_teachers', 'Преподаватели' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Добавьте преподавателей. Выводятся на странице подготовки к школе в секции Преподаватели.' )
->add_fields( array(
Field::make( 'text', 'teacher_name', 'ФИО' )
->set_width( 40 ),
Field::make( 'image', 'teacher_photo', 'Фото' )
->set_help_text( 'Оптимально: 240×240 px, квадратное.' )
->set_width( 30 ),
Field::make( 'text', 'teacher_subject', 'Предмет / должность' )
->set_width( 30 ),
Field::make( 'textarea', 'teacher_about', 'О педагоге' )
->set_width( 100 ),
)),
))
// ====================================================
// Вкладка: Программы подготовки к школе
// ====================================================
->add_tab('📚 Программы', array(
Field::make( 'html', 'pksh_programs_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>#programs</strong>. Карточки программ подготовки к школе. Каждая карточка — отдельная запись.</p>' ),
Field::make( 'complex', 'pksh_programs', 'Программы подготовки' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Добавьте программы подготовки к школе. Каждая — отдельная карточка.' )
->add_fields( array(
Field::make( 'text', 'program_icon', 'Иконка (emoji)' )
->set_width( 20 )
->set_default_value( '🎓' ),
Field::make( 'text', 'program_title', 'Название программы' )
->set_width( 80 ),
Field::make( 'text', 'program_intensity', 'Интенсивность' )
->set_width( 50 )
->set_help_text( 'Например: 2 раза в неделю' ),
Field::make( 'textarea', 'program_schedule', 'Расписание' )
->set_width( 50 )
->set_help_text( 'Каждая строка — один вариант расписания' ),
Field::make( 'text', 'program_duration', 'Длительность занятия' )
->set_width( 30 )
->set_default_value( '90 мин' ),
Field::make( 'textarea', 'program_lesson_structure', 'Структура занятия (timeline)' )
->set_width( 70 )
->set_help_text( 'Каждая строка: время 🔤 Название. Например: "15 мин 🧠 Нейрогимнастика"' ),
Field::make( 'textarea', 'program_includes', 'Программа включает' )
->set_width( 100 )
->set_help_text( 'Каждая строка — пункт списка' ),
Field::make( 'text', 'program_price_subscription', 'Цена абонемента' )
->set_width( 40 )
->set_default_value( '7 500 ₽' )
->set_help_text( 'Например: 7 500 ₽' ),
Field::make( 'text', 'program_price_single', 'Цена разового занятия' )
->set_width( 40 )
->set_default_value( '1 000 ₽' )
->set_help_text( 'Например: 1 000 ₽' ),
Field::make( 'text', 'program_time_required', 'Длительность курса (timeRequired)' )
->set_width( 30 )
->set_help_text( 'Общая длительность курса. Например: "4 месяца", "P4M"' ),
Field::make( 'text', 'program_repeat_frequency', 'Частота занятий (ISO 8601)' )
->set_width( 30 )
->set_help_text(
'Для Schema.org Schedule.repeatFrequency. '
. 'Пример: P1W (раз в неделю), P4D (дважды в неделю). '
. 'program_intensity остаётся для отображения на сайте.'
)
->set_attribute( 'placeholder', 'P1W' ),
Field::make( 'text', 'program_workload', 'Нагрузка (courseWorkload)' )
->set_width( 70 )
->set_help_text( 'Например: "2 занятия в неделю по 90 минут"' ),
Field::make( 'date', 'program_start_date', 'Дата старта программы' )
->set_width( 33 ),
Field::make( 'date', 'program_end_date', 'Дата окончания' )
->set_width( 33 ),
Field::make( 'date', 'program_price_valid', 'Цена действительна до' )
->set_width( 34 )
->set_help_text( 'Дата, до которой актуальна указанная цена.' ),
Field::make( 'text', 'program_button_text', 'Текст кнопки' )
->set_width( 40 )
->set_default_value( 'Записаться' ),
Field::make( 'text', 'program_button_url', 'Ссылка кнопки' )
->set_width( 60 )
->set_default_value( '/zapis/' ),
)),
))
// ====================================================
// Вкладка: JSON-LD Schema (данные для микроразметки)
// ====================================================
->add_tab('🧠 JSON-LD (схема)', array(
Field::make( 'html', 'pksh_schema_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Поля для <strong>JSON-LD микроразметки</strong> Schema.org. Заполняются опционально — если поле пусто, свойство не добавляется в граф.</p>' ),
Field::make( 'textarea', 'pksh_description', 'Описание курса (для микроразметки)' )
->set_help_text(
'Подробное описание программы для Schema.org Course.description. '
. 'Если заполнено — заменяет banner_subtitle в микроразметке. '
. 'Рекомендуется минимум 150 символов. НЕ влияет на отображение страницы.'
)
->set_rows( 4 )
->set_attribute( 'placeholder', 'Комплексная программа подготовки детей 57 лет к первому классу...' ),
Field::make( 'text', 'pksh_level', 'Уровень образования (educationalLevel)' )
->set_width( 50 )
->set_help_text( 'Например: "дошкольное образование" или "5-7 лет"' ),
Field::make( 'text', 'pksh_age', 'Возраст детей' )
->set_width( 50 )
->set_help_text( 'Например: "57 лет"' ),
Field::make( 'text', 'pksh_audience', 'Целевая аудитория (audience)' )
->set_help_text( 'Например: "Дети 57 лет, будущие первоклассники"' ),
Field::make( 'select', 'pksh_course_mode', 'Формат обучения (courseMode)' )
->set_width( 50 )
->set_help_text( 'Формат занятий для Schema.org CourseInstance.courseMode.' )
->add_options( array(
'' => '— выберите —',
'Onsite' => 'Очно (в центре)',
'Online' => 'Онлайн',
'Blended' => 'Смешанный (очно + онлайн)',
) ),
Field::make( 'complex', 'pksh_skills', 'Чему учат на курсе' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Навыки, которые освоит ребёнок. Каждая запись — отдельный навык (дисциплина).' )
->add_fields( array(
Field::make( 'text', 'skill', 'Навык / дисциплина' )
->set_width( 100 )
->set_attribute( 'placeholder', 'Чтение, Счёт до 100, Письмо печатными буквами' )
->set_help_text( 'Например: "Чтение", "Счёт до 100", "Письмо печатными буквами"' ),
)),
));
// ==================================================== // ====================================================
// Уникальное имя контейнера Продлёнки // Уникальное имя контейнера Продлёнки
// ==================================================== // ====================================================
@ -742,6 +975,245 @@ function add_carbon() {
->set_help_text( 'Вставьте полный JS-код интеграции с CRM-формой Битрикс24. Выводится в модальном окне заявки.' ), ->set_help_text( 'Вставьте полный JS-код интеграции с CRM-формой Битрикс24. Выводится в модальном окне заявки.' ),
)); ));
// ════════════════════════════════════════════════════════════
// СТРАНИЦА ОТЗЫВОВ (page-reviews.php)
// ════════════════════════════════════════════════════════════
Container::make('post_meta', '⭐ Настройки страницы Отзывы')
->where('post_template', '=', 'page-reviews.php')
->add_tab('🧑‍🏫 Hero (первый экран)', array(
Field::make( 'html', 'reviews_hero_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">H1 и подзаголовок — статические в шаблоне. Здесь только метрики и CTA.</p>' ),
Field::make( 'separator', 'reviews_metric_sep', 'Метрики доверия' ),
Field::make( 'text', 'reviews_hero_metric_1_number', 'Метрика 1 — число' )
->set_default_value( '500+' )
->set_width( 30 )
->set_help_text( 'Крупная цифра, которая сразу привлекает внимание. Выбирайте самую сильную — ту, что впечатляет больше всего: количество учеников, лет на рынке, % поступления. Добавьте + или % для эффекта. Примеры: «500+», «98%», «15».' ),
Field::make( 'text', 'reviews_hero_metric_1_label', 'Метрика 1 — подпись' )
->set_default_value( 'счастливых семей' )
->set_width( 70 )
->set_help_text( 'Подпись к цифре из левого поля. Объясняет, что именно посчитано. Совет: используйте родительный падеж — звучит весомее. Примеры: «счастливых семей», «выпускников», «учеников». Не пишите просто «семей» — добавьте «счастливых», это продаёт эмоцию.' ),
Field::make( 'text', 'reviews_hero_metric_2_number', 'Метрика 2 — число' )
->set_default_value( '12+' )
->set_width( 30 )
->set_help_text( 'Вторая по силе цифра. Не ставьте 0, 1, 2 — они не впечатляют. Примеры: «12+», «97%», «50».' ),
Field::make( 'text', 'reviews_hero_metric_2_label', 'Метрика 2 — подпись' )
->set_default_value( 'лет в образовании' )
->set_width( 70 )
->set_help_text( 'Подпись к цифре 2. Примеры: «лет на рынке», «лет в образовании», «преподавателей». Старайтесь, чтобы подпись читалась вместе с цифрой как законченная фраза: «12+ лет в образовании».' ),
Field::make( 'text', 'reviews_hero_metric_3_number', 'Метрика 3 — число' )
->set_default_value( '4.9' )
->set_width( 30 )
->set_help_text( 'Третья цифра. Если сильных цифр больше нет — поставьте рейтинг или оценку. Примеры: «4.9», «5.0», «топ-3». Дробные числа пишите через точку.' ),
Field::make( 'text', 'reviews_hero_metric_3_label', 'Метрика 3 — подпись' )
->set_default_value( 'рейтинг на Яндексе' )
->set_width( 70 )
->set_help_text( 'Подпись к цифре 3. Примеры: «рейтинг на Яндексе», «звёзд», «место в рейтинге области». Если указали рейтинг — обязательно добавьте «из 5» или площадку: «рейтинг на Яндексе» звучит убедительнее, чем просто «рейтинг».' ),
Field::make( 'text', 'reviews_hero_metric_4_number', 'Метрика 4 — число' )
->set_default_value( '14' )
->set_width( 30 )
->set_help_text( 'Четвёртая цифра. Например: количество отзывов, количество педагогов, лет опыта.' ),
Field::make( 'text', 'reviews_hero_metric_4_label', 'Метрика 4 — подпись' )
->set_default_value( 'отзывов о школе' )
->set_width( 70 )
->set_help_text( 'Подпись к цифре 4. Пример: «отзывов о школе», «педагогов», «лет на рынке».' ),
// CTA кнопка — статическая в шаблоне, открывает модалку Bitrix24
))
->add_tab('📝 Вступление (SEO-текст)', array(
Field::make( 'html', 'reviews_intro_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.reviews-intro</strong> — SEO-текст в начале страницы. Управляется из админки, чтобы можно было править текст без редактирования шаблона.</p>' ),
Field::make( 'text', 'reviews_intro_price_text', 'Стоимость обучения (текст)' )
->set_default_value( 'от 40 000 ₽ в месяц' )
->set_width( 100 )
->set_help_text( 'Текст стоимости в блоке .reviews-intro__info. Пример: «от 40 000 ₽ в месяц» или «от 35 000 ₽/мес». Если поле пусто — строка не выводится.' ),
Field::make( 'textarea', 'reviews_intro_lead', 'Лид-абзац (первый, крупный)' )
->set_default_value( 'Частная школа Декарт в Щёлково — это образовательное пространство, где каждый ребёнок получает внимание, заботу и качественные знания. Мы работаем в Щёлковском городском округе с 2010 года и за это время помогли раскрыть потенциал сотням учеников. Отзывы родителей о школе Декарт — это реальные истории семей, которые доверили нам образование своих детей. Мы публикуем их, чтобы вы могли принять взвешенное решение.' )
->set_help_text( 'Первый, самый заметный абзац. Отображается крупнее остальных, отделён линией. 150180 символов — оптимально. Должен вовлекать и отвечать на главный вопрос посетителя.' ),
Field::make( 'textarea', 'reviews_intro_card_1', 'Блок «О школе» (карточка 1)' )
->set_default_value( 'В нашей частной школе в Щёлково учатся дети с 1 по 11 класс. Мы предлагаем программы начального общего образования, подготовки к ОГЭ и ЕГЭ, углублённое изучение английского языка. Классы небольшие — до 12 человек, поэтому каждый ученик получает индивидуальный подход. Родители, которые ищут качественное образование в Щёлково и Московской области, выбирают Декарт за профессионализм педагогов и тёплую атмосферу.' )
->set_help_text( 'Левая карточка в сетке из двух колонок. Здесь — факты о школе: классы, программы, преимущества. 100200 символов.' ),
Field::make( 'textarea', 'reviews_intro_card_2', 'Блок «О странице» (карточка 2)' )
->set_default_value( 'Мы ценим каждое мнение и постоянно работаем над улучшением обучения. Ниже вы найдёте реальные отзывы родителей о нашей частной школе, достижения учеников и ответы на частые вопросы. Приятного чтения!' )
->set_help_text( 'Правая карточка в сетке. Объясняет, что пользователь найдёт на странице. 80120 символов.' ),
))
->add_tab('🏠 Почему выбирают (локальный)', array(
Field::make( 'html', 'reviews_local_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.reviews-local</strong> — список причин выбрать школу с привязкой к городу. Заголовок секции статический в шаблоне, подставляет город из настроек филиала.</p>' ),
Field::make( 'complex', 'reviews_local_items', 'Причины' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( '46 причин, почему родители выбирают именно Декарт. Каждая — жирный заголовок + описание. Текст должен быть с гео-привязкой (упоминания города, района, местных реалий).' )
->add_fields( array(
Field::make( 'text', 'title', 'Заголовок' )
->set_width( 30 )
->set_help_text( 'Короткий, ёмкий заголовок. 25 слов. ✅ «Удобное расположение» ❌ «Мы находимся в удобном месте с хорошей транспортной доступностью».' ),
Field::make( 'textarea', 'text', 'Описание' )
->set_width( 70 )
->set_help_text( '23 предложения с гео-привязкой. Упомяните город, район, местные реалии. Родитель должен почувствовать: «Это про мой город, это для меня».' ),
)),
))
->add_tab('📊 Наши результаты (цифрами)', array(
Field::make( 'html', 'reviews_results_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.results</strong></p>' ),
Field::make( 'text', 'reviews_results_title', 'Заголовок секции' )
->set_default_value( 'Декарт в цифрах' )
->set_width( 100 )
->set_help_text( 'Заголовок блока с результатами. Должен привлекать внимание и обещать пользу. Примеры: «Декарт в цифрах», «Наши достижения», «Почему нам доверяют родители». Избегайте канцеляризмов: «Статистические показатели» — плохо.' ),
Field::make( 'complex', 'reviews_results', 'Показатели' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( '4-6 цифр, которые доказывают качество школы. Каждый показатель должен отвечать на вопрос родителя: «Почему мы должны вам доверять?». Примеры: % поступивших в вузы, средний балл учеников, стаж преподавателей, количество учеников. Избегайте абстрактных цифр вроде «более 50 довольных клиентов» — звучит неубедительно.' )
->add_fields( array(
Field::make( 'text', 'number', 'Число' )
->set_width( 25 )
->set_help_text( 'Сама цифра — должна выглядеть эффектно и убедительно. ✅ «100%», «85%», «500+», «50». ❌ «много», «больше 10», «каждый второй» — размытые формулировки не работают. Используйте % и + чтобы цифра казалась больше.' ),
Field::make( 'select', 'icon', 'Иконка' )
->set_width( 20 )
->set_help_text( 'Иконка рядом с цифрой. Подбирайте по смыслу: 🏆 достижения, 📖 учёба, 👥 ученики, ❤️ любовь/забота. Иконка помогает быстро понять суть.' )
->add_options( array(
'ph-graduation-cap' => '🎓 Диплом / шапочка',
'ph-chats' => '💬 Речевой пузырь',
'ph-users-three' => '👥 Люди / группа',
'ph-star' => '⭐ Звезда',
'ph-shield-check' => '🛡️ Галочка / щит',
'ph-book' => '📖 Книга',
'ph-heart' => '❤️ Сердце',
'ph-clock' => '🕐 Часы',
'ph-house' => '🏠 Дом',
'ph-trophy' => '🏆 Кубок',
)),
Field::make( 'text', 'label', 'Подпись' )
->set_width( 55 )
->set_help_text( 'Пояснение к цифре. Текст должен быть понятен САМ ПО СЕБЕ, даже без цифры. ✅ «учеников сдают ВПР на 4 и 5», ❌ «сдают ВПР» (что? кто?). Длина — 3-5 слов. Примеры: «лет на рынке», «выпускников поступают в топ-вузы», «средний балл учеников по математике».' ),
)),
))
->add_tab('❓ FAQ (вопросы-ответы)', array(
Field::make( 'html', 'reviews_faq_tab_desc', '' )
->set_html( '<div style="background:#f0f6fc;border-left:4px solid #72aee6;padding:16px 20px;margin:0 0 16px;border-radius:4px;font-size:13px;line-height:1.6;max-width:680px">
<p style="margin:0 0 8px;font-weight:700;font-size:14px">🎯 Какие вопросы добавлять на эту страницу</p>
<p style="margin:0 0 12px">Эта страница последний шаг перед записью. Человек уже почти решился, он читает отзывы чтобы окончательно убедиться. Вопросы и ответы здесь должны <strong>снимать последние сомнения</strong>, а не рассказывать о центре в целом.</p>
<p style="margin:0 0 6px;font-weight:600;color:#2c3338"> Правильные вопросы про доверие и финальные сомнения:</p>
<ul style="margin:0 0 12px;padding-left:20px">
<li> Что если после первого занятия не понравится?</li>
<li> Можно ли сначала прийти на пробный урок?</li>
<li> Мой ребёнок сильно отстаёт от программы вам это не помешает?</li>
<li> Как быстро будет виден результат?</li>
<li> Что если мы пропустим занятие?</li>
</ul>
<p style="margin:0 0 6px;font-weight:600;color:#b32d2e"> Неправильные вопросы их размещайте на других страницах:</p>
<ul style="margin:0 0 12px;padding-left:20px">
<li> Какие предметы вы преподаёте? <em>на главную или страницу курсов</em></li>
<li> Сколько стоит обучение? <em>на страницу цен</em></li>
<li> Где вы находитесь? <em>на страницу контактов</em></li>
<li> Чем вы отличаетесь от других центров? <em>на страницу «О нас»</em></li>
</ul>
<p style="margin:0 0 4px;font-weight:600">📐 Сколько вопросов добавлять:</p>
<p style="margin:0">Оптимально <strong>46</strong>. Меньше не закрывает сомнения, больше отвлекает от записи.</p>
</div>' ),
// Заголовок секции — статический в шаблоне
Field::make( 'complex', 'reviews_faq', 'Вопросы и ответы' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Вопросы — снимают последние сомнения перед записью. 46 штук.' )
->add_fields( array(
Field::make( 'textarea', 'question', 'Вопрос' )
->set_width( 30 )
->set_help_text( 'Вопрос должен звучать так, как его задаст реальный родитель — простыми словами, без канцелярита. ✅ «А если ребёнок не захочет ходить?» ❌ «Как преодолеть сопротивление ребёнка к посещению занятий?». Первое — вызывает доверие, второе — выглядит как копипаста с другого сайта.' ),
Field::make( 'textarea', 'answer', 'Ответ' )
->set_width( 70 )
->set_help_text( 'Ответ должен снимать страх или сомнение, а не просто сухо информировать. Используйте слова-усилители: «да», «конечно», «бесплатно», «попробуйте», «всегда можно». Структура: 1) Поймите чувства родителя («Это нормально, что вы переживаете…»). 2) Дайте решение («…поэтому у нас есть пробный урок»). 3) Позовите к действию («Приходите познакомиться — это бесплатно»).' ),
)),
))
->add_tab('⭐ Отзывы (карточки)', array(
Field::make( 'html', 'reviews_cards_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.reviews-grid</strong>. Карточки отзывов, которые видят посетители.</p>' ),
Field::make( 'complex', 'reviews_cards', 'Отзывы' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Карточки отзывов родителей — главный элемент доверия на странице. Лучше 3-4 подробных отзыва, чем 10 коротких «всё нравится». Хороший отзыв содержит: с чего начали → что изменилось → какой результат. Эмоциональные отзывы продают лучше фактов.' )
->add_fields( array(
Field::make( 'text', 'review_author', 'Имя автора' )
->set_width( 22 )
->set_help_text( 'Реальное имя родителя: «Анна», «Сергей», «Екатерина М.». Имена повышают доверие. Можно добавить статус: «Анна, мама Артёма». НЕ используйте анонимные «Родитель» или «Гость» — это вызывает подозрение.' ),
Field::make( 'image', 'review_photo', 'Фото автора' )
->set_width( 13 )
->set_value_type( 'url' )
->set_help_text( 'Фото родителя (реальное лицо). Если не загружено — показывается аватар-заглушка с первой буквой имени. Фото повышает доверие к отзыву. Рекомендуемый размер: 96×96px, формат: WebP/JPG.' ),
Field::make( 'text', 'review_rating', 'Оценка (15)' )
->set_width( 10 )
->set_attribute( 'type', 'number' )
->set_help_text( 'Оценка от 1 до 5. Ставьте 5 — если отзыв восторженный. 4 — если есть мелкие замечания (это добавляет правдоподобности). Оценки 3 и ниже публиковать не стоит — они снижают доверие.' ),
Field::make( 'date', 'review_date', 'Дата отзыва' )
->set_storage_format( 'd.m.Y' )
->set_input_format( 'd.m.Y', 'd.m.Y' )
->set_picker_options( array(
'altInput' => false,
) )
->set_help_text( 'Дата отзыва. Свежие отзывы (2024-2025) работают лучше старых. Формат: ДД.ММ.ГГГГ (например: 10.03.2025). Можно открыть календарь — иконка справа.' )
->set_width( 20 ),
Field::make( 'association', 'review_teacher', 'Преподаватель' )
->set_width( 30 )
->set_max( 1 )
->set_types( array(
array(
'type' => 'post',
'post_type' => 'teacher',
),
) )
->set_help_text( 'Если родитель хвалит конкретного учителя — привяжите отзыв к нему. Тогда на странице преподавателя тоже появится этот отзыв. Необязательно.' ),
Field::make( 'textarea', 'review_text', 'Текст отзыва' )
->set_width( 35 )
->set_help_text( 'Полный текст отзыва. 3-5 предложений — оптимально. Хорошая структура: 1) С чего начали («Пришли с проблемой…»), 2) Что изменилось («Через месяц ребёнок…»), 3) Результат («Сейчас читает лучше всех в классе»). Эмоции и конкретные детали убеждают сильнее общих фраз. Орфографию можно поправить, но сохраните живую речь.' ),
)),
))
// 📝 Форма отзыва — тексты статические в шаблоне page-reviews.php
// 🎯 Финальный CTA — статический в шаблоне page-reviews.php.
// Скрипт для кнопок — branch_button_script (Settings API → Настройки филиала).
->add_tab('🔗 Перелинковка', array(
Field::make( 'html', 'reviews_cross_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.cross-links</strong> — ссылки на другие разделы сайта. Улучшает навигацию и SEO.</p>' ),
Field::make( 'text', 'reviews_cross_title', 'Заголовок' )
->set_default_value( 'Наши программы' )
->set_width( 100 )
->set_help_text( 'Заголовок блока со ссылками. По умолчанию «Наши программы». Альтернативы: «Узнайте больше», «Другие направления», «Выберите программу». Важно: заголовок должен подталкивать перейти по ссылкам.' ),
Field::make( 'complex', 'reviews_cross_links', 'Ссылки на другие страницы' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( '5-7 ссылок на другие страницы сайта. Это помогает родителям найти нужную программу и улучшает SEO (перелинковка). Обязательно добавьте: подготовка к школе, начальная школа, средняя школа, ВПР, ОГЭ/ЕГЭ, продлёнка, летний лагерь.' )
->add_fields( array(
Field::make( 'text', 'title', 'Название' )
->set_width( 50 )
->set_help_text( 'Текст ссылки, который увидит посетитель. ✅ «Подготовка к школе», «Группа продлённого дня». Название должно быть понятно без контекста.' ),
Field::make( 'text', 'url', 'Ссылка' )
->set_width( 50 )
->set_help_text( 'URL страницы. Можно указать сокращённо: /podgotovka-k-shkole/ или полный адрес. Скопируйте из адресной строки браузера при просмотре нужной страницы.' ),
)),
))
->add_tab('💎 Видеоотзывы', array(
Field::make( 'html', 'reviews_video_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.video-reviews</strong>. Видеоотзывы — самый убедительный формат контента. Добавьте 2-4 коротких видео (1-3 минуты).</p>' ),
Field::make( 'text', 'reviews_video_title', 'Заголовок секции' )
->set_default_value( 'Видеоотзывы родителей' )
->set_width( 100 )
->set_help_text( 'Заголовок секции с видео. По умолчанию «Видеоотзывы родителей». Можно изменить на «Смотрите, что говорят родители», «Истории наших семей» — более эмоциональные варианты работают лучше.' ),
Field::make( 'complex', 'reviews_video', 'Видеоотзывы' )
->set_layout( 'tabbed-horizontal' )
->set_help_text( 'Добавьте видеоотзывы — это повышает доверие сильнее, чем текст. Видео должны быть на YouTube, в открытом доступе, длительностью 1-3 минуты. Хорошо, если родитель рассказывает историю: пришли с проблемой → как школа помогла → результат.' )
->add_fields( array(
Field::make( 'text', 'name', 'Имя автора' )
->set_width( 30 )
->set_help_text( 'Имя родителя из видео. Например: «Мария, мама Артёма». Если в видео несколько человек — укажите главного героя.' ),
Field::make( 'url', 'url', 'Ссылка на видео (YouTube)' )
->set_width( 70 )
->set_help_text( 'Ссылка на видео. Поддерживаются: YouTube (https://www.youtube.com/watch?v=...), VK Видео (https://vk.com/video-... или https://vkvideo.ru/...), Rutube и другие oEmbed-провайдеры. Видео должно быть в открытом доступе. Проверьте ссылку перед публикацией.' ),
)),
))
->add_tab('📸 Фото с занятий', array(
Field::make( 'html', 'reviews_photos_tab_desc', '' )
->set_html( '<p style="color: #64748b; font-size: 13px; margin: 0 0 12px;">Секция: <strong>.reviews-photos</strong> — фотогалерея с занятий. 4-8 фото повышают доверие и показывают атмосферу школы. Рекомендуемый размер: 800×600px, формат WebP.</p>' ),
Field::make( 'text', 'reviews_photos_title', 'Заголовок секции' )
->set_default_value( 'Фото с занятий' )
->set_width( 100 )
->set_help_text( 'Заголовок галереи. По умолчанию «Фото с занятий». Альтернативы: «Как проходят занятия», «Наши будни», «Атмосфера школы».' ),
Field::make( 'media_gallery', 'reviews_photos', 'Фотографии' )
->set_type( array( 'image' ) )
->set_help_text( 'Загрузите 4-8 фотографий. РЕКОМЕНДУЕМЫЙ РАЗМЕР: 800×600px (4:3), формат WebP. Все фото в галерее автоматически обрезаются до одинакового размера на странице (4:3), соотношение сторон сохраняется.' ),
));
} }
// функция получения id курса по его названию // функция получения id курса по его названию
@ -810,6 +1282,24 @@ function replaceQuotes($text) {
return $text; return $text;
} }
/**
* Санитация для поля branch_button_script.
* Сохраняет <script> тэги (в отличие от wp_kses_post).
*/
function dekart_sanitize_script_field( $value ) {
$allowed = wp_kses_allowed_html( 'post' );
$allowed['script'] = array(
'src' => true,
'async' => true,
'defer' => true,
'data-b24-form' => true,
'data-skip-moving'=> true,
'type' => true,
'charset' => true,
);
return wp_kses( $value, $allowed );
}
function enqueue_template_specific_styles() { function enqueue_template_specific_styles() {
if ( is_page_template('page-camp.php') ) { if ( is_page_template('page-camp.php') ) {
wp_enqueue_style( wp_enqueue_style(
@ -826,6 +1316,27 @@ function enqueue_template_specific_styles() {
true true
); );
} }
if ( is_page_template('page-reviews.php') ) {
wp_enqueue_style(
'phosphor-icons',
'https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css',
array(),
'2.1.1'
);
wp_enqueue_style(
'reviews-page-style',
get_template_directory_uri() . '/assets/css/reviews-page.css',
array(),
filemtime(get_template_directory() . '/assets/css/reviews-page.css')
);
wp_enqueue_script(
'reviews-page-script',
get_template_directory_uri() . '/assets/js/reviews-page.js',
array(),
filemtime(get_template_directory() . '/assets/js/reviews-page.js'),
true
);
}
if ( is_page_template('page-after-school.php') ) { if ( is_page_template('page-after-school.php') ) {
wp_enqueue_style( wp_enqueue_style(
'template-after-style', 'template-after-style',
@ -834,6 +1345,29 @@ function enqueue_template_specific_styles() {
'1.1' '1.1'
); );
} }
if ( is_page_template('page-podgotovka-k-shkole.php') ) {
wp_enqueue_style(
'template-pksh-style',
get_template_directory_uri() . '/assets/css/podgotovka-k-shkole.css',
array(),
'2.1.' . filemtime(get_template_directory() . '/assets/css/podgotovka-k-shkole.css')
);
}
if ( is_page_template('front-page.php') || is_front_page() ) {
wp_enqueue_style(
'front-page-style',
get_template_directory_uri() . '/assets/css/front-page.css',
array(),
filemtime(get_template_directory() . '/assets/css/front-page.css')
);
wp_enqueue_script(
'front-page-script',
get_template_directory_uri() . '/assets/js/front-page.js',
array(),
filemtime(get_template_directory() . '/assets/js/front-page.js'),
true
);
}
} }
add_action('wp_enqueue_scripts', 'enqueue_template_specific_styles'); add_action('wp_enqueue_scripts', 'enqueue_template_specific_styles');
@ -1121,8 +1655,6 @@ function dekart_branch_register_settings() {
'branch_postal_code' => 'text', 'branch_postal_code' => 'text',
'branch_phone' => 'text', 'branch_phone' => 'text',
'branch_email' => 'email', 'branch_email' => 'email',
'branch_whatsapp' => 'url',
'branch_telegram' => 'url',
'branch_name' => 'text', 'branch_name' => 'text',
'branch_price_range' => 'text', 'branch_price_range' => 'text',
); );
@ -1131,6 +1663,19 @@ function dekart_branch_register_settings() {
} }
register_setting( 'dekart_branch_group', 'branch_logo', array( 'sanitize_callback' => 'absint' ) ); register_setting( 'dekart_branch_group', 'branch_logo', array( 'sanitize_callback' => 'absint' ) );
register_setting( 'dekart_branch_group', 'branch_social' ); register_setting( 'dekart_branch_group', 'branch_social' );
register_setting( 'dekart_branch_group', 'branch_messengers' );
register_setting( 'dekart_branch_group', 'branch_button_script', array( 'sanitize_callback' => 'dekart_sanitize_script_field' ) );
register_setting( 'dekart_branch_group', 'branch_hours', array( 'sanitize_callback' => 'sanitize_text_field' ) );
register_setting( 'dekart_branch_group', 'branch_myreviews_widget', array( 'sanitize_callback' => 'sanitize_text_field' ) );
register_setting( 'dekart_branch_group', 'branch_ext_yandex', array( 'sanitize_callback' => 'esc_url_raw' ) );
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_other_name', array( 'sanitize_callback' => 'sanitize_text_field' ) );
register_setting( 'dekart_branch_group', 'branch_ext_other_url', array( 'sanitize_callback' => 'esc_url_raw' ) );
// Поля branch_map_url, branch_founded, branch_photos — удалены.
// Они дублировали CF таб «SEO и микроразметка» (schema_*).
// Чтение: inc/schema.php → dekart_schema_option() → CF → Settings API (backward compat).
} }
function dekart_branch_settings_page() { function dekart_branch_settings_page() {
@ -1139,10 +1684,26 @@ function dekart_branch_settings_page() {
<h1>Настройки филиала</h1> <h1>Настройки филиала</h1>
<form method="post" action="options.php"> <form method="post" action="options.php">
<?php settings_fields( 'dekart_branch_group' ); ?> <?php settings_fields( 'dekart_branch_group' ); ?>
<style>.branch-section{background:#fff;padding:20px;margin:20px 0;border:1px solid #c3c4c7;border-radius:4px;max-width:800px}.branch-section h2{margin-top:0;padding-bottom:10px;border-bottom:1px solid #eee}.branch-field{margin-bottom:16px}.branch-field label{display:block;font-weight:600;margin-bottom:4px}.branch-field input[type=text],.branch-field input[type=email],.branch-field input[type=url]{width:100%;max-width:400px}.branch-field .desc{color:#646970;font-size:12px;margin:4px 0 0}.branch-media-wrap{display:flex;align-items:center;gap:12px}.branch-media-wrap img{max-width:120px;max-height:80px;border:1px solid #ddd;border-radius:4px}.branch-social-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.branch-social-row input{flex:1}</style> <style>
.branch-section{background:#fff;padding:20px;margin:20px 0;border:1px solid #c3c4c7;border-radius:4px;max-width:800px}
.branch-section h2{margin-top:0;padding-bottom:10px;border-bottom:1px solid #eee;cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}
.branch-section h2::after{content:'';font-size:20px;line-height:1;color:#999;transition:transform .2s}
.branch-section.collapsed h2::after{content:'+'}
.branch-section.collapsed .branch-section-body{display:none}
.branch-section.collapsed{border-color:#e0e0e0;background:#fcfcfc}
.branch-field{margin-bottom:16px}
.branch-field label{display:block;font-weight:600;margin-bottom:4px}
.branch-field input[type=text],.branch-field input[type=email],.branch-field input[type=url]{width:100%;max-width:400px}
.branch-field .desc{color:#646970;font-size:12px;margin:4px 0 0}
.branch-media-wrap{display:flex;align-items:center;gap:12px}
.branch-media-wrap img{max-width:120px;max-height:80px;border:1px solid #ddd;border-radius:4px}
.branch-social-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}
.branch-social-row input{flex:1}
</style>
<div class="branch-section"> <div class="branch-section collapsed">
<h2>🌍 ГЕО</h2> <h2>🌍 ГЕО</h2>
<div class="branch-section-body">
<div class="branch-field"> <div class="branch-field">
<label>Город (с предлогом)</label> <label>Город (с предлогом)</label>
<input type="text" name="branch_city_prep" value="<?php echo esc_attr( get_option( 'branch_city_prep', 'в Щёлково' ) ); ?>"> <input type="text" name="branch_city_prep" value="<?php echo esc_attr( get_option( 'branch_city_prep', 'в Щёлково' ) ); ?>">
@ -1171,9 +1732,11 @@ function dekart_branch_settings_page() {
<input type="text" name="branch_postal_code" value="<?php echo esc_attr( get_option( 'branch_postal_code', '' ) ); ?>"> <input type="text" name="branch_postal_code" value="<?php echo esc_attr( get_option( 'branch_postal_code', '' ) ); ?>">
</div> </div>
</div> </div>
</div>
<div class="branch-section"> <div class="branch-section collapsed">
<h2>📞 КОНТАКТЫ</h2> <h2>📞 КОНТАКТЫ</h2>
<div class="branch-section-body">
<div class="branch-field"> <div class="branch-field">
<label>Телефон</label> <label>Телефон</label>
<input type="text" name="branch_phone" value="<?php echo esc_attr( get_option( 'branch_phone', '+7 (495) 120-00-78' ) ); ?>"> <input type="text" name="branch_phone" value="<?php echo esc_attr( get_option( 'branch_phone', '+7 (495) 120-00-78' ) ); ?>">
@ -1182,19 +1745,101 @@ function dekart_branch_settings_page() {
<label>Email</label> <label>Email</label>
<input type="email" name="branch_email" value="<?php echo esc_attr( get_option( 'branch_email', 'info@dekart.school' ) ); ?>"> <input type="email" name="branch_email" value="<?php echo esc_attr( get_option( 'branch_email', 'info@dekart.school' ) ); ?>">
</div> </div>
<div class="branch-field">
<label>WhatsApp</label>
<input type="url" name="branch_whatsapp" value="<?php echo esc_attr( get_option( 'branch_whatsapp', '' ) ); ?>">
<p class="desc">https://wa.me/79991234567</p>
</div>
<div class="branch-field">
<label>Telegram</label>
<input type="url" name="branch_telegram" value="<?php echo esc_attr( get_option( 'branch_telegram', '' ) ); ?>">
</div> </div>
</div> </div>
<div class="branch-section"> <div class="branch-section collapsed">
<h2>🌐 СОЦСЕТИ И МЕССЕНДЖЕРЫ</h2>
<div class="branch-section-body">
<p class="desc">Добавьте как соцсети (VK, YouTube, Instagram*), так и мессенджеры (WhatsApp, Telegram, Viber и др.). Все они отображаются в блоке «Наши соцсети» на странице. *Meta признана экстремистской организацией.</p>
<div class="branch-field">
<label>Социальные сети</label>
<p class="desc" style="margin-bottom:8px;">Название + ссылка на профиль.</p>
<div id="branch-social-repeater">
<?php
$social = get_option( 'branch_social', array() );
if ( ! is_array( $social ) || empty( $social ) ) {
$social = array( array( 'name' => '', 'url' => '' ) );
}
foreach ( $social as $i => $item ) {
$name = esc_attr( $item['name'] ?? '' );
$url = esc_attr( $item['url'] ?? '' );
echo '<div class="branch-social-row"><input type="text" name="branch_social[' . $i . '][name]" value="' . $name . '" placeholder="Название"><input type="url" name="branch_social[' . $i . '][url]" value="' . $url . '" placeholder="https://..."><button type="button" class="button branch-social-remove">—</button></div>';
}
?>
</div>
<button type="button" class="button" id="branch-social-add">+ Добавить соцсеть</button>
</div>
<div class="branch-field">
<label>Мессенджеры</label>
<p class="desc" style="margin-bottom:8px;">Название + ссылка для связи.</p>
<div id="branch-messengers-container">
<?php
$messengers = get_option( 'branch_messengers', array() );
if ( ! empty( $messengers ) && is_array( $messengers ) ) :
foreach ( $messengers as $i => $m ) :
$m_name = $m['name'] ?? '';
$m_url = $m['url'] ?? '';
?>
<div class="branch-social-row">
<input type="text" name="branch_messengers[<?php echo intval( $i ); ?>][name]" value="<?php echo esc_attr( $m_name ); ?>" placeholder="Название (напр. WhatsApp)" style="max-width:160px;">
<input type="url" name="branch_messengers[<?php echo intval( $i ); ?>][url]" value="<?php echo esc_attr( $m_url ); ?>" placeholder="https://..." style="max-width:300px;">
<button type="button" class="button branch-messenger-remove"></button>
</div>
<?php endforeach;
endif; ?>
</div>
<button type="button" class="button" id="branch-messenger-add" style="margin-top:4px;">+ Добавить мессенджер</button>
</div>
<script>
(function() {
// Соцсети
var socialContainer = document.getElementById('branch-social-repeater');
var socialAdd = document.getElementById('branch-social-add');
if (socialAdd) {
socialAdd.addEventListener('click', function() {
var idx = socialContainer.children.length;
var row = document.createElement('div');
row.className = 'branch-social-row';
row.innerHTML = '<input type="text" name="branch_social[' + idx + '][name]" placeholder="Название"><input type="url" name="branch_social[' + idx + '][url]" placeholder="https://..."><button type="button" class="button branch-social-remove">—</button>';
socialContainer.appendChild(row);
row.querySelector('.branch-social-remove').addEventListener('click', function() { row.remove(); });
});
socialContainer.addEventListener('click', function(e) {
if (e.target.classList.contains('branch-social-remove')) {
e.target.parentElement.remove();
}
});
}
// Мессенджеры
var messengerContainer = document.getElementById('branch-messengers-container');
var messengerAdd = document.getElementById('branch-messenger-add');
var idx = messengerContainer.children.length;
if (messengerAdd) {
messengerAdd.addEventListener('click', function() {
var row = document.createElement('div');
row.className = 'branch-social-row';
row.innerHTML = '<input type="text" name="branch_messengers[' + idx + '][name]" placeholder="Название (напр. WhatsApp)" style="max-width:160px;">' +
'<input type="url" name="branch_messengers[' + idx + '][url]" placeholder="https://..." style="max-width:300px;">' +
'<button type="button" class="button branch-messenger-remove"></button>';
messengerContainer.appendChild(row);
idx++;
row.querySelector('.branch-messenger-remove').addEventListener('click', function() { row.remove(); });
});
messengerContainer.addEventListener('click', function(e) {
if (e.target.classList.contains('branch-messenger-remove')) {
e.target.parentElement.remove();
}
});
}
})();
</script>
</div>
</div>
<div class="branch-section collapsed">
<h2>🏢 ОРГАНИЗАЦИЯ</h2> <h2>🏢 ОРГАНИЗАЦИЯ</h2>
<div class="branch-section-body">
<div class="branch-field"> <div class="branch-field">
<label>Полное название организации</label> <label>Полное название организации</label>
<input type="text" name="branch_name" value="<?php echo esc_attr( get_option( 'branch_name', 'Декарт' ) ); ?>"> <input type="text" name="branch_name" value="<?php echo esc_attr( get_option( 'branch_name', 'Декарт' ) ); ?>">
@ -1210,36 +1855,85 @@ function dekart_branch_settings_page() {
<p class="desc">ID изображения из Медиафайлы. Для JSON-LD image.</p> <p class="desc">ID изображения из Медиафайлы. Для JSON-LD image.</p>
</div> </div>
</div> </div>
</div>
<div class="branch-section"> <div class="branch-section collapsed">
<h2>🔗 СОЦСЕТИ</h2> <h2> РЕЖИМ РАБОТЫ</h2>
<p class="desc">Добавьте ссылки (название и URL).</p> <div class="branch-section-body">
<div id="branch-social-repeater"> <div class="branch-field">
<?php <label>Режим работы школы</label>
$social = get_option( 'branch_social', array() ); <input type="text" name="branch_hours" value="<?php echo esc_attr( get_option( 'branch_hours', 'ежедневно с 8:00 до 19:00, кроме воскресенья' ) ); ?>" style="width:100%;max-width:500px">
if ( ! is_array( $social ) || empty( $social ) ) { <p class="desc">Текст с режимом работы. Появляется на странице отзывов после вступительного текста.</p>
$social = array( array( 'name' => '', 'url' => '' ) );
}
foreach ( $social as $i => $item ) {
$name = esc_attr( $item['name'] ?? '' );
$url = esc_attr( $item['url'] ?? '' );
echo '<div class="branch-social-row"><input type="text" name="branch_social[' . $i . '][name]" value="' . $name . '" placeholder="Название"><input type="url" name="branch_social[' . $i . '][url]" value="' . $url . '" placeholder="https://..."><button type="button" class="button branch-social-remove">—</button></div>';
}
?>
</div> </div>
<button type="button" class="button" id="branch-social-add">+ Добавить</button> </div>
</div>
<div class="branch-section collapsed">
<h2>🎯 СКРИПТ ДЛЯ КНОПОК</h2>
<div class="branch-section-body">
<p class="desc">Bitrix24-скрипт, который выполняется при клике на CTA-кнопки (например, «Записаться на экскурсию»). Вставьте полный JS-код формы.</p>
<div class="branch-field">
<label>Скрипт (JS/HTML)</label>
<textarea name="branch_button_script" rows="8" style="width:100%;max-width:700px;font-family:monospace;font-size:13px"><?php echo esc_textarea( get_option( 'branch_button_script', '' ) ); ?></textarea>
</div>
</div>
</div>
<div class="branch-section collapsed">
<h2> MYREVIEWS</h2>
<div class="branch-section-body">
<p class="desc">Виджет отзывов от MyReviews. Вставьте ссылку на фид.</p>
<div class="branch-field">
<label>Ссылка фида MyReviews</label>
<input type="text" name="branch_myreviews_widget" value="<?php echo esc_attr( get_option( 'branch_myreviews_widget', '' ) ); ?>" style="width:100%;max-width:500px">
<p class="desc">Например: https://feed.myreviews.ru/27645</p>
</div>
</div>
</div>
<div class="branch-section collapsed">
<h2>🔗 E-E-A-T: Внешние платформы</h2>
<div class="branch-section-body">
<p class="desc">Ссылки на внешние площадки с отзывами для повышения E-E-A-T. Отображаются под заголовком «Отзывы на внешних площадках».</p>
<div class="branch-field">
<label>Яндекс Карты (ссылка на отзывы)</label>
<input type="url" name="branch_ext_yandex" value="<?php echo esc_attr( get_option( 'branch_ext_yandex', '' ) ); ?>" style="width:100%;max-width:500px">
<p class="desc">https://yandex.ru/maps/org/.../reviews/</p>
</div>
<div class="branch-field">
<label>Google Карты (ссылка на отзывы)</label>
<input type="url" name="branch_ext_google" value="<?php echo esc_attr( get_option( 'branch_ext_google', '' ) ); ?>" style="width:100%;max-width:500px">
<p class="desc">https://maps.google.com/maps/place/.../ ...</p>
</div>
<div class="branch-field">
<label>2ГИС (ссылка на отзывы)</label>
<input type="url" name="branch_ext_2gis" value="<?php echo esc_attr( get_option( 'branch_ext_2gis', '' ) ); ?>" style="width:100%;max-width:500px">
<p class="desc">https://2gis.ru/.../</p>
</div>
<div class="branch-field">
<label>Отзовик / IRecommend</label>
<input type="url" name="branch_ext_otzovik" value="<?php echo esc_attr( get_option( 'branch_ext_otzovik', '' ) ); ?>" style="width:100%;max-width:500px">
</div>
<div class="branch-field">
<label>Другая платформа (название)</label>
<input type="text" name="branch_ext_other_name" value="<?php echo esc_attr( get_option( 'branch_ext_other_name', '' ) ); ?>" style="width:100%;max-width:500px">
</div>
<div class="branch-field">
<label>Другая платформа (ссылка)</label>
<input type="url" name="branch_ext_other_url" value="<?php echo esc_attr( get_option( 'branch_ext_other_url', '' ) ); ?>" style="width:100%;max-width:500px">
</div>
</div>
</div>
<script> <script>
document.getElementById('branch-social-add')?.addEventListener('click', function(){ // Сворачивание/разворачивание блоков
var c = document.querySelectorAll('#branch-social-repeater .branch-social-row').length; document.querySelectorAll('.branch-section > h2').forEach(function(h2){
var d = document.createElement('div'); d.className = 'branch-social-row'; h2.addEventListener('click', function(){
d.innerHTML = '<input type="text" name="branch_social['+c+'][name]" value="" placeholder="Название"><input type="url" name="branch_social['+c+'][url]" value="" placeholder="https://..."><button type="button" class="button branch-social-remove">—</button>'; this.parentElement.classList.toggle('collapsed');
document.getElementById('branch-social-repeater').appendChild(d);
}); });
document.addEventListener('click', function(e){
if(e.target.classList.contains('branch-social-remove')) e.target.parentElement.remove();
}); });
// Соцсети и мессенджеры: их JS инициализируется внутри секции «СОЦСЕТИ И МЕССЕНДЖЕРЫ»
</script> </script>
</div>
<?php submit_button('Сохранить настройки филиала'); ?> <?php submit_button('Сохранить настройки филиала'); ?>
</form> </form>
@ -1247,6 +1941,29 @@ function dekart_branch_settings_page() {
<?php <?php
} }
/* ════════════════════════════════════════════════════════════
🎯 REDESIGN 2026 Schema + H1 filter + Bento CSS/JS
════════════════════════════════════════════════════════════ */
/**
* Подключаем динамический генератор Schema.org JSON-LD
* Единый @graph в wp_head. Только Carbon Fields данные.
*/
require_once __DIR__ . '/inc/schema.php';
/**
* Фильтр: замена H1 для страницы "Подготовка к школе" (ID 2)
* Добавляет Risk Reversal в заголовок
*/
// podgotovka-k-shkole.css enqueued in enqueue_template_specific_styles()
/**
* Счётчики доверия в админку Carbon Fields добавляем поля
* "выпускники", "лет опыта", "рейтинг"
* (настройки темы уже есть используем статические)
*/
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// ACF: Отзывы (Лагерь) — группа полей // ACF: Отзывы (Лагерь) — группа полей
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────

File diff suppressed because it is too large Load Diff