</jj marine>
Agent-built yacht dealership site: 46 boats, four languages, live in Dubai.

live
jjmarine.ae · replaced a legacy Wix site
46
boats · every one its own page and structured data
4
languages · English, Arabic (RTL), Russian, French
320
sitemap URLs
SPECIFICATIONS
| ROLE | TECHNICAL LEAD |
|---|---|
| YEAR | 2024 |
| TYPE | WEB |
| STATUS | LIVE |
| STACK | next.js · typescript · tailwind · supabase · next-intl · playwright · json-ld · multi-agent +2 more |
| LINKS | [live ↗] |
| CLIENT | JJ Marine · Dubai, UAE |
| SOURCE | handed off to client, NDA |


“JJ Marine sells and charters yachts in Dubai off a Wix site with 18 live URLs.”
js catalogue that generates a page and structured data for every boat in a Supabase table.ae across English, Arabic, Russian and French.The interesting part is not what the site contains but how it was built: an agent-coordination protocol, adversarial review agents, and two agents that collided badly enough to break the sprint numbering.
== WHAT IS THIS ==
A full digital rebuild for JJ Marine, a yacht and boat dealership in Dubai, replacing a Wix site with a Next.js catalogue that generates a page and structured data for every boat in a Supabase table. Live at jjmarine.ae across English, Arabic, Russian and French. The interesting part is not what the site contains but how it was built: an agent-coordination protocol, adversarial review agents, and two agents that collided badly enough to break the sprint numbering.
== </the problem> ==
JJ Marine sells and charters yachts in Dubai off a Wix site with 18 live URLs. The catalogue lived as PDF brochures, which meant the specifications buyers search for existed nowhere a crawler or an answer engine could read them, and the inventory could only be updated by hand, page by page. A dealership whose stock turns over cannot run on hand-edited pages. It also sells into a market where the buyer is as likely to read Arabic or Russian as English, and the old site was English only.
role & context
Technical Lead at the dealership since 2024 — an ongoing role, not a fixed-scope engagement, covering engineering, design, social media and marketing. The internal dashboards, the lead pipeline and the agent workflows behind the build are commercially confidential, so what is written up here is the shippable proof: the public site at jjmarine.ae, and the parts of the process that are mine to describe. No lead or traffic figures appear on this page, because the tables were purged of test and seeded data and are genuinely empty.
== </my approach> ==
Rebuild the site as a database-backed catalogue where the boat row is the source of truth, then treat search visibility as a first-class output of the data model rather than a marketing layer bolted on afterwards. Every boat renders its specifications as on-page HTML and emits Product plus Offer structured data from the same row. The migration off Wix was planned before any of it: all 18 live Wix URLs accounted for — 6 kept their slug, 7 got a 308, 4 became hard 410s — plus 7 redirects for the old brochure PDF deep-links, so nothing that had accrued authority landed on a soft 404. The build itself ran as agent work under a coordination protocol, with adversarial review agents as a scheduled phase rather than a nice-to-have.
== </the story> ==
The plan was 7 epics and about 50 micro-sprints. It finished at 111 sprint commits, numbered up to S165, across 22 epics. Epics 7 through 21 were all emergent, driven by feedback while the thing was already running: a matcher for buyers who do not know what they want, mobile touch behaviour, four languages, a launch runbook, a self-hosted analytics layer, an admin worklist. The gap between the plan and the log is not a planning failure so much as the actual shape of building in-house, where the people who own the business see the site every day and keep having opinions. It is worth stating plainly because a 50-sprint estimate is what I would have committed to.
What it replaced was a Wix site with 18 live URLs and a catalogue delivered as PDF brochures. The rebuild moved 46 boats (27 power, 12 sail, 3 pre-owned, 4 charter) into a Supabase table where every row auto-generates its own page plus JSON-LD, added 14 editorial guides, and grew the sitemap from 80 URLs to 320 as the four locales came online. Migration discipline was written before the launch, not after: 15 redirect rules plus 4 hard 410s, covering every one of the 18 old URLs that did not keep its slug.
Worth being precise about, because the phrase "AI" attaches itself to marine dealerships easily: the public site has no chatbot, no LLM in the shipped bundle and no AI dependency in its package.json. What runs internally is a separate matter and stays internal. Everything described on this page is the public site.
The true story is better. The site is built by agents, under a protocol. S156 wired the repo into an Aethereum coordination room with 13 agent surfaces and autopilot on, and AGENTS.md makes the loop mandatory: pull team context before touching cross-service code, share intent, declare a contract for every new interface, claim and release before a refactor, check the blast radius before changing a contract others depend on. Agent-to-agent briefs exist as committed artifacts. The whole of S158 is one agent writing a brief for another about the leads view, ending with a question back it refuses to build past. S162's commit body opens by answering it. Adversarial review agents are a phase in the build: S140 fixed three live search defects found in S138's own code, and S162's three headline bugs came out of adversarial review rather than from a user.
It also collided. MASTERPLAN records that sprint numbering is broken for 2026-08-05, because two agents worked the repo concurrently and S156, S157, S158, S159 and S160 each name two different commits. S162's commit body records two renames being swept into the other agent's commit, because git add -A from either working tree picks up whatever the other has staged. Nobody lost work, but the history is no longer readable by sprint number and has to be disambiguated by hash. That is a real cost of a genuinely new way of working, and it is in the repo rather than tidied away.
== </architecture> ==
Next.js 16.2.9 App Router on Turbopack, React 19.2.4, TypeScript strict, Tailwind CSS v4 with @theme tokens, Motion 12.42 under LazyMotion, next-intl 4.13, Supabase, Vercel. 24 page routes. The accent colour is a single token, so a rebrand is a one-line change rather than a search-and-replace across components.
The catalogue is the spine. Boats live in a Supabase table and every row generates its detail page and its Product plus Offer JSON-LD from the same source, alongside filter, sort, facet and full-text search over the collection. Specifications render as on-page HTML instead of living only inside a brochure PDF, which was the single biggest search and answer-engine win in the whole build, and 44 of the 46 boats still carry their brochure as a download. dynamicParams is false on /boats/[slug], so a slug that is not in the table returns a true 404 rather than a rendered empty shell; boats that do exist revalidate hourly through ISR. 41 of 46 have multi-image galleries.
Internationalisation runs across en, ar-AE, ru and fr with localePrefix 'as-needed' and full RTL for Arabic. S143 to S147 alone moved roughly 500 strings, on top of 4 metadata catalogs at 146 keys each, 138 boat translations and 42 translated guides. That is what turned an 80-URL English site into a 320-URL sitemap with an hreflang matrix and x-default across all four.
The discovery layer is deliberately wide: JSON-LD for AutoDealer, CollectionPage, Product with Offer, BreadcrumbList, WebSite, BlogPosting, Person, Service and FAQPage, an Atom feed, llms.txt and llms-full.txt, an AI-crawler group in robots.txt naming 12 bots (GPTBot, ClaudeBot, meta-externalagent and others), plus an IndexNow key file and ping script. Lead capture writes to an insert-only Supabase table behind RLS, with Turnstile and a honeypot live and Upstash rate limiting wired but fail-open until its keys are set in production. Analytics is self-hosted: a site_events table and a navigator.sendBeacon client, with no third-party analytics as a dependency (Vercel's own Analytics and Speed Insights tags stay mounted as a free cross-check, but nothing reads from them). A password-gated /admin, unlinked, disallowed in robots.txt and excluded from the sitemap, carries a leads worklist, lead volume over time, traffic, boats ranked by enquiries, the newest 50 leads and CSV export. A separate social pipeline renders 20 posts and 92 slides from live catalogue data through satori and resvg and pushes approved ones to Buffer as drafts.
== </key features> ==
Database-driven catalogue
46 boats (27 power, 12 sail, 3 pre-owned, 4 charter) in a Supabase table. Every row generates its own page, its on-page HTML specs and its Product plus Offer JSON-LD. Filter, sort, facets and full-text search across the collection.
Four locales including RTL Arabic
en, ar-AE, ru and fr through next-intl with localePrefix 'as-needed', full right-to-left layout for Arabic, an hreflang matrix with x-default, and 320 sitemap URLs where the English site had 80.
Clean migration off Wix
15 redirect rules plus 4 hard 410s, covering all 18 live Wix URLs — 6 kept their slug, 7 were redirected, 4 declared gone, plus 7 old brochure deep-links. Nothing that had accrued authority lands on a soft 404, and pages that are genuinely gone say so.
AI-discovery surface
llms.txt and llms-full.txt, an AI-crawler group in robots.txt naming 12 bots (GPTBot, ClaudeBot, meta-externalagent and others), nine JSON-LD types, an Atom feed, and an IndexNow key file with a ping script.
Self-hosted analytics and a gated admin
A site_events table and a sendBeacon client — first-party data the dashboard actually reads, with no vendor API as a dependency. A password-gated /admin, unlinked and excluded from robots and sitemap, holds the leads worklist, boats ranked by enquiries, traffic and CSV export.
Agent coordination protocol
The repo is wired into an Aethereum room with 13 agent surfaces. AGENTS.md makes the loop mandatory: team context before cross-service code, declared contracts for every new interface, claims before refactors, blast-radius checks before contract changes.
Adversarial review as a build phase
Review agents run against the build's own output. S140 fixed three live search defects found in S138's code, S143 to S147 closed a copy gap a two-agent translation review surfaced, and S162's three headline bugs were review findings, not user reports.
== </key decisions> ==
DECISION 01
Specifications ship as on-page HTML, not as a PDF download, because a brochure is invisible to both crawlers and answer engines. The cost is real: someone has to keep 46 rows of structured spec data accurate, where before the brochure was the single artifact and it updated itself when the manufacturer sent a new one. We kept the PDFs on 44 of 46 boats for buyers who want them, and accepted the maintenance burden on the table, because a dealership that cannot be found in a spec search is not being found at all.
DECISION 02
dynamicParams is false on the boat route, which means an unknown slug 404s instead of rendering. The tradeoff is that a newly added boat is not reachable until the next build or revalidation, rather than appearing the instant its row lands. I took that because a yacht catalogue accumulates dead slugs as stock sells, and a soft 200 on a boat that no longer exists is worse for both the buyer and the index than an honest 404. Existing boats revalidate hourly, which bounds the staleness.
DECISION 03
The build runs under a mandatory coordination protocol, and I am publishing what it cost. Two agents worked this repo concurrently on 2026-08-05 and the sprint numbering broke: S156 through S160 each name two different commits, and S162's body records two renames swept into the other agent's commit, because git add -A from either tree picks up the other's staged work. The protocol prevented interface collisions, which is what it is for. It did not prevent working-tree collisions, which nothing in the loop was watching. The honest conclusion is that agent coordination has to reach down to the index and the branch, not stop at the contract layer.
DECISION 04
Analytics is self-hosted rather than depending on a third-party product. A site_events table plus a sendBeacon client means the data is first-party and fully owned by the business in a jurisdiction where that matters, and nothing on the dashboard depends on a vendor's API. Vercel's own tags stay mounted as a free cross-check, but they are never a data source. The cost is that everything a hosted product gives away has to be built: retention, purge tooling, an environment guard, the dashboard itself. S160 caught the guard failing on localhost, where an undefined VERCEL_ENV short-circuited the check and let local browsing write into the live table. Eleven rows of my own dev traffic were sitting in production data.
DECISION 05
The leads worklist exists because lead notification is currently broken, and the banner on it says so. WhatsApp alerting was removed, the email sender no-ops, and the API key is unset in production, so a lead inserts and nobody is told. Rather than pretend, /admin became the inbox: a worklist pinned to the top of the page, a handled_at timestamp that is deliberately separate from the status vocabulary so that marking something handled cannot claim the customer was contacted, and a getWorklist() that takes no arguments at all, so no future date filter can quietly let an old unhandled lead age out of the queue. The banner deletes itself when email is wired. That is the current state, not a finished one.
== </what i learned> ==
Agent coordination that stops at the contract layer is not enough. Two agents declared their interfaces correctly and still collided in the working tree, because git add -A from either tree stages the other's work. The protocol has to cover the index and the branch, not just the shape of the API.
A plan of 50 sprints became 111 because the team could see the site the whole time. Continuous visibility is the right call and it is also what makes an estimate meaningless. I would now quote the first 7 epics and be explicit that everything after them is feedback-shaped.
The environment guard is the thing to test, not the feature. An undefined VERCEL_ENV short-circuited a boolean and let local development write straight into the production analytics table, and it was only caught by reading a dashboard number that could not possibly be real.
Writing 'no AI in this product' is more useful than claiming one. The shipped site has no LLM anywhere in it, and the genuinely novel work is on the build side and the machine-readable read side. Overstating the first would have buried the second.
Finish the nameserver move at the registrar — the A records are already swapped and the Vercel zone is staged — then wire Resend, which cannot verify its domain until DNS leaves Wix.
next.js · typescript · tailwind · supabase · next-intl · playwright · json-ld · multi-agent · seo · vercel
LIVE · jjmarine.ae