# How to Automate Publishing to CSDN and WeChat MP Using Playwright (When APIs Fail)

> Source: <https://dev.to/quarktimes/how-to-automate-publishing-to-csdn-and-wechat-mp-using-playwright-when-apis-fail-4g01>
> Published: 2026-06-15 03:05:28+00:00

Today's focus was on automating article publishing to CSDN and WeChat MP (微信公众号) using Playwright, after CSDN deprecated its public Open API. Key achievements include: injecting Markdown content into CSDN's dynamic editor, handling title input quirks, implementing QR code login for WeChat MP, updating the Dev.to API publisher, and consolidating platform configs into a single YAML file. We also fixed session log capture after a Claude Code update changed the log file path.

**Background**: In early 2026, CSDN silently shut down its public Open API. All endpoints returned 404/403. We needed a fallback to keep publishing to China's largest developer platform.

**Solution**: Use Playwright to simulate a real user login and article creation. The approach:

`csdn_cookies.json`

.**Code snippet**:

``` python
import asyncio
from playwright.async_api import async_playwright

async def publish_to_csdn(title: str, content_md: str):
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context(storage_state="csdn_cookies.json" if exists else None)
        page = await context.new_page()
        await page.goto("https://mp.csdn.net/mp_blog/creation/editor")
        # Inject content
        await page.evaluate(f'''() => {{
            const editor = document.querySelector('.editor-content');
            if (editor) {{
                editor.innerHTML = `{escaped_content}`;
                editor.dispatchEvent(new Event('input', {{ bubbles: true }}));
            }}
        }}''')
        # Fill title
        await page.fill('#title-input', title)
        await page.click('button:has-text("发布")')
        await page.wait_for_url("**/mp_blog/manage/article*")
        if not exists:
            await context.storage_state(path="csdn_cookies.json")
        await browser.close()
```

**Result**: First run requires manual QR scan; subsequent runs are fully automated. The browser approach is 3–5 seconds slower than an API call, but it works.

**Problem**: CSDN's Markdown editor is not a simple `<textarea>`

. It's a nested rich-text component with shadow DOM and dynamic elements. `page.fill()`

and `page.type()`

failed to inject content correctly.

**Root Cause**: The editor uses `contenteditable`

but its state is managed by a frontend framework (Vue/React). Direct fill doesn't trigger the internal state update.

**Solution**: Use `page.evaluate()`

to set `innerHTML`

and manually dispatch an `input`

event. For the title input, first focus, then simulate typing with `page.keyboard.type()`

with a delay.

```
await page.click('#title-input')
await page.wait_for_timeout(300)
await page.keyboard.type(title, delay=50)
```

**Result**: Content and title injection now works reliably over 10 consecutive tests.

**Background**: After upgrading to Claude Code 2.1.143, our session capture hook found no data in `~/.claude/history.jsonl`

.

**Root Cause**: Version 2.1.143 moved per-project logs to `~/.claude/projects/<project-name>/logs/`

.

**Solution**: Update the hook to check the new path first, with a fallback to the old path. Also detect version to decide.

``` python
import pathlib
import subprocess

def get_history_path():
    version = subprocess.run(["claude", "--version"], capture_output=True, text=True).stdout
    if parse_version(version) >= (2, 1, 143):
        return pathlib.Path.home() / ".claude" / "projects" / get_current_project() / "logs"
    else:
        return pathlib.Path.home() / ".claude" / "history.jsonl"
```

**Result**: Session capture works again without data loss.

**Chosen**: Playwright for browser automation.

**Alternatives**: Selenium WebDriver + ChromeDriver.

**Why**:

`time.sleep()`

.**Trade-off**: Larger package size (≈100MB), less team familiarity. But stability wins.

**Chosen**: Store all platform settings (publisher class, cookie file, selectors, endpoints) in `platforms.yaml`

.

**Alternatives**: Hardcode configs or use environment variables.

**Why**:

```
platforms:
  csdn:
    publisher_class: publishers.csdn.CSDNPublisher
    login_url: "https://passport.csdn.net/login"
    editor_url: "https://mp.csdn.net/mp_blog/creation/editor"
    cookie_file: "csdn_cookies.json"
  wechat_mp:
    publisher_class: publishers.wechat_mp.WeChatMPPublisher
    login_qrcode_selector: "#login-qrcode"
    cookie_file: "wechat_cookies.json"
```

**Trade-off**: Requires validation and error handling, but long-term maintenance is easier.

**Chosen**: Use Playwright to automate WeChat MP login via QR code scanning, then cache cookies.

**Alternatives**: Unofficial APIs (risky, may be banned).

**Why**:

**Trade-off**: Requires human intervention on first run. But can be mitigated by notification to ops team.

Today's work proves that multi-platform publishing is feasible even without open APIs. The key is building flexible and resilient automation that can adapt to real-world changes.
