Dual-brand bilingual sites for an EV campervan rental startup — and a guardrail that stops the LLM inventing addresses.
Two brands for the same EV campervan rental business — a German-language travel journal and an English-language US site — built and deployed from one monorepo to two separate Cloudflare Pages projects via GitHub Actions.
Static site generation with Vite 6, runtime language switching rather than duplicated route trees, hreflang pairing across the 40 informational pages, lead capture through Google Apps Script, and Core Web Vitals tuned for the slowest realistic connection.
Both sites carry Klausy, an AI concierge running on a Cloudflare Worker.
Klausy's job is to answer questions about routes and vehicles. What it kept doing instead was inventing a pickup address — a specific street, a suite number, a named airport terminal — because that is exactly the shape of text that follows “where do I pick up the van?” in a language model's training data.
This is a legal problem, not a cosmetic one. Under the rental terms, pickup is near the airport and never at the terminal, and the exact meeting point is agreed after booking. A fabricated address is a promise the business cannot keep.
The first fix was a stronger system prompt. That reduced the rate and did not reach zero, which is the whole lesson: a prompt is advisory, and advisory is not a guarantee. The guarantee had to live in code, on the response path, where it cannot be argued with.
Pulled straight from the production codebase — unedited except for trimming.
console.error is intentional: it makes the trip rate visible in Workers observability instead of silent. Exported by name purely so it can be unit-tested without a live model call.const PICKUP_RED_FLAGS = [
// Street address: "4255 Spring Mountain Rd", "1 Main Street"
/\b\d{1,5}\s+(?:[A-Z][\w.'-]*\s+){1,4}(?:Rd|Road|St|Street|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Ln|Lane|Way|Pkwy|Parkway)\b/,
// Suite / unit numbers
/\b(?:Suite|Ste\.?|Unit)\s*#?\s*\d+/i,
// US state + ZIP: "Las Vegas, NV 89102"
/\b(?:NV|CA|AZ|UT)\s*\d{5}\b/,
// A NAMED airport ("McCarran International Airport", "Las Vegas Airport").
// Lowercase "near the airport" is correct and deliberately NOT matched.
/\b(?:[A-Z][\w.'-]+\s+){1,3}(?:International\s+)?Airport\b/,
// Airport codes, incl. the renamed-since-2021 McCarran
/\b(?:LAS|LAX|SAN)\b/,
/\bMcCarran\b/i,
];
// PICKUP_SAFE_REPLY holds the approved DE/EN fallback copy (omitted here).
export function sanitizePickupClaims(reply, lang) {
const hit = PICKUP_RED_FLAGS.find((re) => re.test(reply));
if (!hit) return reply;
// Logged so Workers observability shows how often the model needs catching.
console.error('Pickup guard tripped:', hit.toString(), '| reply:', reply.slice(0, 200));
return PICKUP_SAFE_REPLY[lang] || PICKUP_SAFE_REPLY.de;
}
From the running application.

The repository is private, but I'm glad to walk through the codebase live — architecture, tradeoffs, the parts that went wrong first.