All work WEB PLATFORM / SEO live

20  /  WEB PLATFORM / SEO

VLT — Vending Lab Technologies

Years of search visibility rested on 388 URLs in three languages, and my rebuild had to move every one intact.

July 2023 – August 2026Next.js 15React 19TypeScriptPayload CMS 3PostgreSQLnext-intlBitrix24 RESTTechnical SEO

The challenge

VLT sells robotic cafés out of Dubai. From 2023 I handled the website and both its on-page and off-page SEO, and years of search visibility settled on the WordPress site's addresses. Replacing that site meant 388 live URLs across English, French and Arabic had to survive the move intact. The lead path was quietly broken as well: the handler posted email and phone to a Bitrix24 endpoint that has no such fields, so every deal reached the sales team carrying a name and nothing else.

What I built

I rebuilt the site on Next.js 15 with Payload 3 on Postgres, deployed to a VPS behind Cloudflare. I crawled the old site into a written specification first, then rebuilt against it in 21 phases: 32 hand-built page routes, 38 event pages, 40 articles migrated into Payload, and three ROI calculators with regression tests. I rewrote the lead pipeline end to end. The site also publishes a machine-readable surface: llms.txt, per-bot robots directives, `Accept: text/markdown` negotiation and five agent skills under /.well-known/agent-skills/.

How it works

ON THE SITE IN BITRIX24 Click lands with gclid captured on every page load Kept in sessionStorage an old click is not reused Contact created first email, phone, city, country Deal linked to it CONTACT_ID, UTMs as fields If the Contact step fails, the Deal is still created a lead is never lost Why Contact first: Bitrix Deals have no email or phone fields crm.deal.add accepted EMAIL and PHONE and silently discarded them, so every deal reached the sales team with a name and nothing else
The mechanism, drawn from the build.

What it looks like

VLT — Vending Lab Technologies — screenshot
Running locallyWEB PLATFORM / SEO

The detail

01

Spec before code

The crawl produced 122 page specs per locale, an image map, and a URL map of the 388 live addresses across English, French and Arabic. My verify-urls harness walks all 388 against a running server and reports any that fail to route; the final run passed 388 of 388. A known-slug allowlist keeps genuine 404s returning 404.

02

The CRM bug that looked like a form bug

Every deal in Bitrix24 arrived with a name and nothing else. The cause was not the form. Bitrix Deals have no email or phone fields, so `crm.deal.add` accepted EMAIL and PHONE and silently discarded them, and I had reproduced the old handler faithfully enough to inherit the behaviour and carry it past cutover. My fix creates a Contact first, then a Deal linked by CONTACT_ID with every detail mirrored into the comment; if the contact step fails the deal is still created, so a lead is never lost.

03

Attribution that survives the walk to the form

The lead form is a popup that can open from any page. A Google Ads visitor lands on /?gclid=…, reads for a while, and by submit time the query string is long gone, so reading the URL at submit captures nothing for exactly the traffic worth attributing. I now capture on every page load and persist to sessionStorage rather than localStorage, so an organic return visit weeks later is not credited to an old click; I write UTMs as real Bitrix fields, gbraid and wbraid stand in for gclid on iOS traffic, and I also write the click id into the deal comment as a fallback.

04

Pasted articles, contained

Editors wanted to paste a complete, self-styled HTML article and have it render as designed, but letting its `<style>` through would have allowed a rule like `body{background:#06070d}` to repaint the whole site. So I hand-rolled a CSS walker that rewrites every selector to sit under one wrapper class, folds :root, html and body onto that wrapper, recurses into @media and @supports, and resolves bare filenames in src, srcset and url() to the Payload media library. It runs server-side, so no iframe and no Shadow DOM: the article stays in the real document and stays indexable.

05

The LCP that was never recorded

After launch the rebuilt homepage had no mobile PageSpeed score at all, because Chrome recorded no LCP candidate. Three separate causes, all in my new code: a first-visit loading veil occluded the hero, a page-transition transform made the wrapper a containing block for the fixed background blobs and shifted them (the captured report measured 0.95 CLS), and a scroll-snap carousel finalised LCP with zero candidates. I then moved analytics off the measured trace by injecting GTM, GA and the Pixel on first interaction instead of on load, and code-split the popup bundle out of the initial hydration pass; the project's own record for the fix reports a mobile score of 97 with LCP 0.6s, checked on pagespeed.web.dev, with tuning continuing against later runs the same day.

The outcome

The site is live at vendinglab.tech in English, French and Arabic, with content editing in Payload. Leads now arrive in Bitrix24 as a Contact and a linked Deal carrying email, phone, city, country and UTM campaign fields, with the Google Ads click id written to the deal comment and to a configurable custom field. I built a repair script to enrich the historic name-only deals in place from the records Payload had kept, matching contacts by email, idempotent and dry-run by default. The earlier SEO engagement I report on my CV as top-five rankings for terms including robotic cafe, coffee robot and robot rental in Dubai; those rankings are self-reported.