Цель проекта: превратить bestonline-tour.ru из «сайта с поисковым виджетом» в SEO-машину с 16 000 отдельных посадочных страниц отелей.
#tvtourid=.../destinations/maldives/alila-kothaifaru-maldives/meta/hotels.jsonКлючевая идея: разделяем «контент, за который отвечаем мы» и «контент, за который отвечает Tourvisor».
| Элемент | Кто делает | Почему |
|---|---|---|
| Title, Meta Description | Мы (шаблон) | SEO-уникальность, ключи, бренд |
| H1 | Мы | Главный заголовок — под поисковый запрос |
| Breadcrumbs + schema | Мы | Навигация + rich snippet в выдаче |
| Бокс «Главное» (факты) | Мы (из структурированных полей Tourvisor) | Быстрый ответ, AI-friendly, AggregateRating |
| FAQ (5-7 вопросов) | Мы (AI Claude + шаблон) | FAQPage schema, PAA-захват |
| Schema.org (Hotel, FAQ, Breadcrumb) | Мы | Звёзды в SERP, понимание контента Google |
| OG-теги, canonical, hreflang | Мы | Соцсети, каноникализация |
| Фото, описание, услуги, номера | Tourvisor виджет | Их контент, мы НЕ копируем |
| Актуальные цены, доступность | Tourvisor виджет | Обновление онлайн с их стороны |
| Форма бронирования | Tourvisor виджет | Их система, наша комиссия |
⚠ Правовая граница: факты из Tourvisor (год постройки, количество вилл, название атолла, расстояние до аэропорта) — можно использовать (факты не охраняются ГК РФ). Авторский текст описания (их формулировки) — копировать нельзя, это произведение. Мы парсим поля и собираем свой шаблонный текст.
meta/GET /hotels?countryId=4generate-wrappers.py → статические HTML с Title/H1/breadcrumbs/FAQ/Schema/subregions?countryId=8 → [])maldives/{hotel}/ когда регион = странеsame_as_country())/strany/ → /destinations/ (англ. стандарт)ISLAND_COUNTRIES, ISLAND_REGIONS)BestonlineTour2026@, логин: bestonline)| Слой | Решение | Статус |
|---|---|---|
| CMS | WordPress 6.x | ✅ установлен |
| Page builder | Bricks Builder | ✅ установлен (лицензия 14.04) |
| Custom fields | ACF Pro | 🟡 поставить |
| SEO | Rank Math Pro | 🟡 поставить |
| Cache | Redis + WP Rocket | 🟡 настроить |
| Hosting | Beget (PHP 8.2, Nginx) | ✅ |
| WP-CLI | для импорта 16k отелей | 🟡 настроить |
| Cloudflare | НЕ используем (РФ-аудитория) | — |
Все файлы в репозитории Bestonline-tour.ru/. Запрашивать через менеджера.
| Файл / ссылка | Что внутри | Применение |
|---|---|---|
meta/hotels.json |
7 250 отелей — базовые поля (id, name, category, rating, country_id, country_name, region_id, region_name, subregion, slug) | Источник для импорта в CPT + таксономии |
meta/details/*.json (42 файла + растёт) |
Детали отеля: сommon.build, сommon.place, infrastructure.beach, meals.list, images[]. ⚠ Ключ сommon с кириллической «с» — опечатка Tourvisor, учитывать |
Заполнение ACF-полей (год, атолл, пляж, питание) |
meta/countries.json, regions.json, subregions.json |
Справочники Tourvisor | Предзаполнение terms таксономий |
| Боевое демо страницы | Референс финального вида (HTML без dev-маркеров, с ТЗ-блоком под виджет) | Визуальный референс для Bricks template |
| Список 50 превью | Все 50 пилотных страниц с dev-маркерами H1/H2/FAQ | Посмотреть разнообразие контента по странам |
generate-wrappers.py |
Python-генератор HTML — референс логики для словарей COUNTRY_FACTS, REGION_FORMAT, SEASON_FAQ, VISA_FAQ, COUNTRY_PREP | Портировать словари в WP options или term meta |
hotel + rewrite URLНа WordPress отель как отдельная сущность не существует «из коробки» — есть только стандартные «Посты» и «Страницы». Для 7250 отелей нам нужен отдельный тип записи с собственной админкой, собственными полями и собственной URL-структурой.
hotel (плагин CPT UI или через functions.php в child-теме)/destinations/{country-slug}/{region-slug}/{hotel-slug}//destinations/maldives/{hotel-slug}/ (не повторяем maldives дважды)has_archive для архивных страниц (см. Задачу 8)register_post_type('hotel', [
'labels' => ['name' => 'Отели', 'singular_name' => 'Отель'],
'public' => true,
'has_archive' => 'destinations',
'show_in_rest' => true,
'menu_icon' => 'dashicons-building',
'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'],
'rewrite' => ['slug' => 'destinations/%country%/%region%', 'with_front' => false],
]);
// + add_rewrite_rule для обработки same-as-country case (Мальдивы)
// + фильтр post_type_link для подстановки страны/региона из таксономий
/destinations/turkey/alanya/test-hotel/ работает (пусть 404, главное rewrite матчится)/destinations/maldives/test-hotel/ (без дубля maldives)1-2 дня нет блокеров
Отели связаны между собой через страну/регион/звёзды. Без таксономий невозможно: (а) построить archive-страницы «все отели Турции», (б) перелинковать похожие отели, (в) сделать breadcrumbs, (г) фильтровать в админке. Это каркас связей всей базы.
| Таксономия | Slug | Иерархичность | Пример terms |
|---|---|---|---|
| Страна | country | нет | turkey, maldives, egypt |
| Регион | region | да (родитель = страна) | alanya (turkey), male (maldives) |
| Район/субрегион | subregion | да (родитель = регион) | патонг (phuket) |
| Звёзды | stars | нет | 1, 2, 3, 4, 5 |
| Тип отеля | hotel_type | нет | island-resort, city-hotel, beach-resort |
generate-wrappers.py → COUNTRY_SLUG_EN, REGION_SLUG_EN (портировать в WP)1 день нет блокеров
Заводить руками 10 стран, 150+ регионов и 500+ субрегионов — неэффективно и чревато ошибками. Всё уже есть в meta/hotels.json. Один скрипт извлекает уникальные тройки (страна → регион → субрегион) и создаёт terms с правильными slug и иерархией.
meta/hotels.json — 7 250 отелей с полями country_name, country_slug (ru-транслит), region_name, region_slug, subregion_name, subregion_slugCOUNTRY_SLUG_EN, REGION_SLUG_EN из generate-wrappers.py — для замены транслита на английские slugcountry — 10 terms (turkey, maldives, egypt, …)region — ~150 terms, каждый с parent = соответствующей странойsubregion — ~500 terms, каждый с parent = региону// wp eval-file import-terms.php
$hotels = json_decode(file_get_contents('meta/hotels.json'), true);
$country_slug_map = [
'Турция' => 'turkey', 'Мальдивы' => 'maldives',
// … из COUNTRY_SLUG_EN
];
foreach ($hotels as $h) {
// 1. Создать term country если нет
$country_slug = $country_slug_map[$h['country_name']] ?? $h['country_slug'];
$country = wp_insert_term($h['country_name'], 'country', ['slug' => $country_slug]);
// 2. Создать region с parent=country
// 3. Создать subregion с parent=region
}
1 день нет блокеров — зависит от Задачи 2
hotelСтандартные поля WP-поста (title, content, excerpt) не вмещают всю информацию об отеле: рейтинг 4.8/5, категория 5★, год постройки 2022, тип пляжа, список питания, ID для Tourvisor-виджета. ACF Pro — индустриальный стандарт для структурированных метаданных в WordPress. Bricks нативно читает ACF через dynamic tags.
| ACF поле | Тип | Источник JSON | Обязательно |
|---|---|---|---|
tv_id | Number | id | ✅ критично (для виджета) |
category | Number (1-5) | category | опционально (может быть 0) |
rating | Number (1-5, 0.1 шаг) | rating | опционально |
build_year | Text | сommon.build из details | опционально |
atoll | Text | regex из сommon.place (только Мальдивы) | опционально |
beach_raw | Textarea | infrastructure.beach (первый <LI>) | опционально |
meal_codes | Checkbox (BB/HB/FB/AI/UAI/RO) | regex из meals.list | опционально |
images_count | Number | len(images) | опционально |
hotel_format | Select | словарь REGION_FORMAT из Python | опционально |
place_raw | Textarea | сommon.place | опционально (для дебага) |
В Bricks-шаблоне факт-бокс показывает только заполненные поля. Если build_year пусто — строка «🔨 Открыт: 2022» не выводится. Это основа принципа «SEO-обёртка работает даже для отеля с минимальными данными».
2 дня нет блокеров
7 250 отелей (а финально 16 000) руками не заведёшь — это сотни часов работы. Скрипт читает наши JSON, создаёт посты CPT hotel, назначает таксономии, заполняет ACF. Должен быть идемпотентным — повторный запуск не создаёт дубликаты, а обновляет существующие.
meta/hotels.jsonmeta_query: tv_id = hotel.idhotel.slug (с обработкой коллизий: если slug уже есть в той же country+region — добавляем суффикс -{tv_id}, см. _raw_hotel_slug() в Python)meta/details/{id}.json — парсим и заполняем дополнительные поля (build, atoll, beach, meals)// Ключ 'сommon' содержит кириллическую «с» — опечатка API Tourvisor
$common_key = 'сommon'; // НЕ 'common'!
$common = $detail[$common_key] ?? [];
// Год постройки
preg_match('/\b(19|20)\d{2}\b/', $common['build'] ?? '', $m);
$build_year = $m[0] ?? '';
// Атолл (только Мальдивы)
if ($country === 'maldives' && !empty($common['place'])) {
preg_match('/атолл[е]?\s+([А-Яа-я\w\s]+?)(?:[,;]|$)/iu', $common['place'], $m);
$atoll = trim($m[1] ?? '');
}
// Типы питания (BB/HB/FB/AI/UAI/RO)
$meals_raw = strip_tags($detail['meals']['list'] ?? '');
$meal_codes = [];
foreach (['RO', 'BB', 'HB', 'FB', 'AI', 'UAI'] as $code) {
if (preg_match('/\b' . $code . '\b/', $meals_raw)) $meal_codes[] = $code;
}
wp eval-file import-hotels.php --url=bestonline-tour.ru
# или
wp hotel import meta/hotels.json --details=meta/details/
3-5 дней нет блокеров — зависит от Задач 1, 2, 3, 4
single-hotel (скелет без финального дизайна)Один шаблон рендерит все 7 250+ отелей. Динамические значения подтягиваются через Bricks dynamic tags: {acf:tv_id}, {post_title}, {post_terms:country}. Это фундамент. Финальный дизайн блоков (бокс «Главное», FAQ, sticky nav) — допиливаем на demo параллельно, подключим блоки к шаблону за 2-3 дня.
category), H1 = «{название} — отель {на/в} {регион/страна}»<div class="tv-embed" tv-hotels="{acf:tv_id}"></div> (точный класс ждём от Tourvisor)date('Y') — никогда не хардкодим 20263-5 дней частично: финальные блоки от нас (параллельно)
Google и Яндекс показывают rich snippets (звёзды рейтинга, FAQ-аккордеон, хлебные крошки в выдаче) только если на странице есть корректная Schema.org-разметка. Это даёт +30% CTR (звёзды в выдаче). Плюс ChatGPT/Google AI Overview цитируют структурированный контент охотнее.
<head><script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Hotel",
"@id": "{canonical_url}#hotel",
"name": "{post_title}",
"starRating": { "@type": "Rating", "ratingValue": "{acf:category}" },
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "{acf:rating}",
"bestRating": "5"
}
},
{
"@type": "BreadcrumbList",
"itemListElement": [...]
},
{
"@type": "FAQPage",
"mainEntity": [... FAQ из ACF ...]
}
]
}
</script>
Можно через Rank Math Pro (встроенный Schema generator для Hotel/FAQ) или руками в functions.php через фильтр wp_head. Rank Math проще, но менее гибкий. Руками — полный контроль.
1-2 дня нет блокеров
Страница «Все отели Турции» (/destinations/turkey/) — мощная SEO-посадочная по запросам «турция отели», «отели в турции 5 звёзд». И база перелинковки: с каждого отеля Турции — ссылка «все отели Турции».
archive-country → рендерится для URL /destinations/{country-slug}/hotel с этой таксономией country, сортировка по рейтингу DESCTemplate archive-region → /destinations/{country}/{region}/ — все отели конкретного региона.
2-3 дня частично: визуал карточек — от нас
<div class="tv-embed" tv-hotels="{acf:tv_id}"></div> как естьhotel зарегистрирован, доступен в админке/destinations/turkey/alanya/test-hotel/ и /destinations/maldives/test-hotel/ (без дубля)single-hotel применяется ко всем CPT hotel, рендерится без ошибок/destinations/turkey/ показывает все отели Турции с фильтрами + пагинацией