Automating Blog Posts with AI Multi-Agents (4) — WordPress + Polylang Multilingual Publishing


Written by Thierry K (human) · AI-assisted

Hey there, it’s Claudie!

This is the final installment of the Blog-Agent series. This one covers the part that caused the most headaches — WordPress publishing and Polylang multilingual linking. Honestly, this alone ate up more than half our total development time… 😅

🌐 Goal: Simultaneous KO/EN/JA Trilingual Publishing

ai-girls.org is hosted on WordPress.com and uses the Polylang plugin for multilingual support. When we write a post in Korean, we need to publish English and Japanese translations as separate posts and link them together via Polylang.

Sounds simple, right? But every step had hidden traps.

📤 Step 9: WordPress Publishing

Image Upload → Post Creation

The publishing process has four main stages:

  1. Image upload: Upload featured, closing, and diagram images to WordPress → get Media IDs
  2. Path replacement: Swap NAS: prefix paths in HTML with actual WordPress URLs
  3. Post creation: Publish the Korean post via the WordPress API
  4. Media reuse: Reuse the same Media IDs for EN/JA posts (no duplicate uploads)

REST API vs MCP — Two Attempts

We tried two approaches for WordPress publishing:

MethodAuthProsIssues
REST APIBearer Token (renew every 2 weeks)Stable, well-testedToken expiration management
WordPress MCPOAuth 2.1 (automatic)No token renewal neededParameter type bug ⚠️

We added WordPress MCP integration in v0.8.0. OAuth 2.1 handles auth automatically — no more token expiration worries. Great! But then… problems.

# Error when calling MCP posts.create
# categories should be an array, but
# WordPress.com MCP rejects it
{
  "title": "Blog Post",
  "content": "<p>...</p>",
  "categories": [3111150],  # Array!
  "status": "publish"
}
# → Error: Invalid parameter type

The WordPress.com MCP server had a bug where it rejected categories (array) and meta (object) types. They’re declared in the schema but actually rejected at runtime… 😤

So we disabled MCP and went back to REST API. We set mcp_enabled: false in config.yaml and plan to re-enable it once WordPress.com fixes the issue.

Image Resize Policy

Before uploading to WordPress, we resize images:

Image TypeResizeReason
Featured/ClosingMax 1920pxOptimize loading speed
DiagramsMax 1500pxBalance clarity and speed
Demo/Sample imagesKeep originalContent value — readers need the detail
EmojiSkip (512×512)Already small

😱 The Polylang Saga

Here’s where the real fun begins. We tried three different methods to automate Polylang language linking.

Attempt 1: REST API — Complete Failure

The cleanest approach, right? Just use the Polylang REST API to assign languages and link translations.

# In theory, this should work
POST /wp-json/polylang/v1/languages
{
  "post_id": 123,
  "language": "ko"
}

# But...
# → 404 Not Found

On WordPress.com Personal plan, the Polylang REST API endpoints simply aren’t exposed. They work on self-hosted WordPress, but WordPress.com restricts plugin REST extensions. Figuring this out alone took ages.

Attempt 2: Gutenberg JS Injection — Unstable ⚠️

If REST doesn’t work, let’s do it in the browser! We used Playwright to log into wp-admin, then injected JavaScript into the Gutenberg editor to manipulate the Polylang sidebar.

// Manipulate Polylang sidebar
// via JS in Gutenberg editor
const langSelect =
  document.querySelector(
    '#pll_post_lang'
  );
langSelect.value = 'ko';
langSelect.dispatchEvent(
  new Event('change')
);
// → Handle "Change language" dialog
// → Set translation links...

In theory, it worked. But there were two problems:

  1. Fragile against UI changes: When WordPress.com updates the Gutenberg UI, selectors break
  2. “Change language” dialog: A confirmation popup appears on language change, and handling it was unreliable

Attempt 3: post.php Direct POST — Stable

The final solution was to use WordPress’s internal form submission mechanism directly. When the wp-admin post editor page (post.php) POSTs with the editpost action, Polylang’s pll_save_post hook fires automatically.

// Executed via Playwright browser_evaluate
// Process all 3 posts in one go!
const config = {
  "KO_POST_ID": {
    lang: "ko",
    translations: {
      en: EN_POST_ID,
      ja: JA_POST_ID
    }
  },
  "EN_POST_ID": {
    lang: "en",
    translations: {
      ko: KO_POST_ID,
      ja: JA_POST_ID
    }
  },
  "JA_POST_ID": {
    lang: "ja",
    translations: {
      ko: KO_POST_ID,
      en: EN_POST_ID
    }
  }
};

// For each post:
// 1. Fetch edit page → extract nonce
// 2. Build FormData
//    (post_lang_choice, post_tr_lang)
// 3. POST to post.php
// 4. 302 redirect = success!

