Global-2026-07-14

This commit is contained in:
Dekart Deploy Bot 2026-07-14 12:46:14 +00:00
parent d8f11ba4d1
commit caecf7da52
6 changed files with 379 additions and 425 deletions

3
.gitignore vendored
View File

@ -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

251
AGENTS.md
View File

@ -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.

View File

@ -32,7 +32,7 @@
<img width="19" height="32"
src="<?php echo esc_url( get_template_directory_uri() ); ?>/assets/images/icon-location.svg"
alt="" role="presentation" loading="lazy">
<span>г. <?php echo esc_html( carbon_get_theme_option('site_city') ); ?> <br class="mobile"> <?php echo esc_html( carbon_get_theme_option('site_address') ); ?></span>
<span>г. <?php echo esc_html( get_option('branch_city') ?: carbon_get_theme_option('site_city') ); ?> <br class="mobile"> <?php echo esc_html( get_option('branch_address') ?: carbon_get_theme_option('site_address') ); ?></span>
</div>
<a class="footer__link-icon"
href="tel:<?php echo preg_replace('/[^0-9+]/', '', carbon_get_theme_option('site_phone')); ?>">

View File

@ -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() {
<label>Почтовый индекс</label>
<input type="text" name="branch_postal_code" value="<?php echo esc_attr( get_option( 'branch_postal_code', '' ) ); ?>">
</div>
<div class="branch-field">
<label>Города обслуживания (areaServed)</label>
<input type="text" name="branch_area_served" value="<?php echo esc_attr( get_option( 'branch_area_served', 'Щёлково, Фрязино, Монино, Ивантеевка, Королёв' ) ); ?>">
<p class="desc">Через запятую. Для Schema.org LocalBusiness.areaServed</p>
</div>
<div class="branch-field">
<label>Google Place ID</label>
<input type="text" name="branch_google_place_id" value="<?php echo esc_attr( get_option( 'branch_google_place_id', '' ) ); ?>">
<p class="desc">Из Google Business Profile. Для Schema.org identifier + @id</p>
</div>
<div class="branch-field">
<label>Google Maps URL</label>
<input type="url" name="branch_google_maps_url" value="<?php echo esc_attr( get_option( 'branch_google_maps_url', '' ) ); ?>">
<p class="desc">Полная ссылка на профиль в Google Maps. Для Schema.org hasMap</p>
</div>
<div class="branch-field">
<label>Пешком от ориентира</label>
<input type="text" name="branch_logistics_walk" value="<?php echo esc_attr( get_option( 'branch_logistics_walk', '5 минут пешком от ТЦ «Глобус»' ) ); ?>">
@ -2169,6 +2187,11 @@ function dekart_branch_settings_page() {
<h2>🔗 E-E-A-T: Внешние платформы</h2>
<div class="branch-section-body">
<p class="desc">Ссылки на внешние площадки с отзывами для повышения E-E-A-T. Отображаются под заголовком «Отзывы на внешних площадках».</p>
<div class="branch-field">
<label>Google Business Profile (ссылка на профиль)</label>
<input type="url" name="branch_google_business_url" value="<?php echo esc_attr( get_option( 'branch_google_business_url', '' ) ); ?>" style="width:100%;max-width:500px">
<p class="desc">Полная ссылка на профиль в Google Business Profile. Добавляется в sameAs[] Schema.org и выводится как кнопка на странице. <br><strong>Где взять:</strong> откройте Google Карты найдите свою организацию нажмите «Поделиться» скопируйте ссылку. Пример: https://maps.google.com/?cid=1234567890 или https://g.page/r/your-business-slug</p>
</div>
<div class="branch-field">
<label>Яндекс Карты (ссылка на отзывы)</label>
<input type="url" name="branch_ext_yandex" value="<?php echo esc_attr( get_option( 'branch_ext_yandex', '' ) ); ?>" style="width:100%;max-width:500px">

View File

@ -140,7 +140,7 @@ if ( is_front_page() ) {
<img width="15" height="25"
src="<?php echo esc_url( get_template_directory_uri() ); ?>/assets/images/icon-location.svg"
alt="Адрес" loading="lazy">
<span><?php echo esc_html( carbon_get_theme_option('site_address') ); ?></span>
<span><?php echo esc_html( get_option('branch_address') ?: carbon_get_theme_option('site_address') ); ?></span>
</div>
<a target="_blank" rel="noopener noreferrer"
href="<?php echo esc_url( carbon_get_theme_option('social_header_wt') ); ?>"

View File

@ -20,24 +20,24 @@ defined( 'ABSPATH' ) || exit;
add_filter( 'wpseo_json_ld_output', '__return_false' );
/**
* Принудительно меняем og:type на 'website' для страницы Подготовки к школе (ID=2),
* Принудительно меняем 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( 2 ) || is_page_template( 'page-camp.php' ) ) {
if ( is_page_template( 'page-podgotovka-k-shkole.php' ) || is_page_template( 'page-camp.php' ) ) {
return 'website';
}
return $type;
}
/**
* Убираем article:* OG-теги для страницы Подготовки к школе (ID=2),
* Убираем 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( 2 ) ) {
if ( ! is_page_template( 'page-podgotovka-k-shkole.php' ) ) {
return $presenters;
}
$presenters = array_filter( $presenters, function( string $p ): bool {
@ -53,12 +53,21 @@ add_filter( 'wpseo_frontend_presenter_classes', function( array $presenters ): a
/**
* Главная функция: сборка @graph и вывод в wp_head.
* Срабатывает только на странице ID=2 (Подготовка к школе).
* Срабатывает только на странице с шаблоном 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( 2 ) ) {
if ( ! is_page_template( 'page-podgotovka-k-shkole.php' ) ) {
return;
}
@ -146,12 +155,24 @@ function dekart_schema_output(): void {
$graph[] = $faq;
}
// 11b. QAPage — ключевые вопросы для AI citation readiness
$qa = dekart_schema_qapage();
if ( $qa ) {
$graph[] = $qa;
}
// 11c. 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' => 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 ?: 'Подготовка к школе для детей 57 лет в Щёлково. Чтение, письмо, математика, логика.';
// 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' ) ?: 'Подготовка к школе 57 лет в Щёлково';
$name = carbon_get_post_meta( get_the_ID(), 'banner_h1' ) ?: 'Подготовка к школе 57 лет в Щёлково';
// 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;
}
/* =====================================================================
Вспомогательные функции
===================================================================== */