Bilingual Blog and Full Publishing Pipeline
Bilingual Blog and Full Publishing Pipeline
Today was one of those days where I sit down to do one thing and end up doing seven. The plan was simple: get CN content online. What actually happened was a full audit of the publishing stack, two rewritten SOPs, a handful of React components, and a lesson in why agent guardrails matter. Splendid fun, really.
"The system you build to ship your work is itself work worth shipping."
— me, rationalising an entire day of infrastructure instead of actual content
The Publishing Pipeline: Wired End-to-End
The Ianfluencer content operation now has a real pipeline. In theory it existed before — Riti drafts, Poti posts. In practice, it was vague enough to break the moment anything non-trivial happened. Which, naturally, is exactly when you need it most.
So I made it concrete:
Riti's job: Draft EN content in Obsidian. Once finalised, translate to CN and save to the Posts CN/ folder with correct frontmatter including lang: "zh". Hand off to Poti with the file path. Simple enough, even for an overachieving writer bot.
Poti's job: Push EN to GitHub first. Confirm it's live on Vercel. Then distribute CN content to Zhihu, RedNote, WeChat, Weibo, Yuque, and Jianshu via wechatsync. Then post EN to X and create a Substack draft for manual review.
Both agents now have updated SOPs covering the full sequence — platform routing, credential expiry, error handling, and what to do when things go sideways (which they did, several times, today).
Bilingual Blog: The Technical Side
The GitHub blog's UI layer already had language switching built in — LanguageContext, LangText components, a toggle in the header. What it didn't have was any awareness of which posts to show in which language. Brilliant design, that.
The fix involved three things:
1. lib/posts.ts — lang field
Added a lang field to the PostData interface. Posts without the field default to "en". As a fallback, posts whose GitHub slug ends in -cn are automatically tagged "zh" — a safety net for any file that slips through without explicit frontmatter. Because they will.
2. Client-side post list components
The DevLog and Reflections pages were server components calling getPostsByType() and rendering the list inline. That pattern can't read the language context (client-side state). The fix: extract the list rendering into two new client components — DevLogPostList and ReflectionsPostList — that take all posts as a prop from the server and filter by the active language on the client. Server components stay static and fast; language filtering happens in the browser. Everyone wins.
3. Image path resolution
Thumbnails on CN posts were broken — the image frontmatter field stores just a filename (DevLog_20260421_Blog-Redesign.jpg) but the component was using it as a bare src. Added a resolveImage() helper that prepends /images/ to any bare filename, so images in public/images/ resolve correctly. The things I do for relative paths.
The Moment Poti Refused
This was the most interesting part of the day. After writing the code fixes, I asked Poti to push them to GitHub. She refused — correctly — citing her SOP's #1 rule: she is only allowed to touch files in content/posts/. Modifying lib/posts.ts or React components is explicitly off-limits.
She was right to refuse. The rule exists precisely to prevent a content agent from accidentally breaking the site by touching code. The fix was simple — I pushed the code changes myself directly from the local repo. But the moment raised something worth noting: the agent system has no dev agent. Code changes currently fall to me. That's fine for now, but it's a gap that will matter as the system grows. Remind me to complain about this later.
Substack, Finally
substack-mcp-plus is now configured in Hermes. Auth uses a session cookie grabbed from Chrome DevTools rather than browser automation (Playwright couldn't connect reliably, because of course it couldn't). The cookie is stored in ~/.hermes/config.yaml and ~/.config/substack-mcp-plus/credentials.json. Riti wrote an SOP so Poti can self-refresh the token when it expires — the process takes about three minutes and requires a fresh cookie value from me. Which means it takes me three minutes.
Running substack-mcp-plus --test standalone throws an error because the env vars aren't set outside of Hermes. This is expected. The real test is asking Poti to list recent posts from inside a Hermes session — that goes through the properly configured MCP. Works a treat.
What's Still Open
The CN posts are queued for Poti to re-push with the corrected frontmatter. There was a staging pass today but the lang: "zh" field was missing from the files already on GitHub, so no CN posts showed when toggling. Riti has since corrected the Obsidian source files; Poti's next task is to overwrite the GitHub versions.
LinkedIn remains a placeholder in the pipeline. No urgency — when the time comes, it drops into the existing pattern.
The blog is now, structurally, ready for bilingual operation. Whether the content actually gets there is a matter of consistently running the pipeline — which is now, at least, clearly documented. Whether I actually follow my own documentation is another matter entirely.
—————————————————————-
Ian Xie
April 22, 2026
ian.us.ci
