@bablic-io/sdk-vercel
v1.2.0
Published
Bablic server-side rendering for Next.js on Vercel.
Readme
@bablic-io/sdk-vercel
Server-side rendering for Bablic on Next.js and Vercel. Your translated pages are rendered on the server, so visitors and search engines receive finished, indexable HTML in their language. In the browser, Bablic keeps translating content that appears later.
Astro site? Use @bablic-io/sdk-astro instead, on Vercel too. This package is for Next.js only.
You need a Bablic account and project. New to Bablic? Sign up.
Requirements
- A Bablic project with the Bablic script tag on every page of your site, in the HTML the server sends (not added by a tag manager or
next/scriptwithafterInteractive). - A Bablic Pro plan or a free trial. On other plans the SDK serves the original page unchanged (
x-moxie: skipped,x-moxie-reason: please subscribe to pro tier) and Bablic keeps translating in the browser. After a plan change, allow about 15 minutes. Plans: Bablic dashboard. - Translated URLs set in your Bablic dashboard: language folders (for example
example.com/fr-fr/…) or language subdomains (for examplefr-fr.example.com). Cookie and query-parameter setups have no separate URL per language, so they keep translating in the browser only: the SDK serves their pages unchanged (x-moxie: skipped), even with a customresolve. - The dashboard values must be exactly the URLs you want: a folder
/fr-fr/serves/fr-fr/…, never/fr/…. - Next.js 16 or later with the App Router, on Vercel, on the Node.js runtime (not Edge).
- Node.js 22.
Install
npm install @bablic-io/sdk-vercelSet up
Keep the Bablic script tag in your root layout.
// app/layout.tsx <head> <script src="https://edge.bablic.com/moxie.min.js" data-oljs="P0000" async /> </head>Copy the exact tag from your Bablic dashboard.
P0000stands for your project key.Add
data-glweb-opacity="1"to the tag. Without it, the Bablic script hides each original-language page (near-zero opacity) until its settings arrive, which costs first paint and Lighthouse score. Translated pages served by the SDK never hide; the SDK sets this attribute on them. The tag can also load withdefer. Keep thePat the start ofdata-oljs.Let the proxy see client-side navigations. Without this, Next.js strips the
rscheader and_rscparameter before the proxy runs.// next.config.ts const nextConfig = { skipProxyUrlNormalize: true }; export default nextConfig;Add the proxy with your project key. The SDK reads your translated URLs from your Bablic dashboard settings, so you don't list languages or URLs in code.
// proxy.ts import { withMoxie } from '@bablic-io/sdk-vercel'; export const proxy = withMoxie(undefined, { project: 'P0000' }); // Every page. Requests your project doesn't translate pass straight through. export const config = { matcher: ['/((?!_next/|api/|.*\\..*).*)'], };Already have a proxy? Pass it as the first argument:
withMoxie(myProxy, { project: 'P0000' }). Requests the SDK doesn't translate go to it unchanged.Vercel Preview deployments with Deployment Protection: in your Vercel project, open Settings → Deployment Protection, turn on Protection Bypass for Automation, then redeploy. Without it, the SDK's fetch of each original page is sent to the Vercel login and no page is translated. The SDK sends the secret Vercel provides (
VERCEL_AUTOMATION_BYPASS_SECRET) on its own.Preview domains: Bablic only answers for domains your project allows, and link rules are set per domain. Add the preview's domain as your project's staging domain in the dashboard. Use the branch's stable domain (
<project>-git-<branch>-<team>.vercel.app) or a custom preview domain, since each deployment's own URL changes.
When you change your languages or URLs
The SDK reads your project settings from Bablic and caches them for 5 minutes; Bablic's own cache adds up to 10. A change in the dashboard reaches the SDK within about 15 minutes, with no code change or redeploy. To drop the SDK's copy sooner, POST {"config": true} to the revalidate route (Clearing the cache).
Check it works
Open one of your translated URLs twice, for example a page under a language folder:
curl -sI https://<your-translated-url> # x-moxie: MISS, content-language: <language>
curl -sI https://<your-translated-url> # x-moxie: HITOn a protected Vercel Preview, add the bypass secret: curl -sI -H "x-vercel-protection-bypass: <secret>" https://<your-translated-url>.
View the page source: <html … data-moxie-mode="ssr">, one <script type="application/json" id="moxie-manifest">, and the text in the translated language.
While your project is in Staging, add ?bablic_preview=1 to the URL. Staging responses are never cached.
How it works
- A request arrives. The SDK checks it against your project's translated URLs (cached). Anything else passes straight through.
- A cached translation is served at once (
x-moxie: HIT). - On a miss, the SDK fetches the original page from the same deployment (for a language folder, the same path without the folder) and translates it on the server with your project's translations.
- It also translates the Next.js data React hydrates from, so the page stays translated after it loads, caches the page for
pageTtl, and serves it (x-moxie: MISS). Client-side navigations are translated the same way. - In the browser, the Bablic script translates content added later, without redoing the server's work.
Links and redirects follow your project's link rules in Bablic for the domain the request is on, the same rules Bablic applies in the browser. The SDK never invents a URL: with no rules, links stay as they are. The translated page's <html lang> is the language's code from your dashboard (unless your project sets its own HTML language), so browsers don't offer to translate it again. Translated URLs keep the original path under the language folder (/fr-fr/pricing); translated slugs aren't supported yet.
If translation fails or takes longer than timeoutMs, nothing partial is served or cached. Search engines get 503 with Retry-After: 60; visitors get the original page with X-Robots-Tag: noindex and Cache-Control: private, no-store. x-moxie: failed and x-moxie-reason say why.
Options
| Option | Default | Meaning |
| --------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------- |
| project | — | Your Bablic project key (data-oljs in the script tag). Required unless you pass resolve. |
| pageTtl | 3600 | Seconds a translated page is fresh; then served stale and refreshed in the background. |
| staleTtl | 604800 | Seconds an expired page may still be served while it refreshes. |
| timeoutMs | 3000 | Translation budget per page while a visitor waits, in ms. |
| backgroundTimeoutMs | 15000 | Budget for background refreshes and retries after a timeout, in ms. |
| cache | Vercel Runtime Cache | Any store with get, set and expireTag. |
| editorCookie | 'opemoxieoverride' | Requests with this cookie (the Bablic editor) get the original page, uncached. false turns this off. |
| onResult | — | Called after each translation with { url, result, cache }, for logging. |
| onUnsupported | — | Called once if your project uses cookie or query-parameter URLs, which can't be rendered on the server. |
| resolve | your dashboard settings | Custom routing: map a request URL to { locale, sourceUrl }, or undefined to leave it. Overrides project. |
| moxie | { mode: 'page' } | Run the Bablic script your page's tag loads. { mode: 'pinned', source, version } runs a copy you supply. |
| host | https://edge.bablic.com/ | Bablic service URL. |
| fetch | globalThis.fetch | fetch for the original page and Bablic's translation API. |
Response header x-moxie
| Value | Meaning |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| HIT | Served from the cache. |
| STALE | Served from the cache while a newer translation is prepared in the background. |
| MISS | Translated for this request, then cached. |
| redirect | The original page redirects; Location follows your link rules. x-moxie-reason shows both URLs. |
| pass | The original page isn't a 200 HTML page (404, 410, JSON …); served as is, see x-moxie-reason. |
| skipped | Not translated on the server: your plan doesn't include it, or the project doesn't translate this page (not live, no script tag, or translation off). |
| failed | Translation failed; the original page was served under the failure rule. See x-moxie-reason. |
| bypass | Request from the Bablic editor; original page, uncached. |
Requests that aren't translated URLs get no x-moxie header: they go to your app (or your own proxy) untouched.
SEO
What the SDK does on every translated page:
<html lang>and thecontent-languageheader carry the language's code from your dashboard, as a BCP-47 tag (fr-FR).- A canonical,
og:urlortwitter:urlthat names the original page is pointed at the translated URL, so each language is its own page, not a duplicate. A canonical that names a different page is left alone. - Titles, descriptions, Open Graph and Twitter tags, JSON-LD and image
alttext are translated with the rest of the page. - JSON-LD (structured data) points at the translated page:
url,item,mainEntityOfPageand@idvalues that name a page of your site get the translated URL, andinLanguagethe page's language. Site-wide nodes, whose@idis your home page plus a#name (#organization,#website), stay the same on every page. Files and other sites' URLs are left alone. - hreflang values are valid BCP-47 tags. A language code with a private-use region, such as Spanish (Universal)
ES-XM, is writtenes, in hreflang and inog:locale, since search engines ignore a region that doesn't exist.
What your project and site decide:
- hreflang. Bablic adds alternates for every language plus
x-defaultwhen hreflang is enabled for your project (ask Bablic support), and only on pages with no hreflang tags of their own. If your site prints its own hreflang tags, print one for every language, or none, and let Bablic add them. A partial set (onlyenandx-default) stops Bablic from adding the rest. - Sitemap. The SDK doesn't change your sitemap. Add each language's URLs (your original paths under each language folder) with your sitemap tool.
- Images.
og:imageand other images stay as your site sets them, unless your project translates images. - Links. Links follow your project's link rules for the domain the page is served on. A domain with no rules keeps the original links, so set rules for your production domain and for any staging or preview domain.
Caching and speed
- Fresh for
pageTtl(1 hour): served from the cache (x-moxie: HIT). - Stale after that, or after a new deploy: still served at once (
x-moxie: STALE), and refreshed in the background. After a deploy, a page whose original HTML is unchanged keeps its translation (one fetch of the original, no new translation). An expired page is always translated again, so edits in Bablic show up withinpageTtl. - Kept for
staleTtl(7 days) after it expires; only a page nobody visits that long is translated while a visitor waits. - Slow first translation: if a page's first translation runs past
timeoutMs, the visitor gets the failure rule and the SDK finishes it in the background (backgroundTimeoutMs, 15 s), so the next visitor gets aHIT. - Warming: request each translated URL once after a deploy (without
?bablic_preview=1, which is never cached) to fill the cache before visitors arrive. Server-Timingon every response shows where the time went, for examplemx;desc="MISS", cache;dur=3, source;dur=42, translate;dur=870, total;dur=918. Browser dev tools and Lighthouse show it.
Hosting rules
- Turn off ISR for your translated URLs. ISR ignores cookies, so it would cache Staging and editor responses.
- A CDN in front of Vercel must not cache HTML for requests carrying the
opemoxieoverridecookie. - Keep the Bablic script tag on every page, including the original-language pages.
- Language subdomains must point at the same Vercel project as your main domain.
Clearing the cache
Pages refresh on their own after pageTtl. To clear them sooner, add a signed purge route:
// app/api/moxie/revalidate/route.ts
import { createRevalidateHandler, runtimeCache } from '@bablic-io/sdk-vercel';
export const POST = createRevalidateHandler({
secret: process.env.MOXIE_REVALIDATE_SECRET!,
cache: runtimeCache(),
});POST {"tkey":"<translation key>"} (or add "url_hashes": [...] for single pages), or {"config": true} to re-read your project settings, with the header x-moxie-signature: sha256=<hex HMAC-SHA256 of the body, keyed with the secret>.
Links
- Bablic
- Sign up or log in
- Astro sites:
@bablic-io/sdk-astro
License
MIT
