SuperAPI AI Directory

x-article-publisher

Writing & research

View repository

Updated 10-15 18:05

About this skill

X Article Publisher

Publish Markdown content to X (Twitter) Articles editor, preserving formatting with rich text conversion.

Prerequisites

  • Playwright MCP for browser automation
  • User logged into X with Premium Plus subscription
  • Python 3.9+ with dependencies:
    • macOS: pip install Pillow pyobjc-framework-Cocoa
    • Windows: pip install Pillow pywin32 clip-util
  • For Mermaid diagrams: npm install -g @mermaid-js/mermaid-cli

Scripts

Located in ~/.claude/skills/x-article-publisher/scripts/:

parse_markdown.py

Parse Markdown and extract structured data:

python parse_markdown.py <markdown_file> [--output json|html] [--html-only]

Returns JSON with: title, cover_image, content_images, dividers (with block_index for positioning), html, total_blocks

copy_to_clipboard.py

Copy image or HTML to system clipboard (cross-platform):

# Copy image (with optional compression)
python copy_to_clipboard.py image /path/to/image.jpg [--quality 80]

# Copy HTML for rich text paste
python copy_to_clipboard.py html --file /path/to/content.html

table_to_image.py

Convert Markdown table to PNG image:

python table_to_image.py <input.md> <output.png> [--scale 2]

Use when X Articles doesn't support native table rendering or for consistent styling.

Pre-Processing (Optional)

Before publishing, scan the Markdown for elements that need conversion:

Tables → PNG

# Extract table to temp file, then convert
python ~/.claude/skills/x-article-publisher/scripts/table_to_image.py /tmp/table.md /tmp/table.png
# Replace table in markdown with: ![Table](/tmp/table.png)

Mermaid Diagrams → PNG

# Extract mermaid block to .mmd file, then convert
mmdc -i /tmp/diagram.mmd -o /tmp/diagram.png -b white -s 2
# Replace mermaid block with: ![Diagram](/tmp/diagram.png)

Dividers (---)

Dividers are automatically detected by parse_markdown.py and output in the dividers array. They must be inserted via X Articles' Insert > Divider menu (HTML <hr> tags are ignored by X).

Workflow

Strategy: "Text First, Images Second, Dividers Last"

For articles with images and dividers, paste ALL text content first, then insert images and dividers at correct positions using block index.

  1. (Optional) Pre-process: Convert tables/mermaid to images
  2. Parse Markdown with Python script → get title, images, dividers with block_index, HTML
  3. Navigate to X Articles editor
  4. Upload cover image (first image)
  5. Fill title
  6. Copy HTML to clipboard (Python) → Paste with Cmd+V
  7. Insert content images at positions specified by block_index
  8. Insert dividers at positions specified by block_index (via Insert > Divider menu)
  9. Save as draft (NEVER auto-publish)

Efficiency Guidelines

Goal: Minimize waiting time between operations to achieve a smooth automated experience.

1. Avoid unnecessary browser_snapshot

Most browser operations (click, type, press_key, etc.) include the page state in their return result.Do notcall browser_snapshotseparately after each operation; just use the page state returned by the operation.

❌ 错误做法:
browser_click → browser_snapshot → 分析 → browser_click → browser_snapshot → ...

✅ 正确做法:
browser_click → 从返回结果中获取页面状态 → browser_click → ...

2. Avoid unnecessary browser_wait_for

Use browser_wait_for:

  • only in the following cases: waiting for image upload to complete (textGone="正在上传媒体")
  • waiting for initial page load (very rare cases)

Do notuse browser_wait_for to wait for buttons or input boxes to appear - they are immediately available after the page finishes loading.

3. Execute independent operations in parallel

When two operations have no dependency relationship, you can call multiple tools in parallel in the same message:

✅ 可以并行:
- 填写标题 (browser_type) + 复制HTML到剪贴板 (Bash)
- 解析Markdown生成JSON + 生成HTML文件

❌ 不能并行(有依赖):
- 必须先点击create才能上传封面图
- 必须先粘贴内容才能插入图片

4. Execute browser operations consecutively

The page state returned by each browser operation contains all the element references needed. Use these references directly for the next operation:

