Publish articles to DEV.to by controlling the user's real Chrome via AppleScript. No Playwright needed.
Claude Code → osascript → Chrome (logged into DEV.to) → CSRF API → Published
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
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:
osascript -e 'tell application "Google Chrome" to tell active tab of first window to set URL to "https://dev.to"'
sleep 3
(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;
}
})()
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.
Always end with the article link:
| Platform | Title | Link |
|---|---|---|
| ---------- | ------- | ------ |
| DEV.to | "Your Article Title" | https://dev.to/username/article-slug |
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}})()"});
'
---DEV.to parses standalone --- lines as YAML front matter delimiters. Strip them:
import re
body = re.sub(r'^---$', '', body, flags=re.MULTILINE)
tags: ["tag1", "tag2", "tag3", "tag4"]The DEV.to editor has multiple issues:
The CSRF API bypasses all of these. Always prefer the API.
If the API doesn't work for some reason, you can fill the editor form directly:
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\";
}
"'
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\";
}
"'
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\"; }
"'
[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!
| 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 |
| 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 |
| Tool | Problem |
|---|---|
| ------ | --------- |
| Playwright | Extra setup, may fail on editor interactions |
| AppleScript | Controls real Chrome, uses existing login, reliable |
Canlah AI — Run performance marketing without breaking your brand.
共 2 个版本