XingGuo Song

宋玉の世界

如何将语雀文章发布到Hexo博客

发布于 # 前端

目录

写在前面

实现语雀上的文章自动化发布到个人博客,首先是因为语雀本身是一个优秀的创作的平台,能方便的编辑和管理文章,但缺点是它本身是一个封闭的系统,就像微信公众号一样,不利于 SEO。其次个人博客是由自己搭建,没有任何限制,也能更好的进行 SEO。基于这两点原因,语雀+个人博客就是一个完美的组合,个人博客作为前台,语雀作为后台,既能很好的创造和管理文章,有能够将需要共享的文章发布到个人博客,供大家搜索。下面要做的就是建立起两者的桥梁。

传送门

如何将语雀文章发布到Hexo博客 20240606112044177

设计思路

1c35ecd6b97ece67648a697e29149539 MD5

整体的设计思路是每次语雀更新文档都通过 Webhook 发送钉钉消息,当手动点击【发布】的时候再触发 GithubAction 将 slug 对应的文章从语雀拉取下去并推送到博客仓库,最后部署博客。触发流程虽然是从语雀到 Github 的过程,但开发的过程是优先处理 Github 部分然后再由语雀触发 Webhook,开发过程主要包括以下几个步骤。

  1. 配置yuque-heox-publish
  2. 配置.github/workflows/publish.yml
  3. 使用云函数触发 GithubAction
  4. 使用云函数推送钉钉消息
  5. 配置语雀 Webhook

yuque-hexo-publish是什么

[yuque-hexo](https://github.com/x-cold/yuque-hexo)是一个很优秀的插件,能将语雀的文章完全同步到个人博客上。但这并不满足我的需求,我需要的是发布而不是同步,我并不想把所有文章都同步到个人博客上,也不希望只能有一个知识库里面的文章能发布到个人博客,而是希望能把我想发布的文章都能发布到个人博客上。基于这个这个考虑我在yuque-hexo的基础上新增了发布功能,yuque-hexo-publish就此诞生了。yuque-hexo-publish 在功能上和 yuque-hexo 是完全一致的,只是新增了命令 publish。

具体实施

Hexo 博客开发

项目配置

  1. package.json中配置 yuque-hexo-publish。
  "yuqueConfig": {
    "postPath": "source/_posts", //文章目录
    "baseUrl": "https://www.yuque.com/api/v2",
    "login": "songxingguo",//用户名
    "repo": "devhints", //知识库名称
    "onlyPublished": false,
    "onlyPublic": false,
    "imgCdn": {
      "concurrency": 1,
      "imageBed": "github", //图床类型
      "enabled": true,
      "bucket": "songxingguo.github.io", //仓库地址
      "prefixKey": "static/images" //图片文件上传到仓库后的目录
    }
  }

更详细的配置说明可以查看[yuque-hexo-publish](https://github.com/songxingguo/yuque-hexo/tree/feature-publish)

  1. package.json中添加任务脚本。
  "scripts": {
    "build": "hexo generate",
    "pulish": "yuque-hexo publish",
  },
  1. 根目录下新建 .github/workflows/publish.yml
name: Deploy To Github Pages
on: [repository_dispatch]
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v3
        with:
          persist-credentials: false

      - name: Setup Node.js
        uses: actions/setup-node@master
        with:
          node-version: "12.0.0"

      - name: Install and Build 🔧
        env:
          YUQUE_TOKEN: ${{ secrets.YUQUE_TOKEN }}
          SLUG: ${{ github.event.client_payload.slug }}
          SECRET_ID: songxingguo
          SECRET_KEY: ${{ secrets.ACCESS_TOKEN }}
        run: |
          npm i
          npm run publish
          npm run build

      - name: 配置Git用户名邮箱
        run: |
          git config --global user.name "songxingguo"
          git config --global user.email "1328989942@qq.com"

      - name: 提交yuque拉取的文章到GitHub仓库
        run: |
          git pull
          git add .
          git commit -m "feat:提交文章" -a

      - name: 推送文章到仓库
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.ACCESS_TOKEN }}

      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
          BRANCH: master
          FOLDER: public
          CLEAN: true

