shkola/wp-content/themes/dekart/inc/schema.php
2026-06-28 12:13:20 +00:00

1246 lines
41 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Schema.org JSON-LD Generator — единый @graph
*
* Генерирует микроразметку для страницы «Подготовка к школе».
* Только динамические данные из Carbon Fields. Никаких хардкод-значений.
*
* @package Dekart
* @since 2026-05-31
* @see https://schema.org/Course
*/
defined( 'ABSPATH' ) || exit;
/**
* Отключаем Yoast SEO JSON-LD на странице Подготовки к школе,
* чтобы избежать дублирования WebPage + Organization.
* Аналогично page-camp.php.
*/
add_filter( 'wpseo_json_ld_output', '__return_false' );
/**
* Принудительно меняем og:type на 'website' для страницы Подготовки к школе (ID=2),
* чтобы избежать несоответствия article/website в социальных сниппетах.
*/
add_filter( 'wpseo_opengraph_type', 'dekart_schema_og_type', 10, 1 );
function dekart_schema_og_type( string $type ): string {
if ( is_page( 2 ) ) {
return 'website';
}
return $type;
}
/**
* Главная функция: сборка @graph и вывод в wp_head.
* Срабатывает только на странице ID=2 (Подготовка к школе).
*/
add_action( 'wp_head', 'dekart_schema_output', 1 );
function dekart_schema_output(): void {
if ( ! is_page( 2 ) ) {
return;
}
// Guard: без Carbon Fields — тихий выход (не может быть фатала)
if ( ! function_exists( 'carbon_get_post_meta' ) || ! function_exists( 'carbon_get_theme_option' ) ) {
return;
}
$graph = array();
// 1. WebPage
$webpage = dekart_schema_webpage();
if ( $webpage ) {
$graph[] = $webpage;
}
// 2. WebSite
$website = dekart_schema_website();
if ( $website ) {
$graph[] = $website;
}
// 3. BreadcrumbList
$breadcrumb = dekart_schema_breadcrumb();
if ( $breadcrumb ) {
$graph[] = $breadcrumb;
}
// 4. Parent Organization (юридическое лицо, без адреса)
$organization = dekart_schema_organization();
if ( $organization ) {
$graph[] = $organization;
}
// 5. LocalBusiness (филиал/центр, с адресом)
$local_business = dekart_schema_local_business();
if ( $local_business ) {
$graph[] = $local_business;
}
// 6. Course
$course = dekart_schema_course();
if ( $course ) {
$graph[] = $course;
}
// 7. Offer / AggregateOffer (conditional)
$offers = dekart_schema_offer();
if ( ! empty( $offers ) ) {
if ( isset( $offers[0] ) && is_array( $offers[0] ) ) {
// Вернулся массив Offer'ов — добавляем каждый в граф
foreach ( $offers as $offer ) {
$graph[] = $offer;
}
} else {
// Один Offer (для обратной совместимости)
$graph[] = $offers;
}
}
// 8. AggregateRating (conditional — есть ли отзывы с оценками)
$rating = dekart_schema_rating();
if ( $rating ) {
$graph[] = $rating;
}
// 9. FAQPage (conditional — есть ли вопросы)
$faq = dekart_schema_faq();
if ( $faq ) {
$graph[] = $faq;
}
// 10. Review[] — поштучные отзывы (conditional)
$reviews = dekart_schema_reviews();
if ( ! empty( $reviews ) && is_array( $reviews ) ) {
foreach ( $reviews as $review ) {
$graph[] = $review;
}
}
// 11. Person[] — преподаватели (conditional)
$teachers = dekart_schema_teachers();
if ( ! empty( $teachers ) && is_array( $teachers ) ) {
foreach ( $teachers as $person ) {
$graph[] = $person;
}
}
if ( empty( $graph ) ) {
return;
}
$json = wp_json_encode(
array(
'@context' => 'https://schema.org',
'@graph' => $graph,
),
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
if ( false === $json ) {
return; // тихий выход вместо WSOD
}
echo '<script type="application/ld+json">' . "\n" . $json . "\n" . '</script>' . "\n";
}
/* =====================================================================
Сущности @graph
===================================================================== */
/**
* WebPage — текущая страница.
* @see https://schema.org/WebPage
*/
function dekart_schema_webpage(): ?array {
$url = home_url( '/podgotovka-k-shkole/' );
// _yoast_wpseo_title — Yoast SEO заголовок
$yoast_title = get_post_meta( 2, '_yoast_wpseo_title', true );
$name = $yoast_title ?: wp_get_document_title();
// _yoast_wpseo_metadesc — Yoast SEO описание
$description = get_post_meta( 2, '_yoast_wpseo_metadesc', true );
$description = $description ?: get_post_meta( 2, 'rank_math_description', true );
$description = $description ?: carbon_get_post_meta( 2, 'banner_subtitle' );
$description = $description ?: 'Подготовка к школе для детей 57 лет в Щёлково. Чтение, письмо, математика, логика.';
// has_post_thumbnail — изображение страницы (OG-совместимость)
$image_url = '';
if ( has_post_thumbnail( 2 ) ) {
$image_url = get_the_post_thumbnail_url( 2, 'full' );
}
// pksh_level — уровень образования (about)
$about_text = carbon_get_post_meta( 2, 'pksh_level' ) ?: 'Подготовка к школе';
// branch_city — город для about и keywords
$city = dekart_branch_option( 'branch_city', 'Щёлково' );
$webpage = array(
'@type' => 'WebPage',
'@id' => $url . '#webpage',
'url' => $url,
'name' => wp_strip_all_tags( $name ),
'description' => wp_strip_all_tags( $description ),
'inLanguage' => str_replace( '_', '-', determine_locale() ?: 'ru' ),
'isPartOf' => array(
'@id' => home_url( '/' ) . '#website',
),
'breadcrumb' => array(
'@id' => $url . '#breadcrumb',
),
// mainEntity — основной курс на странице
'mainEntity' => array(
'@id' => $url . '#course',
),
// speakable — для голосовых помощников и AI-поиска
'speakable' => array(
'@type' => 'SpeakableSpecification',
'cssSelector' => array(
'h1.hero-pksh__title',
'.hero-pksh__subtitle',
),
),
// about — тематика страницы (образовательная услуга)
'about' => array(
'@type' => 'Thing',
'name' => wp_strip_all_tags( sprintf( '%s в %s', $about_text, $city ) ),
),
// keywords — метки для поиска
'keywords' => wp_strip_all_tags(
'подготовка к школе, дошкольное образование, обучение чтению, обучение письму, '
. 'математика для дошкольников, развитие логики, курсы для детей 5-7 лет, '
. $city
),
);
if ( $image_url ) {
$webpage['image'] = esc_url( $image_url );
}
// datePublished / dateModified — даты публикации страницы
$post_obj = get_post( 2 );
if ( $post_obj ) {
$webpage['datePublished'] = get_the_date( 'c', $post_obj );
$webpage['dateModified'] = get_the_modified_date( 'c', $post_obj );
}
// hasPart — ссылка на FAQPage этой страницы
$faq_items = carbon_get_post_meta( 2, 'pksh_faq_items' );
if ( ! empty( $faq_items ) && is_array( $faq_items ) ) {
$webpage['hasPart'] = array(
'@id' => $url . '#faq',
);
}
return $webpage;
}
/**
* WebSite — главный сайт.
* name из branch_name (единый источник с лагерем), а не из site_name.
*/
function dekart_schema_website(): ?array {
$site_name = dekart_branch_option( 'branch_name', 'Детский центр «Декарт»' );
$home_url = home_url( '/' );
return array(
'@type' => 'WebSite',
'@id' => $home_url . '#website',
'url' => $home_url,
'name' => wp_strip_all_tags( $site_name ),
'potentialAction' => array(
'@type' => 'SearchAction',
'target' => array(
'@type' => 'EntryPoint',
'urlTemplate' => $home_url . '?s={search_term_string}',
),
'query-input' => 'required name=search_term_string',
),
);
}
/**
* BreadcrumbList — хлебные крошки для страницы.
*/
function dekart_schema_breadcrumb(): ?array {
$page_url = home_url( '/podgotovka-k-shkole/' );
$home_url = home_url( '/' );
return array(
'@type' => 'BreadcrumbList',
'@id' => $page_url . '#breadcrumb',
'itemListElement' => array(
array(
'@type' => 'ListItem',
'position' => 1,
'name' => 'Главная',
'item' => $home_url,
),
array(
'@type' => 'ListItem',
'position' => 2,
'name' => 'Подготовка к школе',
'item' => $page_url,
),
),
);
}
/**
* Helper: получить значение из Carbon Fields (schema_*) с fallback на Settings API (branch_*).
* Работает с любыми типами (включая массивы complex/media_gallery).
* Приоритет: carbon_get_theme_option('schema_*') → get_option('branch_*') → $default.
*/
function dekart_schema_option( string $schema_key, string $branch_key, $default = '' ) {
$val = carbon_get_theme_option( $schema_key );
if ( ! empty( $val ) ) {
return $val;
}
$val = get_option( $branch_key );
if ( ! empty( $val ) ) {
return $val;
}
return $default;
}
/**
* Helper: получить значение из Настроек филиала (branch_*).
* Единый источник: get_option('branch_*').
* Схема-поля (schema_*) читать через dekart_schema_option() — CF → Settings API.
*/
function dekart_branch_option( string $key, string $default = '' ): string {
$val = get_option( $key );
if ( ! empty( $val ) && is_string( $val ) ) {
return trim( $val );
}
return $default;
}
/**
* Parent Organization — юридическое лицо (без адреса филиала).
* @id совпадает с лагерем — это одна организация.
*/
function dekart_schema_organization(): ?array {
$home_url = home_url( '/' );
$site_name = dekart_branch_option( 'branch_name', 'Детский центр «Декарт»' );
// Логотип: единая цепочка с page-camp.php
$logo_url = dekart_resolve_logo();
$org = array(
'@type' => 'Organization',
'@id' => $home_url . '#organization',
'name' => wp_strip_all_tags( $site_name ),
'url' => $home_url,
'logo' => esc_url( $logo_url ),
);
// legalName — юридическое наименование (CF schema_legal_name → Settings API branch_legal_name)
$legal_name = dekart_schema_option( 'schema_legal_name', 'branch_legal_name' );
if ( ! empty( $legal_name ) && is_string( $legal_name ) ) {
$org['legalName'] = wp_strip_all_tags( trim( $legal_name ) );
}
// contactPoint — контактный телефон для связи
$cp_phone = dekart_branch_option( 'branch_phone', '' );
if ( $cp_phone ) {
$org['contactPoint'] = array(
'@type' => 'ContactPoint',
'telephone' => wp_strip_all_tags( $cp_phone ),
'contactType' => 'customer service',
'areaServed' => 'RU',
'availableLanguage' => 'Russian',
);
}
return $org;
}
/**
* Helper: цепочка разрешения логотипа (аналог page-camp.php:92-103).
* branch_logo (ACF) → banner_img (CF) → custom_logo (WP) → post_thumbnail → fallback SVG.
*/
function dekart_resolve_logo(): string {
// 1. branch_logo — ACF опция филиала
$branch_logo = get_option( 'branch_logo' );
if ( ! empty( $branch_logo ) ) {
if ( is_array( $branch_logo ) && isset( $branch_logo['url'] ) ) {
return $branch_logo['url'];
}
$url = wp_get_attachment_url( $branch_logo );
if ( $url ) {
return $url;
}
}
// 2. banner_img — CF поле страницы
$banner_img = carbon_get_post_meta( 2, 'banner_img' );
if ( ! empty( $banner_img ) ) {
if ( is_array( $banner_img ) && isset( $banner_img['url'] ) ) {
return $banner_img['url'];
}
$url = wp_get_attachment_url( $banner_img );
if ( $url ) {
return $url;
}
}
// 3. custom_logo — WP Customizer
$logo_id = get_theme_mod( 'custom_logo' );
if ( $logo_id ) {
$url = wp_get_attachment_url( $logo_id );
if ( $url ) {
return $url;
}
}
// 4. post_thumbnail страницы
if ( has_post_thumbnail( 2 ) ) {
$url = get_the_post_thumbnail_url( 2, 'full' );
if ( $url ) {
return $url;
}
}
// 5. fallback — SVG из темы
return get_template_directory_uri() . '/assets/images/logo-svg.svg';
}
/**
* LocalBusiness — филиал/центр с адресом, телефоном.
* Данные из Настроек филиала (branch_*), единый источник с лагерем.
* @id на уровне страницы, parentOrganization → #organization.
*
* @see https://schema.org/LocalBusiness
* @see https://schema.org/EducationalOrganization
*/
function dekart_schema_local_business(): ?array {
$page_url = home_url( '/podgotovka-k-shkole/' );
$home_url = home_url( '/' );
// branch_name — название филиала (get_option)
$site_name = dekart_branch_option( 'branch_name', 'Детский центр «Декарт»' );
// branch_address — улица
$street = dekart_branch_option( 'branch_address', 'Комсомольская ул., 11' );
// branch_city — город
$city = dekart_branch_option( 'branch_city', 'Щёлково' );
// branch_region — регион/область
$region = dekart_branch_option( 'branch_region', 'Московская область' );
// branch_phone — телефон
$phone = dekart_branch_option( 'branch_phone', '' );
// branch_email — email
$email = dekart_branch_option( 'branch_email', '' );
// branch_coords — координаты "lat, lng" из Настроек филиала (Settings API)
$coords = get_option( 'branch_coords' );
$lat = '';
$lng = '';
if ( ! empty( $coords ) && is_string( $coords ) && false !== strpos( $coords, ',' ) ) {
$parts = explode( ',', $coords );
if ( count( $parts ) >= 2 ) {
$lat = trim( $parts[0] );
$lng = trim( $parts[1] );
}
}
// branch_postal_code — почтовый индекс (CF schema_postal_code → Settings API)
$zip = dekart_schema_option( 'schema_postal_code', 'branch_postal_code' );
// branch_price_range — диапазон цен (Settings API)
$price_range = get_option( 'branch_price_range' );
// branch_founded — год основания (CF schema_founded → Settings API)
$founded = dekart_schema_option( 'schema_founded', 'branch_founded' );
// branch_map_url — ссылка на карту для hasMap (CF schema_map_url → Settings API)
$map_url = dekart_schema_option( 'schema_map_url', 'branch_map_url' );
// branch_hours — часы работы: CF complex (schema_hours) → textarea (branch_hours)
$hours_cf = carbon_get_theme_option( 'schema_hours' );
$lb = array(
'@type' => array( 'EducationalOrganization', 'LocalBusiness' ),
'@id' => $page_url . '#localbusiness',
'name' => wp_strip_all_tags( $site_name ),
'url' => $home_url,
'address' => array(
'@type' => 'PostalAddress',
'streetAddress' => wp_strip_all_tags( $street ),
'addressLocality' => wp_strip_all_tags( $city ),
'addressRegion' => wp_strip_all_tags( $region ),
'addressCountry' => 'RU',
),
'parentOrganization' => array(
'@id' => $home_url . '#organization',
),
);
// postalCode — почтовый индекс (условно)
if ( ! empty( $zip ) && is_string( $zip ) ) {
$lb['address']['postalCode'] = trim( $zip );
}
// telephone — телефон (условно)
if ( $phone ) {
$lb['telephone'] = wp_strip_all_tags( $phone );
}
// email — почта (условно)
if ( $email ) {
$lb['email'] = wp_strip_all_tags( $email );
}
// geo — GeoCoordinates (условно: обе координаты не пусты)
if ( ! empty( $lat ) && ! empty( $lng ) ) {
$lb['geo'] = array(
'@type' => 'GeoCoordinates',
'latitude' => floatval( $lat ),
'longitude' => floatval( $lng ),
);
}
// hasMap — ссылка на карту (условно)
if ( ! empty( $map_url ) && is_string( $map_url ) ) {
$lb['hasMap'] = esc_url( trim( $map_url ) );
}
// openingHoursSpecification — часы работы
// Формат 1 (CF complex, schema_hours): [['day_of_week' => 'Monday', 'opens' => '09:00', 'closes' => '20:00', 'day_off' => '']]
// Формат 2 (Settings API textarea, branch_hours): "Monday|09:00|20:00"
$hours_spec = array();
if ( ! empty( $hours_cf ) && is_array( $hours_cf ) ) {
foreach ( $hours_cf as $entry ) {
$day = isset( $entry['day_of_week'] ) ? trim( $entry['day_of_week'] ) : '';
if ( empty( $day ) ) {
continue;
}
$is_off = ! empty( $entry['day_off'] );
if ( $is_off ) {
$hours_spec[] = array(
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => $day,
'opens' => '00:00',
'closes' => '00:00',
);
} else {
$from = isset( $entry['opens'] ) ? trim( $entry['opens'] ) : '';
$to = isset( $entry['closes'] ) ? trim( $entry['closes'] ) : '';
if ( $from && $to ) {
$hours_spec[] = array(
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => $day,
'opens' => $from,
'closes' => $to,
);
}
}
}
} else {
$hours_raw = get_option( 'branch_hours' );
if ( ! empty( $hours_raw ) && is_string( $hours_raw ) ) {
$lines = explode( "\n", $hours_raw );
foreach ( $lines as $line ) {
$line = trim( $line );
if ( empty( $line ) ) {
continue;
}
$parts = explode( '|', $line );
$day = isset( $parts[0] ) ? trim( $parts[0] ) : '';
if ( empty( $day ) ) {
continue;
}
if ( isset( $parts[2] ) && 'closed' === strtolower( trim( $parts[2] ) ) ) {
$hours_spec[] = array(
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => $day,
'opens' => '00:00',
'closes' => '00:00',
);
continue;
}
$from = isset( $parts[1] ) ? trim( $parts[1] ) : '';
$to = isset( $parts[2] ) ? trim( $parts[2] ) : '';
if ( $day && $from && $to ) {
$hours_spec[] = array(
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => $day,
'opens' => $from,
'closes' => $to,
);
}
}
}
}
if ( ! empty( $hours_spec ) ) {
$lb['openingHoursSpecification'] = $hours_spec;
}
// priceRange — строка диапазона цен (условно)
if ( ! empty( $price_range ) && is_string( $price_range ) ) {
$lb['priceRange'] = trim( $price_range );
}
// areaServed — город присутствия (условно)
$lb['areaServed'] = array(
'@type' => 'City',
'name' => wp_strip_all_tags( $city ),
);
// foundingDate — год основания (условно)
if ( ! empty( $founded ) && is_string( $founded ) ) {
$lb['foundingDate'] = trim( $founded );
}
// sameAs — массив URL соцсетей
// Формат 1 (CF schema_social complex): [['platform' => 'VK', 'url' => 'https://...']]
// Формат 2 (Settings API branch_social): [['name' => 'VK', 'url' => 'https://...']]
$social_cf = carbon_get_theme_option( 'schema_social' );
$same_as = array();
if ( ! empty( $social_cf ) && is_array( $social_cf ) ) {
foreach ( $social_cf as $entry ) {
$url = isset( $entry['url'] ) ? trim( $entry['url'] ) : '';
if ( $url && filter_var( $url, FILTER_VALIDATE_URL ) ) {
$same_as[] = esc_url( $url );
}
}
} else {
$social_raw = get_option( 'branch_social' );
if ( ! empty( $social_raw ) && is_array( $social_raw ) ) {
foreach ( $social_raw as $entry ) {
$url = isset( $entry['url'] ) ? trim( $entry['url'] ) : '';
if ( $url && filter_var( $url, FILTER_VALIDATE_URL ) ) {
$same_as[] = esc_url( $url );
}
}
}
}
if ( ! empty( $same_as ) ) {
$lb['sameAs'] = $same_as;
}
// image — массив URL фотографий филиала
// Формат 1 (CF schema_photos media_gallery): [123, 456]
// Формат 2 (Settings API branch_photos): "123, 456"
$photos_cf = carbon_get_theme_option( 'schema_photos' );
$images = array();
if ( ! empty( $photos_cf ) && is_array( $photos_cf ) ) {
foreach ( $photos_cf as $id ) {
if ( ! is_numeric( $id ) || $id <= 0 ) {
continue;
}
$url = wp_get_attachment_url( (int) $id );
if ( $url ) {
$images[] = esc_url( $url );
}
}
} else {
$photos_raw = get_option( 'branch_photos' );
if ( ! empty( $photos_raw ) && is_string( $photos_raw ) ) {
$ids = explode( ',', $photos_raw );
foreach ( $ids as $id ) {
$id = trim( $id );
if ( ! is_numeric( $id ) || $id <= 0 ) {
continue;
}
$url = wp_get_attachment_url( (int) $id );
if ( $url ) {
$images[] = esc_url( $url );
}
}
}
}
if ( ! empty( $images ) ) {
$lb['image'] = $images;
}
// employee — преподаватели (Person), если есть
$teachers = carbon_get_post_meta( 2, 'pksh_teachers' );
if ( ! empty( $teachers ) && is_array( $teachers ) ) {
$employee_ids = array();
foreach ( $teachers as $i => $t ) {
$name = isset( $t['teacher_name'] ) ? trim( $t['teacher_name'] ) : '';
if ( $name ) {
$employee_ids[] = array(
'@id' => $page_url . '#teacher-' . $i,
);
}
}
if ( ! empty( $employee_ids ) ) {
$lb['employee'] = $employee_ids;
}
}
return $lb;
}
/**
* Course — основной курс «Подготовка к школе».
* Данные из Carbon Fields: banner_h1, banner_subtitle.
*
* @see https://schema.org/Course
* @see https://schema.org/CourseInstance
*/
function dekart_schema_course(): ?array {
$page_url = home_url( '/podgotovka-k-shkole/' );
$home_url = home_url( '/' );
// banner_h1 — заголовок H1 (Course.name)
$name = carbon_get_post_meta( 2, 'banner_h1' ) ?: 'Подготовка к школе 57 лет в Щёлково';
// pksh_description — полное описание курса для микроразметки (textarea)
// Если заполнено — приоритет над banner_subtitle
$pksh_description = carbon_get_post_meta( 2, 'pksh_description' );
// banner_subtitle — подзаголовок (Course.description)
$description = ! empty( $pksh_description ) ? $pksh_description : ( carbon_get_post_meta( 2, 'banner_subtitle' ) ?: 'Научим читать, считать и писать до школы. Бесплатное пробное занятие.' );
// Пробное занятие — есть ли в подзаголовке упоминание бесплатно
$has_trial = ! empty( $description ) && false !== mb_stripos( $description, 'бесплатно' );
// pksh_level — уровень образования
$level = carbon_get_post_meta( 2, 'pksh_level' );
// pksh_age — возраст
$age = carbon_get_post_meta( 2, 'pksh_age' );
// pksh_audience — целевая аудитория
$audience = carbon_get_post_meta( 2, 'pksh_audience' );
// pksh_course_mode — формат обучения (очная/ONLINE/OFFLINE)
$course_mode = carbon_get_post_meta( 2, 'pksh_course_mode' );
// pksh_skills — список навыков
$skills = carbon_get_post_meta( 2, 'pksh_skills' );
// banner_img — изображение курса
$course_img = carbon_get_post_meta( 2, 'banner_img' );
// pksh_programs — программы (для timeRequired + CourseInstance)
$programs = carbon_get_post_meta( 2, 'pksh_programs' );
$course = array(
'@type' => 'Course',
'@id' => $page_url . '#course',
'name' => wp_strip_all_tags( $name ),
'description' => wp_strip_all_tags( $description ),
'inLanguage' => str_replace( '_', '-', determine_locale() ?: 'ru' ),
'provider' => array(
'@id' => $page_url . '#localbusiness',
),
'offers' => array_merge(
array( array( '@id' => $page_url . '#offer' ) ),
$has_trial ? array( array( '@id' => $page_url . '#offer-trial' ) ) : array()
),
'aggregateRating' => array(
'@id' => $page_url . '#rating',
),
);
// educationalLevel — уровень образования (условно)
if ( ! empty( $level ) && is_string( $level ) ) {
$course['educationalLevel'] = wp_strip_all_tags( trim( $level ) );
}
// audience — EducationalAudience (условно)
if ( ! empty( $audience ) && is_string( $audience ) ) {
$course['audience'] = array(
'@type' => 'EducationalAudience',
'name' => wp_strip_all_tags( trim( $audience ) ),
);
}
// teaches — массив навыков (условно, из pksh_skills complex)
if ( ! empty( $skills ) && is_array( $skills ) ) {
$teaches = array();
foreach ( $skills as $skill ) {
$name = isset( $skill['skill'] ) ? trim( $skill['skill'] ) : '';
if ( $name ) {
$teaches[] = array(
'@type' => 'Thing',
'name' => wp_strip_all_tags( $name ),
);
}
}
if ( ! empty( $teaches ) ) {
$course['teaches'] = $teaches;
}
}
// timeRequired — длительность курса из первой программы, где заполнено
if ( ! empty( $programs ) && is_array( $programs ) ) {
foreach ( $programs as $prog ) {
if ( ! empty( $prog['program_time_required'] ) ) {
$course['timeRequired'] = wp_strip_all_tags( trim( $prog['program_time_required'] ) );
break;
}
}
}
// image — изображение курса (условно, из banner_img)
if ( ! empty( $course_img ) ) {
if ( is_array( $course_img ) && isset( $course_img['url'] ) ) {
$course['image'] = esc_url( $course_img['url'] );
} else {
$url = wp_get_attachment_url( $course_img );
if ( $url ) {
$course['image'] = esc_url( $url );
}
}
} elseif ( has_post_thumbnail( 2 ) ) {
// fallback: post_thumbnail страницы
$url = get_the_post_thumbnail_url( 2, 'full' );
if ( $url ) {
$course['image'] = esc_url( $url );
}
}
// hasCourseInstance — по одной CourseInstance на каждую программу с данными
$instances = array();
if ( ! empty( $programs ) && is_array( $programs ) ) {
foreach ( $programs as $prog ) {
$prog_workload = isset( $prog['program_workload'] ) ? trim( $prog['program_workload'] ) : '';
$prog_intensity = isset( $prog['program_intensity'] ) ? trim( $prog['program_intensity'] ) : '';
$prog_start = isset( $prog['program_start_date'] ) ? trim( $prog['program_start_date'] ) : '';
$prog_end = isset( $prog['program_end_date'] ) ? trim( $prog['program_end_date'] ) : '';
if ( empty( $prog_workload ) && empty( $prog_intensity ) && empty( $prog_start ) && empty( $prog_end ) ) {
continue; // нет данных для CourseInstance
}
$instance = array(
'@type' => 'CourseInstance',
'courseMode' => 'Onsite',
'location' => array(
'@id' => $page_url . '#localbusiness',
),
);
// courseMode — формат обучения (глобальный, единый для всех программ)
// Допустимые Schema.org: Onsite, Online, Blended
if ( ! empty( $course_mode ) && is_string( $course_mode ) ) {
$cm = strtolower( trim( $course_mode ) );
// Нормализация legacy-значений
$mode_map = array(
'offline' => 'Onsite',
'online' => 'Online',
'очная' => 'Onsite',
'очно' => 'Onsite',
);
$cm_normalized = isset( $mode_map[ $cm ] ) ? $mode_map[ $cm ] : ucfirst( $cm );
$valid_modes = array( 'Onsite', 'Online', 'Blended' );
if ( in_array( $cm_normalized, $valid_modes, true ) ) {
$instance['courseMode'] = $cm_normalized;
}
}
// courseSchedule — расписание (Schedule)
if ( $prog_workload || $prog_intensity || $prog_start || $prog_end ) {
$schedule = array( '@type' => 'Schedule' );
// repeatFrequency — ISO 8601 из program_repeat_frequency (новое поле)
$repeat_freq = isset( $prog['program_repeat_frequency'] ) ? trim( $prog['program_repeat_frequency'] ) : '';
if ( $repeat_freq ) {
$schedule['repeatFrequency'] = wp_strip_all_tags( $repeat_freq );
} elseif ( $prog_intensity ) {
// fallback: program_intensity — только если нет ISO-поля
$schedule['repeatFrequency'] = wp_strip_all_tags( $prog_intensity );
}
if ( $prog_start ) {
$schedule['startDate'] = $prog_start;
}
if ( $prog_end ) {
$schedule['endDate'] = $prog_end;
}
$instance['courseSchedule'] = $schedule;
}
// courseWorkload — учебная нагрузка
if ( $prog_workload ) {
$instance['courseWorkload'] = wp_strip_all_tags( $prog_workload );
}
$instances[] = $instance;
}
}
// fallback: если нет программ с данными, но есть courseMode — одна общая CourseInstance
if ( empty( $instances ) && ! empty( $course_mode ) && is_string( $course_mode ) ) {
$cm = strtolower( trim( $course_mode ) );
$mode_map = array(
'offline' => 'Onsite',
'online' => 'Online',
'очная' => 'Onsite',
'очно' => 'Onsite',
);
$cm_normalized = isset( $mode_map[ $cm ] ) ? $mode_map[ $cm ] : ucfirst( $cm );
$valid_modes = array( 'Onsite', 'Online', 'Blended' );
$instances[] = array(
'@type' => 'CourseInstance',
'courseMode' => ( in_array( $cm_normalized, $valid_modes, true ) ) ? $cm_normalized : 'Onsite',
'location' => array(
'@id' => $page_url . '#localbusiness',
),
);
}
if ( ! empty( $instances ) ) {
$course['hasCourseInstance'] = $instances;
}
return $course;
}
/**
* AggregateOffer — цены программ + бесплатное пробное.
* Извлекает числовые значения из program_price_subscription / program_price_single.
* Если программ нет — возвращает Offer на бесплатное пробное.
* Если есть программы — возвращает AggregateOffer + отдельный Offer для free trial.
*
* @see https://schema.org/AggregateOffer
* @see https://schema.org/Offer
*/
function dekart_schema_offer(): ?array {
$page_url = home_url( '/podgotovka-k-shkole/' );
$programs = carbon_get_post_meta( 2, 'pksh_programs' );
// priceValidUntil — самая поздняя дата из всех программ
$price_valid = '';
if ( ! empty( $programs ) && is_array( $programs ) ) {
foreach ( $programs as $prog ) {
if ( ! empty( $prog['program_price_valid'] ) ) {
$d = trim( $prog['program_price_valid'] );
if ( $d > $price_valid ) {
$price_valid = $d;
}
}
}
}
$prices = array();
if ( ! empty( $programs ) && is_array( $programs ) ) {
foreach ( $programs as $program ) {
// program_price_subscription — цена абонемента
if ( ! empty( $program['program_price_subscription'] ) ) {
$num = dekart_parse_price( $program['program_price_subscription'] );
if ( $num > 0 ) {
$prices[] = $num;
}
}
// program_price_single — цена разового занятия
if ( ! empty( $program['program_price_single'] ) ) {
$num = dekart_parse_price( $program['program_price_single'] );
if ( $num > 0 ) {
$prices[] = $num;
}
}
}
}
if ( empty( $prices ) ) {
// Нет цен в программах — Offer на бесплатное пробное
$offer = array(
'@type' => 'Offer',
'@id' => $page_url . '#offer',
'price' => '0',
'priceCurrency' => 'RUB',
'availability' => 'https://schema.org/LimitedAvailability',
'description' => 'Бесплатное пробное занятие',
);
// priceValidUntil — дата действия (условно)
if ( ! empty( $price_valid ) ) {
$offer['priceValidUntil'] = trim( $price_valid );
}
return $offer;
}
$low = min( $prices );
$high = max( $prices );
$aggregate = array(
'@type' => 'AggregateOffer',
'@id' => $page_url . '#offer',
'priceCurrency' => 'RUB',
'lowPrice' => (string) $low,
'highPrice' => (string) $high,
'offerCount' => (string) count( $prices ),
'availability' => 'https://schema.org/InStock',
// category — категория услуги
'category' => 'Образовательные услуги',
// offeredBy — кто предоставляет
'offeredBy' => array(
'@id' => $page_url . '#localbusiness',
),
);
// priceValidUntil — дата действия цен (условно)
if ( ! empty( $price_valid ) ) {
$aggregate['priceValidUntil'] = trim( $price_valid );
}
// Возвращаем массив: сначала AggregateOffer, потом (опционально) Offer на бесплатное пробное
$offers = array( $aggregate );
// banner_subtitle содержит фразу "бесплатное пробное" — создаём отдельный Offer
$subtitle = carbon_get_post_meta( 2, 'banner_subtitle' );
if ( ! empty( $subtitle ) && false !== mb_stripos( $subtitle, 'бесплатно' ) ) {
$trial = array(
'@type' => 'Offer',
'@id' => $page_url . '#offer-trial',
'price' => '0',
'priceCurrency' => 'RUB',
'availability' => 'https://schema.org/LimitedAvailability',
'description' => wp_strip_all_tags( $subtitle ),
'category' => 'Бесплатное пробное занятие',
'offeredBy' => array(
'@id' => $page_url . '#localbusiness',
),
);
if ( ! empty( $price_valid ) ) {
$trial['priceValidUntil'] = trim( $price_valid );
}
$offers[] = $trial;
}
return $offers;
}
/**
* AggregateRating — рейтинг из отзывов Carbon Fields.
* Вычисляется динамически из pksh_reviews_list.
* Если отзывов < 1 — блок не выводится.
*/
function dekart_schema_rating(): ?array {
$page_url = home_url( '/podgotovka-k-shkole/' );
$reviews = carbon_get_post_meta( 2, 'pksh_reviews_list' );
if ( empty( $reviews ) || ! is_array( $reviews ) ) {
return null;
}
$ratings = array();
foreach ( $reviews as $review ) {
$rating = isset( $review['review_rating'] ) ? intval( $review['review_rating'] ) : 0;
if ( $rating >= 1 && $rating <= 5 ) {
$ratings[] = $rating;
}
}
if ( count( $ratings ) < 1 ) {
return null;
}
$average = round( array_sum( $ratings ) / count( $ratings ), 1 );
return array(
'@type' => 'AggregateRating',
'@id' => $page_url . '#rating',
'ratingValue' => (string) $average,
'bestRating' => '5',
'worstRating' => '1',
'ratingCount' => (string) count( $ratings ),
'itemReviewed' => array(
'@id' => $page_url . '#course',
),
);
}
/**
* FAQPage — вопросы и ответы из pksh_faq_items.
* Если вопросов нет — блок не выводится.
*/
function dekart_schema_faq(): ?array {
$page_url = home_url( '/podgotovka-k-shkole/' );
$faq_items = carbon_get_post_meta( 2, 'pksh_faq_items' );
if ( empty( $faq_items ) || ! is_array( $faq_items ) ) {
return null;
}
$main_entity = array();
foreach ( $faq_items as $item ) {
$question = isset( $item['faq_question'] ) ? trim( $item['faq_question'] ) : '';
$answer = isset( $item['faq_answer'] ) ? trim( $item['faq_answer'] ) : '';
if ( empty( $question ) || empty( $answer ) ) {
continue;
}
$main_entity[] = array(
'@type' => 'Question',
'name' => wp_strip_all_tags( $question ),
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => wp_strip_all_tags( $answer ),
),
);
}
if ( empty( $main_entity ) ) {
return null;
}
return array(
'@type' => 'FAQPage',
'@id' => $page_url . '#faq',
'mainEntity' => $main_entity,
);
}
/**
* Review[] — поштучные отзывы из pksh_reviews_list.
* Каждый отзыв — отдельная сущность Review с автор-персоной и оценкой.
* itemReviewed → Course. Фильтр некорректных оценок (>5 или <1).
*
* @return array[] Массив Review-сущностей для @graph.
* @see https://schema.org/Review
*/
function dekart_schema_reviews(): array {
$page_url = home_url( '/podgotovka-k-shkole/' );
// pksh_reviews_list — complex-поле отзывов
$reviews = carbon_get_post_meta( 2, 'pksh_reviews_list' );
if ( empty( $reviews ) || ! is_array( $reviews ) ) {
return array();
}
$result = array();
$review_seq = 0;
foreach ( $reviews as $i => $review ) {
// review_author — имя автора (Person.name)
$author_name = isset( $review['review_author'] ) ? trim( $review['review_author'] ) : '';
// review_text — текст отзыва
$body = isset( $review['review_text'] ) ? trim( $review['review_text'] ) : '';
// review_rating — оценка 1-5
$rating_val = isset( $review['review_rating'] ) ? intval( $review['review_rating'] ) : 0;
// Пропускаем без автора или текста
if ( empty( $author_name ) || empty( $body ) ) {
continue;
}
// Фильтруем оценки вне диапазона 1-5
if ( $rating_val < 1 || $rating_val > 5 ) {
continue;
}
$review_seq++;
$entity = array(
'@type' => 'Review',
'@id' => $page_url . '#review-' . $review_seq,
'author' => array(
'@type' => 'Person',
'name' => wp_strip_all_tags( $author_name ),
),
'reviewBody' => wp_strip_all_tags( $body ),
'itemReviewed' => array(
'@id' => $page_url . '#course',
),
'reviewRating' => array(
'@type' => 'Rating',
'ratingValue' => (string) $rating_val,
'bestRating' => '5',
'worstRating' => '1',
),
);
// datePublished — дата отзыва (условно, из review_date)
$review_date = isset( $review['review_date'] ) ? trim( $review['review_date'] ) : '';
if ( $review_date ) {
$entity['datePublished'] = sanitize_text_field( $review_date );
}
$result[] = $entity;
}
return $result;
}
/**
* Person[] — преподаватели из pksh_teachers complex.
* Каждый преподаватель — отдельная сущность Person.
* Связаны с LocalBusiness через employee @id.
*
* @return array[] Массив Person-сущностей для @graph.
* @see https://schema.org/Person
*/
function dekart_schema_teachers(): array {
$page_url = home_url( '/podgotovka-k-shkole/' );
// pksh_teachers — complex-поле преподавателей
$teachers = carbon_get_post_meta( 2, 'pksh_teachers' );
if ( empty( $teachers ) || ! is_array( $teachers ) ) {
return array();
}
$result = array();
foreach ( $teachers as $i => $teacher ) {
// teacher_name — ФИО преподавателя
$name = isset( $teacher['teacher_name'] ) ? trim( $teacher['teacher_name'] ) : '';
// teacher_subject — предмет / должность
$subject = isset( $teacher['teacher_subject'] ) ? trim( $teacher['teacher_subject'] ) : '';
// teacher_about — описание / биография
$about = isset( $teacher['teacher_about'] ) ? trim( $teacher['teacher_about'] ) : '';
// teacher_photo — фото (attachment ID или массив)
$photo = $teacher['teacher_photo'] ?? 0;
if ( empty( $name ) ) {
continue;
}
$person = array(
'@type' => 'Person',
'@id' => $page_url . '#teacher-' . $i,
'name' => wp_strip_all_tags( $name ),
);
// jobTitle — должность (условно)
if ( ! empty( $subject ) ) {
$person['jobTitle'] = wp_strip_all_tags( $subject );
}
// description — о преподавателе (условно)
if ( ! empty( $about ) ) {
$person['description'] = wp_strip_all_tags( $about );
}
// image — фото преподавателя (условно)
if ( ! empty( $photo ) ) {
if ( is_array( $photo ) && isset( $photo['url'] ) ) {
$person['image'] = esc_url( $photo['url'] );
} else {
$url = wp_get_attachment_url( $photo );
if ( $url ) {
$person['image'] = esc_url( $url );
}
}
}
$result[] = $person;
}
return $result;
}
/* =====================================================================
Вспомогательные функции
===================================================================== */
/**
* Парсит цену из форматированной строки (например "7 500 ₽" → 7500).
* Поддерживает "1 234,56" (comma-decimal) и "1,234.56" (dot-decimal).
*
* @param string $raw Строка с ценой.
* @return float Числовое значение.
*/
function dekart_parse_price( string $raw ): float {
// Удаляем всё кроме цифр, точки, запятой
$clean = preg_replace( '/[^0-9.,]/', '', $raw );
// Определяем формат: если последний разделитель — запятая → comma-decimal
$last_comma = strrpos( $clean, ',' );
$last_dot = strrpos( $clean, '.' );
if ( false !== $last_comma && ( false === $last_dot || $last_comma > $last_dot ) ) {
// Европейский формат: 1.234,56 → убираем точки, меняем запятую на точку
$clean = str_replace( '.', '', $clean );
$clean = str_replace( ',', '.', $clean );
} else {
// Стандартный/русский: убираем всё кроме последней точки
$clean = str_replace( ',', '', $clean );
$clean = preg_replace( '/\.(?=.*\.)/', '', $clean );
}
return floatval( $clean );
}