Car Dealership Website Development on 1C-Bitrix
A car dealership website is a catalog where a single item costs anywhere from twenty to two hundred thousand. The visitor doesn't add a car to a shopping cart — they browse, compare, calculate financing, and book a test drive. The average session on a dealer site runs 6-9 minutes, 3-4 filter adjustments, 2-3 vehicle cards opened. If the make-model filter is sluggish, photos load one by one, and the loan calculator requires a page reload — the visitor leaves for an aggregator where all of this works. The goal is to build an architecture that matches aggregator-level catalog performance while remaining manageable from the Bitrix admin panel.
Catalog Architecture: New vs. Used Vehicles
The first architectural decision — store new and used vehicles in one info block or separate ones. The answer depends on business logic, not technical constraints.
Single info block with sections by type suits a multi-brand dealer with up to 2,000 vehicles. New and used cars share 80%+ of their properties: make, model, year, engine, transmission, color, price, photos. Differences: new cars have trim levels and warranty terms; used cars have mileage, VIN, number of owners, service history. Empty properties don't create rows in b_iblock_element_property — the table doesn't bloat. Advantage: a single bitrix:catalog.smart_filter, unified search, one admin feed.
Two info blocks make sense when new vehicles are built from a configurator (make -> model -> trim -> color -> options) while used vehicles are concrete units with a VIN. The configurator has its own nesting logic (Highload blocks for trims, option packages) that used cars don't need. Mixing them clutters the admin. Downside: searching "all BMW X5" across both info blocks requires a custom component.
Recommended structure for a mid-size dealer (500-3,000 vehicles):
- Info block "Vehicles" — sections: New, Pre-Owned
- Highload block "Makes" — reference table of brands (BMW, Toyota, Kia...) with logos
-
Highload block "Models" — models linked to makes via
UF_BRAND_ID - Highload block "Generations" — model generations (body style, production years)
- Highload block "Trims" — for new cars: option sets plus pricing
-
Info block "Promotions" — special offers linked to vehicles via
PROPERTY_CAR_IDS
A vehicle record needs at least 40 fields. The ones critical for filtering and feed generation:
| Property | Code | Type | Indexing |
|---|---|---|---|
| Make | BRAND | S:Highload | Faceted index |
| Model | MODEL | S:Highload | Faceted index |
| Year | YEAR | N (number) | Faceted index |
| Price | PRICE | N | Faceted index |
| Mileage | MILEAGE | N | Faceted index |
| Engine type | ENGINE_TYPE | L (list: petrol, diesel, hybrid, electric) | Faceted index |
| Engine volume | ENGINE_VOLUME | N | Faceted index |
| Transmission | TRANSMISSION | L (manual, automatic, DCT, CVT) | Faceted index |
| Drivetrain | DRIVE | L (FWD, RWD, AWD) | Faceted index |
| Body type | BODY_TYPE | L (sedan, SUV, hatchback...) | Faceted index |
| Color | COLOR | S (string) | Faceted index |
| VIN | VIN | S | None (unique, exact-match lookups) |
| Status | STATUS | L (in stock, in transit, reserved) | Faceted index |
| Exterior photos | PHOTOS | F (file, multiple) | None |
| Interior photos | PHOTOS_INTERIOR | F (multiple) | None |
| 360 exterior | SPIN_360_URL | S | None |
| Interior panorama | INTERIOR_PANORAMA | S | None |
VIN is stored as a string property without a faceted index — it's searched by exact match, not range. VIN lookup goes through CIBlockElement::GetList with ['=PROPERTY_VIN' => $vin].
Dependent Filtering: Make -> Model Cascade
The built-in bitrix:catalog.smart_filter shows all values of all properties at once. The user selects BMW as the make — but the model dropdown still lists Camry, Ceed, Solaris. This breaks dealership UX. You need cascading filters: selecting a make recalculates the model list, selecting a model recalculates the generation list.
Approach 1: custom AJAX controller. A dedicated endpoint /api/catalog/filter-values/ that accepts current filter selections and returns valid values for the remaining properties.
// Cascading filter controller
class FilterValuesController extends \Bitrix\Main\Engine\Controller
{
public function getModelsAction(int $brandId): array
{
$models = [];
$res = \CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => CAR_IBLOCK_ID,
'ACTIVE' => 'Y',
'PROPERTY_BRAND' => $brandId,
],
['PROPERTY_MODEL' => 'CNT'], // GROUP BY MODEL
false,
['PROPERTY_MODEL']
);
while ($row = $res->Fetch()) {
$models[] = [
'id' => $row['PROPERTY_MODEL_VALUE'],
'count' => $row['CNT'],
];
}
return $models;
}
}
On the frontend — a change event on the make select triggers BX.ajax.runAction('controller.filterValues.getModels', {data: {brandId: val}}), which updates the model dropdown. For selects with 50+ options, use Choices.js or Tom Select instead of a native <select>.
Approach 2: preloaded dependency tree. On catalog page render, a JSON object of all dependencies is injected into JavaScript: {brand_1: [model_5, model_12, ...], brand_2: [...]}. With 30 makes and 200 models, this is 3-5 KB of JSON. Selecting a make filters models on the client with zero latency. But it doesn't reflect current stock — it shows a model even if there are zero vehicles of that model in inventory right now. Fix: include counts in the JSON {model_5: {name: "X5", count: 7}} and hide models where count: 0.
Approach 3: hybrid. Preload the make-model-generation tree on first visit (JSON), then AJAX-recalculate counts when any filter changes. This gives instant response on make selection (the model list appears without delay) and accurate counts (recalculated considering price, year, mileage filters).
Range sliders for price, mileage, and year follow the same pattern as any Bitrix catalog: bitrix:catalog.smart_filter exposes MIN_VALUE/MAX_VALUE, the frontend renders noUiSlider, values pass as GET parameters arrFilter_P1_MIN/arrFilter_P1_MAX. AJAX result loading via bitrix:catalog.section with AJAX_MODE=Y.
XML Feeds for Auto Aggregators: Three Formats, Three Headaches
Auto aggregators are the primary traffic source for a dealer. auto.ru, Avito Auto, av.by — each has its own XML schema, its own required fields, its own validation rules. A feed error means listings get pulled, and the dealer loses calls.
auto.ru uses a cars.xml format based on the Yandex standard:
<cars>
<car>
<mark_id>BMW</mark_id>
<folder_id>X5</folder_id>
<modification_id>xDrive30d</modification_id>
<body_type>ALLROAD_5_DOORS</body_type>
<year>2024</year>
<run>0</run>
<color>BLACK</color>
<transmission>AUTOMATIC</transmission>
<engine_type>DIESEL</engine_type>
<engine_volume>3.0</engine_volume>
<price>7890000</price>
<currency>RUR</currency>
<vin>WBAJC51090B123456</vin>
<unique_id>car_12345</unique_id>
<images>
<image>https://site.com/upload/cars/12345/photo1.jpg</image>
</images>
</car>
</cars>
Critical detail: mark_id and folder_id must match auto.ru's own reference data — not arbitrary strings but specific identifiers from their directory. If your Highload block stores the make as "Bayerische Motoren Werke" but auto.ru expects BMW, the feed gets rejected. You need a mapping layer — an additional field UF_AUTORU_CODE in the Makes and Models Highload blocks.
Avito Auto uses the Autoload format. Same car, different language: body type is in Russian ("Vnedorozhnik" instead of "ALLROAD_5_DOORS"), engine type is "Dizel" rather than "DIESEL", generation must include the restyling marker. Every aggregator speaks its own dialect. Attempting to pass identical strings to both feeds guarantees validation failure.
av.by (Belarus) has its own XML format with Russian-language field values, requires a city from av.by's reference directory, and mandates brand_id and model_id from their internal database.
Feed generator architecture:
-
Abstract class
BaseFeedGenerator— selects elements from the info block viaCIBlockElement::GetList, iterates, writes XML usingXMLWriter. -
Concrete classes:
AutoRuFeedGenerator,AvitoAutoFeedGenerator,AvByFeedGenerator— override property-to-XML-tag mapping and value transformation (automatic -> AUTOMATIC for auto.ru, automatic -> "Avtomaticheskaya" for Avito). -
Mapping table — Highload block
FeedMappingwith fieldsUF_PROPERTY_CODE,UF_FEED_TYPE,UF_LOCAL_VALUE,UF_FEED_VALUE. A manager can add mappings without a developer: "All-wheel drive" ->ALL_WHEEL_DRIVE(auto.ru), "Polnyy" (Avito). -
Bitrix agent (
CAgent) runs every 30-60 minutes, generates files at/upload/feeds/auto_ru.xml,/upload/feeds/avito_auto.xml,/upload/feeds/av_by.xml. With 1,000 vehicles — generation takes 5-15 seconds, XML is ~2-4 MB. -
Pre-publish validation. After generation — validate against the XSD schema (auto.ru publishes theirs). If the feed is invalid — send a notification to the manager, don't overwrite the previous working feed.
Vehicle Comparison
Comparison is a side-by-side specs table for two to four vehicles. The user adds cars via a "Compare" button from the catalog or the vehicle card. IDs are stored in localStorage (unauthenticated users) or a Highload block UserCompare (authenticated). Maximum four vehicles — beyond that, the table loses readability.
The comparison component: CIBlockElement::GetList on an array of IDs, load all properties, render a table. Properties with identical values across all cars are dimmed; differences get a highlight color. Horizontal scroll on mobile.
Trade-In Calculator
A form to estimate the value of the customer's current vehicle. Inputs: make, model, year, mileage, condition (excellent / good / fair). Client-side JavaScript formula: fetch the base price from a reference table (Highload block TradeInPrices with UF_BRAND, UF_MODEL, UF_YEAR, UF_BASE_PRICE), apply coefficients for mileage and condition. The result is an approximate value — a precise appraisal requires an on-site inspection.
The loan calculator uses the annuity formula in JavaScript: three sliders (vehicle price, down payment, term), output is the monthly payment. Rates are pulled from a "Partner Banks" Highload block. For leasing — a separate formula that accounts for the residual value.
1C Integration and Inventory Sync
A dealer site with stale inventory is a useless storefront. A customer calls about a listing, but the car was sold yesterday. Synchronization options:
CommerceML — Bitrix's built-in exchange mechanism with 1C. The sale module supports catalog import via /bitrix/admin/1c_exchange.php. The problem: CommerceML was designed for SKU-based products, not vehicles with 40 properties. VIN, mileage, number of owners — there are no standard fields for these. Solution: mapping via the OnBeforeCatalogImport1C event or a supplementary handler that parses <PropertyValue> nodes from the XML and populates info block properties.
1C REST API — more flexible. An HTTP service on the 1C side returns JSON with the current vehicle list. On the Bitrix side — an agent or webhook that polls every 15-30 minutes for updates: creates new vehicles, deactivates sold ones, updates changed records. Reconciliation by VIN as the unique key.
360 View and Visual Content
360 exterior view — a set of 36-72 photos taken around the vehicle. On the frontend — a JavaScript library (SpriteSpin, 360-image-viewer, or a custom canvas script) that animates frame switching on mouse drag. Photos are stored in /upload/cars/{ID}/360/, with the directory URL in the SPIN_360_URL property.
Interior panorama — a spherical photo rendered through Pannellum.js or a Google Street View-style viewer. Stored as a URL in INTERIOR_PANORAMA.
Test Drive and Service Booking
Test drive form: vehicle selection (pre-filled if the user came from a vehicle card), date, time, name, phone. Data is submitted via bitrix:form.result.new or Bitrix24 REST API (crm.lead.add with SOURCE_ID = "TEST_DRIVE", custom field UF_CRM_CAR_ID). The manager gets a CRM notification, the customer receives an SMS confirmation via the messageservice module.
Service appointment — a similar form with work type selection (scheduled maintenance, tire service, body repair) and preferred date. Data flows into a Bitrix24 deal in the "Service" pipeline.
SEO and Schema.org for Vehicles
Every vehicle card is a landing page for queries like "buy BMW X5 2024 diesel." SEO templates via info block settings:
- Title:
Buy #PROPERTY_BRAND# #PROPERTY_MODEL# #PROPERTY_YEAR# — #PROPERTY_CITY# | Dealer - Description:
#PROPERTY_BRAND# #PROPERTY_MODEL# #PROPERTY_YEAR#, #PROPERTY_ENGINE_TYPE#, #PROPERTY_TRANSMISSION#, #PROPERTY_MILEAGE# km
Schema.org Vehicle + Offer markup:
{
"@context": "https://schema.org",
"@type": "Vehicle",
"name": "BMW X5 xDrive30d",
"brand": {"@type": "Brand", "name": "BMW"},
"model": "X5",
"vehicleModelDate": "2024",
"mileageFromOdometer": {
"@type": "QuantitativeValue",
"value": "0",
"unitCode": "KMT"
},
"fuelType": "Diesel",
"vehicleTransmission": "Automatic",
"color": "Black",
"vehicleIdentificationNumber": "WBAJC51090B123456",
"image": ["https://site.com/upload/cars/12345/photo1.jpg"],
"offers": {
"@type": "Offer",
"price": "7890000",
"priceCurrency": "RUB",
"availability": "https://schema.org/InStock",
"seller": {
"@type": "AutoDealer",
"name": "Dealer Name"
}
}
}
Google recognizes Vehicle and can render a rich result with a photo, price, and mileage.
Stages and Timelines
- Analysis, prototyping (1-2 weeks) — catalog structure, make-model tree, Figma prototypes
- Design (2-3 weeks) — catalog, vehicle card, filters, mobile layouts
- Core development (3-5 weeks) — info blocks, Highload references, cascading filter, vehicle card
- Integrations (2-4 weeks) — XML feeds, 1C sync, CRM, calculators
- Visual content (1-2 weeks) — 360 view, panoramas, gallery
- Testing, SEO (1-2 weeks) — feed validation, Schema.org, load testing
- Launch (3-5 days) — deployment, catalog import, feed monitoring
| Scale | Timeline |
|---|---|
| Single-brand dealer, up to 200 vehicles, basic filter | 6-8 weeks |
| Multi-brand dealer, 500-2,000 vehicles, feeds + 1C | 10-14 weeks |
| Dealer network, 3,000+ vehicles, multisite, CRM | 14-24 weeks |
Timelines exclude vehicle photography and 360 content production — those are parallel processes that start during the development phase.







