Hotel Website Development on 1C-Bitrix
A hotel website is not a standard catalog. The visitor isn't choosing a product — they're selecting a time slot. A room by itself is just a set of specs: square meters, bed configuration, view from the window. A room without available dates is a dead card. The entire project revolves around the availability calendar and booking engine, not around layout polish. If the booking engine works, everything else is templates and content. If it doesn't, no amount of panoramic tours or guest reviews will save the conversion rate.
Room Types: Info Block Structure
Each room type is an element in the "Room Inventory" info block. Important distinction: a room type is not a physical room. "Standard Double" might represent 20 physical rooms. Separating types from physical units is the foundational architectural decision.
Info block properties for room types:
| Property | Type | Purpose |
|---|---|---|
| CAPACITY | N (number) | Base occupancy |
| CAPACITY_EXTRA | N | Extra beds (rollaway, crib) |
| AREA | N | Square meters |
| AMENITIES | L (list, multiple) | Wi-Fi, AC, minibar, safe, balcony |
| BED_TYPE | L (list) | Double, twin, king |
| VIEW | L (list) | Sea, city, garden, courtyard |
| FLOOR_RANGE | S (string) | Floors: "3-5" |
| GALLERY | F (file, multiple) | Photo gallery |
| PANORAMA_URL | S | 360 panorama link |
| ROOM_COUNT | N | Physical rooms of this type |
| MIN_STAY | N | Minimum nights |
| BASE_RATE | N | Base nightly rate (before seasonal adjustments) |
Amenities are a multiple list property — not a Highload block — because the set is fixed (30-50 items) and doesn't need standalone management. Each list value (WIFI, AC, MINIBAR, SAFE, BALCONY, BATHTUB) maps to a frontend icon via a config file.
Photo gallery and virtual tour. Multiple file property for the gallery, rendered with Swiper.js and lazy loading. Thumbnails via CFile::ResizeImageGet() at 600x400 with BX_RESIZE_IMAGE_PROPORTIONAL. For 360 panoramas — Pannellum.js accepts an equirectangular image and renders an interactive viewer inside a <div>. The panorama URL is stored in a string property pointing to /upload/panoramas/. Hotspots (labeled points of interest within the panorama) can be stored in a JSON property or a dedicated Highload block if the hotel staff need to edit them through the admin panel.
Booking Engine and Availability Management
This is the core. Everything else is decoration. The task: a guest selects check-in and check-out dates, the system returns available room types with prices, the guest books, and the room is locked.
Storing availability — Highload block RoomInventory. Each row represents one night for one physical room.
| Field | Type | Description |
|---|---|---|
| UF_DATE | date | Night date (2025-07-15 = night of July 15-16) |
| UF_ROOM_ID | integer | Physical room ID (not the type) |
| UF_ROOM_TYPE_ID | integer | Room type ID (info block element) |
| UF_STATUS | integer | 0 = available, 1 = booked, 2 = blocked, 3 = checked-in |
| UF_BOOKING_ID | integer | Order ID (from the sale module) |
| UF_RATE | float | Rate for this night (season-adjusted) |
Why a Highload block instead of a raw table? Highload blocks in Bitrix are an ORM wrapper over a regular table with auto-generated API. HighloadBlockTable::compileEntity() produces a class with getList, add, update, delete methods. For 100 rooms over a 365-day horizon — 36,500 rows. For 500 rooms — 182,500 rows. Highload blocks handle this, but indexes are mandatory: a composite index on (UF_DATE, UF_ROOM_TYPE_ID, UF_STATUS) for availability queries and (UF_BOOKING_ID) for order lookups.
The availability check algorithm. The guest provides: check_in, check_out, guests. The system must return room types that have at least one physical room free on every night of the range.
The naive query WHERE UF_STATUS = 0 AND UF_DATE IN (...) returns rooms free on at least one night — not the same thing. The correct approach: find rooms that are free on all requested nights using GROUP BY with HAVING COUNT(*) = {nights}.
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
This returns physical rooms available on all three nights. Group by UF_ROOM_TYPE_ID to get types with available unit counts.
The alternative — exclusion-based: find rooms busy on any night in the range, then select everything not in that set. Both approaches perform comparably at this data scale.
Frontend availability calendar. Two fields: check-in and check-out. Implementation — flatpickr in range mode or a custom React component built on react-day-picker. On calendar open, an AJAX request fetches the availability matrix: an array of dates with availability flags and minimum rates.
{
"2025-07": {
"15": {"available": true, "min_rate": 85},
"16": {"available": true, "min_rate": 85},
"17": {"available": false, "min_rate": null},
"18": {"available": true, "min_rate": 120}
}
}
Unavailable dates are disabled in the picker. The minimum rate displays on hover. This matrix query is heavy — it aggregates the entire RoomInventory for a month. Caching is mandatory: Bitrix\Main\Data\Cache keyed by availability_{month}_{year}, invalidated on any RoomInventory change via an OnAfterUpdate handler.
Seasonal rate management. The nightly rate depends on season, day of week, and occupancy levels. Storage — Highload block RatePlan:
| Field | Type |
|---|---|
| UF_ROOM_TYPE_ID | integer |
| UF_DATE_FROM | date |
| UF_DATE_TO | date |
| UF_WEEKDAY_RATE | float |
| UF_WEEKEND_RATE | float |
| UF_PRIORITY | integer |
When calculating a booking total, the system iterates each night, finds the matching RatePlan (by date range and room type, highest priority wins), checks the day of week, and picks UF_WEEKDAY_RATE or UF_WEEKEND_RATE. The total is the sum across all nights. A Bitrix agent pre-fills UF_RATE in RoomInventory for each row — this is a calculated cache so the availability endpoint doesn't need to compute rates on every request.
Race conditions during booking. Two guests searching at the same time see the same room as available. Both click "Book." Without protection, both bookings go through — overbooking. Solution: a transactional lock at the moment of booking confirmation. Before creating the order, the code runs SELECT ... FOR UPDATE on the relevant RoomInventory rows, checks status, sets UF_STATUS = 1, then commits. If the status was already changed by another transaction, the booking is rejected with a "room no longer available" message. This must happen inside a single database transaction — $connection->startTransaction() / $connection->commitTransaction().
Channel Manager Integration
A hotel doesn't sell rooms only on its own site. Booking.com, Expedia, Ostrovok — all need synchronized availability. Without sync, overbooking is inevitable. The channel manager is the middleware layer.
iCal synchronization — the simplest approach. Booking.com and Airbnb both consume and produce .ics files with blocked dates. A Bitrix agent runs every 15 minutes:
- Fetches
.icsfrom each channel's URL (cURL orfile_get_contents) - Parses
VEVENTblocks — extractsDTSTART,DTEND,SUMMARY - Updates
RoomInventory: setsUF_STATUS = 2(blocked) for the corresponding dates and rooms - Generates an outbound
.icswith the site's bookings, writes to/upload/ical/room_{id}.ics
iCal limitations: no rate transmission, no booking confirmation, sync delay up to 15 minutes. Acceptable for a small property (under 30 rooms). For 100+ rooms with high occupancy — you need an API connector.
OTA API integration — Booking.com Connectivity API uses OTA (OpenTravel Alliance) XML messages: OTA_HotelAvailNotifRQ for availability updates, OTA_HotelRatePlanNotifRQ for rates, OTA_HotelResNotifRS for receiving bookings. This is enterprise-grade and requires connectivity certification.
PMS Integration
PMS (Property Management System) handles check-in, check-out, housekeeping, accounting. Common systems: 1C:Hotel, Fidelio, Opera PMS.
1C:Hotel — exchange via COM object or an HTTP service on the 1C side. Bitrix sends booking data (guest, dates, room, amount), 1C creates a reservation document. Reverse sync: 1C notifies the website of status changes (checked-in, checked-out, cancelled) via webhook to a Bitrix endpoint.
Opera / Fidelio — SOAP interface. WSDL descriptor, method calls: CreateReservation, ModifyReservation, CancelReservation. Authentication via WS-Security. Bitrix-side implementation — a wrapper class over SoapClient with request/response logging.
Online Payment and Deposits
Booking goes through the sale module. An order equals one line item: "Stay in {room type}, {check_in} — {check_out}". Price — sum of nightly rates. Order properties: PROPERTY_CHECK_IN, PROPERTY_CHECK_OUT, PROPERTY_ROOM_TYPE_ID, PROPERTY_GUESTS.
Deposit payment — typically 20-30% or the first night's rate, not the full amount. Implementation: a custom handler on OnSaleBeforeOrderAdd that recalculates the payable amount. Full amount stored in PROPERTY_TOTAL_AMOUNT, payable amount in the cart's PRICE field. Remainder collected at check-in.
Payment gateways: the sale module supports YooKassa, CloudPayments, Stripe out of the box. Configuration through the admin panel, no code required.
Guest Personal Account
Authentication — email/password, OAuth via Google, Apple. After login:
-
My Bookings — order list from
salefiltered byUSER_ID. Statuses: awaiting payment, paid, confirmed, checked-in, completed, cancelled - Stay History — completed bookings with the option to leave a review
-
Loyalty Program — points earned per booking. Highload block
LoyaltyPoints:USER_ID,POINTS,OPERATION(credit/debit),BOOKING_ID,DATE. Points credited viaOnSaleStatusOrderChangeevent handler when order transitions to "Completed"
Multilingual Support
For international guests — at minimum 2-3 languages. Bitrix handles multilingual via site language versions (/en/, /de/). Info block content — through dedicated properties (NAME_EN, DESCRIPTION_EN) or through the multi-site module with info blocks bound to site IDs.
hreflang tags in <head>:
<link rel="alternate" hreflang="ru" href="https://hotel.com/rooms/standard/" />
<link rel="alternate" hreflang="en" href="https://hotel.com/en/rooms/standard/" />
Currency switcher — not real-time conversion but fixed rates in the currency module settings. Rates updated daily by a Bitrix agent pulling from a central bank API or set manually.
Schema.org Markup
Type Hotel combined with LodgingBusiness:
{
"@context": "https://schema.org",
"@type": "Hotel",
"name": "Grand Hotel Riviera",
"address": {
"@type": "PostalAddress",
"streetAddress": "15 Waterfront St",
"addressLocality": "Sochi"
},
"starRating": {
"@type": "Rating",
"ratingValue": "4"
},
"amenityFeature": [
{"@type": "LocationFeatureSpecification", "name": "Pool", "value": true},
{"@type": "LocationFeatureSpecification", "name": "Parking", "value": true}
]
}
Each room type gets HotelRoom markup with occupancy, bed, and amenityFeature. Offers use Offer with priceSpecification and availabilityStarts.
Photo-Heavy Design
A hotel site is 60% photography. Loading requirements: WebP with JPEG fallback, <picture> elements with srcset for retina displays, lazy loading via loading="lazy". Originals up to 3000px on the long side, thumbnails — 800x600 for cards, 400x300 for lists. The iblock module auto-generates resizes through CFile::ResizeImageGet(), but WebP conversion requires server-side support (GD or Imagick with WebP) and a custom handler.
Reviews
Internal review system — Highload block Reviews: USER_ID, ROOM_TYPE_ID, RATING (1-5), TEXT, DATE, STATUS (pending/published). Publication after manual moderation or auto-publish after 24 hours. Aggregate rating recalculated on new review submission, stored in the info block property AVG_RATING.
External review integration (TripAdvisor, Google Reviews) — via widgets or API, not stored in Bitrix.
Stages and Timelines
| Scale | Timeline |
|---|---|
| Boutique hotel, 10-20 rooms, basic booking | 4-8 weeks |
| Mid-size hotel, 50-100 rooms, Channel Manager, PMS | 10-16 weeks |
| Hotel chain, multi-site, loyalty program | 16-24 weeks |
- Analysis and prototyping (1-2 weeks) — room inventory map, booking logic, wireframes
- Design (2-3 weeks) — photo-centric UI, mobile version, calendar components
- Booking engine core (3-5 weeks) — RoomInventory, availability checks, order flow, payment
- Integrations (2-4 weeks) — PMS, Channel Manager, payment gateways
- Content and SEO (1-2 weeks) — Schema.org markup, multilingual setup, meta templates
- Testing and launch (1-2 weeks) — load testing, cross-browser, deployment
Timelines exclude professional room photography and 360 panorama creation — those are parallel processes best started during the design phase.







