--- name: x-article-publisher description: | 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. --- # 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 - Dedicated persistent browser profile (recommended) to avoid repeated login - 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 `~/.codex/skills/x-article-publisher/scripts/`: ### parse_markdown.py Parse Markdown and extract structured data: ```bash python parse_markdown.py [--output json|html] [--html-only] ``` Returns JSON with: title, cover_image, content_media, content_images, content_videos, **dividers** (with block_index for positioning), html, total_blocks ### copy_to_clipboard.py Copy image or HTML to system clipboard (cross-platform): ```bash # 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: ```bash python table_to_image.py [--scale 2] ``` Use when X Articles doesn't support native table rendering or for consistent styling. ### open_x_articles_browser.sh Open X Articles with a dedicated persistent profile: ```bash bash ~/.codex/skills/x-article-publisher/scripts/open_x_articles_browser.sh ``` Defaults: - Profile path: `~/.codex/browser-profiles/x-articles` - Can override with env var: `X_ARTICLES_PROFILE=/custom/path` ### prepare_article_source.py Auto-route source input: - Feishu/Lark URL -> download to local markdown (with video fetch) - Local markdown path -> pass through directly ```bash python ~/.codex/skills/x-article-publisher/scripts/prepare_article_source.py "" ``` Output JSON includes: - `mode`: `feishu_url` or `local_markdown` - `markdown_path`: local markdown path to publish - `videos_downloaded` / `videos_appended`: video handling summary - `callouts_normalized`: number of Feishu callout labels such as `Tip` or `[!TIP]` removed while preserving quoted content ### optimize_media_blocks.py Reduce body media block count before upload by merging adjacent image runs into vertical collage images: ```bash python ~/.codex/skills/x-article-publisher/scripts/optimize_media_blocks.py article.md \ --max-body-media 24 \ --output article.optimized.md ``` Use this when parsed `content_media` is near or above the practical X Articles body-media limit. It preserves videos as separate upload blocks and merges only adjacent body images, leaving the first image/cover alone. ## Persistent Profile (Recommended) Always launch X Articles with the dedicated profile: ```bash bash ~/.codex/skills/x-article-publisher/scripts/open_x_articles_browser.sh ``` Why: - Avoids repeated login and lowers account risk from frequent sign-ins - Isolated from your main Google Chrome profile - Does **not** overwrite or log out your existing Chrome account session Note: - First run may still trigger one-time X security verification (new device/profile) - After that, reuse the same profile path and login stays stable ## Pre-Processing (Optional) Before publishing, scan the Markdown for elements that need conversion: ### Body Media Budget X Articles can silently drop new body-media uploads after roughly **25 body media blocks**. The file picker may accept a file while the editor never enters `Uploading media...` and the media count never increases. Before browser upload, parse media count: ```bash python ~/.codex/skills/x-article-publisher/scripts/parse_markdown.py article.md > article.json jq '.content_media | length' article.json ``` If body media count is `>= 24`, run `optimize_media_blocks.py` and re-parse the optimized Markdown: ```bash python ~/.codex/skills/x-article-publisher/scripts/optimize_media_blocks.py article.md \ --max-body-media 24 \ --output article.optimized.md > optimize.json python ~/.codex/skills/x-article-publisher/scripts/parse_markdown.py article.optimized.md > article.json ``` Prefer merging adjacent image runs before upload. Do **not** merge videos; videos should keep their own block and original anchor. ### Feishu Callouts / Highlight Blocks Feishu highlighted blocks may export to Markdown as blockquotes prefixed with `Tip`, `Note`, or `[!TIP]`. These labels are not useful in X Articles and can break reading flow. `prepare_article_source.py` removes those marker lines but keeps the quoted multiline content intact. If `callouts_normalized > 0`, this cleanup happened. ### Video Upload Preflight Before uploading video-heavy articles, inspect video size and bitrate. Large 40-90MB/high-bitrate videos can close or destabilize the browser session even when X eventually accepts smaller files. When a video is large or upload is unstable, transcode to a 1280px-wide H.264/AAC upload copy: ```bash ffmpeg -y -i input.mov \ -vf "scale='min(1280,iw)':-2" \ -c:v libx264 -preset medium -crf 25 -pix_fmt yuv420p \ -c:a aac -b:a 64k -movflags +faststart \ output.x1280.mp4 ``` Use the transcoded file for X upload, but keep the original Markdown anchor and block order. ### GIF Upload Preflight Treat large GIF files as video-like media, not static screenshots. Clipboard image paste can turn a GIF into a still image, and X can silently accept a large GIF file input while never adding a media block. If a GIF is large, or if uploading it does not increase the editor media count, convert it to MP4 and insert the MP4 at the original GIF anchor: ```bash ffmpeg -y -i input.gif \ -vf "scale='if(gt(iw,ih),min(1280,iw),-2)':'if(gt(iw,ih),-2,min(1280,ih))',format=yuv420p" \ -movflags +faststart -c:v libx264 -preset medium -crf 25 -an \ input.gif.mp4 ``` In final audit, count this converted GIF as one video-like media block at the original GIF position. ### Tables → PNG ```bash # Extract table to temp file, then convert python ~/.codex/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 ```bash # 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 `
` tags are ignored by X). ## Workflow **Strategy: "先文后媒体后分割线" (Text First, Media Second, Dividers Last)** For articles with media (images/videos) and dividers, paste ALL text content first, then insert media and dividers at correct positions using block index. 1. Route input source (`prepare_article_source.py`) 2. Parse Markdown once and check body-media budget 3. **(Optional)** Pre-process: Convert tables/mermaid to images; if body media is near/above 24, run `optimize_media_blocks.py` and re-parse 4. Parse final Markdown with Python script → get title, media, **dividers** with block_index, HTML 4. Navigate to X Articles editor 5. Upload cover image (first image) 6. Fill title 7. Copy HTML to clipboard (Python) → Paste with Cmd+V 8. Insert content images at positions specified by block_index 9. Insert content videos at positions specified by block_index 10. **Insert dividers at positions specified by block_index** (via Insert > Divider menu) 11. Open Preview and audit media count plus anchor order (`anchor text -> following media type`) 12. Save as draft (NEVER auto-publish) ## Input Routing Two trigger modes are supported: 1. Feishu URL mode Input contains a Feishu/Lark doc link (`feishu.cn` / `larksuite.com` / `feishu.sg`) - Run `prepare_article_source.py ""` - It calls `feishu2md dl --dump` to download markdown - It fetches video file blocks (if any) into local `static/` - It appends `