TL;DR: With an n8n workflow, Ghost webhooks, and a translation API (DeepL or OpenAI), you can automatically translate every new blog article and publish it as a linked DE/EN pair. No more manual copy-paste – publish once, cross-post automatically.
🌍 Introduction: Why Bilingual Blogging Is So Tedious
If you run a blog in two languages, you know the problem: every article needs to be translated, formatted, and published as a separate post. Links, images, tags – everything has to be correct. It's time-consuming and error-prone.
The good news: with n8n and the Ghost Admin API, this process can be fully automated. In this article, I'll show you how to build a workflow that automatically creates a translated version every time you publish.
🎯 The Challenge: Keeping DE/EN Pairs Consistent
A bilingual blog presents several challenges:
- Synchronization: Every article needs a counterpart in the other language
- Linking: The language versions must be linked to each other
- Formatting: HTML structure, images, and code blocks must not break
- Updates: When the original article is updated, the translation should follow
Manually managing this becomes nearly impossible as the number of articles grows. Automation is the key.
🏗️ Architecture: How the Workflow Works
The architecture is fundamentally simple:
- Ghost Webhook fires on
post.published - n8n Webhook Node receives the event
- Ghost Admin API delivers the complete post content
- Translation API (DeepL or OpenAI) translates the HTML content
- Ghost Admin API creates the translated post with relation tags
The interplay between Ghost and n8n makes this pipeline particularly elegant – both tools are built API-first and combine beautifully.
⚡ Setting Up the Ghost Webhook: Trigger on post.published
In Ghost, set up a webhook under Settings → Integrations → Custom Integration:
- Event:
post.published - Target URL: The URL of your n8n webhook node (e.g.,
https://n8n.example.com/webhook/translate-post)
Ghost sends a JSON payload with the Post ID and basic metadata every time you publish. We then fetch the complete content via the Admin API.
{
"post": {
"current": {
"id": "63a1b2c3d4e5f6...",
"title": "My New Article",
"slug": "my-new-article",
"status": "published"
}
}
}🔧 n8n Workflow Design: Step by Step
The workflow in n8n consists of five main nodes:
1. Webhook Trigger Node
Create a Webhook Node as trigger. Select POST as the HTTP method. The node receives the Ghost webhook payload and starts the workflow.
Important: Add a condition that checks whether the post is already a translated post (e.g., by checking the tags). Otherwise, you'll create an infinite loop!
2. HTTP Request: Fetch Post Content
Using the Post ID from the webhook, fetch the complete article via the Ghost Admin API:
GET /ghost/api/admin/posts/{{postId}}/?formats=html,mobiledocYou need the html format for translation and the metadata for tags, feature image, and excerpt.
3. Translation Node: DeepL or OpenAI
For the actual translation, you have two good options:
Option A – DeepL API:
// n8n Function Node – Prepare DeepL request
const html = $input.item.json.posts[0].html;
const title = $input.item.json.posts[0].title;
return {
json: {
text: [html, title],
source_lang: "DE",
target_lang: "EN",
tag_handling: "html"
}
};DeepL has the advantage that it natively understands HTML tags and preserves the structure (tag_handling: "html").
Option B – OpenAI API:
// n8n Function Node – OpenAI Prompt
const html = $input.item.json.posts[0].html;
return {
json: {
model: "gpt-4o",
messages: [
{
role: "system",
content: "Translate the following HTML blog post from German to English. Keep ALL HTML tags intact. Do not translate code blocks. Return only the translated HTML."
},
{
role: "user",
content: html
}
]
}
};OpenAI is more flexible with context and style, but needs a precise prompt to avoid altering the HTML structure.
4. HTTP Request: Create Translated Post
The translated content is created as a new post via the Ghost Admin API:
// n8n Function Node – Prepare post data
const original = $input.item.json.posts[0];
const translatedHtml = $input.item.json.translated_html;
const translatedTitle = $input.item.json.translated_title;
return {
json: {
posts: [{
title: translatedTitle,
html: translatedHtml,
status: "draft", // Start as draft for review
feature_image: original.feature_image,
feature_image_alt: original.feature_image_alt,
feature_image_caption: original.feature_image_caption,
custom_excerpt: translatedExcerpt,
tags: [
{name: "#en"},
{name: "#crosspost"},
{name: `#rel-en-${original.id}`}
]
}]
}
};5. Setting Relation Tags
The link between the original and the translation works via internal tags:
- Original article gets:
#rel-de-{id} - Translated article gets:
#rel-en-{id}
In Ghost, tags with the # prefix are internal and not visible to readers. However, you can use them in templates and the API to find language pairs.
🖼️ Images and Feature Images: Just Reuse the URLs
The beauty of Ghost: images are referenced via URLs. You don't need to copy or re-upload them – the same URLs work in both language versions.
Simply carry over the following fields 1:1 from the original:
feature_image– The main image of the articlefeature_image_alt– Alt text (translate if needed!)feature_image_caption– Image caption (translate!)- All
<img>tags in the HTML – URLs stay the same
Tip: You should also translate alt texts and captions – it's good for SEO and accessibility.
🔑 Ghost JWT Auth in n8n: Code Snippet for the Function Node
Ghost uses JWT tokens for Admin API authentication. In n8n, you can generate the token in a Function Node:
// n8n Function Node – Generate Ghost JWT token
const crypto = require('crypto');
const adminKey = $env.GHOST_ADMIN_API_KEY; // Format: "id:secret"
const [id, secret] = adminKey.split(':');
// Header
const header = Buffer.from(JSON.stringify({
alg: 'HS256',
kid: id,
typ: 'JWT'
})).toString('base64url');
// Payload
const now = Math.floor(Date.now() / 1000);
const payload = Buffer.from(JSON.stringify({
iat: now,
exp: now + 5 * 60, // Valid for 5 minutes
aud: '/admin/'
})).toString('base64url');
// Signature
const signingInput = `${header}.${payload}`;
const signature = crypto
.createHmac('sha256', Buffer.from(secret, 'hex'))
.update(signingInput)
.digest('base64url');
const token = `${signingInput}.${signature}`;
return {
json: {
token,
ghostUrl: $env.GHOST_API_URL
}
};You then set this token in the HTTP Request Node as the Authorization: Ghost {token} header.
⚠️ Edge Cases: Avoiding Updates and Infinite Loops
A few pitfalls to watch out for:
Preventing Infinite Loops
When the translated post is published, the webhook fires again. Without protection, n8n translates the translation back – infinitely.
Solution: Check in the workflow whether the post has a #crosspost tag. If so → abort the workflow.
// n8n IF Node condition
const tags = $input.item.json.post.current.tags || [];
const isCrosspost = tags.some(t => t.name === '#crosspost');
// If isCrosspost === true → Stop
return !isCrosspost;Updating Existing Translations
When the original article is updated, you don't want a new translated post – you want to update the existing one.
Solution: Before creating, search for a post with the matching relation tag:
// Check if a translation already exists
const originalId = $input.item.json.post.current.id;
const relTag = `#rel-en-${originalId}`;
// Ghost API: Filter posts by internal tag
// GET /ghost/api/admin/posts/?filter=tag:hash-rel-en-{id}
// If result exists: PUT (update), otherwise: POST (create)Additional Edge Cases
- Don't translate code blocks: DeepL with
tag_handling: "html"doesn't always recognize<pre>/<code>. Exclude them manually if needed. - Slug generation: Let Ghost automatically generate the slug from the translated title – don't set it manually.
- Rate limits: DeepL Free allows 500,000 characters/month. With many posts, that can run out quickly.
🎬 Conclusion
With n8n, Ghost Webhooks, and a Translation API, you can fully automate your bilingual blog. The initial setup effort pays off quickly: instead of manually translating every article, you simply publish in your primary language – the rest happens automatically.
The combination of Ghost and n8n is particularly powerful because both tools think API-first and integrate seamlessly. And with relation tags, you have a clean method to programmatically link language pairs.
Next steps for you:
- Set up an n8n instance (self-hosted or cloud)
- Create a Ghost Custom Integration + Webhook
- Get a DeepL or OpenAI API key
- Import and test the workflow
Happy automating! 🚀
Discover more articles
Blog-Artikel automatisiert übersetzen und cross-posten mit n8n
Mit n8n und der Ghost Admin API kannst du Blog-Artikel automatisch übersetzen und als DE/EN-Paare cross-posten. So baust du den Workflow.
Custom Nodes in n8n: Build Your Own Workflow Extensions 🔧
n8n offers hundreds of nodes – but sometimes you need your own. Here's how to develop custom nodes with TypeScript for n8n.
n8n Unleashed: Ultimate Workflow Automation
Automate boring and repetitive processes quickly and easily 👇