Розробка сайту готелю на 1С-Бітрікс
Готельний сайт відрізняється від звичайного каталогу однією річчю: відвідувач обирає не товар, а часовий слот. Номер сам по собі — набір характеристик: площа, місткість, вид з вікна. Але номер без вільних дат — мертва картка. Увесь проєкт будується навколо календаря доступності та механіки бронювання, а не навколо гарної верстки. Якщо двигун бронювання працює — решта вирішується шаблонами та контентом. Якщо ні — жодні панорами й відгуки не врятують конверсію.
Типи номерів: інфоблок та його властивості
Кожен тип номера — елемент інфоблоку «Номерний фонд». Тип «Стандарт двомісний» може об'єднувати 20 фізичних кімнат. Розділення на типи та фізичні одиниці — ключове архітектурне рішення.
Структура інфоблоку:
| Властивість | Тип | Призначення |
|---|---|---|
| CAPACITY | N (число) | Місткість (основні місця) |
| CAPACITY_EXTRA | N | Додаткові місця (розкладачка, дитяче ліжко) |
| AREA | N | Площа, м² |
| AMENITIES | L (список, множинне) | Зручності: Wi-Fi, кондиціонер, міні-бар, сейф |
| BED_TYPE | L (список) | Тип ліжка: double, twin, king |
| VIEW | L (список) | Вид: море, місто, сад, подвір'я |
| FLOOR_RANGE | S (рядок) | Поверхи: «3-5» |
| GALLERY | F (файл, множинне) | Фотогалерея |
| PANORAMA_URL | S | Посилання на 360-панораму |
| ROOM_COUNT | N | Кількість фізичних номерів цього типу |
| MIN_STAY | N | Мінімальна кількість ночей |
| BASE_RATE | N | Базовий тариф за ніч |
Зручності (AMENITIES) — множинна властивість типу «Список», а не Highload-блок, бо набір зручностей фіксований (30-50 позицій) і не потребує окремого управління. Кожне значення (WIFI, AC, MINIBAR, SAFE, BALCONY, BATHTUB) мапиться на іконку через конфіг на фронті.
Фотогалерея та віртуальний тур. Множинна властивість типу «Файл» для галереї. Рендер — Swiper.js з lazy-завантаженням, прев'ю через CFile::ResizeImageGet() 600x400 з BX_RESIZE_IMAGE_PROPORTIONAL. Для 360-панорами — Pannellum.js: бібліотека приймає equirectangular-зображення і рендерить інтерактивний огляд у <div>. URL панорами зберігається в рядковій властивості, що вказує на /upload/panoramas/. Hotspots (мітки цікавих точок усередині панорами) можна зберігати в JSON-властивості або окремому Highload-блоці, якщо персонал готелю потребує адмінку для їхнього редагування.
Двигун бронювання та управління доступністю
Це ядро проєкту. Задача: гість обирає дати заїзду та виїзду, система показує доступні типи номерів з цінами, гість бронює, номер блокується.
Зберігання доступності — Highload-блок RoomInventory. Кожен рядок — одна ніч для одного фізичного номера.
| Поле | Тип | Опис |
|---|---|---|
| UF_DATE | date | Дата ночі (2025-07-15 = ніч із 15 на 16 липня) |
| UF_ROOM_ID | integer | ID фізичного номера |
| UF_ROOM_TYPE_ID | integer | ID типу номера (елемент інфоблоку) |
| UF_STATUS | integer | 0 = вільний, 1 = заброньований, 2 = заблокований, 3 = заселений |
| UF_BOOKING_ID | integer | ID замовлення (модуль sale) |
| UF_RATE | float | Тариф за цю ніч (з урахуванням сезону) |
Чому Highload-блок? Це ORM-обгортка над звичайною таблицею з автогенерацією API. HighloadBlockTable::compileEntity() створює клас із методами getList, add, update, delete. При 100 номерах і горизонті 365 днів — 36 500 рядків. При 500 — 182 500. Highload-блок впорається, але індекси обов'язкові: складений індекс на (UF_DATE, UF_ROOM_TYPE_ID, UF_STATUS) для запитів доступності та (UF_BOOKING_ID) для зв'язку із замовленням.
Алгоритм перевірки доступності. Гість вводить дати заїзду й виїзду та кількість гостей. Система має повернути типи номерів, у яких є щонайменше один фізичний номер, вільний на всі ночі діапазону.
Наївний запит WHERE UF_STATUS = 0 AND UF_DATE IN (...) поверне номери, вільні хоча б в одну ніч — це не те саме. Правильний підхід — група з перевіркою повноти:
SELECT UF_ROOM_ID, UF_ROOM_TYPE_ID
FROM hl_room_inventory
WHERE UF_DATE IN ('2025-07-15','2025-07-16','2025-07-17')
AND UF_STATUS = 0
GROUP BY UF_ROOM_ID, UF_ROOM_TYPE_ID
HAVING COUNT(*) = 3
Цей запит повертає фізичні номери, вільні на всі три ночі. Далі групуємо за UF_ROOM_TYPE_ID і отримуємо типи з кількістю доступних одиниць.
Альтернатива — виключення: знайти номери, зайняті хоча б в одну ніч, і відфільтрувати їх. Обидва підходи працюють порівнянно на такому масштабі даних.
Календар доступності на фронті. Два поля: дата заїзду та виїзду. Реалізація — flatpickr у режимі range або кастомний React-компонент на базі react-day-picker. При відкритті календаря — AJAX-запит за матрицею доступності: масив дат із прапорцем «є вільні номери / немає» та мінімальним тарифом. Endpoint повертає JSON:
{
"2025-07": {
"15": {"available": true, "min_rate": 2200},
"16": {"available": true, "min_rate": 2200},
"17": {"available": false, "min_rate": null},
"18": {"available": true, "min_rate": 3100}
}
}
Недоступні дати блокуються в календарі. Мінімальний тариф показується при наведенні. Запит матриці — важкий: потрібно агрегувати весь RoomInventory за місяць. Кешування обов'язкове: Bitrix\Main\Data\Cache з ключем availability_{month}_{year}, інвалідація при будь-якій зміні в RoomInventory через обробник OnAfterUpdate.
Сезонне ціноутворення. Тариф за ніч залежить від сезону, дня тижня, рівня завантаженості. Зберігання — Highload-блок RatePlan:
| Поле | Тип |
|---|---|
| UF_ROOM_TYPE_ID | integer |
| UF_DATE_FROM | date |
| UF_DATE_TO | date |
| UF_WEEKDAY_RATE | float |
| UF_WEEKEND_RATE | float |
| UF_PRIORITY | integer |
Під час розрахунку вартості бронювання система перебирає кожну ніч, знаходить відповідний RatePlan (за діапазоном дат і типом номера, найвищий пріоритет виграє), визначає день тижня і бере UF_WEEKDAY_RATE або UF_WEEKEND_RATE. Загальна сума — сума тарифів за всі ночі. Бітрікс-агент заздалегідь заповнює поле UF_RATE у RoomInventory — це кеш розрахованої ціни.
Перегони при бронюванні (race condition). Двоє гостей одночасно бачать вільний номер. Обидва натискають «Забронювати». Без захисту — овербукінг. Рішення: транзакційне блокування на момент підтвердження. Перед створенням замовлення код виконує SELECT ... FOR UPDATE на відповідних рядках RoomInventory, перевіряє статус, виставляє UF_STATUS = 1, потім фіксує транзакцію. Якщо статус вже змінено іншою транзакцією — бронювання відхиляється з повідомленням «номер вже зайнятий». Все це має відбуватися в межах однієї транзакції: $connection->startTransaction() / $connection->commitTransaction().
Інтеграція з Channel Manager
Готель продає номери не лише на власному сайті, а й через Booking.com, Expedia, Ostrovok. Без синхронізації — овербукінг. Channel manager — проміжний шар, що синхронізує доступність і тарифи.
iCal-синхронізація — найпростіший варіант. Booking.com та Airbnb обмінюються .ics-файлами із заблокованими датами. Бітрікс-агент кожні 15 хвилин:
- Забирає
.icsза URL кожного каналу - Парсить
VEVENT-блоки — витягуєDTSTART,DTEND,SUMMARY - Оновлює
RoomInventory: ставитьUF_STATUS = 2для відповідних дат і номерів - Генерує вихідний
.icsз бронюваннями сайту →/upload/ical/room_{id}.ics
Обмеження iCal: немає передачі тарифів, немає підтвердження бронювання, затримка синхронізації до 15 хвилин. Для невеликого готелю (до 30 номерів) — прийнятно. Для 100+ — потрібен API-конектор.
API-інтеграція з OTA — Booking.com Connectivity API працює через XML-повідомлення OTA (OpenTravel Alliance): OTA_HotelAvailNotifRQ для оновлення доступності, OTA_HotelRatePlanNotifRQ для тарифів, OTA_HotelResNotifRS для отримання бронювань. Це enterprise-рівень, потребує сертифікації підключення.
Інтеграція з PMS
PMS (Property Management System) — система управління готелем: заселення, виселення, housekeeping, облік. Поширені: 1С:Готель, Fidelio, Opera PMS.
1С:Готель — обмін через COM-об'єкт або HTTP-сервіс на боці 1С. Бітрікс надсилає дані бронювання (гість, дати, номер, сума), 1С створює документ «Бронь». Зворотна синхронізація: 1С повідомляє сайт про зміну статусу (заселений, виселений, скасований) через webhook на endpoint Бітрікса.
Opera / Fidelio — SOAP-інтерфейс. WSDL-описання, виклик методів CreateReservation, ModifyReservation, CancelReservation. Автентифікація — WS-Security. Реалізація з боку Бітрікса — клас-обгортка над SoapClient з логуванням запитів.
Онлайн-оплата та передоплата
Бронювання через модуль sale. Замовлення = одна товарна позиція «Проживання в {тип номера}, {check_in} — {check_out}». Ціна — сума тарифів за ночі. Властивості замовлення: PROPERTY_CHECK_IN, PROPERTY_CHECK_OUT, PROPERTY_ROOM_TYPE_ID, PROPERTY_GUESTS.
Передоплата — зазвичай 20-30% або вартість першої ночі. Реалізація: кастомний обробник OnSaleBeforeOrderAdd, що перераховує суму до сплати. Повна вартість зберігається у PROPERTY_TOTAL_AMOUNT, до сплати — у полі PRICE кошика. Решта — при заселенні.
Платіжні шлюзи: модуль sale підтримує LiqPay, Portmone, Stripe. Налаштування через адмінпанель, без коду.
Особистий кабінет гостя
Авторизація — email/пароль, OAuth через Google, Apple. Після входу:
-
Мої бронювання — список замовлень з
sale, фільтр заUSER_ID. Статуси: очікує оплати, сплачено, підтверджено, заселений, завершено, скасовано - Історія поїздок — завершені бронювання з можливістю залишити відгук
-
Програма лояльності — бали за бронювання. Highload-блок
LoyaltyPoints:USER_ID,POINTS,OPERATION(нарахування/списання),BOOKING_ID,DATE. Бали нараховуються обробникомOnSaleStatusOrderChangeпри переході у статус «Завершено»
Мультимовність
Для міжнародних гостей — мінімум 2-3 мови. Мультимовність Бітрікса через мовні версії сайту (/en/, /de/, /uk/). Контент інфоблоків — через окремі властивості (NAME_EN, DESCRIPTION_EN) або через модуль мультисайтовості з прив'язкою інфоблоків до сайтів.
hreflang у <head>:
<link rel="alternate" hreflang="uk" href="https://hotel.ua/rooms/standard/" />
<link rel="alternate" hreflang="en" href="https://hotel.ua/en/rooms/standard/" />
Перемикач валют — не конвертація в реальному часі, а фіксовані курси в налаштуваннях модуля currency. Курси оновлюються агентом раз на добу через API НБУ або вручну.
SEO та мікророзмітка
Schema.org — тип Hotel + LodgingBusiness:
{
"@context": "https://schema.org",
"@type": "Hotel",
"name": "Grand Hotel Riviera",
"address": {
"@type": "PostalAddress",
"streetAddress": "вул. Набережна, 15",
"addressLocality": "Одеса"
},
"starRating": {
"@type": "Rating",
"ratingValue": "4"
},
"amenityFeature": [
{"@type": "LocationFeatureSpecification", "name": "Басейн", "value": true},
{"@type": "LocationFeatureSpecification", "name": "Паркування", "value": true}
]
}
Для кожного типу номера — розмітка HotelRoom з occupancy, bed, amenityFeature. Для пропозицій — Offer з priceSpecification та availabilityStarts.
Фотоцентричний дизайн
Готельний сайт — це 60% фотографії. Вимоги: WebP з fallback на JPEG, <picture> з srcset для retina, lazy-load через loading="lazy". Оригінали до 3000px по довгій стороні, прев'ю — 800x600 для карток, 400x300 для списків. Модуль iblock генерує ресайзи через CFile::ResizeImageGet(), але WebP-конвертація потребує серверної підтримки (GD або Imagick з WebP) та кастомного обробника.
Відгуки
Внутрішня система — Highload-блок Reviews: USER_ID, ROOM_TYPE_ID, RATING (1-5), TEXT, DATE, STATUS (на модерації / опублікований). Публікація після ручної модерації або автоматично через 24 години. Агрегований рейтинг перераховується при додаванні нового відгуку, зберігається у властивості інфоблоку AVG_RATING.
Інтеграція зовнішніх відгуків (TripAdvisor, Google Reviews) — через віджети або API, без зберігання в Бітріксі.
Етапи та терміни
| Масштаб | Терміни |
|---|---|
| Міні-готель, 10-20 номерів, базове бронювання | 4-8 тижнів |
| Готель, 50-100 номерів, Channel Manager, PMS | 10-16 тижнів |
| Мережа готелів, мультисайт, програма лояльності | 16-24 тижні |
- Аналітика та прототипування (1-2 тижні) — карта номерного фонду, логіка бронювання, прототипи
- Дизайн (2-3 тижні) — фотоцентричний UI, мобільна версія, компоненти календаря
- Ядро бронювання (3-5 тижнів) — RoomInventory, перевірка доступності, оформлення замовлення, оплата
- Інтеграції (2-4 тижні) — PMS, Channel Manager, платіжні шлюзи
- Контент та SEO (1-2 тижні) — мікророзмітка, мультимовність, мета-шаблони
- Тестування та запуск (1-2 тижні) — навантажувальне, кросбраузерне, деплой
Терміни не включають фотозйомку номерів та створення 360-панорам — це паралельний процес, який краще починати на етапі дизайну.