# 理想流程(每步直接执行,不额外等待):
browser_navigate → 从返回状态找create按钮 → browser_click(create)
→ 从返回状态找上传按钮 → browser_click(上传) → browser_file_upload
→ 从返回状态找应用按钮 → browser_click(应用)
→ 从返回状态找标题框 → browser_type(标题)
→ 点击编辑器 → browser_press_key(Meta+v)
→ ...

5. Do preparatory work in advance

Before starting browser operations, complete all preparatory work first:

  1. Parse Markdown to obtain JSON data
  2. Generate HTML files to /tmp/
  3. Record information such as title, cover_image, content_images, etc.

This way, the browser operation phase can be executed consecutively without stopping midway to process data.

Step 1: Parse Markdown (Python)

Use parse_markdown.py to extract all structured data:

python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md

Output JSON:

{
  "title": "Article Title",
  "cover_image": "/path/to/first-image.jpg",
  "cover_exists": true,
  "content_images": [
    {"path": "/path/to/img2.jpg", "original_path": "/md/dir/assets/img2.jpg", "exists": true, "block_index": 5, "after_text": "context..."},
    {"path": "/path/to/img3.jpg", "original_path": "/md/dir/assets/img3.jpg", "exists": true, "block_index": 12, "after_text": "another..."}
  ],
  "html": "<p>Content...</p><h2>Section</h2>...",
  "total_blocks": 45,
  "missing_images": 0
}

Key fields:

  • block_index: The image should be inserted AFTER block element at this index (0-indexed)
  • total_blocks: Total number of block elements in the HTML
  • after_text: Kept for reference/debugging only, NOT for positioning
  • exists: Whether the image file was found (if false, upload will fail)
  • original_path: The path resolved from Markdown (before auto-search)
  • path: The actual path to use (may differ from original_path if auto-searched)
  • missing_images: Count of images not found anywhere

Save HTML to temp file for clipboard:

python parse_markdown.py article.md --html-only > /tmp/article_html.html

Step 2: Open X Articles Editor

Browser error handling

If you encounter Error: Browser is already in use error:

# 方案1:先关闭浏览器再重新打开
browser_close
browser_navigate: https://x.com/compose/articles

# 方案2:如果 browser_close 无效(锁定),提示用户手动关闭 Chrome

# 方案3:使用已有标签页,直接导航
browser_tabs action=list  # 查看现有标签
browser_navigate: https://x.com/compose/articles  # 在当前标签导航

Best practice: Before each start, first use browser_tabs action=list to check the status and avoid creating extra blank tabs.

Navigate to the editor

browser_navigate: https://x.com/compose/articles

Important: After the page loads, it will display the draft list, not the editor. You need to:

  1. Wait for the page to finish loading: Use browser_snapshot to check the page status
  2. Immediately click the "create" button: Do not wait for editor elements such as "Add title"; they only appear after clicking create
  3. Wait for the editor to load: After clicking create, wait for the editor elements to appear
# 1. 导航到页面
browser_navigate: https://x.com/compose/articles

# 2. 获取页面快照,找到 create 按钮
browser_snapshot

# 3. 点击 create 按钮(通常 ref 类似 "create" 或带有 create 标签)
browser_click: element="create button", ref=<create_button_ref>

# 4. 现在编辑器应该打开了,可以继续上传封面图等操作

Note: Do not use browser_wait_for text="添加标题" to wait for the page to load, because this text only appears after clicking create, which will cause a timeout.

If login needed, prompt user to log in manually.

Step 3: Upload Cover Image

  1. Click "Add photos or videos" button
  2. Use browser_file_upload with the cover image path (from JSON output)
  3. Verify image uploaded

Step 4: Fill Title

  • Find textbox with "Add title" placeholder
  • Use browser_type to input title (from JSON output)

Step 5: Paste Text Content (Python Clipboard)

Copy HTML to system clipboard using Python, then paste:

# Copy HTML to clipboard
python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html

Then in browser:

browser_click on editor textbox
browser_press_key: Meta+v

This preserves all rich text formatting (H2, bold, links, lists).

Step 6: Insert Content Images (Text Search Positioning)

Recommended method: Use after_text text search for positioning, which is more intuitive and reliable than block_index .

Positioning principle

Each image's after_text field records the ending text of the paragraph before it (up to 80 characters). Search the editor for the paragraph containing that text, click it, and then insert the image.

Operation steps

For each content image (from content_images array), In descending order of block_index:

