At Wego, every piece of content ships in two languages at once — English and Arabic. Doing that by hand across two CMS entries is slow, but the deeper problem is consistency: the two versions drift apart. I built a pipeline that generates and publishes both language variants from a single source — structurally identical, with no per-language manual step.
This is the engineering deep-dive: the pipeline architecture, the Prismic schema decisions, what broke during testing, and what I’d change if I rebuilt it. For the outcome-level summary — the bulk airline-promo-page runs this approach unlocked — see the Bilingual CMS case study.
The Problem With Manual Bilingual Publishing
The root issue wasn’t just speed — it was consistency. When humans translate and format across two CMS entries, small discrepancies creep in: a field missing in Arabic, a slug that doesn’t match, a publication date off by a day. Those inconsistencies make A/B testing impossible and create SEO cannibalization between the two language versions.
I needed a system where English and Arabic versions were structurally identical — same schema, same metadata, same publish timestamp — and where the translation happened inline, not as a separate manual step.
Pipeline Architecture
The pipeline runs on self-hosted n8n. The trigger is a Google Sheets row — each article to be published starts as a row with a title, a category, and a source body in English. The automation handles everything downstream.
Here’s the full execution sequence:
- Schedule Trigger fires every 30 minutes and checks the Sheets queue for rows with status
ready_to_publish. - The article body is passed to Claude via API, which returns a structured JSON object with both English and Arabic field values: body, SEO title, meta description, and slug.
- An IF node checks whether a Prismic document ID already exists for this row. If yes, it routes to an update; if no, it creates a new document.
- A Prismic HTTP node writes both language versions in a single API call using Prismic’s locale-aware write API.
- On success, the Sheets row is updated to
publishedwith the Prismic document URL and a timestamp.
“The automation doesn’t just save time — it enforces structural parity between English and Arabic that a manual process can’t guarantee at scale.”
The Schema That Made It Work
The most important design decision wasn’t the automation — it was the Prismic schema. We used slice-based content rather than a flat rich-text field, which meant Claude’s output needed to map cleanly onto predefined slice types. Here’s the simplified field mapping the pipeline uses:
| Prismic Field | English Source | Arabic Output | Who Fills It |
|---|---|---|---|
title |
Sheets column A | Claude translation | Automation |
seo_title |
Claude generated | Claude generated | Automation |
meta_description |
Claude generated | Claude generated | Automation |
slug |
Sheets column B | Transliterated | Automation |
body (slices) |
Sheets column C (Markdown) | Claude translation | Automation |
publish_date |
Trigger timestamp | Identical | n8n |
category |
Sheets column D | Sheets column E | Human (pre-set) |
The key insight: categories are set by a human in advance in the Sheets row, not auto-translated. Arabic category taxonomy at Wego doesn’t map 1:1 to English, so we kept that as the one deliberate manual input per article.
What Broke During Testing
First attempt failure: Claude’s JSON output included markdown code fences (
```json) around the response, which broke the n8n JSON parse node every single time. Fixed by adding an explicit instruction to the system prompt: “Return valid JSON only. No markdown. No code fences.”
Three issues surfaced in testing that are worth calling out explicitly:
- Claude output formatting — the model wrapped JSON in markdown fences. Fixed with a firm system prompt instruction and validated with an
n8n Codenode that strips fences before parsing. - RTL slug generation — Arabic transliteration was producing slugs with Arabic characters, which Prismic’s URL system doesn’t support. Fixed by adding a transliteration step using an ASCII mapping function in a separate Code node before the Prismic write.
- Prismic rate limiting — publishing a dozen-plus articles in a batch was hitting Prismic’s write API rate limit. Fixed by adding a
Waitnode with a 2.5s delay between each publish operation.
The Claude Prompt Structure
The system prompt and user message that produce reliable bilingual JSON output:
n8n · Code Nodeconst systemPrompt = `You are a bilingual content specialist for a travel brand.
Return ONLY a valid JSON object. No markdown. No code fences.
Schema: { en: { title, seo_title, meta_description, slug, body },
ar: { title, seo_title, meta_description, slug, body } }
Arabic body must preserve the same HTML structure as English.
Slugs must be ASCII-only, hyphen-separated, lowercase.`;
const userPrompt = `Translate and optimise for SEO:
Title: ${$json.title}
Body: ${$json.body}`;
Results
The win here isn’t a stopwatch figure — it’s structural. Every article ships in English and Arabic from a single trigger, with identical schema, metadata, and publish timestamp, so the two versions can’t drift or cannibalise each other in search. Translation and SEO metadata are generated inline rather than bolted on after the fact.
The same approach scaled past article publishing into bulk runs — 30+ airline promo pages across EN + AR in one unattended run, which is the story the Bilingual CMS case study tells end to end.
What I’d Change If I Rebuilt It
Three things I’d do differently from day one:
- Use structured outputs from the start. Telling Claude to “return JSON” is fragile. Anthropic’s structured output mode (or a JSON schema in the tool-use call) would eliminate the formatting failures entirely and remove the Code node that strips fences.
- Add a human review queue before the Prismic write. Right now, Claude’s output goes directly to the CMS. A Sheets-based staging step — where an editor can flag a row before the IF node fires — would add a safety layer for sensitive or high-traffic content.
- Monitor translation quality as a separate step. There’s no automated quality check on Arabic output. A second Claude call scoring the translation against the English original (on a 1–5 scale) would catch the small fraction of outputs that are clearly degraded before they publish.
If you’re building something similar for a multilingual CMS, the Prismic locale-aware write API is the single most confusing part of the integration — the docs underexplain it. Reach out if you’re stuck and I’m happy to share the exact HTTP node configuration.