- 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:
- Convert Markdown to HTML using Python's
- markdown
- library
- Copy HTML to system clipboard using
- copy_to_clipboard.py html
- Focus the editor content area
- 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: Detect tables in the Markdown file (lines with | forming table structure) Convert each table to PNG using the diagram-to-image skill:
Extract table to temp file
cat
/tmp/table1.md << 'TABLE_EOF' | Column 1 | Column 2 | Column 3 | |----------|----------|----------| | Data 1 | Data 2 | Data 3 | TABLE_EOF
Convert via diagramless.xyz API (auto-detects as table)
- node
- ~/.claude/skills/diagram-to-image/scripts/diagram-to-image.mjs /tmp/table1.md
- -o
- /tmp/table1.png
- Note the position
- of each table in the article for later insertion (after which heading/paragraph)
- 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: 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:
- Click "Create new" in the sidebar
- 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
- Click the title textbox (
- textbox "title"
- )
- Type the article title
- Click the subtitle textbox (
- textbox "Add a subtitle…"
- )
- 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.
- Copy HTML to system clipboard:
- python3 /path/to/copy_to_clipboard.py html
- --file
- /tmp/substack_article.html
- Click the editor content area (
- .ProseMirror
- or paragraph element inside it)
- 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:
- 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)
- Click the Image toolbar button
- (
- button "Image"
- ) — a dropdown menu appears with options: Image, Gallery, Stock photos, Generate image
- Click "Image" menuitem
- from the dropdown — a file chooser dialog opens
- 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:
- Check the "Saved" status indicator (green dot + "Saved" text)
- Take a snapshot to verify content structure
- 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
- NEVER click "Continue"
- - This starts the publish flow. Only save as draft (auto-save handles this)
- Always convert to HTML first
- - Plain Markdown will not be parsed by the Tiptap editor
- Use clipboard paste
- - The only reliable way to insert formatted content
- Check login status
- - Prompt user to login if needed
- Preserve original file
- - Never modify the source Markdown file
- Report completion
- - Tell user the draft is saved and needs manual review
- No
- nl2br
- extension
- - Causes double line breaks
- Tables → images
- - Pre-process tables before pasting content; upload images after paste
- 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 PNG via diagram-to-image skill → 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:
- Wait for page to fully load
- Take a new snapshot
- 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 via diagram-to-image skill, upload as image Images Manual Upload via Image toolbar button → file chooser