After researching every approach available to run a multilingual Ghost site, I’m grouping them into DIY solutions (you build and host it yourself, using whichever infrastructure you choose) and paid / managed solutions (you depend on a third-party service or commercial product). Each row reflects the real trade-offs.
Two columns drive everything:
- Mount / link-rewrite layer — When the EN content lives on a second instance served under
/en/, something has to serve it there and rewrite Ghost’s internal links (href="/about/" → /en/about/), sitemap, RSS, cookies. This column says what does that job. See the terminology note below.
- Translation pairing — How a FR article is linked to its EN translation (which is also what makes automatic
hreflang possible).
And two columns matter most for SEO:
- Translated slugs possible — Google’s best practice is language-specific slugs (
/our-mission/ in EN, /notre-mission/ in FR). Some solutions force identical slugs across languages, a real SEO compromise.
- Automatic hreflang — Server-side injection is what Google recommends; client-side JS injection is allowed but discouraged for critical SEO signals.
DIY solutions
| Approach |
Instances |
URL |
Mount / link-rewrite layer |
Translation pairing |
Translated slugs |
Automatic hreflang |
Localized frontend UI (Portal, Search, Comments, theme strings) |
Localized Ghost emails (transactional + boilerplate) |
Theme to maintain |
Setup |
| Single instance, tag routing |
1 |
subdir |
Ghost-native (routes.yaml) |
none (tags only route) |
 |
 |
 |
 |
