devto-post

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

DEV.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 → Published

Claude 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)"
fi

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)"
fi

Recommended: 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 3
bash
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

步骤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
OK:/username/article-slug
— prepend
https://dev.to
to get the full URL.
bash
sleep 3
osascript -e 'tell application "Google Chrome" to return title of active tab of first window'
标签栏会显示
OK:/username/article-slug
—— 前缀加上
https://dev.to
即可得到完整URL。

Step 4: Session Summary

步骤4:会话总结

Always end with the article link:
PlatformTitleLink
DEV.to"Your Article Title"https://dev.to/username/article-slug

务必以文章链接结尾:
平台标题链接
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
undefined

Write 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]
  1. Feature 1 - description
  2. Feature 2 - description
  3. Feature 3 - description
[对你的解决方案进行简要描述]
  1. 功能1 - 描述
  2. 功能2 - 描述
  3. 功能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

相关链接

Got questions or suggestions? Drop a comment below!

---
有问题或建议?欢迎在下方留言!

---

Tag Recommendations

标签推荐

Project TypeSuggested 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

项目类型推荐标签
Python库
python
,
opensource
,
api
,
showdev
JavaScript/Node
javascript
,
node
,
opensource
,
showdev
AI/ML
ai
,
machinelearning
,
python
,
opensource
DevOps
devops
,
docker
,
automation
,
opensource
Web应用
webdev
,
react
,
opensource
,
showdev
教程
tutorial
,
beginners
,
programming
,
webdev

Error Handling

错误处理

IssueSolution
Not logged inNavigate to dev.to/enter, user logs in manually
CSRF token not foundMake sure you're on dev.to domain first
Tags errorMax 4 tags, all lowercase, no spaces
Content too longSplit into series with
series: "Series Name"
in API body
---
YAML error
Strip standalone
---
lines from body

问题解决方案
未登录导航到dev.to/enter,用户手动登录
找不到CSRF token确保当前处于DEV.to域名下
标签错误最多4个标签,全部小写,无空格
内容过长在API请求体中添加
series: "Series Name"
,拆分为系列文章
---
YAML错误
移除正文中单独的
---

Why AppleScript (Not Playwright)

为什么选择AppleScript(而非Playwright)

ToolProblem
PlaywrightExtra setup, may fail on editor interactions
AppleScriptControls real Chrome, uses existing login, reliable
工具问题
Playwright需要额外配置,编辑器交互可能失败
AppleScript控制真实Chrome浏览器,使用现有登录状态,可靠性更高