用 JSON:API + GitHub Actions 自动发送 Drupal 博客文章:从 0 到 1 的完整记录

api_poster, 7 十月, 2025

用 JSON:API + GitHub Actions 自动发送 Drupal 博客文章

目标:在 Drupal 10 上用 JSON:API 发文章,并在个人仓库加一个 GitHub Actions,把本地或仓库里的 Markdown 自动转换为 HTML、直接发布成文章(不启用内容审核流时可一键发布)。


1)Drupal 侧:创建 API 账户、开权限、开启写入

1.1 创建专用 API 账户

  • 后台 → 人员添加用户(建议命名如 api_poster,仅用于发文)。
  • 给它分配一个最小权限的角色(可以新建“API Poster”角色)。

1.2 给角色勾选必要权限(以内容类型 Article 为例)

  • 后台 → 人员角色 → 编辑该角色:
  • Article创建新内容(Create new content)
  • ✅ (可选)Article编辑自己的内容
  • 使用 Basic HTML 文本格式(Use the Basic HTML text format)
    如果不想开放 HTML,可改用 plain_text,并在后面工作流里把 format 改为 plain_text

1.3 启用模块:JSON:API 与 Basic Auth

  • 扩展(/admin/modules) 勾选:
    • JSON:API
    • HTTP Basic Authentication(模块名 basic_auth
  • 或服务器执行:
drush en jsonapi basic_auth -y
drush cr

1.4 允许 JSON:API 写入(关闭只读)

  • 配置 → Web 服务 → JSON:API(/admin/config/services/jsonapi)
    取消勾选 “Accept only read operations”
  • 或执行:
drush cset jsonapi.settings read_only 0 -y
drush cr

1.5 本机用 curl 快速连通测试(可选)

# 创建草稿
curl -X POST "https://你的域名/jsonapi/node/article" \
 -u "api_poster:密码" \
 -H "Content-Type: application/vnd.api+json" \
 --data '{
   "data": {
     "type": "node--article",
     "attributes": {
       "title": "自动发送测试 - 本机",
       "body": { "value": "<p>正文</p>", "format": "basic_html" },
       "status": false
     }
   }
 }'

# 发布(把上一步响应里的 UUID 填入)
curl -X PATCH "https://你的域名/jsonapi/node/article/<UUID>" \
 -u "api_poster:密码" \
 -H "Content-Type: application/vnd.api+json" \
 --data '{ "data": { "type": "node--article", "id": "<UUID>", "attributes": { "status": true } } }'

备注:未启用内容审核流时,POST 时把 "status": true 就会直接发布


2)Git & GitHub:把站点纳入私有仓库,并准备 Actions

内容与代码分离:文章保存在数据库,不进 Git;Git 只管理你的“脚本/工作流与记录文件”。

2.1 在 GitHub 新建私有仓库

  • New repository → Private;保持空仓库(不自动生成 README)。

2.2 服务器站点目录初始化 Git 并推送

# 站点目录(以 /www/wwwroot/updatebao.com 为例)
cd /www/wwwroot/updatebao.com

git init
git branch -m main

# 写一个 .gitignore(避免上传目录、settings.php、缓存等入库)
cat > .gitignore <<'EOF'
sites/*/files/
sites/*/private/
sites/*/settings.php
sites/*/services.yml
sites/*/files/php/twig/
sites/*/files/styles/
sites/*/files/js/
sites/*/files/css/
core/assets/
vendor/
*.log
tmp/
cache/
.env
.env.*
.DS_Store
Thumbs.db
.idea/
.vscode/
EOF

所有权一致(如果站点属主是 www:www,确保 .git 也归它):

chown -R www:www /www/wwwroot/updatebao.com/.git

以站点用户提交并推送(推荐 SSH deploy key):

# 设置身份(仅对这个仓库)
sudo -u www git -C /www/wwwroot/updatebao.com config user.name "updatebao"
sudo -u www git -C /www/wwwroot/updatebao.com config user.email "you@example.com"

sudo -u www git -C /www/wwwroot/updatebao.com add .
sudo -u www git -C /www/wwwroot/updatebao.com commit -m "chore: initial import"

# GitHub 仓库 → Settings → Deploy keys → Add,勾“Allow write access”
# 服务器生成并添加公钥
sudo -u www -H bash -c 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && ssh-keygen -t ed25519 -C "server:updatebao" -N "" -f ~/.ssh/id_ed25519'
sudo -u www cat /home/www/.ssh/id_ed25519.pub   # 复制到 Deploy key

# 绑定远程并推送
sudo -u www git -C /www/wwwroot/updatebao.com remote add origin git@github.com:<你的用户名>/<仓库名>.git
sudo -u www git -C /www/wwwroot/updatebao.com push -u origin main

Web 服务器配置中建议禁止访问 .git
location ~ /\.git { return 404; }

2.3 在你本地克隆这个仓库(写文档与工作流)

cd D:\work\my-blog
git clone https://github.com/<你的用户名>/<仓库名>.git .

2.4 添加 GitHub Actions:把 Markdown 自动发到 Drupal

在仓库创建工作流文件:/.github/workflows/post-to-drupal.yml

name: Post summary to Drupal

on:
 workflow_dispatch:
   inputs:
     note_path:
       description: '总结文件路径(如 notes/today.md 或 notes/2025-10-07.html)'
       required: true
       default: 'notes/today.md'
     title:
       description: '文章标题(留空自动用文件名)'
       required: false
     publish:
       description: '是否立即发布'
       type: boolean
       default: true
 push:
   paths:
     - 'notes/**/*.md'
     - 'notes/**/*.html'
     - 'notes/*.md'
     - 'notes/*.html'
   branches: [ main ]