# 1. Copy image to clipboard (with compression)
python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py image /path/to/img.jpg --quality 85
# 2. 在 browser_snapshot 中搜索包含 after_text 的段落
#    找到该段落的 ref

# 3. Click the paragraph containing after_text
browser_click: element="paragraph with target text", ref=<paragraph_ref>

# 4. **关键步骤**: 按 End 键移动光标到行尾
#    这一步非常重要!避免点击到段落中的链接导致位置偏移
browser_press_key: End

# 5. Paste image
browser_press_key: Meta+v

# 6. Wait for upload (only use textGone, no time parameter)
browser_wait_for textGone="正在上传媒体"

Why is the End key needed?

Problem: When a paragraph contains a link (such as [链接文字](url)), clicking the paragraph may:

  • Trigger the link editing popup
  • Position the cursor inside the link rather than at the end of the paragraph

Solution: After clicking the paragraph, immediately press the End key:

  • Ensure the cursor moves to the end of the paragraph
  • Avoid link interference
  • The image will be correctly inserted after that paragraph

Positioning strategy

In the structure returned by browser_snapshot, search for after_text keywords:

textbox [ref=editor]:
  generic [ref=p1]:
    - StaticText: "元旦假期我在家里翻手机相册..."  # 如果 after_text 包含这段文字,点击 p1
  heading [ref=h1]:
    - StaticText: "演示"
  generic [ref=p2]:
    - StaticText: "这东西到底有多省事儿?"
    - link [ref=link1]: "Claude Code"  # 注意:段落可能包含链接
  ...

Reverse insertion example

If there are 3 images with block_index 5, 12, 27 respectively:

  1. First insert the image with block_index=27 (after_text search + End + paste)
  2. Then insert the image with block_index=12
  3. Finally insert the image with block_index=5

Insert from largest to smallestThis can avoid position offset issues.

Step 6.5: Insert Dividers (Via Menu)

Important: The --- divider in Markdown cannot be pasted via the HTML <hr> tag (X Articles will ignore it). It must be inserted via the X Articles Insert menu.

Steps

For each divider (from dividers array), in reverse order of block_index:

# 1. Click the block element at block_index position
browser_click on the element at position block_index in the editor

# 2. Open Insert menu (Add Media button)
browser_click on "Insert" or "添加媒体" button

# 3. Click Divider menu item
browser_click on "Divider" or "分割线" menuitem

# Divider is inserted at cursor position

and the insertion order of images

It is recommended to insert all images first, then all dividers. Both follow the block_index from largest to smallestorder:

  1. Insert all images (starting from the largest block_index)
  2. Insert all dividers (starting from the largest block_index)

Step 7: Save Draft

  1. Verify content pasted (check word count indicator)
  2. Draft auto-saves, or click Save button if needed
  3. Click "Preview" to verify formatting
  4. Report: "Draft saved. Review and publish manually."

Critical Rules

  1. NEVER publish - Only save draft
  2. First image = cover - Upload first image as cover image
  3. Rich text conversion - Always convert Markdown to HTML before pasting
  4. Use clipboard API - Paste via clipboard for proper formatting
  5. Block index positioning - Use block_index for precise image/divider placement
  6. Reverse order insertion - Insert images and dividers from highest to lowest block_index
  7. H1 title handling - H1 is used as title only, not included in body
  8. Dividers via menu - Markdown --- must be inserted via Insert > Divider menu (HTML <hr> is ignored)

Supported Formatting

