---
name: article-publisher
description: |
Generate branded banners and publish Markdown articles to X (Twitter) Articles editor.
Use when user wants to publish an article to X, generate a banner image, or convert
markdown for publishing. Handles: banner generation (fal.ai + HTML/Playwright),
markdown parsing into sequential steps, and step-by-step browser automation.
---
# Article Publisher Skill
Publish Markdown articles to X Articles with branded banners. Two main capabilities:
1. **Banner Generation** — fal.ai visual base + HTML/CSS text overlay via Playwright
2. **Article Publishing** — Sequential step-by-step publishing to X Articles editor
## Quick Start
### Generate a banner:
```bash
/Users/melted/clawd/skills/article-publisher/scripts/generate-banner.sh \
--title "My Article Title" \
--subtitle "A deep dive into the topic" \
--tag "ENGINEERING" \
--prompt "abstract dark neural network topology, cinematic, moody" \
--output /tmp/banner.png
```
### Parse an article for publishing:
```bash
python3 /Users/melted/clawd/skills/article-publisher/scripts/parse-article.py \
article.md --output /tmp/article-steps.json
```
### Full publish flow:
1. Generate banner → `banner.png`
2. Parse article → `article-steps.json`
3. Open X Articles editor in browser
4. Upload cover, set title, walk through steps sequentially
---
## Banner Generation
### Usage
```bash
generate-banner.sh [options] --output /path/to/banner.png
```
### Options
| Flag | Required | Description |
|------|----------|-------------|
| `--title` | ✅ | Main title text |
| `--output` | ✅ | Output PNG path |
| `--subtitle` | | Secondary text |
| `--tag` | | Top label (auto-uppercased in template) |
| `--prompt` | | fal.ai prompt for AI background (mutually exclusive with --bg-image) |
| `--bg-image` | | Path to existing background image |
| `--stats` | | Comma-separated `Label:Value` pairs |
| `--pipeline` | | Comma-separated pipeline step names |
| `--brand` | | Bottom-right brand text (default: "Axiom 🔬") |
| `--size` | | `WxH` dimensions (default: `1250x500`) |
| `--template` | | Custom HTML template path |
### Examples
```bash
# Full featured banner
generate-banner.sh \
--output banner.png \
--title "Ship Log: Week 3" \
--subtitle "14 days of autonomous operations" \
--tag "AUTONOMOUS INFRASTRUCTURE" \
--prompt "abstract dark data center, fiber optic lights, cinematic" \
--stats "Transactions:1,057,Uptime:14 days,Bugs Lost:\$0" \
--pipeline "REBALANCE,COMPOUND,HARVEST,BURN" \
--brand "Axiom 🔬"
# Simple banner with existing background
generate-banner.sh \
--output banner.png \
--title "How I Built This" \
--subtitle "From zero to production" \
--bg-image ~/images/abstract-bg.png
# Minimal banner (dark solid background)
generate-banner.sh \
--output banner.png \
--title "Thoughts on Agent Infrastructure"
```
### Dependencies
- Node.js + `npx playwright` (install chromium: `npx playwright install chromium`)
- ffmpeg (`brew install ffmpeg`) — only needed with `--prompt`
- curl — only needed with `--prompt`
- `FAL_API_KEY` in `~/.axiom/wallet.env` — only needed with `--prompt`
### Design
- Dark theme: #0a0a0a background, #4ade80 muted green accents
- Bloomberg × Apple aesthetic — no neon, no glow
- Fonts: Inter (titles), JetBrains Mono (stats, tags) via Google Fonts
- Left-to-right gradient overlay ensures text readability over any background
---
## Article Parsing
### Usage
```bash
python3 parse-article.py input.md [--output steps.json] [--html-only]
```
### Output Format
```json
{
"title": "Article Title (from first H1)",
"steps": [
{"type": "paste_html", "html": "
Intro text...
Section
More text...
"},
{"type": "code_block", "lang": "bash", "code": "#!/bin/bash\necho hello"},
{"type": "paste_html", "html": "Next section...
"},
{"type": "code_block", "lang": "javascript", "code": "const x = 1;"},
{"type": "paste_html", "html": "Conclusion...
"}
]
}
```
### What It Handles
| Markdown | HTML Output |
|----------|-------------|
| `# Title` | Extracted as `title` field (not in steps) |
| `## Heading` | `Heading
` |
| `**bold**` | `bold` |
| `*italic*` | `italic` |
| `[link](url)` | `link` |
| `> quote` | `quote
` |
| `- item` | `` |
| `1. item` | `- item
` |
| `` `code` `` | `code` |
| `---` | `
` |
| ` ```lang ` | `code_block` step |
| `![img]()` | Skipped (X Articles limitation) |
### Flags
- `--output FILE` — Write JSON to file (otherwise prints to stdout)
- `--html-only` — Output single HTML string (code blocks become comments); useful for debugging
### Dependencies
- Python 3 (standard library works; `pip install markdown` optional for better output)
---
## Publishing to X Articles
### The Sequential Approach (Why It Matters)
**Previous approach (broken):** Paste everything at once with code block placeholders, then backtrack to replace each one. This causes misplaced code blocks, cursor failures, and hours of debugging.
**This approach (proven):** Walk through steps linearly. Each step appends at the cursor position. No backtracking.
### Step-by-Step Browser Automation
Use OpenClaw `browser` tool with `profile="chrome"`.
#### 1. Navigate & Create
```
browser navigate url="https://x.com/compose/articles" profile="chrome"
browser snapshot profile="chrome"
# Click "Create" button
```
#### 2. Upload Cover Image
```
browser upload selector="input[type='file'][accept*='image']" paths=["/path/to/banner.png"] profile="chrome"
# Click "Apply" in Edit media dialog
```
#### 3. Set Title
```
# Click title area, type the title
```
#### 4. Paste HTML Segments
For each `paste_html` step, execute this JavaScript in the browser:
```javascript
const dt = new DataTransfer();
dt.setData('text/html', htmlContent);
const el = document.querySelector('[contenteditable="true"][data-testid]')
|| document.querySelector('[contenteditable="true"]');
el.focus();
// Move cursor to end
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
// Paste
el.dispatchEvent(new ClipboardEvent('paste', {
clipboardData: dt, bubbles: true, cancelable: true
}));
```
**⚠️ CRITICAL:** Paste ALL HTML of each segment in ONE ClipboardEvent. Splitting causes H2 headers to merge into preceding blocks.
**⚠️ WHY ClipboardEvent:** System clipboard (Meta+V) does NOT work through Chrome relay CDP. Must use synthetic ClipboardEvent with DataTransfer.
#### 5. Insert Code Blocks
For each `code_block` step:
1. Press Enter (new line)
2. Click **Insert** menu in toolbar
3. Click **Code** option
4. Type language name in search field (use full name: "javascript" not "js")
5. Click matching language option
6. Focus textarea and insert code:
```javascript
const ta = document.querySelectorAll('textarea')[0];
ta.focus();
document.execCommand('insertText', false, codeContent);
```
7. Click **Insert** button
**⚠️ WHY execCommand:** The textarea is React-controlled. Setting `.value` directly or pasting doesn't trigger React state updates. `execCommand('insertText')` is the ONLY method that works.
#### 6. Save & Report
- Draft auto-saves
- **NEVER auto-publish** — always save as draft
- Report: "Draft saved. Review and publish manually."
---
## File Structure
```
skills/article-publisher/
├── SKILL.md # This file
├── references/
│ ├── banner-pipeline.md # Deep dive on banner generation
│ └── article-publishing.md # Deep dive on sequential publishing
├── scripts/
│ ├── generate-banner.sh # Banner generation script
│ ├── parse-article.py # Markdown → steps JSON
│ └── publish-steps.md # Browser automation reference
├── templates/
│ └── banner-default.html # Parameterized HTML banner template
└── examples/
└── sample-article.md # Example article with code blocks
```
---
## Troubleshooting
### Banner: "Playwright not installed"
```bash
npx playwright install chromium
```
### Banner: Fonts not loading
The template uses Google Fonts via `@import`. Needs internet access. If offline, the template will fall back to system fonts.
### Parser: "No module named 'markdown'"
The parser works without the `markdown` library (has built-in converter), but for better output:
```bash
pip install markdown
```
### Publishing: Code block language not found
Use the full language name in your markdown fences:
- ✅ ` ```javascript ` not ` ```js `
- ✅ ` ```typescript ` not ` ```ts `
- ✅ ` ```python ` not ` ```py `
- ✅ ` ```bash ` (both `bash` and `sh` work)
### Publishing: Text formatting looks wrong
Make sure you're using `ClipboardEvent` paste, not `innerHTML` or `textContent`. Only the clipboard approach preserves rich text formatting in the X Articles editor.
### Publishing: "Browser not connected"
Ensure Chrome relay is attached. User must click the OpenClaw Browser Relay toolbar icon on the X tab.