jobs:
 post:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v4
       with:
         fetch-depth: 2

     - name: Decide note path
       id: resolve
       shell: bash
       run: |
         if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
           echo "note_path=${{ inputs.note_path }}" >> "$GITHUB_OUTPUT"
         else
           NOTE_PATH="$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -E '^notes/.*\.(md|html)$' | head -n 1)"
           [[ -n "$NOTE_PATH" && -f "$NOTE_PATH" ]] || { echo "找不到要发布的文件"; exit 1; }
           echo "note_path=$NOTE_PATH" >> "$GITHUB_OUTPUT"
         fi

     - name: Decide title
       id: title
       shell: bash
       run: |
         if [[ -n "${{ inputs.title }}" ]]; then
           T="${{ inputs.title }}"
         else
           F="${{ steps.resolve.outputs.note_path }}"
           B="$(basename "$F")"
           T="${B%.*}"
         fi
         echo "title=$T" >> "$GITHUB_OUTPUT"

     - name: Install tools
       shell: bash
       run: |
         sudo apt-get update -y >/dev/null
         sudo apt-get install -y jq python3 python3-pip >/dev/null
         python3 -m pip install --quiet markdown

     - name: Convert body to HTML (no heredoc)
       id: body
       shell: bash
       env:
         NOTE_PATH: ${{ steps.resolve.outputs.note_path }}
       run: |
         set -e
         if [[ "$NOTE_PATH" == *.md ]]; then
           python3 -c "import os, pathlib, markdown; p=pathlib.Path(os.environ['NOTE_PATH']); html=markdown.markdown(p.read_text(encoding='utf-8'), extensions=['extra','toc','sane_lists']); pathlib.Path('body.html').write_text(html, encoding='utf-8')"
         else
           cp "$NOTE_PATH" body.html
         fi
         echo "body=$(jq -Rs . < body.html)" >> $GITHUB_OUTPUT

     - name: Create node via JSON:API
       id: create
       env:
         BASE: ${{ secrets.DRUPAL_BASE }}
         USER: ${{ secrets.DRUPAL_USER }}
         PASS: ${{ secrets.DRUPAL_PASS }}
         TITLE: ${{ steps.title.outputs.title }}
         BODY: ${{ steps.body.outputs.body }}
         EVENT: ${{ github.event_name }}
         PUBLISH_INPUT: ${{ inputs.publish }}
       shell: bash
       run: |
         if [[ "$EVENT" == "workflow_dispatch" ]]; then
           STATUS_VAL=$([ "$PUBLISH_INPUT" = "true" ] && echo true || echo false)
         else
           STATUS_VAL=true
         fi
         PAYLOAD=$(cat <<EOF
         {
           "data":{
             "type":"node--article",
             "attributes":{
               "title":"$TITLE",
               "body":{"value":$BODY,"format":"basic_html"},
               "status": $STATUS_VAL
             }
           }
         }
         EOF
         )
         echo ">>> POST $BASE/jsonapi/node/article"
         RESP=$(curl -sS -u "$USER:$PASS" -H "Content-Type: application/vnd.api+json" -X POST "$BASE/jsonapi/node/article" --data "$PAYLOAD")
         echo "$RESP" | jq .
         UUID=$(echo "$RESP" | jq -r '.data.id')
         test -n "$UUID" -a "$UUID" != "null" || { echo "创建失败:未返回 UUID"; exit 1; }
         echo "uuid=$UUID" >> "$GITHUB_OUTPUT"

Settings → Secrets and variables → Actions 新建 3 个 Repository secrets

  • DRUPAL_BASE = https://你的域名(不要末尾 /
  • DRUPAL_USER = api_poster
  • DRUPAL_PASS = 该用户密码

保存后 GitHub 不会显示明文,若要修改可覆盖保存。


3)平时怎么用:写 md → 提交 → 自动发布

3.1 在仓库里新增 Markdown

mkdir -p notes
cat > notes/2025-10-07.md <<'MD'
# 10-07 工作小结
- 站点接入 GitHub(deploy key)
- 开启 JSON:API 写入;本机 curl 测试通过
- 新增 GitHub Actions:md → HTML → 文章发布
MD

git add notes/2025-10-07.md
git commit -m "feat(notes): add 2025-10-07"
git push

推送命中 notes/*.mdnotes/*.html,Actions 会自动运行并(默认)直接发布
也可到 Actions → Post summary to Drupal → Run workflow 手动指定 note_path/title 并选择是否立即发布。


常见问题与排查

  • 405 Method Not Allowed:JSON:API 仍是只读 → /admin/config/services/jsonapi 取消 “Accept only read operations”。
  • 401/403 Forbidden
    • 检查 Secrets 是否正确;
    • API 账号是否有“创建 Article 内容”“使用 Basic HTML 文本格式”权限;
    • 临时将 format 改为 plain_text 测试,确认是文本格式权限导致的。
  • 415 Unsupported Media Type:请求头必须 Content-Type: application/vnd.api+json
  • 文本不渲染 HTML:确保该角色被允许使用 basic_html;或把 format 改为你站点允许的其它格式。
  • .git 被外网访问:在 Web 服务器配置中禁掉 /.git 访问。
  • Windows 命令行粘贴 JSON 报错:把 JSON 写到文件里,再用 --data "@file.json";或单行执行。

评论