Add this skill
npx mdskills install wshuyi/x-article-publisherComprehensive automation for publishing Markdown to X Articles with precise image/divider positioning and rich text preservation
1---2name: x-article-publisher3description: |4 Publish Markdown articles to X (Twitter) Articles editor with proper formatting. Use when user wants to publish a Markdown file/URL to X Articles, or mentions "publish to X", "post article to Twitter", "X article", or wants help with X Premium article publishing. Handles cover image upload and converts Markdown to rich text automatically.5---67# X Article Publisher89Publish Markdown content to X (Twitter) Articles editor, preserving formatting with rich text conversion.1011## Prerequisites1213- Playwright MCP for browser automation14- User logged into X with Premium Plus subscription15- Python 3.9+ with dependencies:16 - macOS: `pip install Pillow pyobjc-framework-Cocoa`17 - Windows: `pip install Pillow pywin32 clip-util`18- For Mermaid diagrams: `npm install -g @mermaid-js/mermaid-cli`1920## Scripts2122Located in `~/.claude/skills/x-article-publisher/scripts/`:2324### parse_markdown.py25Parse Markdown and extract structured data:26```bash27python parse_markdown.py <markdown_file> [--output json|html] [--html-only]28```29Returns JSON with: title, cover_image, content_images, **dividers** (with block_index for positioning), html, total_blocks3031### copy_to_clipboard.py32Copy image or HTML to system clipboard (cross-platform):33```bash34# Copy image (with optional compression)35python copy_to_clipboard.py image /path/to/image.jpg [--quality 80]3637# Copy HTML for rich text paste38python copy_to_clipboard.py html --file /path/to/content.html39```4041### table_to_image.py42Convert Markdown table to PNG image:43```bash44python table_to_image.py <input.md> <output.png> [--scale 2]45```46Use when X Articles doesn't support native table rendering or for consistent styling.4748## Pre-Processing (Optional)4950Before publishing, scan the Markdown for elements that need conversion:5152### Tables → PNG53```bash54# Extract table to temp file, then convert55python ~/.claude/skills/x-article-publisher/scripts/table_to_image.py /tmp/table.md /tmp/table.png56# Replace table in markdown with: 57```5859### Mermaid Diagrams → PNG60```bash61# Extract mermaid block to .mmd file, then convert62mmdc -i /tmp/diagram.mmd -o /tmp/diagram.png -b white -s 263# Replace mermaid block with: 64```6566### Dividers (---)67Dividers 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).6869## Workflow7071**Strategy: "先文后图后分割线" (Text First, Images Second, Dividers Last)**7273For articles with images and dividers, paste ALL text content first, then insert images and dividers at correct positions using block index.74751. **(Optional)** Pre-process: Convert tables/mermaid to images762. Parse Markdown with Python script → get title, images, **dividers** with block_index, HTML773. Navigate to X Articles editor784. Upload cover image (first image)795. Fill title806. Copy HTML to clipboard (Python) → Paste with Cmd+V817. Insert content images at positions specified by block_index828. **Insert dividers at positions specified by block_index** (via Insert > Divider menu)839. Save as draft (NEVER auto-publish)8485## 高效执行原则 (Efficiency Guidelines)8687**目标**: 最小化操作之间的等待时间,实现流畅的自动化体验。8889### 1. 避免不必要的 browser_snapshot9091大多数浏览器操作(click, type, press_key 等)都会在返回结果中包含页面状态。**不要**在每次操作后单独调用 `browser_snapshot`,直接使用操作返回的页面状态即可。9293```94❌ 错误做法:95browser_click → browser_snapshot → 分析 → browser_click → browser_snapshot → ...9697✅ 正确做法:98browser_click → 从返回结果中获取页面状态 → browser_click → ...99```100101### 2. 避免不必要的 browser_wait_for102103只在以下情况使用 `browser_wait_for`:104- 等待图片上传完成(`textGone="正在上传媒体"`)105- 等待页面初始加载(极少数情况)106107**不要**使用 `browser_wait_for` 来等待按钮或输入框出现 - 它们在页面加载完成后立即可用。108109### 3. 并行执行独立操作110111当两个操作没有依赖关系时,可以在同一个消息中并行调用多个工具:112113```114✅ 可以并行:115- 填写标题 (browser_type) + 复制HTML到剪贴板 (Bash)116- 解析Markdown生成JSON + 生成HTML文件117118❌ 不能并行(有依赖):119- 必须先点击create才能上传封面图120- 必须先粘贴内容才能插入图片121```122123### 4. 连续执行浏览器操作124125每个浏览器操作返回的页面状态包含所有需要的元素引用。直接使用这些引用进行下一步操作:126127```128# 理想流程(每步直接执行,不额外等待):129browser_navigate → 从返回状态找create按钮 → browser_click(create)130→ 从返回状态找上传按钮 → browser_click(上传) → browser_file_upload131→ 从返回状态找应用按钮 → browser_click(应用)132→ 从返回状态找标题框 → browser_type(标题)133→ 点击编辑器 → browser_press_key(Meta+v)134→ ...135```136137### 5. 准备工作前置138139在开始浏览器操作之前,先完成所有准备工作:1401. 解析 Markdown 获取 JSON 数据1412. 生成 HTML 文件到 /tmp/1423. 记录 title、cover_image、content_images 等信息143144这样浏览器操作阶段可以连续执行,不需要中途停下来处理数据。145146## Step 1: Parse Markdown (Python)147148Use `parse_markdown.py` to extract all structured data:149150```bash151python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md152```153154Output JSON:155```json156{157 "title": "Article Title",158 "cover_image": "/path/to/first-image.jpg",159 "cover_exists": true,160 "content_images": [161 {"path": "/path/to/img2.jpg", "original_path": "/md/dir/assets/img2.jpg", "exists": true, "block_index": 5, "after_text": "context..."},162 {"path": "/path/to/img3.jpg", "original_path": "/md/dir/assets/img3.jpg", "exists": true, "block_index": 12, "after_text": "another..."}163 ],164 "html": "<p>Content...</p><h2>Section</h2>...",165 "total_blocks": 45,166 "missing_images": 0167}168```169170**Key fields:**171- `block_index`: The image should be inserted AFTER block element at this index (0-indexed)172- `total_blocks`: Total number of block elements in the HTML173- `after_text`: Kept for reference/debugging only, NOT for positioning174- `exists`: Whether the image file was found (if false, upload will fail)175- `original_path`: The path resolved from Markdown (before auto-search)176- `path`: The actual path to use (may differ from original_path if auto-searched)177- `missing_images`: Count of images not found anywhere178179Save HTML to temp file for clipboard:180```bash181python parse_markdown.py article.md --html-only > /tmp/article_html.html182```183184## Step 2: Open X Articles Editor185186### 浏览器错误处理187188如果遇到 `Error: Browser is already in use` 错误:189190```191# 方案1:先关闭浏览器再重新打开192browser_close193browser_navigate: https://x.com/compose/articles194195# 方案2:如果 browser_close 无效(锁定),提示用户手动关闭 Chrome196197# 方案3:使用已有标签页,直接导航198browser_tabs action=list # 查看现有标签199browser_navigate: https://x.com/compose/articles # 在当前标签导航200```201202**最佳实践**:每次开始前先用 `browser_tabs action=list` 检查状态,避免创建多余空白标签。203204### 导航到编辑器205206```207browser_navigate: https://x.com/compose/articles208```209210**重要**: 页面加载后会显示草稿列表,不是编辑器。需要:2112121. **等待页面加载完成**: 使用 `browser_snapshot` 检查页面状态2132. **立即点击 "create" 按钮**: 不要等待 "添加标题" 等编辑器元素,它们只有点击 create 后才出现2143. **等待编辑器加载**: 点击 create 后,等待编辑器元素出现215216```217# 1. 导航到页面218browser_navigate: https://x.com/compose/articles219220# 2. 获取页面快照,找到 create 按钮221browser_snapshot222223# 3. 点击 create 按钮(通常 ref 类似 "create" 或带有 create 标签)224browser_click: element="create button", ref=<create_button_ref>225226# 4. 现在编辑器应该打开了,可以继续上传封面图等操作227```228229**注意**: 不要使用 `browser_wait_for text="添加标题"` 来等待页面加载,因为这个文本只有在点击 create 后才出现,会导致超时。230231If login needed, prompt user to log in manually.232233## Step 3: Upload Cover Image2342351. Click "添加照片或视频" button2362. Use browser_file_upload with the cover image path (from JSON output)2373. Verify image uploaded238239## Step 4: Fill Title240241- Find textbox with "添加标题" placeholder242- Use browser_type to input title (from JSON output)243244## Step 5: Paste Text Content (Python Clipboard)245246Copy HTML to system clipboard using Python, then paste:247248```bash249# Copy HTML to clipboard250python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html251```252253Then in browser:254```255browser_click on editor textbox256browser_press_key: Meta+v257```258259This preserves all rich text formatting (H2, bold, links, lists).260261## Step 6: Insert Content Images (Text Search Positioning)262263**推荐方法**: 使用 `after_text` 文字搜索定位,比 `block_index` 更直观可靠。264265### 定位原理266267每张图片的 `after_text` 字段记录了它前一个段落的末尾文字(最多80字符)。在编辑器中搜索包含该文字的段落,点击后插入图片。268269### 操作步骤270271For each content image (from `content_images` array), **按 block_index 从大到小的顺序**:272273```bash274# 1. Copy image to clipboard (with compression)275python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py image /path/to/img.jpg --quality 85276```277278```279# 2. 在 browser_snapshot 中搜索包含 after_text 的段落280# 找到该段落的 ref281282# 3. Click the paragraph containing after_text283browser_click: element="paragraph with target text", ref=<paragraph_ref>284285# 4. **关键步骤**: 按 End 键移动光标到行尾286# 这一步非常重要!避免点击到段落中的链接导致位置偏移287browser_press_key: End288289# 5. Paste image290browser_press_key: Meta+v291292# 6. Wait for upload (only use textGone, no time parameter)293browser_wait_for textGone="正在上传媒体"294```295296### 为什么需要按 End 键?297298**问题**: 当段落包含链接时(如 `[链接文字](url)`),点击段落可能会:299- 触发链接编辑弹窗300- 将光标定位在链接内部而非段落末尾301302**解决方案**: 点击段落后立即按 `End` 键:303- 确保光标移动到段落末尾304- 避免链接干扰305- 图片将正确插入在该段落之后306307### 定位策略308309在 browser_snapshot 返回的结构中,搜索 `after_text` 的关键词:310311```yaml312textbox [ref=editor]:313 generic [ref=p1]:314 - StaticText: "元旦假期我在家里翻手机相册..." # 如果 after_text 包含这段文字,点击 p1315 heading [ref=h1]:316 - StaticText: "演示"317 generic [ref=p2]:318 - StaticText: "这东西到底有多省事儿?"319 - link [ref=link1]: "Claude Code" # 注意:段落可能包含链接320 ...321```322323### 反向插入示例324325如果有3张图片,block_index 分别为 5, 12, 27:3261. 先插入 block_index=27 的图片(after_text 搜索 + End + 粘贴)3272. 再插入 block_index=12 的图片3283. 最后插入 block_index=5 的图片329330**从大到小插入**可以避免位置偏移问题。331332## Step 6.5: Insert Dividers (Via Menu)333334**重要**: Markdown 中的 `---` 分割线不能通过 HTML `<hr>` 标签粘贴(X Articles 会忽略它)。必须通过 X Articles 的 Insert 菜单插入。335336### 操作步骤337338For each divider (from `dividers` array), in **reverse order of block_index**:339340```341# 1. Click the block element at block_index position342browser_click on the element at position block_index in the editor343344# 2. Open Insert menu (Add Media button)345browser_click on "Insert" or "添加媒体" button346347# 3. Click Divider menu item348browser_click on "Divider" or "分割线" menuitem349350# Divider is inserted at cursor position351```352353### 与图片的插入顺序354355建议先插入所有图片,再插入所有分割线。两者都按 block_index **从大到小**的顺序:3563571. 插入所有图片(从最大 block_index 开始)3582. 插入所有分割线(从最大 block_index 开始)359360## Step 7: Save Draft3613621. Verify content pasted (check word count indicator)3632. Draft auto-saves, or click Save button if needed3643. Click "预览" to verify formatting3654. Report: "Draft saved. Review and publish manually."366367## Critical Rules3683691. **NEVER publish** - Only save draft3702. **First image = cover** - Upload first image as cover image3713. **Rich text conversion** - Always convert Markdown to HTML before pasting3724. **Use clipboard API** - Paste via clipboard for proper formatting3735. **Block index positioning** - Use block_index for precise image/divider placement3746. **Reverse order insertion** - Insert images and dividers from highest to lowest block_index3757. **H1 title handling** - H1 is used as title only, not included in body3768. **Dividers via menu** - Markdown `---` must be inserted via Insert > Divider menu (HTML `<hr>` is ignored)377378## Supported Formatting379380| Element | Support | Notes |381|---------|---------|-------|382| H2 (`##`) | Native | Section headers |383| Bold (`**`) | Native | Strong emphasis |384| Italic (`*`) | Native | Emphasis |385| Links (`[](url)`) | Native | Hyperlinks |386| Ordered lists | Native | 1. 2. 3. |387| Unordered lists | Native | - bullets |388| Blockquotes (`>`) | Native | Quoted text |389| Code blocks | Converted | → Blockquotes |390| Tables | Converted | → PNG images (use table_to_image.py) |391| Mermaid | Converted | → PNG images (use mmdc) |392| Dividers (`---`) | Menu insert | → Insert > Divider |393394## Example Flow395396User: "Publish /path/to/article.md to X"397398```bash399# Step 1: Parse Markdown400python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md > /tmp/article.json401python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md --html-only > /tmp/article_html.html402```4034042. Navigate to https://x.com/compose/articles4053. Upload cover image (browser_file_upload for cover only)4064. Fill title (from JSON: `title`)4075. Copy & paste HTML:408 ```bash409 python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html410 ```411 Then: browser_press_key Meta+v4126. For each content image, **in reverse order of block_index**:413 ```bash414 python copy_to_clipboard.py image /path/to/img.jpg --quality 85415 ```416 - Click block element at `block_index` position417 - browser_press_key Meta+v418 - Wait until upload complete4197. Verify in preview4208. "Draft saved. Please review and publish manually."421422## Best Practices423424### 为什么用 block_index 而非文字匹配?4254261. **精确定位**: 不依赖文字内容,即使多处文字相似也能正确定位4272. **可靠性**: 索引是确定性的,不会因为文字相似而混淆4283. **调试方便**: `after_text` 仍保留用于人工核验429430### 为什么用 Python 而非浏览器内 JavaScript?4314321. **本地处理更可靠**: Python 直接操作系统剪贴板,不受浏览器沙盒限制4332. **图片压缩**: 上传前压缩图片 (--quality 85),减少上传时间4343. **代码复用**: 脚本固定不变,无需每次重新编写转换逻辑4354. **调试方便**: 脚本可单独测试,问题易定位436437### 等待策略438439**重要发现**: Playwright MCP 的 `browser_wait_for` 实际行为是 **先等待 time 秒,再检查条件**,而非轮询!440441```javascript442// 实际执行的代码:443await new Promise(f => setTimeout(f, time * 1000)); // 先固定等待444await page.getByText("xxx").waitFor({ state: 'hidden' }); // 再检查445```446447**正确用法**:448- ✅ 只用 `textGone`,不设 `time`:让 Playwright 自己轮询等待449- ✅ 只用 `time`:固定等待指定秒数450- ❌ 同时用 `textGone` + `time`:会先等 time 秒再检查,浪费时间451452```453# 推荐:只用 textGone,让它自动等待条件满足454browser_wait_for textGone="正在上传媒体"455456# 或者:用 browser_snapshot 轮询检查状态457# 每次操作后检查返回的页面状态,无需额外等待458```459460### 图片插入效率461462每张图片的浏览器操作从5步减少到2步:463- 旧: 点击 → 添加媒体 → 媒体 → 添加照片 → file_upload464- 新: 点击段落 → Meta+v465466### 封面图 vs 内容图467468- **封面图**: 使用 browser_file_upload(因为有专门的上传按钮)469- **内容图**: 使用 Python 剪贴板 + 粘贴(更高效)470471## 故障排除472473### MCP 连接问题474475如果 Playwright MCP 工具不可用(报错 `No such tool available` 或 `Not connected`):476477**方案1:重新连接 MCP(推荐)**478```479执行 /mcp 命令,选择 playwright,选择 Restart480```481482**方案2:清理残留进程后重连**483```bash484# 杀掉所有残留的 playwright 进程485pkill -f "mcp-server-playwright"486pkill -f "@playwright/mcp"487488# 然后执行 /mcp 重新连接489```490491**配置文件位置**: `~/.claude/mcp_servers.json`492493### 浏览器错误处理494495如果遇到 `Error: Browser is already in use` 错误:496497```bash498# 方案1:先关闭浏览器再重新打开499browser_close500browser_navigate: https://x.com/compose/articles501502# 方案2:杀掉 Chrome 进程503pkill -f "Chrome.*--remote-debugging"504# 然后重新 navigate505```506507### 图片位置偏移508509如果图片插入位置不正确(特别是点击含链接的段落时):510511**原因**: 点击段落时可能误触链接,导致光标位置错误512513**解决方案**: 点击后**必须按 End 键**移动光标到行尾514515```516# 正确流程5171. browser_click 点击目标段落5182. browser_press_key: End # 关键步骤!5193. browser_press_key: Meta+v # 粘贴图片5204. browser_wait_for textGone="正在上传媒体"521```522523### 图片路径找不到524525如果 Markdown 中的相对路径图片找不到(如 `./assets/image.png` 实际在其他位置):526527**自动搜索**: `parse_markdown.py` 会自动在以下目录搜索同名文件:528- `~/Downloads`529- `~/Desktop`530- `~/Pictures`531532**stderr 输出示例**:533```534[parse_markdown] Image not found at '/path/to/assets/img.png', using '/Users/xxx/Downloads/img.png' instead535```536537**JSON 字段说明**:538- `original_path`: Markdown 中指定的路径(解析后的绝对路径)539- `path`: 实际使用的路径(如果自动搜索成功,会不同于 original_path)540- `exists`: `true` 表示找到文件,`false` 表示未找到(上传会失败)541542**如果仍然找不到**:5431. 检查 JSON 输出中的 `missing_images` 字段5442. 手动将图片复制到 Markdown 文件同目录的 `assets/` 子目录5453. 或修改 Markdown 中的图片路径为绝对路径546
Full transparency — inspect the skill content before installing.