Element Support Notes
H2 (##) Native Section headers
Bold (**) Native Strong emphasis
Italic (*) Native Emphasis
Links ([](url)) Native Hyperlinks
Ordered lists Native 1. 2. 3.
Unordered lists Native - bullets
Blockquotes (>) Native Quoted text
Code blocks Converted → Blockquotes
Tables Converted → PNG images (use table_to_image.py)
Mermaid Converted → PNG images (use mmdc)
Dividers (---) Menu insert → Insert > Divider

Example Flow

User: "Publish /path/to/article.md to X"

# Step 1: Parse Markdown
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md > /tmp/article.json
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md --html-only > /tmp/article_html.html
  1. Navigate to https://x.com/compose/articles
  2. Upload cover image (browser_file_upload for cover only)
  3. Fill title (from JSON: title)
  4. Copy & paste HTML:
    python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html
    
    Then: browser_press_key Meta+v
  5. For each content image, in reverse order of block_index:
    python copy_to_clipboard.py image /path/to/img.jpg --quality 85
    
    • Click block element at block_index position
    • browser_press_key Meta+v
    • Wait until upload complete
  6. Verify in preview
  7. "Draft saved. Please review and publish manually."

Best Practices

Why use block_index instead of text matching?

  1. Precise positioning: Does not depend on text content, and can correctly position even when multiple texts are similar
  2. Reliability: The index is deterministic and will not be confused by similar text
  3. Easy debugging: after_text is still retained for manual verification

Why use Python instead of in-browser JavaScript?

  1. Local processing is more reliable: Python directly manipulates the system clipboard, unaffected by browser sandbox restrictions
  2. Image compression: Compress images before uploading (--quality 85) to reduce upload time
  3. Code reuse: The script stays fixed, with no need to rewrite the conversion logic each time
  4. Easy debugging: The script can be tested separately, making issues easy to locate

Waiting strategy

Important finding: Playwright MCP's browser_wait_for actual behavior is first wait time seconds, then check the condition, rather than polling!

// 实际执行的代码:
await new Promise(f => setTimeout(f, time * 1000));  // 先固定等待
await page.getByText("xxx").waitFor({ state: 'hidden' });  // 再检查

Correct usage:

  • ✅ Use only textGone, do not set time: Let Playwright poll and wait on its own
  • ✅ Use only time: Fixed wait for the specified number of seconds
  • ❌ Using both textGone + time: It will first wait time seconds then check, wasting time
# 推荐:只用 textGone,让它自动等待条件满足
browser_wait_for textGone="正在上传媒体"

# 或者:用 browser_snapshot 轮询检查状态
# 每次操作后检查返回的页面状态,无需额外等待

Image insertion efficiency

Browser operations per image are reduced from 5 steps to 2 steps:

  • Old: Click → Add media → Media → Add photo → file_upload
  • New: Click paragraph → Meta+v

Cover image vs content image

  • Cover image: Use browser_file_upload (because there is a dedicated upload button)
  • Content image: Use Python clipboard + paste (more efficient)

Troubleshooting

MCP connection issues

If the Playwright MCP tool is unavailable (error No such tool available or Not connected):

Option 1: Reconnect MCP (recommended)

执行 /mcp 命令,选择 playwright,选择 Restart

Option 2: Clean up residual processes and reconnect

# 杀掉所有残留的 playwright 进程
pkill -f "mcp-server-playwright"
pkill -f "@playwright/mcp"

# 然后执行 /mcp 重新连接

Configuration file location: ~/.claude/mcp_servers.json

Browser error handling

If you encounter Error: Browser is already in use error:

# 方案1:先关闭浏览器再重新打开
browser_close
browser_navigate: https://x.com/compose/articles

# 方案2:杀掉 Chrome 进程
pkill -f "Chrome.*--remote-debugging"
# 然后重新 navigate

Image position offset

If the image insertion position is incorrect (especially when clicking a paragraph containing a link):

Cause: When clicking a paragraph, you may accidentally trigger a link, causing the cursor position to be incorrect

Solution: After clicking,you must press the End keyto move the cursor to the end of the line

# 正确流程
1. browser_click 点击目标段落
2. browser_press_key: End        # 关键步骤!
3. browser_press_key: Meta+v     # 粘贴图片
4. browser_wait_for textGone="正在上传媒体"

Image path not found

If a relative-path image in Markdown cannot be found (e.g., ./assets/image.png is actually in another location):

Automatic search: parse_markdown.py will automatically search for a file with the same name in the following directories:

  • ~/Downloads
  • ~/Desktop
  • ~/Pictures

stderr output example:

[parse_markdown] Image not found at '/path/to/assets/img.png', using '/Users/xxx/Downloads/img.png' instead

JSON field description:

  • original_path: The path specified in Markdown (resolved absolute path)
  • path: The path actually used (if the automatic search succeeds, it will differ from original_path)
  • exists: true indicates the file was found,false indicates it was not found (the upload will fail)

If it still cannot be found:

  1. Check the missing_images field in the JSON output
  2. Manually copy the image to the same directory as the Markdown file, into the assets/ subdirectory
  3. Or change the image path in Markdown to an absolute path