This guide describes how to set up Bablic server-side translation for a server-rendered website (Drupal, WordPress or another CMS) whose traffic is served through Cloudflare. Once it is in place, translated pages are delivered to visitors and search engines already translated. The original language is never displayed first, and every language version of a page can be indexed.
The setup runs entirely on your infrastructure and uses two npm packages from Bablic:
@bablic-io/sdk-node: Bablic’s translation service, installed on a server running Node.js.@bablic-io/sdk-cloudflare: a Cloudflare Worker that sends your translated pages through that service and caches the results at the edge.
Apart from adding the Bablic script tag, no changes to your site’s code are required.
visitor ──► Cloudflare Worker ──► your origin (original page)
│ ▲
▼ │
Bablic's translation service (@bablic-io/sdk-node)
Summary
Bablic’s side
| # | Step |
|---|---|
| B1 | Confirm your project is on a Pro plan or free trial. Server-side translation is included only on these plans. |
| B2 | Set your languages to language folders (www.example.com/<language-folder>/… ) or language subdomains (<language>.example.com ) in the Bablic dashboard. |
| B3 | Confirm your site's domains are allowed for the project, and set the link rules so links on translated pages stay in their language. |
| B4 | Provide your project key (P0000… ), the same key used in your Bablic script tag. |
Your side
| # | Step | Where |
|---|---|---|
| 1 | Add the Bablic script tag to every page | Your CMS theme |
| 2 | Allow Bablic in your Content Security Policy | Your web server or CMS |
| 3 | Run the translation service (@bablic-io/sdk-node ) behind HTTPS |
A server with Node.js 22 |
| 4 | Create and deploy the Worker (@bablic-io/sdk-cloudflare ) |
Any machine with Node.js, plus your Cloudflare account |
| 5 | Test | Terminal and browser |
For assistance at any step, contact support@bablic.com.
Before you start
- A Bablic account and project (sign up), with steps B1 to B4 done.
- Your site behind Cloudflare (the DNS record for your hostname is proxied, the orange cloud), and permission to create Workers and routes on the zone. A server for the translation service:
- Node.js 22.19 or later;
- reachable from the internet over HTTPS;
- able to make outbound HTTPS requests to
https://edge.bablic.com. It can be the same machine as your website or a separate one.
- Node.js 22 on the workstation used to deploy the Worker.
Replace the following placeholders in the examples with your own values:
| Placeholder | Meaning |
|---|---|
P0000 |
Your Bablic project key |
www.example.com |
Your site's hostname |
example.com |
Your Cloudflare zone |
translate.example.com |
The HTTPS address of the translation service |
<your-translated-url> |
A translated page, e.g. https://www.example.com/<language-folder>/about |
Step 1: Add the Bablic script tag to every page
The tag must be in the HTML your server sends, on every page. A tag inserted later in the browser, for example by a tag manager, is not visible to the translation service.
Copy the exact tag from the Bablic dashboard (Installation & Deployment). It has this form:
<script src="https://edge.bablic.com/moxie.min.js" data-oljs="P0000"></script>
Drupal: add it inside <head> in your front-end theme’s html.html.twig (if the template is not present, copy it from the base theme into your theme), then rebuild caches:
drush cr
Other CMSs: add it to the shared page template or <head> include.
Then purge Cloudflare’s cache for your HTML pages (Caching → Configuration → Purge Cache), so cached pages pick up the tag.
Check:
curl -s https://www.example.com/ | grep -o '<script[^>]*data-oljs[^>]*>'
Step 2: Allow Bablic in your Content Security Policy
If your site sends a Content-Security-Policy header, the browser part of Bablic must be allowed to load and call home. It translates content that appears after the page loads (menus, AJAX results, pop-ups) and pages for signed-in visitors.
Add https://edge.bablic.com to:
script-src(orscript-src-elem), unless it already allows all hosts;connect-src.
Check: open a translated page in your browser and look in the developer-tools console. There should be no “Refused to connect” or “Refused to load” errors that mention a Bablic host. If another Bablic host appears there, add it the same way.
Step 3: Run Bablic’s translation service (@bablic-io/sdk-node)
3a. Install and start it
On your server:
mkdir -p /opt/bablic && cd /opt/bablic
npm init -y
npm install @bablic-io/sdk-node
BABLIC_LISTEN=0.0.0.0 ./node_modules/.bin/bablic-node serve
# bablic-node listening on 0.0.0.0:8790
The translation service stores no project settings. Each request carries your project key; all other settings are read from your Bablic project.
3b. Keep it running (systemd example)
/etc/systemd/system/bablic-node.service:
[Unit]
Description=Bablic translation service
After=network-online.target
[Service]
WorkingDirectory=/opt/bablic
ExecStart=/usr/bin/env node /opt/bablic/node_modules/@bablic-io/sdk-node/dist/cli.js serve
Environment=BABLIC_LISTEN=127.0.0.1
Environment=BABLIC_PORT=8790
Restart=always
User=nobody
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now bablic-node
Docker: a Dockerfile is provided in the @bablic-io/sdk-node README.
3c. Put HTTPS in front of it
Cloudflare reaches the service over HTTPS. Example with nginx on the same server (the service listens on 127.0.0.1:8790):
server {
listen 443 ssl;
server_name translate.example.com;
# ssl_certificate / ssl_certificate_key: your certificate
client_max_body_size 6m; # pages up to 5 MB (BABLIC_MAX_BODY_MB)
location / {
proxy_pass http://127.0.0.1:8790;
proxy_read_timeout 30s;
}
}
The hostname can sit on Cloudflare too (proxied DNS record), or be served directly from the server.
3d. Check it
curl -s https://translate.example.com/healthz
# {"ok":true,"edge":"ok","projects":0}
Then translate one page by hand. Fetch an original page and ask for its translated URL:
curl -s https://www.example.com/about > /tmp/page.html
curl -s -D - -o /tmp/translated.html https://translate.example.com/translate \
-H 'x-bablic-project: P0000' \
-H 'x-bablic-url: https://www.example.com/<language-folder>/about' \
-H 'content-type: text/html' \
--data-binary @/tmp/page.html | grep -i -E '^HTTP|x-moxie'
Expect x-moxie: MISS the first time and HIT on a repeat. /tmp/translated.html holds the translated page.
Main settings
| Variable | Default | |
|---|---|---|
BABLIC_PORT |
8790 |
|
BABLIC_LISTEN |
127.0.0.1 |
Interface to listen on. |
BABLIC_TIMEOUT_MS |
3000 |
Translation budget per page. Past it the visitor gets the original page, and the translation finishes in the background for the next visitor. |
BABLIC_CACHE_MB |
256 |
In-memory cache of translated pages. |
BABLIC_REDIS_URL |
Share the cache between several instances (npm install ioredis next to the service). |
|
BABLIC_CONCURRENCY |
CPUs | Pages translated at once. |
BABLIC_METRICS_PORT |
8791 |
Prometheus /metrics , loopback only. Do not expose this port. |
Full list: the @bablic-io/sdk-node README.
Step 4: Create and deploy the Cloudflare Worker (@bablic-io/sdk-cloudflare)
4a. Generate the Worker project
On any machine with Node.js 22:
npx @bablic-io/sdk-cloudflare init bablic-worker \
--project P0000 \
--engine https://translate.example.com \
--bypass-cookies 'SESS,SSESS' \
--route 'www.example.com/*' --zone example.com
cd bablic-worker
npm install
This writes three files:
wrangler.toml: the Worker’s settings, route and thenodejs_compatflag;package.json;src/index.js: the Worker’s entry point.
Any option can be omitted and set in wrangler.toml afterwards.
--bypass-cookies names the session cookies of signed-in visitors. Their pages are personalized, so the Worker neither translates nor caches them; Bablic translates them in the browser instead.
| Platform | Session cookies |
|---|---|
| Drupal | SESS,SSESS |
| WordPress | wordpress_logged_in_ |
| Other | The cookie your CMS sets at sign-in (check in the browser's developer tools) |
4b. Deploy
npx wrangler login # first time only; opens Cloudflare in your browser
npx wrangler deploy
4c. Routes
- Language folders: the route
www.example.com/*covers everything. The Worker forwards requests for untranslated pages directly to your origin. To have the Worker run only on translated pages, use one route per language folder instead, e.g.www.example.com/<language-folder>/*. - Language subdomains: add a route for each language hostname, e.g.
<language>.example.com/*, and make sure each subdomain has a proxied DNS record pointing at your origin.
Routes can also be managed in the Cloudflare dashboard: Workers & Pages → bablic-translate → Settings → Domains & Routes.
4d. Settings
These are [vars] in wrangler.toml, or Settings → Variables in the Cloudflare dashboard. Run npx wrangler deploy again after changing wrangler.toml.
| Variable | Default | |
|---|---|---|
BABLIC_PROJECT |
Your project key. | |
BABLIC_ENGINE_URL |
The HTTPS address of the translation service, e.g. https://translate.example.com . |
|
BABLIC_BYPASS_COOKIES |
Session cookies of signed-in visitors. | |
BABLIC_EDGE_TTL |
300 |
Seconds a translated page stays in Cloudflare's edge cache. 0 turns it off. |
BABLIC_ENGINE_TIMEOUT_MS |
8000 |
How long the Worker waits for the translation service. |
Step 5: Test
5a. Response headers
curl -sI <your-translated-url> | grep -i -E 'x-bablic-edge|x-moxie'
curl -sI <your-translated-url> | grep -i -E 'x-bablic-edge|x-moxie'
| You see | Meaning |
|---|---|
x-bablic-edge: MISS , x-moxie: MISS |
Translated on this request. |
x-bablic-edge: HIT (second request) |
Served from Cloudflare's edge cache. |
x-moxie: HIT or STALE |
Served from the translation service's cache. |
Untranslated pages carry no x-bablic-edge header, because the Worker forwards them directly to your origin.
5b. The page itself
curl -s <your-translated-url> | grep -o -E '<html[^>]*>|<link[^>]*hreflang[^>]*>' | head
<html lang="…">has the page’s language, anddata-moxie-mode="ssr".hreflanglinks list every language.- In the browser’s View Source, the page text is already translated. This is the version search engines index.
- Links on the page stay inside the language folder or subdomain.
5c. Signed-in visitors and Bablic Studio
Sign in to your site, then open a translated page:
curl -sI -H 'cookie: SESSexample=1' <your-translated-url> | grep -i -E 'x-bablic-edge|cache-control'
# x-bablic-edge: bypass
# cache-control: private, no-store
In the browser, the page is still displayed translated: Bablic translates it on the page. Bablic Studio works on translated pages as before.
5d. Dynamic content
On a translated page, use a pager, a filter, search suggestions or a pop-up. Content that appears after the page loads is translated by Bablic in the browser.
Debugging
Start from the response headers of a translated URL:
curl -sI <your-translated-url> | grep -i -E 'x-bablic-edge|x-moxie'
Whenever a page is not translated, the x-moxie-reason header gives a one-line explanation.
| Symptom | Likely cause | Fix |
|---|---|---|
No x-bablic-edge header on a translated URL |
The Worker's route does not match the URL (www versus the bare domain, or a missing language subdomain), or the DNS record is not proxied |
Check Domains & Routes and the orange cloud on the DNS record |
| The Worker fails as soon as it is deployed | The nodejs_compat compatibility flag is missing |
Keep compatibility_flags = ["nodejs_compat"] in wrangler.toml and redeploy |
x-bablic-edge: pass with "needs BABLIC_PROJECT and BABLIC_ENGINE_URL" |
Worker settings empty | Set both and redeploy |
x-bablic-edge: engine-unavailable |
Cloudflare cannot reach the translation service: a firewall, the certificate, or the service is not running | Run curl https://translate.example.com/healthz from outside your network; check the certificate and systemctl status bablic-node |
/healthz returns 503 |
The server cannot reach https://edge.bablic.com |
Allow outbound HTTPS to that host |
x-moxie: skipped , reason please subscribe to pro tier |
The project's plan does not include server-side translation | Upgrade the plan, or contact support@bablic.com. Allow about 15 minutes for a plan change to take effect |
x-moxie: pass , reason no language folder matches … |
The URL is not one of the project's translated URLs. Folders must match exactly: /fr-fr/ is not /fr/ |
Compare the URL with the folders or subdomains in your Bablic dashboard |
x-moxie: pass , reason … is not a language domain … |
As above, for language subdomains: the hostname is not one of the project's subdomains | Compare the hostname with your Bablic dashboard |
x-moxie: skipped , reason no <script data-oljs> tag |
The tag is not in the server HTML: it is added by a tag manager, or rewritten by Cloudflare Rocket Loader | Place the tag in the theme (Step 1). If Rocket Loader is enabled, add data-cfasync="false" to the tag |
x-moxie: skipped , reason Bablic script has no server-side mode |
The script tag loads an old copy of the Bablic script | Use the tag from your Bablic dashboard, from https://edge.bablic.com/moxie.min.js |
x-moxie: failed , reason timeout… on the first visit to a page |
The first translation of a page took longer than BABLIC_TIMEOUT_MS |
This is expected for new pages: the translation completes in the background and later requests are translated. If it persists, increase BABLIC_TIMEOUT_MS |
x-moxie: failed with a 403 in the reason |
Bablic refused the page's domain for this project | Ask support@bablic.com to add the domain to your project |
| Translated page, but links lead back to the original language | Link rules not set for this hostname | Bablic's side: B3 |
| A form or AJAX request from a translated page returns 404 | The request was sent to a language-folder URL that your origin does not recognize | Report the URL to support@bablic.com, so the link rules leave that form or AJAX address untouched |
| Signed-in visitors see the original language briefly | This is expected: pages for signed-in visitors are translated in the browser, not on the server | No action is required. Confirm that their session cookie is listed in BABLIC_BYPASS_COOKIES , so their pages are never cached |
| A signed-in visitor's name appears on a page for other visitors | Their session cookie is missing from BABLIC_BYPASS_COOKIES |
Add it, redeploy, and set BABLIC_EDGE_TTL to 0 until the old copies expire |
| Browser console shows CSP errors for a Bablic host | Content Security Policy | Step 2 |
| Dashboard changes (languages, folders) are not reflected yet | Bablic's settings are cached for up to about 10 minutes, in addition to the translation service cache and the edge cache | Allow time for the caches to expire, or restart the translation service and set BABLIC_EDGE_TTL=0 while testing |
| A content change is not reflected on the translated page | The edge cache keeps a page for BABLIC_EDGE_TTL seconds; the translation service rechecks pages within the hour |
Lower BABLIC_EDGE_TTL , or allow the cache to expire |
| First requests after a restart are slow | The translation service's in-memory cache is empty after a restart | This is expected. Set BABLIC_REDIS_URL to keep the cache across restarts and instances |
| CMS cache still serves pages without the tag | Drupal page cache, a CDN cache, or both | drush cr (or your CMS's cache clear), then purge Cloudflare |
Useful commands
# Translation service logs (systemd)
journalctl -u bablic-node -f
# Worker logs, live
npx wrangler tail
# Translation service metrics, on the server itself
curl -s http://127.0.0.1:8791/metrics | grep bablic_pages_total
Keeping it up to date
Update both packages when Bablic releases a new version:
# Translation service
cd /opt/bablic && npm install @bablic-io/sdk-node@latest && sudo systemctl restart bablic-node
# Worker
cd bablic-worker && npm install @bablic-io/sdk-cloudflare@latest && npx wrangler deploy
Contact
Bablic support: support@bablic.com. Please include:
- your project key;
- a translated URL;
- the output of
curl -sI <your-translated-url>.