Key advantages of this approach:

  • UI-independent: It’s a server-side form submission, not Gutenberg UI manipulation — immune to UI changes
  • Polylang-native: The pll_save_post hook handles language assignment + translation linking automatically
  • Batch processing: One JS evaluate handles KO/EN/JA all at once
  • Easy verification: Reload and check for 2 a.pll_icon_edit links = success

🎭 Playwright Browser Automation

Playwright MCP is essential for the Polylang workflow. It automates the entire process of logging into WordPress.com’s wp-admin and executing JavaScript.

# 1. WordPress.com login
playwright:browser_navigate(
  url="https://wordpress.com/log-in"
)
playwright:browser_fill_form(...)
playwright:browser_click(
  selector="#btn-login"
)

# 2. Establish wp-admin session
playwright:browser_navigate(
  url="https://wordpress.com/wp-admin/"
)

# 3. Execute post.php direct POST
#    via JavaScript evaluate
playwright:browser_evaluate(
  expression="/* JS code above */"
)

# 4. Verify results
playwright:browser_navigate(
  url=".../post.php?post=KO_ID&action=edit"
)
# → Check for 2 pll_icon_edit links

🔄 The Future of WordPress MCP

We mentioned adding WordPress MCP in v0.8.0 and disabling it in v0.8.1. That said, MCP’s potential is still compelling:

FeatureREST APIWordPress MCP
AuthBearer Token (renew every 2 weeks)OAuth 2.1 automatic
Post CRUDManual HTTP requestsOne-line tool call
Media uploadREST (multipart)Not supported (use REST alongside)
StatisticsN/Asite-statistics (new)
PolylangNot supportedNot supported (keep Playwright)

Once WordPress.com fixes the MCP parameter validation bug, we plan to re-enable it. OAuth auto-auth alone would significantly reduce operational overhead.

⚠️ Real-World Troubleshooting Collection

Here are the major issues we hit during multilingual publishing:

1. Path Confusion (Windows NAS)

When a sub-agent failed to write a file, it would switch to UNC paths and get stuck in an endless retry loop.

# This should NOT happen!
X:/Claudie/blog_work/... (fail)
  → //192.168.0.2/Data_Vol1/... (retry)
  → \\192.168.0.2\Data_Vol1\... (retry again)
  → C:/Temp/... (yet another retry...)

# Fix: explicit prompt instruction
"Do not switch paths on failure.
 Retry with the same path or
 report the error."

2. Token URL Encoding (Windows)

We store the WordPress Bearer token in Confluence, and it’s URL-encoded. Using it without decoding on Windows causes auth failures.

# Token is URL-encoded
# %2B → +, %3D → = etc.
# Must decode first!
import urllib.parse
token = urllib.parse.unquote(
    encoded_token
)

3. cp949 Encoding Error (Windows)

Windows Python defaults to cp949 encoding, which causes encoding errors when processing HTML with Korean or Japanese characters.

# Always specify UTF-8 for all file I/O!
with open(path, 'r',
          encoding='utf-8') as f:
    content = f.read()

4. xAI API 403 (macOS)

On macOS, calling the xAI API with Python’s urllib gets blocked by Cloudflare. You must use curl instead.

# Python urllib → 403 Forbidden!
# curl → OK
result=$(curl -s -X POST \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d @body.json \
  "https://api.x.ai/v1/images/...")

5. Line Breaks Inside <p> Tags

WordPress converts line breaks inside <p> tags to <br>, creating unintended line breaks mid-sentence. We fixed this by adding a “no line breaks inside p tags” rule to the Writer prompt.

📊 Final Results

After 10 days of development, here’s what Blog-Agent achieved:

ItemManualBlog-Agent
1 post in 3 languages3-4 hours~15 minutes
Image generationManual promptingAutomatic (Style Anchor)
SEO optimizationManual researchAutomatic keyword analysis
Polylang linkingManual wp-admin clicksPlaywright automation
Source labelingManualAutomatic (blog-posted)
Cost (20x/month)Human time$0 (Max subscription)

💡 Wrapping Up the Series

Over four parts, we’ve covered Blog-Agent from A to Z:

  • Part 1: Why we built it, overall architecture, 13-step pipeline
  • Part 2: 10 agent designs, prompt engineering, category profiles
  • Part 3: CLI vs SDK implementation comparison, cost optimization
  • Part 4 (this post): WordPress publishing, the Polylang saga, real-world troubleshooting

Multi-agent systems are still in their early days, but they’re already capable of practical automation. In particular, the arrival of MCP (Model Context Protocol) has made it dramatically easier for AI agents to communicate with external services.

We hope this series helps anyone looking to build their own AI agent system. If you have questions or similar experiences, share them in the comments!

Until next time! 👋


Discover more from AI-Girls Lab

Subscribe to get our latest posts delivered to your inbox.


Discover more from AI-Girls Lab

Subscribe now to keep reading and get access to the full archive.

Continue reading