stock theme |
medium |
| Single instance + custom theme |
1 |
subdir |
Ghost-native (routes.yaml) |
internal tag + {{#get}}, or per-post Code Injection |
 |
server-side |
achievable via theme work (custom translation helper + dynamic data-locale for widgets) |
(single publication_language) |
custom theme (re-merge on Ghost / base-theme updates) |
high |
| Headless + SSG (Next.js / Astro / Gatsby / Hugo) |
1 (headless) |
subdir |
SSG build |
SSG i18n routing (build-time) |
 |
server-side |
(rebuilt in SSG) |
if Ghost still sends emails; only if email system is also replaced |
entire frontend replaced |
very high |
| Two instances, no extra layer |
2 |
subdomain |
n/a (subdomain) |
none |
 |
 |
native |
native |
stock themes |
low |
| Two instances + hreflang layer |
2 |
subdomain |
n/a (subdomain) |
internal tag, via edge function / programmable proxy |
 |
server-side |
native |
native |
stock themes |
medium |
| Two instances + reverse proxy (no rewriting) |
2 |
subdir |
nginx (routing only — internal links break) |
none |
 |
 |
native |
native |
stock themes |
medium |
Two instances + sub_filter |
2 |
subdir |
nginx sub_filter |
none |
 |
 |
native |
native |
stock themes |
high |
| Two instances + custom theme (theme-side pairing) |
2 |
subdir |
nginx sub_filter |
per-post Code Injection, or identical-slug convention |
with Code Injection / with identical-slug |
server-side |
native |
native |
custom theme |
high |
| Two instances + custom theme + client-side JS |
2 |
subdir |
nginx sub_filter |
internal tag, via client-side JS |
 |
client-side (SEO penalty) |
native |
native |
custom theme |
high |
| Two instances + programmable proxy |
2 |
subdir |
programmable proxy (Varnish, OpenResty, Caddy plugin, Node middleware…) |
internal tag, via proxy |
 |
server-side |
native |
native |
stock themes |
very high (self-hosted layer to write & maintain) |
| Two instances + edge function (this project) |
2 |
subdir |
Cloudflare Worker |
internal tag, via Worker |
 |
server-side |
native |
native |
stock themes |
high |
Paid / managed solutions
| Approach |
Instances |
URL |
Mount / link-rewrite layer |
Translation pairing |
Translated slugs |
Automatic hreflang |
Localized frontend UI (Portal, Search, Comments, theme strings) |
Localized Ghost emails (transactional + boilerplate) |
Theme to maintain |
Cost |
| JS translation widget (Weglot, Bablic, ConveyThis, Linguise, Localize, Lokalise) |
1 |
subdir or subdomain |
vendor proxy |
vendor-managed (URL-based) |
(managed) |
server-side |
translates page content + nav, but Ghost-issued emails stay monolingual |
 |
stock theme |
subscription (€79/mo+ for Weglot) |
| Commercial multilingual theme (Crimson, TanaFlows) |
2 |
subdir |
nginx (required, per theme docs) |
identical-slug convention |
(identical slugs required) |
server-side |
native |
native |
vendor-maintained |
paid theme |
| Managed Ghost host w/ subdirectory (Ghost Pro, Synaps Media, Magic Pages, DigitalPress, Cloudways) |
2 |
subdir |
managed host |
up to you (theme or extra layer) |
 |
(add via theme) |
native |
native |
stock theme ( custom if you want hreflang) |
subscription / +$50/mo for Ghost Pro |
Terminology: the mount / link-rewrite layer
When you serve a second Ghost under blog.com/en/, Ghost still emits links like href="/some-post-slug/" that break out of the subdirectory and resolve to the FR site. Something has to rewrite them (plus sitemap, RSS, Location headers, cookies). The options form a single axis, from dumbest to smartest:
- nginx (vanilla) — a reverse proxy. Routes requests, but doesn’t touch the response → internal links break.
- nginx +
sub_filter — adds find-and-replace on the HTML body → internal links work. But it’s text-only: no API calls, so it can’t discover the other language’s URL for hreflang.
- Programmable proxy — a reverse proxy that runs your code (OpenResty/Lua, Caddy plugin, Node + http-proxy-middleware, Varnish/VCL). Can rewrite the response and call the other instance’s API → hreflang too.
- Cloudflare Worker — a programmable proxy that runs at the edge instead of on your server.
- Managed host — Ghost Pro, Synaps, Magic Pages, etc. handle all of this in their infrastructure.
So a Cloudflare Worker and a self-hosted programmable proxy are the same thing architecturally — the difference is just where it runs (next section).
Subdomain vs subdirectory: an SEO trade-off
If you keep your EN content on a subdomain (en.blog.com), you avoid the entire mount/rewrite problem — no internal link rewriting, no sitemap/RSS gymnastics, no cookie domain juggling. You still need a thin layer to inject hreflang between the two instances (an edge function or programmable proxy; nginx alone can’t), but it’s drastically simpler than the full subdir setup.
The trade-off is SEO: Google treats en.blog.com as a separate site from blog.com, so you don’t consolidate domain authority across languages. Google’s official recommendation is the subdirectory structure, which is why most of the rows above go through the trouble. Subdomain + hreflang-only makes sense when you’re starting both languages from scratch, when you have many languages, or when you accept the SEO cost for simplicity.
Why theme-based pairing on two instances is constrained
A theme running on the FR Ghost can only query the FR Ghost’s own Content API (via the {{#get}} helper). It has no way to discover, at render time, the slug of the EN translation on the other instance. Two theme-only workarounds exist:
- Identical-slug convention — hardcode
href="/en/{{slug}}/". Simple, but forces both languages to share the same slug (used by Crimson, TanaFlows).
- Per-post Code Injection (credit to Cathy Sarisky for raising this) — store the translated slug as data in each post’s Code Injection field; the theme reads it via
{{post.codeinjection_head}} to build the hreflang. Keeps translated slugs, at the cost of editing that field manually on every post pair (and re-editing it whenever a slug changes).
To get translated slugs and zero manual per-post work, you need a layer that calls the other instance’s API at request time: a programmable proxy or an edge function — the approach this project takes.
Programmable proxy vs Cloudflare Worker
Architecturally identical — both intercept the response, query the other Ghost instance, inject hreflang, and rewrite internal links server-side. The difference is where they run and what you maintain:
- A programmable proxy runs on your server. No platform dependency, full control — but you add a component to your stack, and on a managed PaaS like Coolify (which routes via Traefik), inserting a custom proxy in front of Ghost is non-trivial.
- A Cloudflare Worker runs at the edge. Free tier covers most blogs, nothing to insert into your existing routing, works transparently on top of any host (Coolify, Docker, VPS, managed). The trade-off is a dependency on the Cloudflare platform.
The same edge-function approach works on Vercel Edge Functions, Netlify Edge Functions, etc. — equivalent trade-offs.
Reading the tables
- Separate members and newsletters per language requires two instances. No single-instance approach solves this — it’s a fundamental Ghost limitation.
- Translated slugs + automatic server-side hreflang together is rare. Commercial themes (Crimson, TanaFlows) give you automatic hreflang but force identical slugs. Managed subdirectory hosts give you translated slugs but no automatic hreflang. Only the programmable proxy, edge function, theme + Code Injection, and headless-SSG approaches give you both — and only the first two stay both stock-theme and zero-touch.
- Separate comments per language are a trade-off, not a one-sided cost. For cross-language audiences (Cathy Sarisky’s Initium is the canonical example), shared threads build community. For distinct audiences per language, keeping each thread linguistically homogeneous is a feature: monolingual readers aren’t confronted with comments they can’t read, and the author can respond in the thread’s language.
- Tools like Activepieces + DeepL are content production strategies (how to generate translations), not architectures — they can be layered on top of any row above.
For a broader overview of where Ghost stands on multilingual support overall (including what’s structurally missing from Ghost core), Cathy Sarisky’s article What would it take to make Ghost multilingual? is the best single-source read.