1279 lines
42 KiB
PHP
1279 lines
42 KiB
PHP
<?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;
|
||
}
|
||
|
||
if (! function_exists('carbon_get_post_meta') || ! function_exists('carbon_get_theme_option')) {
|
||
return;
|
||
}
|
||
|
||
$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();
|
||
|
||
$webpage = dekart_schema_webpage();
|
||
if ($webpage) {
|
||
$graph[] = $webpage;
|
||
}
|
||
|
||
$website = dekart_schema_website();
|
||
if ($website) {
|
||
$graph[] = $website;
|
||
}
|
||
|
||
$breadcrumb = dekart_schema_breadcrumb();
|
||
if ($breadcrumb) {
|
||
$graph[] = $breadcrumb;
|
||
}
|
||
|
||
$organization = dekart_schema_organization();
|
||
if ($organization) {
|
||
$graph[] = $organization;
|
||
}
|
||
|
||
$local_business = dekart_schema_local_business();
|
||
if ($local_business) {
|
||
$graph[] = $local_business;
|
||
}
|
||
|
||
$course = dekart_schema_course();
|
||
if ($course) {
|
||
$graph[] = $course;
|
||
}
|
||
|
||
$offers = dekart_schema_offer();
|
||
if (! empty($offers)) {
|
||
if (isset($offers[0]) && is_array($offers[0])) {
|
||
foreach ($offers as $offer) {
|
||
$graph[] = $offer;
|
||
}
|
||
} else {
|
||
$graph[] = $offers;
|
||
}
|
||
}
|
||
|
||
$rating = dekart_schema_rating();
|
||
if ($rating) {
|
||
$graph[] = $rating;
|
||
}
|
||
|
||
$service = dekart_schema_service();
|
||
if ($service) {
|
||
$graph[] = $service;
|
||
}
|
||
|
||
$faq = dekart_schema_faq();
|
||
if ($faq) {
|
||
$graph[] = $faq;
|
||
}
|
||
|
||
$event = dekart_schema_trial_event();
|
||
if ($event) {
|
||
$graph[] = $event;
|
||
}
|
||
|
||
$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),
|
||
);
|
||
}
|
||
|
||
$teachers = dekart_schema_teachers();
|
||
if (! empty($teachers) && is_array($teachers)) {
|
||
foreach ($teachers as $person) {
|
||
$graph[] = $person;
|
||
}
|
||
}
|
||
|
||
$author = dekart_schema_author();
|
||
if ($author) {
|
||
$graph[] = $author;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
$output = '<script type="application/ld+json">' . "\n" . $json . "\n" . '</script>' . "\n";
|
||
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_title = get_post_meta($post_id, '_yoast_wpseo_title', true);
|
||
$name = $yoast_title ?: wp_get_document_title();
|
||
|
||
$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 ?: 'Подготовка к школе для детей 5–7 лет в Щёлково. Чтение, письмо, математика, логика.';
|
||
|
||
$image_url = '';
|
||
if ($post_id && has_post_thumbnail($post_id)) {
|
||
$image_url = get_the_post_thumbnail_url($post_id, 'full');
|
||
}
|
||
|
||
$about_text = carbon_get_post_meta($post_id, 'pksh_level') ?: 'Подготовка к школе';
|
||
$city = dekart_branch_option('branch_city', 'Щёлково');
|
||
|
||
$data = Pksh_Data::instance();
|
||
|
||
$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' => array(
|
||
'@id' => $url . '#course',
|
||
),
|
||
'speakable' => array(
|
||
'@type' => 'SpeakableSpecification',
|
||
'cssSelector' => array(
|
||
'h1.ds-hero__h1',
|
||
'.ds-hero__subtitle',
|
||
),
|
||
),
|
||
'about' => array(
|
||
'@type' => 'Thing',
|
||
'name' => wp_strip_all_tags(sprintf('%s в %s', $about_text, $city)),
|
||
),
|
||
'keywords' => wp_strip_all_tags(
|
||
'подготовка к школе, дошкольное образование, обучение чтению, обучение письму, '
|
||
. 'математика для дошкольников, развитие логики, курсы для детей 5-7 лет, '
|
||
. $city
|
||
),
|
||
'datePublished' => $data->get_page_publish_date(),
|
||
'dateModified' => $data->get_page_modified_date(),
|
||
'author' => array(
|
||
'@id' => $url . '#author',
|
||
),
|
||
);
|
||
|
||
$faq_items = carbon_get_post_meta($post_id, 'pksh_faq_items');
|
||
if (! empty($faq_items) && is_array($faq_items)) {
|
||
$webpage['hasPart'] = array(
|
||
'@id' => $url . '#faq',
|
||
);
|
||
}
|
||
|
||
return $webpage;
|
||
}
|
||
|
||
/**
|
||
* WebSite — главный сайт.
|
||
*/
|
||
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),
|
||
'publisher' => array(
|
||
'@id' => $home_url . '#organization',
|
||
),
|
||
'inLanguage' => str_replace('_', '-', determine_locale() ?: 'ru'),
|
||
'potentialAction' => array(
|
||
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();
|
||
$name = carbon_get_post_meta(get_the_ID(), 'banner_h1') ?: 'Подготовка к школе';
|
||
|
||
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' => wp_strip_all_tags($name),
|
||
'item' => $page_url,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Helper: получить значение из Carbon Fields (schema_*) с fallback на Settings API (branch_*).
|
||
*/
|
||
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_*).
|
||
*/
|
||
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 — юридическое лицо (без адреса филиала).
|
||
*/
|
||
function dekart_schema_organization(): ?array
|
||
{
|
||
$home_url = home_url('/');
|
||
$brand = dekart_branch_option('branch_name', 'Частная школа «Декарт»');
|
||
|
||
$org = array(
|
||
'@type' => 'Organization',
|
||
'@id' => $home_url . '#organization',
|
||
'name' => wp_strip_all_tags($brand),
|
||
'url' => $home_url,
|
||
'logo' => array(
|
||
'@type' => 'ImageObject',
|
||
'url' => dekart_resolve_logo(),
|
||
),
|
||
);
|
||
|
||
$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));
|
||
}
|
||
|
||
$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: цепочка разрешения логотипа.
|
||
*/
|
||
function dekart_resolve_logo(): string
|
||
{
|
||
$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;
|
||
}
|
||
}
|
||
|
||
$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;
|
||
}
|
||
}
|
||
|
||
if (has_custom_logo()) {
|
||
$logo = wp_get_attachment_image_url(get_theme_mod('custom_logo'), 'full');
|
||
if ($logo) {
|
||
return $logo;
|
||
}
|
||
}
|
||
|
||
$post_id = get_the_ID();
|
||
if ($post_id && has_post_thumbnail($post_id)) {
|
||
$url = get_the_post_thumbnail_url($post_id, 'full');
|
||
if ($url) {
|
||
return $url;
|
||
}
|
||
}
|
||
|
||
return get_template_directory_uri() . '/assets/images/logo-svg.svg';
|
||
}
|
||
|
||
/**
|
||
* LocalBusiness — филиал/центр с адресом, телефоном.
|
||
*/
|
||
function dekart_schema_local_business(): ?array
|
||
{
|
||
$page_url = dekart_schema_page_url();
|
||
$brand = dekart_branch_option('branch_name', 'Частная школа «Декарт»');
|
||
$street = get_option('branch_address') ?: carbon_get_theme_option('site_address') ?: 'Талсинская ул., 24';
|
||
$city = dekart_branch_option('branch_city', 'Щёлково');
|
||
$region = dekart_branch_option('branch_region', 'Московская область');
|
||
$phone = dekart_branch_option('branch_phone', '');
|
||
$email = dekart_branch_option('branch_email', '');
|
||
$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]);
|
||
}
|
||
}
|
||
$zip = dekart_schema_option('schema_postal_code', 'branch_postal_code');
|
||
$price_range = get_option('branch_price_range');
|
||
$founded = dekart_schema_option('schema_founded', 'branch_founded');
|
||
$map_url = dekart_schema_option('schema_map_url', 'branch_map_url');
|
||
$google_place_id = get_option('branch_google_place_id');
|
||
$google_maps_url = get_option('branch_google_maps_url');
|
||
$area_served_raw = carbon_get_theme_option('schema_area_served') ?: get_option('branch_area_served', 'Щёлково, Фрязино, Монино, Ивантеевка, Королёв');
|
||
$hours_cf = carbon_get_theme_option('schema_hours');
|
||
|
||
$lb = array(
|
||
'@type' => array( 'EducationalOrganization', 'LocalBusiness' ),
|
||
'@id' => $page_url . '#local-business',
|
||
'parentOrganization' => array(
|
||
'@id' => home_url('/') . '#organization',
|
||
),
|
||
'name' => wp_strip_all_tags($brand),
|
||
'address' => array(
|
||
'@type' => 'PostalAddress',
|
||
'streetAddress' => wp_strip_all_tags($street),
|
||
'addressLocality' => wp_strip_all_tags($city),
|
||
'addressRegion' => wp_strip_all_tags($region),
|
||
'addressCountry' => 'RU',
|
||
),
|
||
'url' => $page_url,
|
||
);
|
||
|
||
if ($phone) {
|
||
$lb['telephone'] = wp_strip_all_tags($phone);
|
||
}
|
||
if ($email) {
|
||
$lb['email'] = wp_strip_all_tags($email);
|
||
}
|
||
if (! empty($zip) && is_string($zip)) {
|
||
$lb['address']['postalCode'] = trim($zip);
|
||
}
|
||
if (! empty($lat) && ! empty($lng)) {
|
||
$lb['geo'] = array(
|
||
'@type' => 'GeoCoordinates',
|
||
'latitude' => floatval($lat),
|
||
'longitude' => floatval($lng),
|
||
);
|
||
}
|
||
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));
|
||
}
|
||
|
||
$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;
|
||
}
|
||
|
||
if (! empty($price_range) && is_string($price_range)) {
|
||
$lb['priceRange'] = trim($price_range);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
$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));
|
||
|
||
if (! empty($founded) && is_string($founded)) {
|
||
$lb['foundingDate'] = trim($founded);
|
||
}
|
||
|
||
$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;
|
||
}
|
||
|
||
$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 (! empty($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);
|
||
}
|
||
}
|
||
}
|
||
|
||
$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);
|
||
}
|
||
}
|
||
|
||
$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);
|
||
}
|
||
}
|
||
|
||
if (! empty($same_as)) {
|
||
$lb['sameAs'] = $same_as;
|
||
}
|
||
|
||
$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;
|
||
}
|
||
|
||
$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 — основной курс «Подготовка к школе».
|
||
* @see https://schema.org/Course
|
||
*/
|
||
function dekart_schema_course(): ?array
|
||
{
|
||
$page_url = dekart_schema_page_url();
|
||
$name = carbon_get_post_meta(get_the_ID(), 'banner_h1') ?: 'Подготовка к школе 5–7 лет в Щёлково';
|
||
|
||
$pksh_description = carbon_get_post_meta(get_the_ID(), 'pksh_description');
|
||
$description = ! empty($pksh_description) ? $pksh_description : ( carbon_get_post_meta(get_the_ID(), 'banner_subtitle') ?: 'Научим читать, считать и писать до школы. Бесплатное пробное занятие.' );
|
||
$has_trial = ! empty($pksh_description) && false !== mb_stripos($description, 'бесплатно');
|
||
|
||
$level = carbon_get_post_meta(get_the_ID(), 'pksh_level');
|
||
$age = carbon_get_post_meta(get_the_ID(), 'pksh_age');
|
||
$audience = carbon_get_post_meta(get_the_ID(), 'pksh_audience');
|
||
$course_mode = carbon_get_post_meta(get_the_ID(), 'pksh_course_mode');
|
||
$skills = carbon_get_post_meta(get_the_ID(), 'pksh_skills');
|
||
$course_img = carbon_get_post_meta(get_the_ID(), 'banner_img');
|
||
$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),
|
||
'url' => $page_url,
|
||
'provider' => array(
|
||
'@id' => $page_url . '#local-business',
|
||
),
|
||
);
|
||
|
||
if ($has_trial) {
|
||
$course['hasCourseInstance'] = array(
|
||
'@type' => 'CourseInstance',
|
||
'courseMode' => 'Onsite',
|
||
'courseWorkload' => '30-40 минут',
|
||
);
|
||
}
|
||
|
||
if (! empty($level) && is_string($level)) {
|
||
$course['educationalLevel'] = wp_strip_all_tags(trim($level));
|
||
}
|
||
if (! empty($age) && is_string($age)) {
|
||
$course['typicalAgeRange'] = wp_strip_all_tags(trim($age));
|
||
}
|
||
if (! empty($audience) && is_string($audience)) {
|
||
$course['audience'] = array(
|
||
'@type' => 'EducationalAudience',
|
||
'name' => wp_strip_all_tags(trim($audience)),
|
||
);
|
||
}
|
||
|
||
if (! empty($skills) && is_array($skills)) {
|
||
$teaches = array();
|
||
foreach ($skills as $skill) {
|
||
$s = isset($skill['skill']) ? trim($skill['skill']) : '';
|
||
if ($s) {
|
||
$teaches[] = wp_strip_all_tags($s);
|
||
}
|
||
}
|
||
if (! empty($teaches)) {
|
||
$course['teaches'] = $teaches;
|
||
}
|
||
}
|
||
|
||
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())) {
|
||
$course['image'] = esc_url(get_the_post_thumbnail_url(get_the_ID(), 'full'));
|
||
}
|
||
|
||
return $course;
|
||
}
|
||
|
||
/**
|
||
* AggregateRating — рейтинг из отзывов Carbon Fields.
|
||
*/
|
||
function dekart_schema_rating(): ?array
|
||
{
|
||
$page_url = dekart_schema_page_url();
|
||
$ratings = array();
|
||
$reviews = carbon_get_post_meta(get_the_ID(), 'pksh_reviews_list');
|
||
|
||
if (empty($reviews) || ! is_array($reviews)) {
|
||
return null;
|
||
}
|
||
|
||
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 . '#aggregate-rating',
|
||
'ratingValue' => (string) $average,
|
||
'bestRating' => '5',
|
||
'worstRating' => '1',
|
||
'ratingCount' => (string) count($ratings),
|
||
'itemReviewed' => array(
|
||
'@id' => $page_url . '#course',
|
||
),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Service — сущность услуги «Подготовка к школе».
|
||
*/
|
||
function dekart_schema_service(): ?array
|
||
{
|
||
$page_url = dekart_schema_page_url();
|
||
$name = carbon_get_post_meta(get_the_ID(), 'banner_h1') ?: 'Подготовка к школе 5–7 лет в Щёлково';
|
||
|
||
return array(
|
||
'@type' => 'Service',
|
||
'@id' => $page_url . '#service',
|
||
'name' => wp_strip_all_tags($name),
|
||
'provider' => array(
|
||
'@id' => $page_url . '#local-business',
|
||
),
|
||
'areaServed' => array(
|
||
'@type' => 'City',
|
||
'name' => dekart_branch_option('branch_city', 'Щёлково'),
|
||
),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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) {
|
||
$q = isset($item['faq_question']) ? trim($item['faq_question']) : '';
|
||
$a = isset($item['faq_answer']) ? trim($item['faq_answer']) : '';
|
||
if (empty($q) || empty($a)) {
|
||
continue;
|
||
}
|
||
$main_entity[] = array(
|
||
'@type' => 'Question',
|
||
'name' => wp_strip_all_tags($q),
|
||
'acceptedAnswer' => array(
|
||
'@type' => 'Answer',
|
||
'text' => wp_strip_all_tags($a),
|
||
),
|
||
);
|
||
}
|
||
|
||
if (empty($main_entity)) {
|
||
return null;
|
||
}
|
||
|
||
return array(
|
||
'@type' => 'FAQPage',
|
||
'@id' => $page_url . '#faq',
|
||
'name' => 'Часто задаваемые вопросы о подготовке к школе',
|
||
'mainEntity' => $main_entity,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Person[] — преподаватели из pksh_teachers complex.
|
||
* Каждый преподаватель — отдельная сущность Person.
|
||
* Связаны с LocalBusiness через employee @id.
|
||
*
|
||
* @return array[] Массив Person-сущностей для @graph.
|
||
*/
|
||
function dekart_schema_teachers(): array
|
||
{
|
||
$page_url = dekart_schema_page_url();
|
||
$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) {
|
||
$t_id = isset($teacher['teacher_id']) ? intval($teacher['teacher_id']) : 0;
|
||
$subject = isset($teacher['teacher_subject']) ? trim($teacher['teacher_subject']) : '';
|
||
$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;
|
||
}
|
||
|
||
$photo = get_post_meta($t_id, '_teacher_avatar', true);
|
||
if (! $photo) {
|
||
$photo = get_post_meta($t_id, '_thumbnail_id', true);
|
||
}
|
||
|
||
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),
|
||
);
|
||
|
||
if (! empty($subject) && is_string($subject)) {
|
||
$person['jobTitle'] = wp_strip_all_tags(trim($subject));
|
||
}
|
||
|
||
if (! empty($about) && is_string($about)) {
|
||
$person['description'] = wp_strip_all_tags(trim($about));
|
||
}
|
||
|
||
if (! empty($photo)) {
|
||
$url = wp_get_attachment_url((int) $photo);
|
||
if ($url) {
|
||
$person['image'] = esc_url($url);
|
||
}
|
||
}
|
||
|
||
$result[] = $person;
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Review[] — поштучные отзывы из pksh_reviews_list.
|
||
* Каждый отзыв — отдельная сущность Review с автор-персоной и оценкой.
|
||
*
|
||
* @return array[] Массив Review-сущностей для @graph.
|
||
*/
|
||
function dekart_schema_reviews(): 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 array();
|
||
}
|
||
|
||
$result = array();
|
||
$review_seq = 0;
|
||
foreach ($reviews as $i => $review) {
|
||
$author_name = isset($review['review_author']) ? trim($review['review_author']) : '';
|
||
$body = isset($review['review_text']) ? trim($review['review_text']) : '';
|
||
$rating_val = isset($review['review_rating']) ? intval($review['review_rating']) : 0;
|
||
|
||
if (empty($author_name) || empty($body)) {
|
||
continue;
|
||
}
|
||
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',
|
||
),
|
||
);
|
||
|
||
$review_date = isset($review['review_date']) ? trim($review['review_date']) : '';
|
||
if ($review_date) {
|
||
$entity['datePublished'] = sanitize_text_field($review_date);
|
||
}
|
||
|
||
$result[] = $entity;
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Author Person — автор контента страницы.
|
||
* Берётся из CF полей вкладки "🤖 GEO (AI-поиск)".
|
||
*/
|
||
function dekart_schema_author(): ?array
|
||
{
|
||
$page_url = dekart_schema_page_url();
|
||
$data = Pksh_Data::instance();
|
||
$name = $data->get_author_name();
|
||
$years = $data->get_years_on_market();
|
||
|
||
// Должность — хардкор + авто-расчёт лет
|
||
$title = 'Образовательный центр с гослицензией';
|
||
if ($years > 0) {
|
||
$title .= ', ' . $years . ' лет на рынке';
|
||
}
|
||
|
||
$person = array(
|
||
'@type' => 'Person',
|
||
'@id' => $page_url . '#author',
|
||
'name' => wp_strip_all_tags($name),
|
||
'jobTitle' => wp_strip_all_tags($title),
|
||
);
|
||
|
||
$person['description'] = wp_strip_all_tags(sprintf(
|
||
'Автор контента %s — %s',
|
||
$name,
|
||
$title
|
||
));
|
||
|
||
return $person;
|
||
}
|
||
|
||
/**
|
||
* Event — бесплатное пробное занятие.
|
||
* @see https://schema.org/Event
|
||
*/
|
||
function dekart_schema_trial_event(): ?array
|
||
{
|
||
if (! is_page_template('page-podgotovka-k-shkole.php')) {
|
||
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)) {
|
||
$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',
|
||
),
|
||
);
|
||
|
||
$post_id = get_the_ID();
|
||
|
||
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' => ( $post_id && has_post_thumbnail($post_id) )
|
||
? get_the_post_thumbnail_url($post_id, 'full')
|
||
: '',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Offer / AggregateOffer с UnitPriceSpecification.
|
||
* @see https://schema.org/UnitPriceSpecification
|
||
*/
|
||
function dekart_schema_offer(): ?array
|
||
{
|
||
$page_url = dekart_schema_page_url();
|
||
$base_price = carbon_get_post_meta(get_the_ID(), 'pksh_prog_base_price_sub');
|
||
$prem_price = carbon_get_post_meta(get_the_ID(), 'pksh_prog_prem_price_sub');
|
||
$price_valid = carbon_get_post_meta(get_the_ID(), 'pksh_price_valid_until');
|
||
$offers = array();
|
||
$prices = array();
|
||
|
||
foreach (array( $base_price, $prem_price ) as $raw) {
|
||
if (! empty($raw) && is_string($raw)) {
|
||
$parsed = dekart_parse_price($raw);
|
||
if ($parsed > 0) {
|
||
$prices[] = $parsed;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty($prices)) {
|
||
$offer = array(
|
||
'@type' => 'Offer',
|
||
'@id' => $page_url . '#offer',
|
||
'price' => '0',
|
||
'priceCurrency' => 'RUB',
|
||
'description' => 'Подготовка к школе — свяжитесь для уточнения стоимости',
|
||
'availability' => 'https://schema.org/InStock',
|
||
'category' => 'Образовательные услуги',
|
||
);
|
||
if (! empty($price_valid)) {
|
||
$offer['priceValidUntil'] = trim($price_valid);
|
||
}
|
||
$offers[] = $offer;
|
||
return $offers;
|
||
}
|
||
|
||
$low = min($prices);
|
||
$high = max($prices);
|
||
|
||
$aggregate = array(
|
||
'@type' => 'AggregateOffer',
|
||
'@id' => $page_url . '#offer',
|
||
'name' => 'Стоимость подготовки к школе',
|
||
'lowPrice' => (string) $low,
|
||
'highPrice' => (string) $high,
|
||
'offerCount' => (string) count($prices),
|
||
'availability' => 'https://schema.org/InStock',
|
||
'category' => 'Образовательные услуги',
|
||
'offeredBy' => array(
|
||
'@id' => $page_url . '#local-business',
|
||
),
|
||
'priceSpecification' => array(
|
||
'@type' => 'UnitPriceSpecification',
|
||
'name' => 'Абонемент за 4 недели',
|
||
'minPrice' => (string) $low,
|
||
'maxPrice' => (string) $high,
|
||
'unitCode' => 'MON',
|
||
'unitText' => 'месяц',
|
||
),
|
||
);
|
||
|
||
if (! empty($price_valid)) {
|
||
$aggregate['priceValidUntil'] = trim($price_valid);
|
||
}
|
||
|
||
$offers[] = $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 . '#local-business',
|
||
),
|
||
);
|
||
if (! empty($price_valid)) {
|
||
$trial['priceValidUntil'] = trim($price_valid);
|
||
}
|
||
$offers[] = $trial;
|
||
}
|
||
|
||
return $offers;
|
||
}
|
||
|
||
/**
|
||
* Парсит цену из форматированной строки (например "7 500 ₽" → 7500).
|
||
*/
|
||
function dekart_parse_price(string $raw): float
|
||
{
|
||
$clean = preg_replace('/[^0-9.,]/', '', $raw);
|
||
|
||
if (empty($clean)) {
|
||
return 0.0;
|
||
}
|
||
|
||
$last_dot = strrpos($clean, '.');
|
||
$last_comma = strrpos($clean, ',');
|
||
|
||
if (false !== $last_comma && ( false === $last_dot || $last_comma > $last_dot )) {
|
||
$clean = str_replace('.', '', $clean);
|
||
$clean = str_replace(',', '.', $clean);
|
||
} else {
|
||
$clean = str_replace(',', '', $clean);
|
||
$clean = preg_replace('/\.(?=.*\.)/', '', $clean);
|
||
}
|
||
|
||
return (float) $clean;
|
||
}
|