devto-post
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDEV.to Article Posting Skill (AppleScript Chrome Control)
DEV.to 文章发布技能(AppleScript Chrome 控制)
Publish articles to DEV.to by controlling the user's real Chrome via AppleScript. No Playwright needed.
通过AppleScript控制用户的真实Chrome浏览器,将文章发布到DEV.to。无需使用Playwright。
How It Works
工作原理
Claude Code → osascript → Chrome (logged into DEV.to) → CSRF API → PublishedClaude Code → osascript → Chrome(已登录DEV.to)→ CSRF API → 发布完成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
- 仅支持macOS(AppleScript是macOS专属技术)
- Chrome:依次点击「视图」→「开发者」→「允许来自Apple事件的JavaScript」(启用后重启Chrome)
- 用户已在Chrome中登录DEV.to
Method Detection
方法检测
bash
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)"
fibash
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)"
fiRecommended: DEV.to Internal API (CSRF Token)
推荐方案:DEV.to 内部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:
这是最可靠的方法。编辑器表单存在React状态问题(标签会拼接、自动保存的草稿在重新加载后仍保留错误状态)。建议改用受CSRF保护的内部API:
Step 1: Navigate to DEV.to
步骤1:导航到DEV.to
bash
osascript -e 'tell application "Google Chrome" to tell active tab of first window to set URL to "https://dev.to"'
sleep 3bash
osascript -e 'tell application "Google Chrome" to tell active tab of first window to set URL to "https://dev.to"'
sleep 3Step 2: Publish via CSRF API
步骤2:通过CSRF API发布
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: "# 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;
}
})()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: "# 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
步骤3:获取已发布文章的URL
bash
sleep 3
osascript -e 'tell application "Google Chrome" to return title of active tab of first window'The title will contain — prepend to get the full URL.
OK:/username/article-slughttps://dev.tobash
sleep 3
osascript -e 'tell application "Google Chrome" to return title of active tab of first window'标签栏会显示 —— 前缀加上 即可得到完整URL。
OK:/username/article-slughttps://dev.toStep 4: Session Summary
步骤4:会话总结
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:
bash
undefined对于篇幅过长无法内联到JS中的文章,可以将内容写入临时文件再注入:
bash
undefinedWrite article content to temp JSON file
将文章内容写入临时JSON文件
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)
"
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
使用JXA读取文件并发布
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}})()"});
'
---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:
---python
import re
body = re.sub(r'^---$', '', body, flags=re.MULTILINE)DEV.to会将单独的行解析为YAML前置分隔符。请移除这些行:
---python
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"]
- 每篇文章最多4个标签
- 标签必须为小写
- 以数组形式传递:
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.
DEV.to编辑器存在多个问题:
- 标签输入拼接问题:在React组件中按回车键无法分隔标签
- 自动保存草稿持久化:错误状态(如格式错误的标签)在页面重新加载后仍会保留
- React受控组件冲突:原生值设置器可能破坏React状态
CSRF API可以避免所有这些问题。请优先使用API方案。
Alternative: Editor Form (Fallback Only)
备选方案:编辑器表单(仅作为 fallback)
If the API doesn't work for some reason, you can fill the editor form directly:
如果API方案因某些原因无法使用,可以直接填充编辑器表单:
Fill Title
填充标题
bash
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\";
}
"'bash
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
填充正文
bash
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\";
}
"'bash
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
点击发布按钮
bash
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\"; }
"'bash
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
开源项目文章模板
markdown
[Opening hook - 1-2 sentences about what you built and why]markdown
[开篇钩子 - 1-2句话介绍你构建的内容及原因]The Problem
问题背景
[Describe the pain point you're solving]
- Bullet point 1
- Bullet point 2
- Bullet point 3
[描述你要解决的痛点]
- 痛点1
- 痛点2
- 痛点3
The Solution: [Project Name]
解决方案:[项目名称]
[Brief description of your solution]
- Feature 1 - description
- Feature 2 - description
- Feature 3 - description
[对你的解决方案进行简要描述]
- 功能1 - 描述
- 功能2 - 描述
- 功能3 - 描述
Getting Started
快速开始
```bash
git clone https://github.com/username/repo
cd repo
pip install -r requirements.txt
```
```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
相关链接
Tag Recommendations
标签推荐
| Project Type | Suggested Tags |
|---|---|
| Python library | |
| JavaScript/Node | |
| AI/ML | |
| DevOps | |
| Web app | |
| Tutorial | |
| 项目类型 | 推荐标签 |
|---|---|
| Python库 | |
| JavaScript/Node | |
| AI/ML | |
| DevOps | |
| Web应用 | |
| 教程 | |
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 |
| Strip standalone |
| 问题 | 解决方案 |
|---|---|
| 未登录 | 导航到dev.to/enter,用户手动登录 |
| 找不到CSRF token | 确保当前处于DEV.to域名下 |
| 标签错误 | 最多4个标签,全部小写,无空格 |
| 内容过长 | 在API请求体中添加 |
| 移除正文中单独的 |
Why AppleScript (Not Playwright)
为什么选择AppleScript(而非Playwright)
| Tool | Problem |
|---|---|
| Playwright | Extra setup, may fail on editor interactions |
| AppleScript | Controls real Chrome, uses existing login, reliable |
| 工具 | 问题 |
|---|---|
| Playwright | 需要额外配置,编辑器交互可能失败 |
| AppleScript | 控制真实Chrome浏览器,使用现有登录状态,可靠性更高 |