devto-post

SKILL.md

DEV.to Article Posting Skill (AppleScript Chrome Control)

Publish articles to DEV.to by controlling the user's real Chrome via AppleScript. No Playwright needed.


How It Works

Claude Code → osascript → Chrome (logged into DEV.to) → CSRF API → Published

Prerequisites

  • macOS only (AppleScript is a macOS technology)
  • Chrome: View → Developer → Allow JavaScript from Apple Events (restart after enabling)
  • User logged into DEV.to in Chrome

Method Detection

WINDOWS=$(osascript -e 'tell application "Google Chrome" to return count of windows' 2>/dev/null)
if [ "$WINDOWS" = "0" ] || [ -z "$WINDOWS" ]; then
    echo "METHOD 2 (System Events + Console)"
else
    echo "METHOD 1 (execute javascript)"
fi

Recommended: DEV.to Internal API (CSRF Token)

This is the most reliable method. The editor form has React state issues (tags concatenate, auto-save drafts persist bad state across reloads). Use the CSRF-protected internal API instead:

Step 1: Navigate to DEV.to

osascript -e 'tell application "Google Chrome" to tell active tab of first window to set URL to "https://dev.to"'
sleep 3

Step 2: Publish via CSRF API

(async()=>{
  try {
    var csrf = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
    var resp = await fetch('/articles', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrf
      },
      credentials: 'include',
      body: JSON.stringify({
        article: {
          title: "Your Title",
          body_markdown: "# Full markdown content here...",
          tags: ["opensource", "showdev", "tutorial", "programming"],
          published: true
        }
      })
    });
    var result = await resp.json();
    if (result.current_state_path) {
      document.title = "OK:" + result.current_state_path;
    } else {
      document.title = "ERR:" + JSON.stringify(result);
    }
  } catch(e) {
    document.title = "ERR:" + e.message;
  }
})()

Step 3: Get Published URL

sleep 3
osascript -e 'tell application "Google Chrome" to return title of active tab of first window'

The title will contain OK:/username/article-slug — prepend https://dev.to to get the full URL.

Step 4: Session Summary

Always end with the article link:

Platform Title Link
DEV.to "Your Article Title" https://dev.to/username/article-slug

For Long Articles: File-Based Approach

For articles too long to inline in JS, write the body to a temp file and inject:

# Write article content to temp JSON file
python3 -c "
import json
with open('/tmp/devto_body.md') as f:
    body = f.read()
with open('/tmp/devto_body.json', 'w') as f:
    json.dump(body, f)
"

# Use JXA to read the file and publish
osascript -l JavaScript -e '
var chrome = Application("Google Chrome");
var tab = chrome.windows[0].activeTab;
var body = JSON.parse($.NSString.alloc.initWithContentsOfFileEncodingError("/tmp/devto_body.json", $.NSUTF8StringEncoding, null).js);
tab.execute({javascript: "(async()=>{try{var csrf=document.querySelector(\"meta[name=csrf-token]\").getAttribute(\"content\");var resp=await fetch(\"/articles\",{method:\"POST\",headers:{\"Content-Type\":\"application/json\",\"X-CSRF-Token\":csrf},credentials:\"include\",body:JSON.stringify({article:{title:\"YOUR TITLE\",body_markdown:" + JSON.stringify(body) + ",tags:[\"tag1\",\"tag2\"],published:true}})});var r=await resp.json();document.title=r.current_state_path?\"OK:\"+r.current_state_path:\"ERR:\"+JSON.stringify(r)}catch(e){document.title=\"ERR:\"+e.message}})()"});
'

Important Gotchas

Never start body with ---

DEV.to parses standalone --- lines as YAML front matter delimiters. Strip them:

import re
body = re.sub(r'^---$', '', body, flags=re.MULTILINE)

Tag Rules

  • Maximum 4 tags per article
  • Tags must be lowercase
  • Pass as array: tags: ["tag1", "tag2", "tag3", "tag4"]

