gora.
Aperwind — Thailand property catalogue

Business logic

The market, the buyer, the path through the site and the rules by which the catalogue decides what to show.

~5 min read · 1027 words

TL;DR

Aperwind is a public catalogue of Thai new-build property for foreign buyers. 839 catalogue rows in the database, 657 listings on the storefront, 370 projects, four regions: Phuket, Bangkok, Samui, Pattaya. Two locales, Russian and English, each page at its own URL.

The data does not come from our admin panel. It comes from the agency's Google sheet, maintained by its managers. That sheet changes daily and without warning: listing codes get renamed, rows get reordered, project names get retyped with misspellings, prices get edited. Hence the logic of the project — it answers not "how do we display an object" but "how do we avoid lying to the visitor when the source is different today".

What it is and how it works

The market and the buyer

Someone buying a Thai new build is almost always in another country. They cannot come and look, they choose by photographs and numbers, and they have no way to verify either. The niche is built so that verification is exactly what is needed.

The price is quoted by a manager, not by the site. A typical catalogue writes "price on request" or shows last year's figure. Here prices come from developer price lists and are checked against the supplier's sheet every day; a listing without a price stays on the storefront as "Price on request" and is excluded from minimums, medians and yield calculations.

Most photographs are renders. Of 195 storefront covers only 39 were real photography. The file itself tells you nothing — EXIF is stripped from all 564 files in folders containing the word "render", and the filename lies in both directions. The frames were reviewed by eye, and the decision on each one lives in git with its justification.

A real photograph can still show the wrong building. One project of 276 apartments had as its cover an honest photograph of a single-storey villa from its own folder. Two developments by the same developer shared one file byte for byte. There is a separate data check for duplicate covers, comparing by md5.

Completion dates and payment plans outlive the construction. A completed building must not show a future handover date or a "payment during construction" plan; a frozen one must not promise delivery. The project status from the sheet controls which blocks the page renders at all.

The supplier's sheet contains service rows and third-party contacts. The managers' comment column is never exposed: it holds phone numbers and messenger handles of outside agents. Only numbers are taken from it — contract term, deposit, prepayment.

The visitor's path

Step 1. Arrival

Search traffic does not land on the home page; it lands on an area, a project or a listing. So every section has its own URL of the form /{lang}/buy/{city}/{district}/{type}/{slug}, and any of the six levels opens on its own. The language is chosen from the Accept-Language header including q weights: a header saying "poor English, good Russian" used to send a Russian visitor to the English version.

Step 2. Selection

The catalogue filters by price, bedrooms and property type and sorts by price, floor area or "recommended". The filter is a plain GET form: it works the same without JavaScript, and empty fields are stripped from the URL by a redirect so that one result set does not acquire a second address. Prices are computed in baht; switching to dollars, euro or roubles happens on the client using rates refreshed by a nightly cron.

Step 3. Verification

A listing page shows the gallery, floor plans, a video tour, the payment schedule, a purchase-cost calculator and neighbouring offers in the same project. A project page collects every listing in that development, an area guide covers the market around it. There are 100 guides; their texts live in git and are edited through a pipeline that verifies numbers — a text promising "from 4.2M" must not silently outlive a change in the catalogue.

Step 4. Enquiry

The form writes the enquiry to the database and sends a Telegram notification. The enquiry is saved even for a visitor who opted out of analytics: a lead is a business record, it does not depend on statistics.

What is unusual

Our own listing number. The supplier's code is not an identifier: five "project + code" pairs repeat and nine rows carry a placeholder. So each listing is issued its own permanent number once (882 issued so far), and numbers are never reused. Both the page URL and the last-modified date in the sitemap hang off it.

Removing from the storefront is not deleting. A removed card disappears from lists, counters, selections and guides, but its page stays alive, indexable and present in the sitemap. Otherwise every movement in the supplier's sheet would hit positions already earned in search.

Three numbers instead of one. 839 rows in the database, 657 on the storefront, 600 in the sitemap. The difference is withdrawn resale and rental stock, service rows, and thin pages with neither photographs nor description, which have no business being in a sitemap.

Manual decisions survive the sync. The daily transfer rebuilds the listings, so manual decisions — removal, card order, recommended projects — live in separate tables keyed by "project slug + code", and filters such as the withdrawn resale stock live in the application code.

Geography and language

Eight locales are described in the code, two are switched on: Russian and English. Russian is the agency's core audience; English covers the rest of the world and is the x-default in hreflang. A new language is one line in a list: the header menu, hreflang, og:locale and the sitemap are built from that single source.

Geography is taken from the sheet owner's reference list and never invented. Region and district are different levels, a district is not guessed from a project's name, and distance to the beach is shown in metres with the beach named. For Bangkok the sea is not shown at all: nine source rows there say "40 km from the See", a known exception rather than data.