--- name: publish-substack-article description: Publish Markdown articles to Substack as drafts. Use when user wants to publish a Markdown file to Substack, or mentions "发布到 Substack", "Substack article", "publish to Substack". Handles Markdown-to-HTML conversion and saves as draft (never auto-publish). --- # Publish Substack Article Publish Markdown content to Substack post editor, converting Markdown to HTML and pasting as rich text. Saves as draft for user review before publishing. ## Prerequisites - Browser automation MCP (either one): - **Chrome DevTools MCP** (`mcp__chrome-devtools__*`) - **Playwright MCP** (`mcp__playwright__*`) - User logged into Substack - Python 3 with `markdown` package (`pip install markdown`) - `copy_to_clipboard.py` script (shared from publish-zsxq-article skill) ## Browser MCP Tool Mapping This skill works with both Chrome DevTools MCP and Playwright MCP. Use whichever is available: | Action | Chrome DevTools MCP | Playwright MCP | |--------|---------------------|----------------| | Navigate | `navigate_page` | `browser_navigate` | | Take snapshot | `take_snapshot` | `browser_snapshot` | | Take screenshot | `take_screenshot` | `browser_take_screenshot` | | Click element | `click` | `browser_click` | | Fill text | `fill` | `browser_type` | | Press key | `press_key` | `browser_press_key` | | Evaluate JS | `evaluate_script` | `browser_evaluate` | **Detection**: Check available tools at runtime. If `mcp__chrome-devtools__navigate_page` exists, use Chrome DevTools MCP. If `mcp__playwright__browser_navigate` exists, use Playwright MCP. ## Key URLs - Substack dashboard: `https://{publication}.substack.com/publish` - Post editor: `https://{publication}.substack.com/publish/post/{postId}` - Default publication: `verysmallwoods` ## Editor Interface The Substack post editor uses **Tiptap** (ProseMirror-based WYSIWYG editor). ### Key Elements - Title input: `textbox "title"` (placeholder: "Title") - Subtitle input: `textbox "Add a subtitle…"` - Content area: `.ProseMirror` (Tiptap editor, "Start writing...") - Save status: `button "Saved"` (auto-saves) - Preview button: `button "Preview"` - Continue button: `button "Continue"` (publish flow - DO NOT USE) - Settings sidebar: `button "Settings"` (title, description, thumbnail) ### Settings Sidebar (left panel) When "Settings" or "File Settings" is open: - Title: `textbox "Add a title..."` - Description: `textbox "Add a description..."` - Thumbnail: Upload button (3:2 aspect ratio) ### Toolbar Bold, Italic, Strikethrough, Code, Link, Image, Audio, Video, Quote, Lists (bullet/ordered), Button, More (Code block, Divider, Footnote, LaTeX, etc.) ## Content Insertion Method **CRITICAL: Use clipboard paste with HTML content**, NOT direct fill or plain Markdown paste. The Tiptap editor handles HTML paste natively and renders it as rich content. The workflow is: 1. Convert Markdown to HTML using Python's `markdown` library 2. Copy HTML to system clipboard using `copy_to_clipboard.py html` 3. Focus the editor content area 4. Press Cmd+V (macOS) or Ctrl+V (Windows/Linux) to paste **Why HTML paste?** - `fill` tool → Content treated as plain text, no formatting - Plain Markdown paste → Tiptap does NOT parse Markdown on paste - HTML paste → Tiptap renders HTML as rich content (headings, code blocks, links, bold, etc.) **Known limitation**: Substack's editor does NOT support HTML tables. Tables will be collapsed into plain text. See **Step 0: Pre-Processing** for converting tables to images. ## Main Workflow ### Step 0: Pre-Processing — Convert Tables to Images Substack does NOT render HTML tables. They collapse into plain text. Any Markdown table must be converted to a PNG image and uploaded separately. **Workflow:** 1. **Detect tables** in the Markdown file (lines with `|` forming table structure) 2. **Create styled HTML** for each table: ```html
``` 3. **Render to screenshot**: Open the HTML file in a browser tab, take a screenshot, close the tab: ``` # Open HTML in new tab browser_navigate or new_page: file:///tmp/table1.html # Take screenshot browser_take_screenshot: filename=/tmp/table1.png, fullPage=true # Close tab and return to editor browser_tabs: action=close ``` 4. **Note the position** of each table in the article for later insertion (after which heading/paragraph) 5. **Remove table Markdown** from the content before HTML conversion (so it won't appear as plain text in the pasted content) **Image upload** happens after pasting the main content — see Step 7. ### Step 1: Prepare Content Read the Markdown file and extract: - **Title**: from YAML frontmatter `title` field, or H1 header `# Title`, or filename - **Subtitle**: from YAML frontmatter `excerpt` or `description` field - **Content**: full Markdown body (strip YAML frontmatter and any cross-reference links) ### Step 2: Convert Markdown to HTML Use Python's `markdown` library with `tables` and `fenced_code` extensions: ```python import markdown import re with open('/path/to/article.md', 'r') as f: content = f.read() # Strip YAML frontmatter content = re.sub(r'^---\n.*?\n---\n', '', content, flags=re.DOTALL) # Strip cross-reference links (e.g., English version link) # Adjust pattern as needed for your articles content = re.sub(r'^> .* available at.*\n\n?', '', content, flags=re.MULTILINE) # Convert to HTML html = markdown.markdown(content, extensions=['tables', 'fenced_code']) # Write to temp file with open('/tmp/substack_article.html', 'w') as f: f.write(html) ``` **IMPORTANT**: Do NOT use `nl2br` extension - it converts single newlines to `
` tags, causing extra line breaks in the editor. ### Step 3: Navigate to Substack Navigate to the Substack dashboard and create a new post: ``` # Navigate to Substack dashboard navigate to: https://verysmallwoods.substack.com/publish ``` If not logged in, prompt user to log in: ``` 请先登录 Substack,登录完成后告诉我。 Please log in to Substack first, then let me know. ``` ### Step 4: Create New Post From the dashboard, create a new text post: 1. Click "Create new" in the sidebar 2. Select "Text post" (or navigate directly to a new post URL) Alternatively, if the editor is already open with an empty post, proceed directly. ### Step 5: Fill Title and Subtitle 1. Click the title textbox (`textbox "title"`) 2. Type the article title 3. Click the subtitle textbox (`textbox "Add a subtitle…"`) 4. Type the subtitle/excerpt ``` click: title textbox fill/type: article title click: subtitle textbox fill/type: article subtitle ``` ### Step 6: Insert HTML Content (via Clipboard Paste) **CRITICAL: Do NOT use `fill` tool** - it inserts plain text without formatting. 1. Copy HTML to system clipboard: ```bash python3 /path/to/copy_to_clipboard.py html --file /tmp/substack_article.html ``` 2. Click the editor content area (`.ProseMirror` or paragraph element inside it) 3. Press Cmd+V to paste: ``` press_key: Meta+v (macOS) press_key: Control+v (Windows/Linux) ``` This triggers Tiptap's HTML paste handler, which renders the content as rich text with proper formatting. ### Step 7: Insert Table Images If the article had tables converted to images in Step 0, insert them now: 1. **Navigate to the correct position** in the editor — click on the paragraph or empty line where the table should appear (after the relevant heading/text) 2. **Click the Image toolbar button** (`button "Image"`) — a dropdown menu appears with options: Image, Gallery, Stock photos, Generate image 3. **Click "Image" menuitem** from the dropdown — a file chooser dialog opens 4. **Upload the image** via file chooser: - Playwright MCP: `browser_file_upload` with the image path - Chrome DevTools MCP: `upload_file` with the image path **Important notes:** - **File path restriction**: Playwright MCP only allows file uploads from within allowed roots (project directories). If your image is in `/tmp/`, copy it to the project directory first - **Repeat for each table**: Position cursor at the correct location, then upload each table image - **Delete residual text**: If table content was pasted as plain text (because it wasn't removed in pre-processing), select it (triple-click to select paragraph) and delete before inserting the image ### Step 8: Verify Draft After pasting: 1. Check the "Saved" status indicator (green dot + "Saved" text) 2. Take a snapshot to verify content structure 3. Optionally take a screenshot for visual verification The editor auto-saves, so no explicit save action is needed. ### Step 9: Report Completion ``` 草稿已保存到 Substack。请在 Substack 中预览并手动发布。 Draft saved to Substack. Please preview and publish manually. Post URL: https://verysmallwoods.substack.com/publish/post/{postId} ``` ## Complete Example Flow User: "把 /path/to/my-article.md 发布到 Substack" ``` 0. Pre-process tables (if any) - Detect Markdown tables - Create styled HTML for each table - Render to screenshots (open in browser, screenshot, close tab) - Remove table Markdown from content - Note insertion positions 1. Read /path/to/my-article.md - Extract title from frontmatter or H1 - Extract subtitle from frontmatter excerpt - Get full Markdown content (with tables removed) 2. Convert Markdown to HTML - Strip frontmatter - Use markdown.markdown() with ['tables', 'fenced_code'] - Write to /tmp/substack_article.html 3. Navigate to Substack dashboard or new post 4. Check if logged in - If not, prompt user to login 5. Fill title and subtitle 6. Copy HTML to clipboard + Paste - python3 copy_to_clipboard.py html --file /tmp/substack_article.html - Click editor content area - Press Cmd+V 7. Insert table images at correct positions - For each table: click position → Image button → Image menuitem → file upload 8. Verify draft saved - Check "Saved" status 9. Report success - "草稿已保存,请手动预览并发布" ``` ## Critical Rules 1. **NEVER click "Continue"** - This starts the publish flow. Only save as draft (auto-save handles this) 2. **Always convert to HTML first** - Plain Markdown will not be parsed by the Tiptap editor 3. **Use clipboard paste** - The only reliable way to insert formatted content 4. **Check login status** - Prompt user to login if needed 5. **Preserve original file** - Never modify the source Markdown file 6. **Report completion** - Tell user the draft is saved and needs manual review 7. **No `nl2br` extension** - Causes double line breaks 8. **Tables → images** - Pre-process tables before pasting content; upload images after paste 9. **Playwright file paths** - Playwright MCP restricts file uploads to allowed roots; copy temp files to project directory before uploading ## Troubleshooting ### Content Shows as Plain Text (No Formatting) If you see raw HTML tags or unformatted text: - **Cause**: Content was inserted using `fill` tool instead of clipboard paste - **Solution**: Use the `copy_to_clipboard.py` + Cmd+V method (see Step 6) ### Tables Not Rendering (Shows Plain Text) Substack's Tiptap editor does not support HTML tables. They collapse into inline plain text. - **Solution**: Convert tables to styled HTML → render as screenshots → upload as images (see Step 0 and Step 7) - **Alternative**: Restructure simple tables as formatted lists - **If plain text already pasted**: Triple-click the plain text paragraph to select it, press Backspace to delete, then insert the table image at that position ### Login Required If page shows login prompt: ``` 请先登录 Substack: https://verysmallwoods.substack.com 登录完成后告诉我。 ``` ### Editor Not Loading If editor elements are not visible: 1. Wait for page to fully load 2. Take a new snapshot 3. If still not loading, refresh the page ### Clipboard Copy Fails If `copy_to_clipboard.py` fails: - Ensure dependencies: `pip install pyobjc-framework-Cocoa` (macOS) - Check the HTML file exists and is readable - Try copying a smaller test string first ## Element Reference | Element | Selector/Identifier | Description | |---------|---------------------|-------------| | Title input | `textbox "title"` | Post title | | Subtitle input | `textbox "Add a subtitle…"` | Post subtitle | | Content area | `.ProseMirror` (Tiptap editor) | Post content | | Save status | `button "Saved"` | Auto-save indicator | | Preview button | `button "Preview"` | Preview post | | Continue button | `button "Continue"` | DO NOT USE - starts publish flow | | Settings button | `button "Settings"` | Open settings sidebar | | Exit button | `button "Exit"` | Exit editor | | Image button | `button "Image"` | Opens image upload dropdown | | Image menuitem | `menuitem "Image"` | Opens file chooser for image upload | | Author button | `button "{PublicationName}"` | Author/publication selector | ## Technical Details ### Editor Stack - **Tiptap**: A headless, framework-agnostic rich-text editor built on ProseMirror - **ProseMirror**: The underlying rich-text editing framework - **Paste handling**: Tiptap natively parses HTML from clipboard and converts to its internal document model ### Content Conversion Pipeline ``` Markdown file ↓ (Python markdown library) HTML string ↓ (copy_to_clipboard.py) System clipboard (text/html + text/plain) ↓ (Cmd+V keyboard shortcut) Tiptap ProseMirror editor ↓ (auto-save) Substack draft ``` ### Supported Formatting The following Markdown elements are correctly rendered after HTML conversion and paste: | Markdown Element | Substack Support | Notes | |-----------------|-----------------|-------| | Headings (H2-H6) | Yes | H1 not recommended (title is separate) | | Bold / Italic | Yes | | | Inline code | Yes | | | Code blocks | Yes | Syntax highlighting may vary | | Links | Yes | | | Blockquotes | Yes | | | Bullet lists | Yes | | | Ordered lists | Yes | | | Horizontal rules | Yes | | | Tables | No → Image | Convert to styled HTML, screenshot, upload as image | | Images | Manual | Upload via Image toolbar button → file chooser |