Why NOT to Use the Editor Form

The DEV.to editor has multiple issues:

  • Tag input concatenation: Enter key doesn't separate tags in the React component
  • Auto-save draft persistence: Bad state (e.g., malformed tags) persists across page reloads
  • React controlled component conflicts: Native value setters can corrupt React state

The CSRF API bypasses all of these. Always prefer the API.


Alternative: Editor Form (Fallback Only)

If the API doesn't work for some reason, you can fill the editor form directly:

Fill Title

osascript -e 'tell application "Google Chrome" to tell active tab of first window to execute javascript "
  var titleInput = document.querySelector(\"#article-form-title\");
  if (!titleInput) titleInput = document.querySelector(\"input[placeholder*=\\\"title\\\"]\");
  if (titleInput) {
    var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, \"value\").set;
    nativeInputValueSetter.call(titleInput, \"Your Article Title Here\");
    titleInput.dispatchEvent(new Event(\"input\", { bubbles: true }));
    document.title = \"TITLE_SET\";
  } else {
    document.title = \"TITLE_NOT_FOUND\";
  }
"'

Fill Body

osascript -e 'tell application "Google Chrome" to tell active tab of first window to execute javascript "
  var textarea = document.querySelector(\"#article_body_markdown\");
  if (!textarea) textarea = document.querySelector(\"textarea\");
  if (textarea) {
    var nativeTextareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, \"value\").set;
    nativeTextareaSetter.call(textarea, \"YOUR MARKDOWN CONTENT\");
    textarea.dispatchEvent(new Event(\"input\", { bubbles: true }));
    document.title = \"BODY_SET\";
  }
"'

Publish Button

osascript -e 'tell application "Google Chrome" to tell active tab of first window to execute javascript "
  var publishBtn = document.querySelector(\"button[aria-label*=\\\"Publish\\\"]\");
  if (!publishBtn) {
    var buttons = document.querySelectorAll(\"button\");
    for (var b of buttons) { if (b.textContent.trim() === \"Publish\") { publishBtn = b; break; } }
  }
  if (publishBtn) { publishBtn.click(); document.title = \"PUBLISHED\"; }
  else { document.title = \"PUBLISH_NOT_FOUND\"; }
"'

Article Template for Open Source Projects

[Opening hook - 1-2 sentences about what you built and why]

## The Problem

[Describe the pain point you're solving]
- Bullet point 1
- Bullet point 2
- Bullet point 3

## The Solution: [Project Name]

[Brief description of your solution]

1. **Feature 1** - description
2. **Feature 2** - description
3. **Feature 3** - description

## Getting Started

\`\`\`bash
git clone https://github.com/username/repo
cd repo
pip install -r requirements.txt
\`\`\`

## Key Features

### Feature Name
[Code example]

## Why Open Source?

[Personal story about why you're sharing this]

## Links

- **GitHub**: https://github.com/username/repo

Got questions or suggestions? Drop a comment below!

Tag Recommendations

Project Type Suggested Tags
Python library python, opensource, api, showdev
JavaScript/Node javascript, node, opensource, showdev
AI/ML ai, machinelearning, python, opensource
DevOps devops, docker, automation, opensource
Web app webdev, react, opensource, showdev
Tutorial tutorial, beginners, programming, webdev

Error Handling

Issue Solution
Not logged in Navigate to dev.to/enter, user logs in manually
CSRF token not found Make sure you're on dev.to domain first
Tags error Max 4 tags, all lowercase, no spaces
Content too long Split into series with series: "Series Name" in API body
--- YAML error Strip standalone --- lines from body

Why AppleScript (Not Playwright)

Tool Problem
Playwright Extra setup, may fail on editor interactions
AppleScript Controls real Chrome, uses existing login, reliable
Weekly Installs
2
First Seen
Feb 9, 2026
Installed on
openclaw2
antigravity2
replit2
claude-code2
codex2
gemini-cli2