Build Guide · Rankee Add-On

Website Translation

Turn a client's English site into an indexed Spanish one. How the engine works, why it costs about 2½ cents, and exactly how to install it on a client site.

Boom Media SaaS · Built 2026-07-20 · eric@boommedia.us · App-boundary ruling #10

Contents

  1. What it does — and the one thing that makes it worth money
  2. Why Rankee owns it
  3. How the engine works
  4. What it costs (and what you charge)
  5. Installing it on a client site
  6. Verifying it actually worked
  7. Known limits and what isn't built

01What it does

And the single distinction that separates a real product from a toy.

A client's site is in English. This add-on gives it a Spanish version at /es/* — real URLs, rendered on the server, with hreflang tags — so Google indexes the Spanish pages and ranks them.

Server-rendered, or it's worthless

The tempting shortcut is a JavaScript widget that swaps visible text when someone clicks a flag. It's easy, it demos well, and it is commercially pointless — Google never sees the Spanish version, so it never ranks, and the client gets zero new customers.

This is why the whole thing lives in Rankee. The buyer isn't paying for a language toggle. They're paying to be findable by people searching in Spanish.

✗ Widget approach (what we didn't build)

// client-side swap
document.querySelectorAll('p')
  .forEach(translate)

Same URL. Nothing in view-source. Googlebot sees English. Zero SEO value.

✓ Subpath approach (what we built)

guarapos.com/menu      (en)
guarapos.com/es/menu   (es)

<link rel="alternate"
      hreflang="es" …>

Real URL, server-rendered, in view-source. Indexed and rankable.

02Why Rankee owns it

App-boundary ruling #10 — decided 2026-07-14.

Rankee owns website translation as a multilingual-SEO add-on: translated subpath pages (/es/*, /pt/*), hreflang tags, indexed translated content. Rationale: the buyer's motive is search reach — Rankee's job.

The fences that keep the creep zones closed:

AppMay doMay not do
RankeeEverything in this doc — the engine, the cache, the dashboard, billing
BloggyMultilingual authoring of its own content; may hand off to Rankee's engineTranslate a whole site
ReplyeeAnswer chat in the visitor's languageTranslate the site
Compliee / LocaleyNo translation features at all
Compliee would have been the tempting home — it already injects a widget on every client site, so distribution is free. That's exactly the trap in §1: a Compliee-hosted switcher would be client-side and unindexable, and it would push an ADA-scanning product toward claiming it fixes things. The ruling got this right.

03How the engine works

Three ideas: hash, batch, cache. The cost model depends entirely on the third.

FIRST REQUEST for /es/menu client site middleware │ collects source strings ▼ POST rankee.online/api/translate/strings │ { siteKey, locale: 'es', strings: [...] } ▼ hash each string (whitespace + unicode normalized) │ ├─ CACHE HIT → return from Postgres (no API call) │ └─ CACHE MISS → batch 60/request → Claude Haiku 4.5 → persist → return EVERY LATER REQUEST 100% cache hits. Claude is never called again. A cache miss is the only path that ever costs money.

Why hashing matters

Source text is normalized (whitespace collapsed, unicode normalized) before hashing. So reformatting a paragraph, changing indentation, or reflowing HTML does not invalidate the cache. Only a genuine wording change triggers a re-translation.

Why batching matters — this is the cost model

Strings go to Claude 60 at a time, not one at a time. The system prompt is ~600 tokens; sending it once per string instead of once per batch would multiply input cost by roughly 60×. That's the difference between pennies and real money per site.

The system prompt does real work

Human edits are permanent

Editing a translation in the dashboard marks it status: 'reviewed'. A later machine pass can never overwrite a reviewed string. So once you fix a menu item, it stays fixed — which is what makes this safe to point at a restaurant menu at all.

Where everything lives

PiecePath (in Rankee)
Migrationsupabase/sql/migration_add_translate.sql
Tablesrk_translate_sites · _strings · _values
Hashingsrc/lib/translate/hash.ts
Claude callssrc/lib/translate/translate.ts
Read-through cachesrc/lib/translate/cache.ts
Rate limitingsrc/lib/translate/rate-limit.ts
APIsrc/app/api/translate/strings + sites CRUD + values
Dashboard/translate
Client-site filesdistributable/boom-translate/

04What it costs — and what you charge

The margin here is unusual even by SaaS standards.

Your cost

For a typical 200-string restaurant site, one language:

ItemAmount
Strings~200
Average length~12 tokens
Content tokens~2,400
System prompt, amortized over 4 batches~2,400
Total input~5,000
Total output~4,000
Cost @ Haiku 4.5 ($1 / $5 per MTok)≈ $0.025

≈ 2.5 cents per language, per site, once

Ten client sites in two languages ≈ $0.50 total, forever. Every page render after the first is a Postgres read costing effectively nothing.

Against the ruling's $19–29/mo first language, ~$10/language after, one client at $29/mo returns your entire translation cost for the whole client base in the first hour of the first month.

Estimate, not a measurement. These numbers are modelled from token counts, not from a real run. Translate one actual client site and check the Anthropic console before quoting anyone. Sites with long-form copy (a law firm's practice pages) will cost more than a menu.

What to charge

TierPriceNotes
First language$19–29/moPer ruling #10
Each additional~$10/moMarginal cost is ~2.5¢ one time
Setup (optional)$99–199Covers your review pass on menu items — the part that actually takes your time

The honest pitch to a client isn't "we translate your site." It's "we make you findable to Spanish-speaking customers searching in your area." That's what the subpath + hreflang architecture buys, and it's what justifies a monthly rather than a one-off.

05Installing it on a client site

Client sites are separate Next.js apps, so this is a copy-in, not an npm install.

Why copy-in and not a package: your agency client sites (bm-vercel, rlg-vercel, haw-portal) are independent Next.js apps deployed separately. There's no monorepo and no private npm registry, so distribution follows the same file-copy pattern already proven by boom-responsive.css and boom-theme.css.
1

Run the migration

In the Supabase SQL editor at db.boommedia.us, run:

Rankee/supabase/sql/migration_add_translate.sql
2

Confirm Rankee's API key

Make sure ANTHROPIC_API_KEY is set on Rankee's deployment. Note Rankee is the one app on Coolify, not Vercel — so this is set in Coolify's environment settings, not the Vercel dashboard.

3

Register the site in Rankee

Go to Rankee → Translation. Add the client's site URL, enable Spanish, and copy the generated site key (it starts with rkt_).

4

Copy four files into the client repo

From Rankee/distributable/boom-translate/:

boom-translate.config.ts   // site key + locales
middleware.ts              // detects /es/, rewrites to base route
translate-server.ts        // server-side render helper
hreflang.ts                // <link rel="alternate"> + sitemap

If the client site already has a middleware.ts, merge rather than overwrite — the README covers this.

5

Set two environment variables

BOOM_TRANSLATE_SITE_KEY=rkt_...          # from step 3
NEXT_PUBLIC_SITE_ORIGIN=https://client.com # for hreflang URLs
6

Deploy and warm the cache

Visit each /es/ page once. The first visit translates and caches; every visit afterward is a database read. Then review the results in the Rankee dashboard and fix any menu items the model got wrong — those edits become permanent.

06Verifying it actually worked

Two checks. If either fails, the add-on has no commercial value — do not bill for it.

Check 1 — the Spanish must be in view-source. Open /es/menu, press Ctrl+U, and search for a Spanish phrase. If it's not in the raw HTML, it's being injected client-side, Googlebot will never see it, and the pages will never rank. This is the whole product.
Check 2 — /es/menu's canonical must point at itself. If the Spanish page's canonical points back at the English page, you are explicitly telling Google "don't index this." Both pages should carry reciprocal hreflang tags and self-referencing canonicals.

Full checklist

07Known limits and what isn't built

Flagged honestly during the build rather than discovered later.

Nothing has been compiled. The Boom working copies on Google Drive have 0-byte stub node_modules (typescript/bin/tsc is empty and exits 0 misleadingly). Every file was verified by reading it back, but no typecheck or build has run. Do npm install && npx tsc --noEmit on a local checkout before deploying.
LimitationImpact
No Stripe billingDeliberate. TODOs mark the $19–29/mo + ~$10/locale model. Nothing meters or charges yet.
enabled_locales is unmeteredA user could enable unlimited languages and spend tokens. Fine while you're the only user; must be capped before this is self-serve.
Rate limiting is an in-process MapEffective on Rankee's single Coolify instance. Silently multiplies by instance count if Rankee scales or moves to Vercel.
?key= auth pattern unverifiedWritten from the documented Displayee partner-API behavior but not diffed against Displayee's actual code. Worth a look.
PostgREST embed syntaxThe ownership check in the values route is correct per docs but wants a runtime smoke test.
Table prefix inconsistencyRankee's existing tables are unprefixed (tracked_websites); these use rk_ because the Postgres is shared across apps. Consistent with the shared DB, inconsistent with Rankee itself. Cosmetic.
Images and PDFs untouchedA menu embedded in a JPEG stays English. Worth knowing before promising a client "full translation."

Recommended pilot

One Cuban restaurant, one language

You have four Cuban restaurants in Palm Beach County — Guarapo's, La Casita, Don Ramon Wellington, Don Ramon WPB. They almost certainly have Spanish-speaking customers searching in Spanish and finding nothing.

Pick one. Structured menu content is the easiest thing to translate well, the audience is real, and within a week of Search Console data you'll know whether the quality holds up well enough to sell to the other three.