DEV Community

AI Marcus
AI Marcus

Posted on

Building an AI website builder, 6 weeks in: what I learned about multi-tenant SEO

Six weeks ago I posted a Show DEV about AI Marcus, an AI website builder I'm building. The post got zero comments. That's fair — I led with marketing, not engineering. Here's the technical post I should have written.

The problem nobody warned me about: multi-tenant SEO

When you let users publish hosted sites under your domain, Google's default behavior will destroy your SEO if you don't architect for it.

Every hosted site at yoursite.aimarcus.eu or on a custom domain example.com needs to look to Googlebot like its own canonical entity, not like a path under aimarcus.eu. Get this wrong, and Google folds everything into duplicate-content limbo. Worse — your editorial pages get deindexed because Google thinks they're orphan tenant pages.

What I had to ship to fix this:

  1. Per-tenant sitemap.xml generated dynamically from project metadata, pinned to the tenant's canonical host. Not the platform's sitemap.

  2. Per-tenant robots.txt that points to the tenant's sitemap.

  3. Absolute canonical and og:url on every tenant page, on the tenant's own host. Never /s/{slug}/.... If the AI generator emits a relative canonical, the routing layer rewrites it.

  4. The platform's aimarcus.eu/robots.txt disallows /s/ and /blog/ for AhrefsBot specifically — otherwise Site Audit reports drown in "orphan page" alerts that aren't actually orphans, just multi-tenant.

  5. IndexNow per-tenant keys. Each project gets a 32-hex key on first save, reachable at tenant-host/{key}.txt for ownership proof. Bing, Yandex, Naver, Seznam all read this.

  6. Sitemap index at /sitemap.xml references the platform sitemap + a hand-maintained niche sitemap (108 URLs across 12 languages). Was getting wiped on every server restart by the auto-regenerator — fixed by making the niche sitemap part of the regenerator's output, not a side file.

The numbers (4 weeks of actual SEO traffic)

  • 0 ranked keywords across EE / RU / UA / UK locales (DataForSEO check)
  • 1 keyword ranking position 71-80 in US (negligible)
  • ~20 organic clicks per month from Google
  • ~3 clicks/month from ChatGPT (yes, Bing-routed referrers exist now)
  • 1 paying customer

So this post is not a victory lap — it's a diary entry from the part of the journey where the architecture is finally right but the audience isn't yet.

What I'd skip if I started again

  • The blog with 343K programmatic city × niche pages. Google indexed none of them. Burned 1 GB of disk and 6 weeks of crawl budget for zero impressions. Killed it last week.
  • Service-account-only Google Search Console verification. The siteFullUser permission is enough for sitemap submit, but https://... URL-prefix property fails 403; only sc-domain: works. Save yourself the debugging.

Shipped today: built-in affiliate tracking

This morning a paying customer asked me one question: "does it support affiliate program tracking?" By evening I'd shipped a network-agnostic helper that captures a click_id from inbound URL into localStorage, fills any href / form action containing {click_id} with the live value, attaches a hidden click_id input to every form submit, and exposes window.aimMarcusConversion(amount, currency) which fires a postback pixel to the user's tracker on goal completion.

The placeholder map covers the 6 networks I see most often in this segment: Voluum, Affise, Tune (HasOffers), ClickMagick, Keitaro, Binom — plus generic. Users paste their postback URL with the network's native tokens ({sub1}/{payout}/{payout_currency} for Keitaro, {cid} for Voluum, {clickid}/{sum} for Affise, etc.) and the helper translates them at runtime.

The interesting design decision was rejecting the obvious "let users write JavaScript in a custom-code box" path. That's how spam pixels and tracker leaks get into otherwise clean tenant pages. Instead the helper is generated from a structured config (affiliate.network, affiliate.postbackUrl, affiliate.clickIdParam) and validated server-side: no postback URL = no script, postback URL must be HTTPS, click-id param sanitized to [A-Za-z0-9_-]{1,40}. The blast radius is bounded.

What's next

Toolify auto-discovered the listing on Apr 13. Bing's giving us 12% conv on 17 visits. The funnel finally shipped clean. Now I need to move "page visit → input click" from 11% to 25% and figure out an honest acquisition motion for the Stripe markets.

Site: https://aimarcus.eu — first generation is free, no signup.
Repo: not open-sourced. Happy to share architecture details in comments.

Top comments (0)