shkola/wp-content/themes/dekart/inc/schema.php
2026-07-17 18:50:14 +00:00

1556 lines
53 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' для страницы Подготовки к школе,
* чтобы избежать несоответствия article/website в социальных сниппетах.
*/
add_filter( 'wpseo_opengraph_type', 'dekart_schema_og_type', 10, 1 );
function dekart_schema_og_type( string $type ): string {
if ( is_page_template( 'page-podgotovka-k-shkole.php' ) || is_page_template( 'page-camp.php' ) ) {
return 'website';
}
return $type;
}
/**
* Убираем article:* OG-теги для страницы Подготовки к школе,
* чтобы не было конфликта og:type=website с article:modified_time.
* Аналог page-camp.php:15-27.
*/
add_filter( 'wpseo_frontend_presenter_classes', function( array $presenters ): array {
if ( ! is_page_template( 'page-podgotovka-k-shkole.php' ) ) {
return $presenters;
}
$presenters = array_filter( $presenters, function( string $p ): bool {
return strpos( $p, 'Article_Modified_Time' ) === false
&& strpos( $p, 'Article_Published_Time' ) === false
&& strpos( $p, 'Article_Author' ) === false
&& strpos( $p, 'Article_Publisher' ) === false
&& strpos( $p, 'Label1' ) === false
&& strpos( $p, 'Data1' ) === false;
} );
return $presenters;
} );
/**
* Главная функция: сборка @graph и вывод в wp_head.
* Срабатывает только на странице с шаблоном page-podgotovka-k-shkole.php.
*/
add_action( 'wp_head', 'dekart_schema_output', 1 );
/**
* Возвращает URL текущей страницы для Schema.org @id.
* Динамически через get_permalink() — не зависит от хардкода ID.
*/
function dekart_schema_page_url(): string {
$id = get_the_ID();
return $id ? get_permalink( $id ) : home_url( '/podgotovka-k-shkole/' );
}
function dekart_schema_output(): void {
if ( ! is_page_template( 'page-podgotovka-k-shkole.php' ) ) {
return;
}
// Guard: без Carbon Fields — тихий выход (не может быть фатала)
if ( ! function_exists( 'carbon_get_post_meta' ) || ! function_exists( 'carbon_get_theme_option' ) ) {
return;
}
// Object Cache key: page ID + locale + schema version
$cache_key = 'dekart_schema_graph_' . get_the_ID() . '_' . determine_locale();
$cached = wp_cache_get( $cache_key, 'dekart_schema' );
if ( false !== $cached ) {
echo $cached;
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. Service — сущность услуги (только для страниц услуг, не для /otzyvy/)
$service = dekart_schema_service();
if ( $service ) {
$graph[] = $service;
}
// 11. FAQPage (conditional — есть ли вопросы)
$faq = dekart_schema_faq();
if ( $faq ) {
$graph[] = $faq;
}
// 11b. Event — бесплатное пробное занятие
$event = dekart_schema_trial_event();
if ( $event ) {
$graph[] = $event;
}
// 12. ItemList — обёртка для отзывов (conditional)
$reviews = dekart_schema_reviews();
if ( ! empty( $reviews ) && is_array( $reviews ) ) {
$graph[] = array(
'@type' => 'ItemList',
'@id' => dekart_schema_page_url() . '#reviews',
'name' => 'Отзывы о подготовке к школе',
'description' => 'Отзывы родителей о курсе подготовки к школе в Детском центре «Декарт»',
'itemListElement' => array_map( function( $review, $index ) {
return array(
'@type' => 'ListItem',
'position' => $index + 1,
'item' => $review,
);
}, $reviews, array_keys( $reviews ) ),
'numberOfItems' => count( $reviews ),
);
}
// 13. 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
}
$output = '<script type="application/ld+json">' . "\n" . $json . "\n" . '</script>' . "\n";
// Cache for 1 hour (3600 seconds) in 'dekart_schema' group
wp_cache_set( $cache_key, $output, 'dekart_schema', 3600 );
echo $output;
}
/* =====================================================================
Сущности @graph
===================================================================== */
/**
* WebPage — текущая страница.
* @see https://schema.org/WebPage
*/
function dekart_schema_webpage(): ?array {
$post_id = get_the_ID();
$url = get_permalink( $post_id ) ?: dekart_schema_page_url();
// _yoast_wpseo_title — Yoast SEO заголовок
$yoast_title = get_post_meta( $post_id, '_yoast_wpseo_title', true );
$name = $yoast_title ?: wp_get_document_title();
// _yoast_wpseo_metadesc — Yoast SEO описание
$description = get_post_meta( $post_id, '_yoast_wpseo_metadesc', true );
$description = $description ?: get_post_meta( $post_id, 'rank_math_description', true );
$description = $description ?: carbon_get_post_meta( $post_id, 'banner_subtitle' );
$description = $description ?: 'Подготовка к школе для детей 57 лет в Щёлково. Чтение, письмо, математика, логика.';
// has_post_thumbnail — изображение страницы (OG-совместимость)
$image_url = '';
if ( has_post_thumbnail( get_the_ID() ) ) {
$image_url = get_the_post_thumbnail_url( 2, 'full' );
}
// pksh_level — уровень образования (about)
$about_text = carbon_get_post_meta( get_the_ID(), '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.ds-hero__h1',
'.ds-hero__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( get_the_ID(), '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 = dekart_schema_page_url();
$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 ),
'image' => 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( get_the_ID(), '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( get_the_ID() ) ) {
$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 = dekart_schema_page_url();
$home_url = home_url( '/' );
// branch_name — название филиала (get_option)
$site_name = dekart_branch_option( 'branch_name', 'Детский центр «Декарт»' );
// branch_address — улица (приоритет: Settings API → CF theme option → хардкод)
$street = get_option( 'branch_address' ) ?: carbon_get_theme_option( 'site_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_google_place_id — Google Place ID
$google_place_id = get_option( 'branch_google_place_id' );
// branch_google_maps_url — Google Maps URL для hasMap
$google_maps_url = get_option( 'branch_google_maps_url' );
// area_served — города обслуживания (через запятую)
// Приоритет: CF schema_area_served → Settings API branch_area_served
$area_served_raw = carbon_get_theme_option( 'schema_area_served' ) ?: get_option( 'branch_area_served', 'Щёлково, Фрязино, Монино, Ивантеевка, Королёв' );
// 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 );
}
// Google Place ID — identifier + @id для Google Business Profile
if ( ! empty( $google_place_id ) && is_string( $google_place_id ) ) {
$lb['identifier'] = array(
'@type' => 'PropertyValue',
'name' => 'Google Place ID',
'value' => wp_strip_all_tags( $google_place_id ),
);
$lb['@id'] = 'https://search.google.com/local/writereview?placeid=' . urlencode( $google_place_id );
}
// areaServed — массив городов (branch_area_served: "Щёлково, Фрязино, Монино...")
$area_cities = array_map( 'trim', explode( ',', $area_served_raw ) );
$lb['areaServed'] = array_map( function( $c ) {
return array( '@type' => 'City', 'name' => wp_strip_all_tags( $c ) );
}, array_filter( $area_cities ) );
// hasMap — Google Maps URL (приоритет Google, потом Yandex)
if ( ! empty( $google_maps_url ) && is_string( $google_maps_url ) ) {
$lb['hasMap'] = esc_url( trim( $google_maps_url ) );
} elseif ( ! empty( $map_url ) && is_string( $map_url ) ) {
$lb['hasMap'] = esc_url( trim( $map_url ) );
}
// 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;
}
// Гарантируем WhatsApp и Telegram в sameAs, даже если не заданы в админке
$ensure_platforms = array(
'WhatsApp' => dekart_branch_option( 'branch_whatsapp', 'https://wa.me/79269311434' ),
'Telegram' => dekart_branch_option( 'branch_telegram', 'https://t.me/dekart_school' ),
);
foreach ( $ensure_platforms as $platform => $url ) {
if ( $url && filter_var( $url, FILTER_VALIDATE_URL ) ) {
$found = false;
foreach ( $same_as as $existing ) {
if ( false !== stripos( $existing, parse_url( $url, PHP_URL_HOST ) ) ) {
$found = true;
break;
}
}
if ( ! $found ) {
$same_as[] = esc_url( $url );
}
}
}
// Google Business Profile — добавляем в sameAs, если ссылка задана
$gbp_url = get_option( 'branch_google_business_url', '' );
if ( ! empty( $gbp_url ) && filter_var( $gbp_url, FILTER_VALIDATE_URL ) ) {
$found_gbp = false;
foreach ( $same_as as $existing ) {
if ( false !== stripos( $existing, parse_url( $gbp_url, PHP_URL_HOST ) ) ) {
$found_gbp = true;
break;
}
}
if ( ! $found_gbp ) {
$same_as[] = esc_url( $gbp_url );
}
}
// Яндекс Бизнес — добавляем в sameAs, если ссылка задана
$yb_url = get_option( 'branch_yandex_business_url', '' );
if ( ! empty( $yb_url ) && filter_var( $yb_url, FILTER_VALIDATE_URL ) ) {
$found_yb = false;
foreach ( $same_as as $existing ) {
if ( false !== stripos( $existing, parse_url( $yb_url, PHP_URL_HOST ) ) ) {
$found_yb = true;
break;
}
}
if ( ! $found_yb ) {
$same_as[] = esc_url( $yb_url );
}
}
// Если после добавления появились элементы, обновляем sameAs
if ( ! empty( $same_as ) && ! isset( $lb['sameAs'] ) ) {
$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( get_the_ID(), 'pksh_teachers' );
if ( ! empty( $teachers ) && is_array( $teachers ) ) {
$employee_ids = array();
foreach ( $teachers as $i => $t ) {
$t_id = isset( $t['teacher_id'] ) ? intval( $t['teacher_id'] ) : 0;
if ( $t_id > 0 ) {
$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 = dekart_schema_page_url();
$home_url = home_url( '/' );
// banner_h1 — заголовок H1 (Course.name)
$name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе 57 лет в Щёлково';
// pksh_description — полное описание курса для микроразметки (textarea)
// Если заполнено — приоритет над banner_subtitle
$pksh_description = carbon_get_post_meta( get_the_ID(), 'pksh_description' );
// banner_subtitle — подзаголовок (Course.description)
$description = ! empty( $pksh_description ) ? $pksh_description : ( carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ) ?: 'Научим читать, считать и писать до школы. Бесплатное пробное занятие.' );
// Пробное занятие — есть ли в подзаголовке упоминание бесплатно
$has_trial = ! empty( $description ) && false !== mb_stripos( $description, 'бесплатно' );
// pksh_level — уровень образования
$level = carbon_get_post_meta( get_the_ID(), 'pksh_level' );
// pksh_age — возраст
$age = carbon_get_post_meta( get_the_ID(), 'pksh_age' );
// pksh_audience — целевая аудитория
$audience = carbon_get_post_meta( get_the_ID(), 'pksh_audience' );
// pksh_course_mode — формат обучения (очная/ONLINE/OFFLINE)
$course_mode = carbon_get_post_meta( get_the_ID(), 'pksh_course_mode' );
// pksh_skills — список навыков
$skills = carbon_get_post_meta( get_the_ID(), 'pksh_skills' );
// banner_img — изображение курса
$course_img = carbon_get_post_meta( get_the_ID(), 'banner_img' );
// pksh_programs — программы (для timeRequired + CourseInstance)
$programs = carbon_get_post_meta( get_the_ID(), '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',
),
);
// review — 1-2 отзыва прямо в Course (Google требует для звёзд в rich snippet)
$reviews_raw = carbon_get_post_meta( get_the_ID(), 'pksh_reviews_list' );
if ( ! empty( $reviews_raw ) && is_array( $reviews_raw ) ) {
$review_items = array();
foreach ( $reviews_raw as $rv ) {
$author = isset( $rv['review_author'] ) ? trim( $rv['review_author'] ) : '';
$body = isset( $rv['review_text'] ) ? trim( $rv['review_text'] ) : '';
$rating = isset( $rv['review_rating'] ) ? intval( $rv['review_rating'] ) : 0;
if ( empty( $author ) || empty( $body ) || $rating < 1 || $rating > 5 ) {
continue;
}
$item = array(
'@type' => 'Review',
'author' => array( '@type' => 'Person', 'name' => wp_strip_all_tags( $author ) ),
'reviewBody' => wp_strip_all_tags( $body ),
'reviewRating' => array(
'@type' => 'Rating',
'ratingValue' => (string) $rating,
'bestRating' => '5',
'worstRating' => '1',
),
'itemReviewed' => array( '@id' => $page_url . '#course' ),
);
$date = isset( $rv['review_date'] ) ? trim( $rv['review_date'] ) : '';
if ( $date ) {
$item['datePublished'] = sanitize_text_field( $date );
}
$review_items[] = $item;
if ( count( $review_items ) >= 2 ) {
break;
}
}
if ( ! empty( $review_items ) ) {
$course['review'] = $review_items;
}
}
// educationalLevel — уровень образования (условно)
if ( ! empty( $level ) && is_string( $level ) ) {
$course['educationalLevel'] = wp_strip_all_tags( trim( $level ) );
}
// typicalAgeRange — возраст детей (условно, из pksh_age)
if ( ! empty( $age ) && is_string( $age ) ) {
$course['typicalAgeRange'] = wp_strip_all_tags( trim( $age ) );
}
// 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[] = 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( get_the_ID() ) ) {
// 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;
}
/**
* AggregateRating — рейтинг из отзывов Carbon Fields.
* Вычисляется динамически из pksh_reviews_list.
* Если отзывов < 1 — блок не выводится.
*/
function dekart_schema_rating(): ?array {
$page_url = dekart_schema_page_url();
$reviews = carbon_get_post_meta( get_the_ID(), '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',
),
);
}
/**
* Service — сущность услуги «Подготовка к школе».
* provider → #organization (единая школа).
* Google видит Service как частный случай услуги организации.
*
* @see https://schema.org/Service
*/
function dekart_schema_service(): ?array {
$page_url = dekart_schema_page_url();
$home_url = home_url( '/' );
$name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе';
$description = carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ) ?: '';
$service = array(
'@type' => 'Service',
'@id' => $page_url . '#service',
'name' => wp_strip_all_tags( $name ),
'provider' => array(
'@id' => $home_url . '#organization',
),
'areaServed' => array(
'@type' => 'City',
'name' => dekart_branch_option( 'branch_city', 'Щёлково' ),
),
);
if ( $description ) {
$service['description'] = wp_strip_all_tags( $description );
}
return $service;
}
/**
* FAQPage — вопросы и ответы из pksh_faq_items.
* Если вопросов нет — блок не выводится.
*/
function dekart_schema_faq(): ?array {
$page_url = dekart_schema_page_url();
$faq_items = carbon_get_post_meta( get_the_ID(), '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 = dekart_schema_page_url();
// pksh_reviews_list — complex-поле отзывов
$reviews = carbon_get_post_meta( get_the_ID(), '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 = dekart_schema_page_url();
// pksh_teachers — complex-поле преподавателей
// Subfields: teacher_id (select → post ID), teacher_subject, teacher_about
// teacher_name и teacher_photo загружаются из карточки преподавателя
$teachers = carbon_get_post_meta( get_the_ID(), 'pksh_teachers' );
if ( empty( $teachers ) || ! is_array( $teachers ) ) {
return array();
}
$result = array();
foreach ( $teachers as $i => $teacher ) {
// teacher_id — ID записи преподавателя из кастомного типа
$t_id = isset( $teacher['teacher_id'] ) ? intval( $teacher['teacher_id'] ) : 0;
// teacher_subject — предмет / должность (с этой страницы)
$subject = isset( $teacher['teacher_subject'] ) ? trim( $teacher['teacher_subject'] ) : '';
// teacher_about — описание / биография (с этой страницы)
$about = isset( $teacher['teacher_about'] ) ? trim( $teacher['teacher_about'] ) : '';
if ( $t_id < 1 ) {
continue;
}
// Загружаем имя и фото из карточки преподавателя
$t_post = get_post( $t_id );
if ( ! $t_post ) {
continue;
}
$name = $t_post->post_title;
if ( empty( $name ) ) {
continue;
}
// Фото: _teacher_avatar → _thumbnail_id → 0
$photo = get_post_meta( $t_id, '_teacher_avatar', true );
if ( ! $photo ) {
$photo = get_post_meta( $t_id, '_thumbnail_id', true );
}
// Если subject/about не заданы на странице — fallback из карточки
if ( empty( $subject ) ) {
$subject = get_post_meta( $t_id, '_teacher_direction', true );
}
if ( empty( $about ) ) {
$about = get_post_meta( $t_id, '_teacher_about', true );
}
$person = array(
'@type' => 'Person',
'@id' => $page_url . '#teacher-' . $i,
'name' => wp_strip_all_tags( $name ),
);
// jobTitle — должность (условно)
if ( ! empty( $subject ) && is_string( $subject ) ) {
$person['jobTitle'] = wp_strip_all_tags( trim( $subject ) );
}
// description — о преподавателе (условно)
if ( ! empty( $about ) && is_string( $about ) ) {
$person['description'] = wp_strip_all_tags( trim( $about ) );
}
// image — фото преподавателя (условно)
if ( ! empty( $photo ) ) {
$url = wp_get_attachment_url( (int) $photo );
if ( $url ) {
$person['image'] = esc_url( $url );
}
}
$result[] = $person;
}
return $result;
}
/* =====================================================================
Новые сущности для Phase 2 (Local SEO + AI)
===================================================================== */
/**
* Event — бесплатное пробное занятие.
* Google Events carousel, высокая конверсия на "пробное занятие [город]".
*
* @see https://schema.org/Event
*/
function dekart_schema_trial_event(): ?array {
if ( ! is_page( 2 ) ) {
return null;
}
$page_url = dekart_schema_page_url();
// Проверяем, есть ли упоминание бесплатного пробного в подзаголовке
$subtitle = carbon_get_post_meta( get_the_ID(), 'banner_subtitle' );
$has_trial = ! empty( $subtitle ) && false !== mb_stripos( $subtitle, 'бесплатно' );
if ( ! $has_trial ) {
return null;
}
// Дата события: ближайший понедельник или "ежедневно"
$start_date = carbon_get_post_meta( get_the_ID(), 'pksh_trial_event_start_date' );
if ( empty( $start_date ) ) {
// Fallback: следующий понедельник
$start_date = date( 'c', strtotime( 'next monday 10:00' ) );
}
$end_date = carbon_get_post_meta( get_the_ID(), 'pksh_trial_event_end_date' );
if ( empty( $end_date ) ) {
$end_date = date( 'c', strtotime( $start_date . ' +1 hour' ) );
}
$location = array(
'@type' => 'Place',
'name' => 'Детский центр «Декарт»',
'address' => array(
'@type' => 'PostalAddress',
'streetAddress' => get_option( 'branch_address' ) ?: carbon_get_theme_option( 'site_address' ),
'addressLocality' => get_option( 'branch_city', 'Щёлково' ),
'addressRegion' => get_option( 'branch_region', 'Московская область' ),
'addressCountry' => 'RU',
),
);
return array(
'@type' => 'Event',
'@id' => $page_url . '#trial-event',
'name' => 'Бесплатное пробное занятие: Подготовка к школе',
'description' => 'Полноценное занятие + диагностика готовности к школе. Ребёнок познакомится с педагогом, выполнит тестовые задания. Родители получат план подготовки.',
'eventStatus' => 'https://schema.org/EventScheduled',
'eventAttendanceMode' => 'https://schema.org/OfflineEventAttendanceMode',
'startDate' => $start_date,
'endDate' => $end_date,
'location' => $location,
'offers' => array(
'@type' => 'Offer',
'@id' => $page_url . '#offer-trial',
'price' => '0',
'priceCurrency' => 'RUB',
'availability' => 'https://schema.org/LimitedAvailability',
'description' => 'Бесплатное пробное занятие',
'category' => 'Бесплатное пробное занятие',
),
'organizer' => array(
'@id' => home_url( '/' ) . '#organization',
),
'image' => get_the_post_thumbnail_url( 2, 'full' ) ?: '',
);
}
/**
* Offer / AggregateOffer с UnitPriceSpecification.
* Добавляет priceSpecification с unitCode: 'MON' для цен за месяц.
*
* @see https://schema.org/UnitPriceSpecification
*/
function dekart_schema_offer(): ?array {
$page_url = dekart_schema_page_url();
$programs = carbon_get_post_meta( get_the_ID(), 'pksh_programs' );
$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 ) {
if ( ! empty( $program['program_price_subscription'] ) ) {
$num = dekart_parse_price( $program['program_price_subscription'] );
if ( $num > 0 ) {
$prices[] = $num;
}
}
if ( ! empty( $program['program_price_single'] ) ) {
$num = dekart_parse_price( $program['program_price_single'] );
if ( $num > 0 ) {
$prices[] = $num;
}
}
}
}
if ( empty( $prices ) ) {
$offer = array(
'@type' => 'Offer',
'@id' => $page_url . '#offer',
'price' => '0',
'priceCurrency' => 'RUB',
'availability' => 'https://schema.org/LimitedAvailability',
'description' => 'Бесплатное пробное занятие',
'category' => 'Бесплатное пробное занятие',
);
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' => 'Образовательные услуги',
'offeredBy' => array(
'@id' => $page_url . '#localbusiness',
),
// UnitPriceSpecification для цен за месяц
'priceSpecification' => array(
'@type' => 'UnitPriceSpecification',
'priceCurrency' => 'RUB',
'minPrice' => (string) $low,
'maxPrice' => (string) $high,
'unitCode' => 'MON', // месяц
'unitText' => 'месяц',
),
);
if ( ! empty( $price_valid ) ) {
$aggregate['priceValidUntil'] = trim( $price_valid );
}
$offers = array( $aggregate );
$subtitle = carbon_get_post_meta( get_the_ID(), '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;
}
/* =====================================================================
Вспомогательные функции
===================================================================== */
/**
* Парсит цену из форматированной строки (например "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 );
}