YUQUE_TOKEACCESS_TOKEN都是 Github 的环境变量,github.event.client_payload.slug[repository_dispatch](https://docs.github.com/zh/actions/using-workflows/events-that-trigger-workflows#repository_dispatch)事件中传递的参数。

如何本地调试

YUQUE_TOKEN=xxx SLUG=xxx yuque-hexo publish

yuque-hexo publish 需要两个环境变量,YUQUE_TOKEN是语雀的授权码SLUG是语雀文章的 id。

阿里云函数开发

初始化云函数

  1. 创建一个处理 http 请求的 node.js 云函数,云函数创建地址

76f3f879b878b3ed83dc201ec1e2739a MD5

  1. 创建好后的效果如下。

9607784fb89da84848255da0ab2bfd3b MD5

调试云函数

  1. 配置测试请求。

659da3a0fc4ddcc5d767857efd44eec3 MD5

  1. 部署代码之后,点击【测试函数】,就可以看到日志输出。

4b18f9b1d2a3666654764ce4b2d464f0 MD5

使用云函数触发 GithubAction

这个函数是通过 Get 请求进行调用的。

https://xx-xx-opbwmocbhe.cn-hangzhou.fcapp.run?slug=${slug}`

首先需要从参数中获取 slug。

const slug = params.queries.slug;

然后使用[repository_dispatch](https://docs.github.com/zh/actions/using-workflows/events-that-trigger-workflows#repository_dispatch)事件触发 GithubAction,并将slug传给client_payload

const axios = require("axios");

const options = {
  method: "POST",
  url: "https://api.github.com/repos/songxingguo/songxingguo.github.io/dispatches",
  headers: {
    "content-type": "application/json",
    Accept: "application/vnd.github+json",
    Authorization: "Bearer GITHUB_TOKEN",
  },
  data: { event_type: "publish", client_payload: { slug } },
};

axios
  .request(options)
  .then(function (response) {
    console.log(response.data);
  })
  .catch(function (error) {
    console.error(error);
  });

其中GITHUB_TOKEN是 github 的授权码,获取和配置可以参考使用 Github Action 部署静态网站event_type是活动类型这个和.github/workflows/publish.yml里面的types是一一对应的,如果.github/workflows/publish.yml中没有指定具体types,那这儿填任何值都是可以。更多信息可以查看详细配置cbc6e13f32c639789af398c5a70e44ec MD5 完整代码如下。

需要注意 ⚠️ 异步执行的问题,可以通过async-await保证请求是在resp.send之前执行的。

var axios = require("axios");
var getRawBody = require("raw-body");
var getFormBody = require("body/form");
var body = require("body");

/*
To enable the initializer feature (https://help.aliyun.com/document_detail/156876.html)
please implement the initializer function as below:
exports.initializer = (context, callback) => {
  console.log('initializing');
  callback(null, '');
};
*/

exports.handler = (req, resp, context) => {
  var params = {
    path: req.path,
    queries: req.queries,
    headers: req.headers,
    method: req.method,
    requestURI: req.url,
    clientIP: req.clientIP,
  };

  getRawBody(req, async (err, body) => {
    for (var key in req.queries) {
      var value = req.queries[key];
      resp.setHeader(key, value);
    }
    resp.setHeader("Content-Type", "text/plain");
    params.body = body.toString();
    const slug = params.queries.slug;
    console.log("slug", slug);

    const options = {
      method: "POST",
      url: "https://api.github.com/repos/songxingguo/songxingguo.github.io/dispatches",
      headers: {
        "content-type": "application/json",
        Accept: "application/vnd.github+json",
        Authorization: "Bearer xxx",
      },
      data: { event_type: "publish", client_payload: { slug } },
    };

    await axios
      .request(options)
      .then(function (response) {
        console.log(response.data);
      })
      .catch(function (error) {
        console.error(error);
      });

    resp.send(JSON.stringify(params, null, "    "));
  });
};

使用云函数推送钉钉消息

调试钉钉消息类型及数据格式
  1. 创建钉钉机器人,并复制 webhook 地址。

c2e722e7abf99f7f952594a4dc56fbb5 MD5

  1. 打开在线接口调试工具,输入 Webhook 地址,配置钉钉消息类型及数据格式

f9de1a97e462e0cd34247b86b8badbcd MD5

  1. 调试到自己想要的效果后复制请求的代码。

8f4ef4c4ca1186b91b78fed745c59395 MD5


这个函数是通过 post 请求进行调用,首先需要从请求体中获取slugtitle

const data = JSON.parse(params.body);
const { slug, title } = data.data;

然后再请求钉钉机器人的 Webhook 地址。

const axios = require("axios");

const options = {
  method: "POST",
  url: "https://oapi.dingtalk.com/robot/send",
  params: {
    access_token: "",
  },
  headers: { "content-type": "application/json" },
  data: {
    msgtype: "actionCard",
    actionCard: {
      title: "文档发布",
      text: `${title}`,
      btnOrientation: "0",
      btns: [
        {
          title: "发布",
          actionURL: `https://xx-xx-xx.cn-hangzhou.fcapp.run?slug=${slug}`,
        }, // 触发GithubAction云函数的公网地址
        {
          title: "Actions",
          actionURL:
            "https://github.com/songxingguo/songxingguo.github.io/actions",
        },
      ],
    },
  },
};

axios
  .request(options)
  .then(function (response) {
    console.log(response.data);
  })
  .catch(function (error) {
    console.error(error);
  });

其中access_token 为钉钉 webhook 地址中的授权码。

https://oapi.dingtalk.com/robot/send?access_token=xx

最终的效果如下所示。

e2edea4df3177d710b3563df917bdf13 MD5 完整代码如下。

var getRawBody = require("raw-body");
var getFormBody = require("body/form");
var body = require("body");
var axios = require("axios");

/*
To enable the initializer feature (https://help.aliyun.com/document_detail/156876.html)
please implement the initializer function as below:
exports.initializer = (context, callback) => {
  console.log('initializing');
  callback(null, '');
};
*/

exports.handler = (req, resp, context) => {
  console.log("hello world");

  var params = {
    path: req.path,
    queries: req.queries,
    headers: req.headers,
    method: req.method,
    requestURI: req.url,
    clientIP: req.clientIP,
  };

  getRawBody(req, async (err, body) => {
    for (var key in req.queries) {
      var value = req.queries[key];
      resp.setHeader(key, value);
    }
    resp.setHeader("Content-Type", "text/plain");
    params.body = body.toString();

    const data = JSON.parse(params.body);
    const { slug, title } = data.data;
    console.log("slug", slug);

    const options = {
      method: "POST",
      url: "https://oapi.dingtalk.com/robot/send",
      params: {
        access_token: "",
      },
      headers: { "content-type": "application/json" },
      data: {
        msgtype: "actionCard",
        actionCard: {
          title: "文档发布",
          text: `${title}`,
          btnOrientation: "0",
          btns: [
            {
              title: "发布",
              actionURL: `https://xx-xx-xx.cn-hangzhou.fcapp.run?slug=${slug}`,
            },
            {
              title: "Actions",
              actionURL:
                "https://github.com/songxingguo/songxingguo.github.io/actions",
            },
          ],
        },
      },
    };

    await axios
      .request(options)
      .then(function (response) {
        console.log(response.data);
      })
      .catch(function (error) {
        console.error(error);
      });

    resp.send(JSON.stringify(params, null, "    "));
  });
};

配置语雀 Webhook

进入语雀webhook 配置页面,填写名称和推送钉钉消息云函数的公网地址,并选择更新文档时触发。 1fc8b945155f48a509889c4b998adef8 MD5

其他

Hoppscotch

Hoppscotch 和 postman 的功能是一样的,但这个是线上的版本,更加的方便。有时请求会出现请求无法到达的问题,需要配置插件代理,将请求的域名加入到插件中。

91e9b7c4d9a7008677ce90256b9d458d MD5

拓展阅读