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:
- Image upload: Upload featured, closing, and diagram images to WordPress → get Media IDs
- Path replacement: Swap
NAS:prefix paths in HTML with actual WordPress URLs - Post creation: Publish the Korean post via the WordPress API
- 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:
| Method | Auth | Pros | Issues |
|---|---|---|---|
| REST API | Bearer Token (renew every 2 weeks) | Stable, well-tested | Token expiration management |
| WordPress MCP | OAuth 2.1 (automatic) | No token renewal needed | Parameter 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 Type | Resize | Reason |
|---|---|---|
| Featured/Closing | Max 1920px | Optimize loading speed |
| Diagrams | Max 1500px | Balance clarity and speed |
| Demo/Sample images | Keep original | Content value — readers need the detail |
| Emoji | Skip (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:
- Fragile against UI changes: When WordPress.com updates the Gutenberg UI, selectors break
- “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_posthook 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_editlinks = 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:
| Feature | REST API | WordPress MCP |
|---|---|---|
| Auth | Bearer Token (renew every 2 weeks) | OAuth 2.1 automatic |
| Post CRUD | Manual HTTP requests | One-line tool call |
| Media upload | REST (multipart) | Not supported (use REST alongside) |
| Statistics | N/A | site-statistics (new) |
| Polylang | Not supported | Not 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:
| Item | Manual | Blog-Agent |
|---|---|---|
| 1 post in 3 languages | 3-4 hours | ~15 minutes |
| Image generation | Manual prompting | Automatic (Style Anchor) |
| SEO optimization | Manual research | Automatic keyword analysis |
| Polylang linking | Manual wp-admin clicks | Playwright automation |
| Source labeling | Manual | Automatic (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! 👋