@bablic-io/sdk-astro
v1.2.0
Published
Translate your Astro site with Bablic: server-rendered, SEO-indexable pages in every language.
Readme
@bablic-io/sdk-astro
Server-side rendering for Bablic on Astro. 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. Built for Astro 6 on @astrojs/vercel; works with any Node adapter.
Next.js site? Use @bablic-io/sdk-vercel instead. This package is for Astro only, on Vercel or any Node host.
You need a Bablic account and project. New to Bablic? Sign up.
Requirements
- A Bablic project with the Bablic script tag on every page, in the HTML the server sends.
- 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 as language folders (for example
example.com/fr-fr/…), or language subdomains on sites rendered on demand. 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 folders must be exactly the paths you want: a folder
/fr-fr/serves/fr-fr/…, never/fr/…. - Astro 6 with a Node adapter (
@astrojs/vercel10 or later on Vercel). - Node.js 22.
Install
npm install @bablic-io/sdk-astroSet up
Keep the Bablic script tag in your base layout.
is:inlinekeeps Astro from bundling it, so it reaches the server HTML.<script is:inline src="https://edge.bablic.com/moxie.min.js" data-oljs="P0000"></script>Copy the exact tag from your Bablic dashboard.
P0000stands for your project key. Don't add the tag from client-side code or a tag manager.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.Add a route or middleware with your project key, depending on how your pages render (below). The SDK reads your translated URLs from your Bablic dashboard settings, so you don't list languages or URLs in code.
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.
Prerendered pages: moxieRoute
@astrojs/vercel runs middleware for prerendered pages at build time only, so add an on-demand catch-all route. Your prerendered pages take precedence; only URLs without a page reach it.
// src/pages/[...path].ts
import { moxieRoute } from '@bablic-io/sdk-astro';
export const prerender = false;
export const GET = moxieRoute({ project: 'P0000' });
export const HEAD = GET;Translated URLs are served translated. Any other URL that reaches the route gets 404, as it would without it, with x-moxie: none and an x-moxie-reason saying why.
Remove prerendered pages under your language folders. A page such as
src/pages/fr/index.astrois served instead of the translation, so the SDK never sees/fr/. Keep one copy of each page, in your original language.Use language folders with this setup. A language subdomain shares its paths with your prerendered pages, so the route never sees those requests.
Already have a catch-all route at the root? Call
moxieRoutefrom it first and fall back to your own handling when it returns 404:// src/pages/[...path].ts import type { APIRoute } from 'astro'; import { moxieRoute } from '@bablic-io/sdk-astro'; export const prerender = false; const bablic = moxieRoute({ project: 'P0000' }); export const GET: APIRoute = async (context) => { const translated = await bablic(context); if (translated.status !== 404 || translated.headers.get('x-moxie') !== 'none') return translated; return yourHandler(context); // your existing catch-all logic }; export const HEAD = GET;Edge middleware keeps working. With
middlewareMode: 'edge', your middleware runs before the route for translated URLs, and again on the SDK's fetch of the original page, so your redirects and status codes (410, 404) apply to translated URLs too. OnlymoxieMiddlewareneeds the Node.js runtime.
On-demand pages: moxieMiddleware
// src/middleware.ts
import { sequence } from 'astro:middleware';
import { moxieMiddleware } from '@bablic-io/sdk-astro';
export const onRequest = sequence(moxieMiddleware({ project: 'P0000' }) /*, your middleware */);It skips prerendered pages and supports language folders and subdomains. It runs Bablic in your middleware, so it needs the Node.js runtime: don't combine it with middlewareMode: 'edge'. For edge middleware, use moxieRoute.
Check it works
curl -sI https://<your-translated-url> # x-moxie: MISS, then HIT on a second requestOn 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"> and one <script type="application/json" id="moxie-manifest">.
While your project is in Staging, add ?bablic_preview=1 to the URL. Staging responses are never cached.
Every response the SDK handles carries x-moxie; all but HIT, STALE, MISS and bypass also carry x-moxie-reason:
| x-moxie | 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. |
| none | Not a translated URL (404). The reason lists the folders the SDK sees and how old its config is, or why the config couldn't be read. |
| redirect | The original page redirects; Location follows your link rules. The reason shows both URLs. |
| pass | The original page isn't a 200 HTML page (404, 410, JSON …); served as is. |
| 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. |
| bypass | Request from the Bablic editor; original page, uncached. |
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. x-moxie-reason on a 404 shows the folders the SDK currently sees. To drop the SDK's copy sooner, POST {"config": true} to the revalidate route (Clearing the cache).
How it works
- A request for a translated URL reaches the route or middleware. 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; the CDN serves prerendered pages) and translates it on the server with your project's translations.
- The translated page is cached and served (
x-moxie: MISS). - In the browser, the Bablic script translates content added later, including text that islands (
client:*) render, 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 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 times out, nothing partial is served or cached: search engines get 503, visitors get the original page with noindex.
Clearing the cache
Pages refresh on their own after pageTtl (1 hour). To clear them sooner, add a signed purge route:
// src/pages/api/bablic/revalidate.ts
import type { APIRoute } from 'astro';
import { createRevalidateHandler, runtimeCache } from '@bablic-io/sdk-astro';
export const prerender = false;
const revalidate = createRevalidateHandler({
secret: import.meta.env.MOXIE_REVALIDATE_SECRET,
cache: runtimeCache(),
});
export const POST: APIRoute = ({ request }) => revalidate(request);POST {"tkey":"<translation key>"} (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>.
Options
project, pageTtl, staleTtl, timeoutMs, backgroundTimeoutMs, cache, editorCookie, onResult, onUnsupported, resolve, moxie, host, fetch, plus buildId and sourceHeaders. Defaults are described in the @bablic-io/sdk-vercel README.
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-defaultto translated pages when hreflang is enabled for your project (ask Bablic support), and only on pages with no hreflang tags of their own. Your original-language pages need the same set, and prerendered pages never pass through the SDK, so add the build step below. Don't print hreflang tags yourself: a partial set (onlyenandx-default) stops Bablic from adding the rest.
hreflang on your original pages
bablicHreflang writes the hreflang set into each prerendered page when the site builds, from your project's languages in the Bablic dashboard. Translated pages still get theirs from Bablic, with the same links.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel';
import { bablicHreflang } from '@bablic-io/sdk-astro';
export default defineConfig({
site: 'https://<your-domain>', // the links' origin
adapter: vercel(),
integrations: [bablicHreflang({ project: 'P0000' })],
});- Rebuild after adding or removing a language in the dashboard. The build log says how many pages got links.
- It follows your
trailingSlashsetting (/fror/fr/for a language home page). - Pages are left alone when they have no Bablic script tag, have their own hreflang tags, are
noindex, have a canonical naming another page, or sit under a language folder. 404 and 500 pages too. - If the project config can't be read, the build goes on without links and logs a warning.
- 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 your site must not cache HTML for requests carrying the
opemoxieoverridecookie. - Keep the Bablic script tag on every page, including the original-language pages.
Links
- Bablic
- Sign up or log in
- Next.js sites:
@bablic-io/sdk-vercel
License
MIT
