diff --git a/.gitignore b/.gitignore index ee54648..62c795d 100644 --- a/.gitignore +++ b/.gitignore @@ -126,9 +126,10 @@ wp-content/upgrade-temp-backup/ # === Agent/Test files — dev-мусор (не в git) === data/ -llms.txt test.php .agents/ +.claude/ +skills-lock.json # === Audit files — dev-мусор (не в git) === *.audit.txt diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c16e70e..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,251 +0,0 @@ -# Site1 — WordPress Project -## Agent -Primary agent: -- @site1 -This agent manages the WordPress project architecture, development workflow, frontend changes, SEO safety, and deployment preparation. ---- -# Project Context -Project type: -- Custom WordPress website. -Architecture: -- WordPress core. -- Custom theme: - `wp-content/themes/dekart/` -Theme contains: -- Templates -- Custom frontend styles -- Carbon Fields integration -- Schema.org JSON-LD logic -- Custom business data logic -Repository: -- Git is the source of truth. -- Theme changes are deployed through Git. -- Production and development environments are separated. -Important: -Before any changes, inspect existing architecture. -Do not assume standard WordPress structure. ---- -# Critical Rules -## Forbidden without explicit confirmation -NEVER modify: -- wp-config.php -- database structure -- production environment files -- uploads directory -- user data -- existing production configuration -Before executing: -- rm -- delete -- overwrite -- database write -- git push -Ask for confirmation. ---- -# WordPress Architecture Rules -## Theme -Location: -wp-content/themes/dekart/ -Rules: -Preserve: -- existing PHP templates -- template hierarchy -- hooks -- filters -- Carbon Fields compatibility -- schema.org output -- SEO logic -Do not: -- rebuild templates -- replace theme architecture -- move functionality into plugins without approval -- introduce page builders ---- -# Carbon Fields -Carbon Fields is intentionally located inside the theme. -Rules: -Do not: -- move Carbon Fields into a plugin -- rename fields -- change option keys -- modify stored data structure -Any changes must preserve backward compatibility. ---- -# Frontend / Redesign Rules -Goal: -Improve visual design without changing markup. -Allowed: -- CSS -- SCSS -- CSS variables -- typography -- colors -- spacing -- borders -- shadows -- animations -- responsive improvements -Forbidden: -- changing HTML structure -- changing block layout -- changing forms -- replacing components -- adding Elementor -- adding visual builders -Priority: -1. Preserve functionality -2. Preserve SEO -3. Preserve accessibility -4. Improve UI consistency ---- -# Modern Web Development -Use Google Chrome Modern Web Guidance principles. -Priorities: -- Accessibility -- Performance -- Responsive design -- Maintainable CSS -- Browser compatibility -Prefer: -- CSS variables -- reusable design tokens -- semantic CSS naming -- modern CSS compatible with Baseline -Avoid: -- unnecessary JavaScript -- duplicated CSS -- layout shifts -- heavy dependencies -Consider: -- Core Web Vitals -- CLS -- LCP -- accessibility contrast -- responsive behavior ---- -# Frontend Workflow -Before changing styles: -1. Audit existing CSS. -2. Identify: -- colors -- typography -- spacing -- components -- duplicated rules -- inconsistent UI patterns -Create design tokens when possible: -Example: -```css -:root { - --color-primary: - --color-secondary: - --color-text: - --color-background: - --border-radius: - --shadow: -} -Do not make blind global replacements. -SEO Safety -Never break: -title/meta logic -schema.org JSON-LD -breadcrumbs -canonical URLs -indexing behavior -structured data -Frontend changes must not affect SEO output. -Performance -Keep: -Core Web Vitals -minimal CSS overhead -optimized assets -no unnecessary scripts -Avoid: -additional libraries without approval -duplicate dependencies -blocking resources -Redis Object Cache -Current architecture: -Redis Object Cache plugin -mu-plugin bootstrap -WordPress object cache API -Rules: -Do not create another caching layer. -Do not create custom Redis clients. -Do not create custom object-cache.php. -Use: -wp_cache_get() -wp_cache_set() -Runtime files: -Never commit: -wp-content/object-cache.php -Development Standards -PHP -Use: -PSR-12 -type declarations where appropriate -WordPress coding standards -JavaScript -Use: -TypeScript strict -ESM -async/await -Avoid unnecessary frameworks. -Git Rules -Commit format: -feat: -fix: -refactor: -style: -chore: -docs: -Before commit: -Run: -git status -git diff -Never commit: -.env -uploads/ -logs/ -wp-content/object-cache.php -cache files -temporary files -Testing -PHP: -vendor/bin/phpunit -vendor/bin/phpstan analyse -JavaScript: -npm run build -npm test -npm run lint -Run only relevant checks for changed area. -Deployment Rules -Before deployment verify: -changed files -dependencies -environment requirements -database impact -cache impact -Never assume production environment matches development. -Required Work Process -For complex tasks: -Analyze current implementation. -Explain planned changes. -Wait for confirmation if architecture changes are required. -Implement minimal changes. -Verify result. -Provide report. -Final Response Format -After completing tasks always provide: -Summary -What was changed. -Files Changed -List files. -Technical Details -Explain implementation. -Validation -Tests and checks performed. -Risks -Possible issues. -Deployment Notes -Required production actions. \ No newline at end of file diff --git a/wp-content/themes/dekart/footer.php b/wp-content/themes/dekart/footer.php index ba73539..7883f63 100644 --- a/wp-content/themes/dekart/footer.php +++ b/wp-content/themes/dekart/footer.php @@ -32,7 +32,7 @@ - г.
+ г.
diff --git a/wp-content/themes/dekart/functions.php b/wp-content/themes/dekart/functions.php index 25ac9ca..6a0b84b 100644 --- a/wp-content/themes/dekart/functions.php +++ b/wp-content/themes/dekart/functions.php @@ -1915,7 +1915,10 @@ function dekart_branch_register_settings() { register_setting( 'dekart_branch_group', 'branch_remaining_seats', array( 'sanitize_callback' => 'absint' ) ); register_setting( 'dekart_branch_group', 'branch_schema_description', array( 'sanitize_callback' => 'sanitize_textarea_field' ) ); register_setting( 'dekart_branch_group', 'branch_area_served', array( 'sanitize_callback' => 'sanitize_text_field' ) ); - register_setting( 'dekart_branch_group', 'branch_logistics_walk', array( 'sanitize_callback' => 'sanitize_textarea_field' ) ); + register_setting( 'dekart_branch_group', 'branch_google_place_id', array( 'sanitize_callback' => 'sanitize_text_field' ) ); + register_setting( 'dekart_branch_group', 'branch_google_maps_url', array( 'sanitize_callback' => 'esc_url_raw' ) ); + register_setting( 'dekart_branch_group', 'branch_google_business_url', array( 'sanitize_callback' => 'esc_url_raw' ) ); +register_setting( 'dekart_branch_group', 'branch_logistics_walk', array( 'sanitize_callback' => 'sanitize_textarea_field' ) ); register_setting( 'dekart_branch_group', 'branch_logistics_parking', array( 'sanitize_callback' => 'sanitize_textarea_field' ) ); register_setting( 'dekart_branch_group', 'branch_logistics_nearby', array( 'sanitize_callback' => 'sanitize_textarea_field' ) ); // Поля branch_map_url, branch_founded, branch_photos — удалены. @@ -1976,6 +1979,21 @@ function dekart_branch_settings_page() { +
+ + +

Через запятую. Для Schema.org LocalBusiness.areaServed

+
+
+ + +

Из Google Business Profile. Для Schema.org identifier + @id

+
+
+ + +

Полная ссылка на профиль в Google Maps. Для Schema.org hasMap

+
@@ -2169,6 +2187,11 @@ function dekart_branch_settings_page() {

🔗 E-E-A-T: Внешние платформы

Ссылки на внешние площадки с отзывами для повышения E-E-A-T. Отображаются под заголовком «Отзывы на внешних площадках».

+
+ + +

Полная ссылка на профиль в Google Business Profile. Добавляется в sameAs[] Schema.org и выводится как кнопка на странице.
Где взять: откройте Google Карты → найдите свою организацию → нажмите «Поделиться» → скопируйте ссылку. Пример: https://maps.google.com/?cid=1234567890 или https://g.page/r/your-business-slug

+
diff --git a/wp-content/themes/dekart/header.php b/wp-content/themes/dekart/header.php index fbd2e5c..dde80d8 100644 --- a/wp-content/themes/dekart/header.php +++ b/wp-content/themes/dekart/header.php @@ -140,7 +140,7 @@ if ( is_front_page() ) { Адрес - +
'ItemList', - '@id' => home_url( '/podgotovka-k-shkole/' ) . '#reviews', + '@id' => dekart_schema_page_url() . '#reviews', 'name' => 'Отзывы о подготовке к школе', 'description' => 'Отзывы родителей о курсе подготовки к школе в Детском центре «Декарт»', 'itemListElement' => array_map( function( $review, $index ) { @@ -206,26 +227,27 @@ function dekart_schema_output(): void { * @see https://schema.org/WebPage */ function dekart_schema_webpage(): ?array { - $url = home_url( '/podgotovka-k-shkole/' ); + $post_id = get_the_ID(); + $url = get_permalink( $post_id ) ?: dekart_schema_page_url(); // _yoast_wpseo_title — Yoast SEO заголовок - $yoast_title = get_post_meta( 2, '_yoast_wpseo_title', true ); + $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( 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 = 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 лет в Щёлково. Чтение, письмо, математика, логика.'; // has_post_thumbnail — изображение страницы (OG-совместимость) $image_url = ''; - if ( has_post_thumbnail( 2 ) ) { + 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( 2, 'pksh_level' ) ?: 'Подготовка к школе'; + $about_text = carbon_get_post_meta( get_the_ID(), 'pksh_level' ) ?: 'Подготовка к школе'; // branch_city — город для about и keywords $city = dekart_branch_option( 'branch_city', 'Щёлково' ); @@ -280,7 +302,7 @@ function dekart_schema_webpage(): ?array { } // hasPart — ссылка на FAQPage этой страницы - $faq_items = carbon_get_post_meta( 2, 'pksh_faq_items' ); + $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', @@ -318,7 +340,7 @@ function dekart_schema_website(): ?array { * BreadcrumbList — хлебные крошки для страницы. */ function dekart_schema_breadcrumb(): ?array { - $page_url = home_url( '/podgotovka-k-shkole/' ); + $page_url = dekart_schema_page_url(); $home_url = home_url( '/' ); return array( @@ -430,7 +452,7 @@ function dekart_resolve_logo(): string { } // 2. banner_img — CF поле страницы - $banner_img = carbon_get_post_meta( 2, 'banner_img' ); + $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']; @@ -451,7 +473,7 @@ function dekart_resolve_logo(): string { } // 4. post_thumbnail страницы - if ( has_post_thumbnail( 2 ) ) { + if ( has_post_thumbnail( get_the_ID() ) ) { $url = get_the_post_thumbnail_url( 2, 'full' ); if ( $url ) { return $url; @@ -471,13 +493,13 @@ function dekart_resolve_logo(): string { * @see https://schema.org/EducationalOrganization */ function dekart_schema_local_business(): ?array { - $page_url = home_url( '/podgotovka-k-shkole/' ); + $page_url = dekart_schema_page_url(); $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_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 — регион/область @@ -505,6 +527,12 @@ function dekart_schema_local_business(): ?array { $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' ); + // branch_area_served — города обслуживания (через запятую) + $area_served_raw = get_option( 'branch_area_served', 'Щёлково, Фрязино, Монино, Ивантеевка, Королёв' ); // branch_hours — часы работы: CF complex (schema_hours) → textarea (branch_hours) $hours_cf = carbon_get_theme_option( 'schema_hours' ); @@ -630,11 +658,28 @@ function dekart_schema_local_business(): ?array { $lb['priceRange'] = trim( $price_range ); } - // areaServed — город присутствия (условно) - $lb['areaServed'] = array( - '@type' => 'City', - 'name' => wp_strip_all_tags( $city ), - ); + // 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 ) ) { @@ -687,6 +732,21 @@ function dekart_schema_local_business(): ?array { } } } + // 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 if ( ! empty( $same_as ) && ! isset( $lb['sameAs'] ) ) { $lb['sameAs'] = $same_as; @@ -728,7 +788,7 @@ function dekart_schema_local_business(): ?array { } // employee — преподаватели (Person), если есть - $teachers = carbon_get_post_meta( 2, 'pksh_teachers' ); + $teachers = carbon_get_post_meta( get_the_ID(), 'pksh_teachers' ); if ( ! empty( $teachers ) && is_array( $teachers ) ) { $employee_ids = array(); foreach ( $teachers as $i => $t ) { @@ -755,32 +815,32 @@ function dekart_schema_local_business(): ?array { * @see https://schema.org/CourseInstance */ function dekart_schema_course(): ?array { - $page_url = home_url( '/podgotovka-k-shkole/' ); + $page_url = dekart_schema_page_url(); $home_url = home_url( '/' ); // banner_h1 — заголовок H1 (Course.name) - $name = carbon_get_post_meta( 2, 'banner_h1' ) ?: 'Подготовка к школе 5–7 лет в Щёлково'; + $name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе 5–7 лет в Щёлково'; // pksh_description — полное описание курса для микроразметки (textarea) // Если заполнено — приоритет над banner_subtitle - $pksh_description = carbon_get_post_meta( 2, 'pksh_description' ); + $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( 2, 'banner_subtitle' ) ?: 'Научим читать, считать и писать до школы. Бесплатное пробное занятие.' ); + $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( 2, 'pksh_level' ); + $level = carbon_get_post_meta( get_the_ID(), 'pksh_level' ); // pksh_age — возраст - $age = carbon_get_post_meta( 2, 'pksh_age' ); + $age = carbon_get_post_meta( get_the_ID(), 'pksh_age' ); // pksh_audience — целевая аудитория - $audience = carbon_get_post_meta( 2, 'pksh_audience' ); + $audience = carbon_get_post_meta( get_the_ID(), 'pksh_audience' ); // pksh_course_mode — формат обучения (очная/ONLINE/OFFLINE) - $course_mode = carbon_get_post_meta( 2, 'pksh_course_mode' ); + $course_mode = carbon_get_post_meta( get_the_ID(), 'pksh_course_mode' ); // pksh_skills — список навыков - $skills = carbon_get_post_meta( 2, 'pksh_skills' ); + $skills = carbon_get_post_meta( get_the_ID(), 'pksh_skills' ); // banner_img — изображение курса - $course_img = carbon_get_post_meta( 2, 'banner_img' ); + $course_img = carbon_get_post_meta( get_the_ID(), 'banner_img' ); // pksh_programs — программы (для timeRequired + CourseInstance) - $programs = carbon_get_post_meta( 2, 'pksh_programs' ); + $programs = carbon_get_post_meta( get_the_ID(), 'pksh_programs' ); $course = array( '@type' => 'Course', @@ -801,7 +861,7 @@ function dekart_schema_course(): ?array { ); // review — 1-2 отзыва прямо в Course (Google требует для звёзд в rich snippet) - $reviews_raw = carbon_get_post_meta( 2, 'pksh_reviews_list' ); + $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 ) { @@ -889,7 +949,7 @@ function dekart_schema_course(): ?array { $course['image'] = esc_url( $url ); } } - } elseif ( has_post_thumbnail( 2 ) ) { + } elseif ( has_post_thumbnail( get_the_ID() ) ) { // fallback: post_thumbnail страницы $url = get_the_post_thumbnail_url( 2, 'full' ); if ( $url ) { @@ -992,131 +1052,14 @@ function dekart_schema_course(): ?array { 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' ); + $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; @@ -1144,7 +1087,7 @@ function dekart_schema_rating(): ?array { 'worstRating' => '1', 'ratingCount' => (string) count( $ratings ), 'itemReviewed' => array( - '@id' => home_url( '/' ) . '#organization', + '@id' => $page_url . '#course', ), ); } @@ -1157,11 +1100,11 @@ function dekart_schema_rating(): ?array { * @see https://schema.org/Service */ function dekart_schema_service(): ?array { - $page_url = home_url( '/podgotovka-k-shkole/' ); + $page_url = dekart_schema_page_url(); $home_url = home_url( '/' ); - $name = carbon_get_post_meta( 2, 'banner_h1' ) ?: 'Подготовка к школе'; - $description = carbon_get_post_meta( 2, 'banner_subtitle' ) ?: ''; + $name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе'; + $description = carbon_get_post_meta( get_the_ID(), 'banner_subtitle' ) ?: ''; $service = array( '@type' => 'Service', @@ -1188,8 +1131,8 @@ function dekart_schema_service(): ?array { * Если вопросов нет — блок не выводится. */ function dekart_schema_faq(): ?array { - $page_url = home_url( '/podgotovka-k-shkole/' ); - $faq_items = carbon_get_post_meta( 2, 'pksh_faq_items' ); + $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; @@ -1234,9 +1177,9 @@ function dekart_schema_faq(): ?array { * @see https://schema.org/Review */ function dekart_schema_reviews(): array { - $page_url = home_url( '/podgotovka-k-shkole/' ); + $page_url = dekart_schema_page_url(); // pksh_reviews_list — complex-поле отзывов - $reviews = carbon_get_post_meta( 2, 'pksh_reviews_list' ); + $reviews = carbon_get_post_meta( get_the_ID(), 'pksh_reviews_list' ); if ( empty( $reviews ) || ! is_array( $reviews ) ) { return array(); @@ -1273,7 +1216,7 @@ function dekart_schema_reviews(): array { ), 'reviewBody' => wp_strip_all_tags( $body ), 'itemReviewed' => array( - '@id' => home_url( '/' ) . '#organization', + '@id' => $page_url . '#course', ), 'reviewRating' => array( '@type' => 'Rating', @@ -1304,11 +1247,11 @@ function dekart_schema_reviews(): array { * @see https://schema.org/Person */ function dekart_schema_teachers(): array { - $page_url = home_url( '/podgotovka-k-shkole/' ); + $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( 2, 'pksh_teachers' ); + $teachers = carbon_get_post_meta( get_the_ID(), 'pksh_teachers' ); if ( empty( $teachers ) || ! is_array( $teachers ) ) { return array(); @@ -1382,6 +1325,244 @@ function dekart_schema_teachers(): array { 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' ) ?: '', + ); +} + +/** + * QAPage — ключевые вопросы для AI citation readiness. + * Google retired FAQ rich results May 2026, но QAPage важен для AI Overviews/ChatGPT/Perplexity. + * + * @see https://schema.org/QAPage + */ +function dekart_schema_qapage(): ?array { + if ( ! is_page( 2 ) ) { + return null; + } + + $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; + } + + // Берём только 3-5 ключевых вопросов (самые частые) + $key_questions = array_slice( $faq_items, 0, 5 ); + + $main_entity = array(); + foreach ( $key_questions 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' => 'QAPage', + '@id' => $page_url . '#qapage', + 'mainEntity' => $main_entity, + ); +} + +/** + * 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; +} + /* ===================================================================== Вспомогательные функции ===================================================================== */