The Challenge
Solmarine is a licensed used camping car dealer in Okcheon, North Chungcheong. They buy, sell, take vehicles on consignment and service them. They had been doing it well for years, and they had no storefront of their own on the web.
That works for people who already know the shop, and not at all for a stranger deciding whether to spend 100 million won. Someone typing "used camping car" into Naver or Google never arrived, because there was nothing of theirs for a search engine to find. Years of the dealer's own writeups explaining what to check on a used motorhome were just as unreachable.
Then there were the questions. A three person shop fields the same ones every day: what is the mileage, is the price negotiable, has it passed inspection, can I come and look at it on Saturday. They arrive at nine in the evening, and increasingly they arrive in languages nobody at the shop reads.
The stock records turned out to be the hardest part. Prices had been kept as free text, so the field held whatever had been typed into it. Two vehicles came through priced at 10,500 won and 17,000 won. Their real prices were 105 million and 170 million. A storefront that published those numbers would have been worse than no storefront at all.
Our Approach
We treated this as one product with two halves that had to agree with each other:
- A storefront a search engine can read, so a buyer arrives without joining anything
- Prices that are traceable, because a wrong price on a public page is a liability, not a typo
- A chatbot grounded in the dealer's own words, not in what a language model believes about camping cars
- A handoff that happens early, because the sale is a phone call or a viewing, and a bot that tries to close it will lose it
The success condition we wrote down at the start was not time on site or form submissions. It was a phone call or a booked viewing.
The Solution
Prices we could defend
Every price was verified against the dealer's own writeups, 34 of the 38, and each listing records where its price was confirmed. Where a price could not be confirmed, the listing shows "가격 문의" rather than a guess. A wrong number is traceable to its source instead of being a mystery.
Nine languages, with Korean at the root
The site publishes in Korean, English, Japanese, Chinese, Vietnamese, Russian, Spanish, Nepali and Thai. Korean sits unprefixed at the root because that is what Naver indexes.
That combination is harder than it sounds on a static site. There is no middleware to rewrite paths, so the Korean pages had to be real files at the URLs they link to, not a rewrite rule that someone drops in a year. Browser language detection is a small script injected into the export after the build, and it is deliberately timid: it only redirects from the Korean root, only when the visitor has made no choice, and only on an exact language match. A browser asking for Brazilian Portuguese gets Korean, because the wrong language is worse than the default one.
Being findable, which was the whole point
The brief underneath every other decision was search. The dealer had nothing a search engine could reach, so they were invisible to the exact person who was already looking for what they sell.
Every one of the 4,803 pages is static HTML on disk. A crawler gets the full content of a listing without executing any JavaScript, which is the difference between being indexed properly and being indexed as an empty shell.
Korean sits unprefixed at the root because that is the shape Naver indexes, and Naver is the search engine that matters for a Korean used vehicle buyer. Every route then declares hreflang alternates for all nine languages plus an x-default, so a Japanese buyer searching in Japanese lands on the Japanese page for that specific vehicle rather than on a Korean homepage.
Titles and descriptions get their own build gate. An untranslated message key had reached production once as a page title, rendering as "meta.vehicles.title | 쏠마린캠핑카", which is the sort of thing nobody notices until it is sitting in a search result. There is now a check that fails the build if a translation key ever renders as visible text.
The rest is speed, because a slow page is a page that ranks lower and gets abandoned before it loads. Pre-built images, fonts loaded per locale rather than shipping megabytes of CJK glyphs to a Spanish reader, and no runtime rendering between the crawler and the file.
24,549 images, and no image server
Every photograph is pre-built into AVIF and WebP at three widths. A 2.9 MB original becomes a 133 KB AVIF. That is 24,549 files generated from 4,091 source photographs, and it means there is no runtime image service anywhere in the system, which is what keeps the whole site static.
The finished export is 53,385 files. That is more than Cloudflare Pages accepts, which is a limit you discover at deploy time rather than in the documentation, so the site is served from an R2 bucket behind a Worker instead. The images live in their own bucket on their own subdomain.
A chatbot that answers from the dealer's writing
The chat sits on every page. Behind it is retrieval over the shop's own material: the curated facts sheet, the business registration, the notices, all 38 listings, the 35 published articles, and the dealer's own written answers to questions they had already been asked. Where the shop never answered there was nothing to learn, so it was left out.
Three decisions in there are worth naming.
Embeddings run locally, on a multilingual model baked into the container image. Questions arrive in nine languages and the corpus is entirely Korean, so the model has to be cross-lingual, and running it locally means no per-query cost and no cold start that depends on someone else's network being up.
There is no vector database. The index is 1,168 passages, which is 1.7 MB of vectors, so search is one matrix multiply against a file. Adding a database here would have been infrastructure without a reason.
Similarity alone is not enough, so passages are weighted by how authoritative they are. The curated facts sheet outranks a listing, which outranks a note written three years ago. An old notice states an old opening time in the present tense, and a language model has no way of knowing it was superseded.
The part that is not retrieval
"Which caravan is the most expensive" is not a passage to find. It is a comparison across the whole stock.
So the inventory is not embedded at all. All 38 vehicles are serialised into a compact table that goes into every prompt, and the superlatives and budget filters are computed in Python and handed to the model as settled fact. This was not theoretical. Asked to reason over the table in prose, the model named a 16 million won vehicle as the cheapest while listing the 12 million won one in the same sentence, filed five over budget vehicles under a "under this price" heading, and counted three vehicles as two.
Prices are printed in both Korean and numeric form for the same reason: a Japanese answer once turned 170 million won into 170 million yen.
Knowing when to stop
This is the part that matters most, and the honest description of it is that the bot is built to get out of the way.
It hands the conversation to a person on contracts, financing approval, final price negotiation on a specific vehicle, booking a viewing, and anything the retrieved material does not cover. There is no confidence score involved; those are named categories, because a threshold is a number you tune forever and a rule is something the dealer can read and agree with.
Two more triggers sit underneath, and neither trusts the model. If the answer contains any price figure that does not appear verbatim in the stock list, the answer is thrown away and the conversation escalates. A price the bot invents is a price the dealer has to honour. And if the model is unavailable or returns something unparseable, that escalates too. The bot structurally cannot dead end.
When it hands over, it does not say "that is not in my material," because to a customer that sounds like the fact does not exist. It says it cannot confirm this and is passing you to someone who can. The visitor is shown a phone number at the same moment, because waiting in a chat panel should be a choice rather than a trap. Staff pick the conversation up in a dashboard, and once a person replies the bot is muted in that thread for good.
One conversation, two languages
Language is chosen by what the visitor typed, not by which page they are on, and the most recent message wins, so switching mid conversation just works.
Then the transcript is bilingual on both sides. Every message is stored in its original language and translated on demand for whoever is reading. A Korean staff member reads a Vietnamese question in Korean; the customer reads the Korean reply in Vietnamese; either can tap to see the original, because a translation of a price or a date is worth checking against what was actually written.
Even the phone number is locale aware. Korean answers get the landline. Every other language gets the mobile in international format, because a landline is not dialable from abroad without knowing to drop the leading zero, and a visitor asking in Thai is exactly the person who will not know that.
Results
- 4,803 pages live in nine languages, every one static HTML with hreflang alternates, indexable and reachable without an account
- 38 vehicles with prices traced to a source, and an explicit "ask us" state instead of a guess
- 973 photographs served as 24,549 pre-built AVIF and WebP files with no image server
- A chatbot answering in nine languages from 1,168 passages of the shop's own material, with no vector database and no per-query embedding cost
- Escalation to a human on rules the dealer can read, plus a price guard that discards any figure not in the stock list
- Conversations retained for 180 days, enforced in code on every deploy rather than promised on a page
Beyond the Numbers
The interesting constraint on this project was never the model. It was deciding what a bot is not allowed to say.
A dealer's reputation is built on quoting the same price twice. Most of the engineering here went into making sure the chatbot cannot damage that: it cannot invent a price, it cannot quote a customer's private post back at a stranger, it cannot pretend a fact does not exist when it simply cannot reach it, and it cannot keep talking once a human has stepped in.
What the shop got is not an assistant that closes sales. It is a night shift that answers the same forty questions in nine languages, and knows exactly which ones belong to a person.
And it stays ours to look after. We do not hand over a repository and leave. The site, the photographs and the chatbot's knowledge are maintained on an ongoing basis, which is why a newly arrived vehicle reaches all nine languages and the chatbot's material without anyone at the shop having to learn a deploy process. A dealer should be selling camping cars, not running a build.