使用 Vrite 在 Dev.to 上更好地撰写博客 - 用于技术内容的无头 CMS

2025-06-11

使用 Vrite 在 Dev.to 上更好地撰写博客 - 用于技术内容的无头 CMS

随着技术写作越来越受欢迎——部分原因在于DEVHashnode等平台的推动——我发现这个领域的工具仍然匮乏,这很有意思。你经常需要编写原始的 Markdown 代码,在不同的编辑器之间切换,并使用许多工具来支持内容制作过程。

正因如此,我决定创建Vrite——一种专为技术写作而设计的新型无头 CMS,并注重良好的开发者体验。从内置的看板管理仪表盘,到支持Markdown 的高级所见即所得编辑器、实时协作、嵌入式代码编辑器以及Prettier集成——Vrite 旨在成为您所有技术内容的一站式商店。

随着本周早些时候发布公开测试版,Vrite 现已开源并可供所有人使用 - 以帮助指导未来的路线图并为所有技术作家打造最好的工具!

开发 API

CMS(尤其是无头CMS)如果没有连接的发布端点,功能就非常有限。以Vrite为例,得益于其API和灵活的内容格式,它可以轻松连接到任何内容,从个人博客到GitHub repo,再到像Dev.to这样的平台。

Dev.to 是一个特别有趣的选择,因为其底层平台 Forem 的 API文档齐全且易于获取。那么,让我们看看如何将它与 Vrite 连接起来!

Vrite 入门

鉴于 Vrite 是开源的,您很快就能自行托管它。不过,我仍在努力完善相关文档和支持。目前,试用 Vrite 的最佳方式是通过 app.vrite.io 上的免费“云”版本

首先注册一个帐户- 直接注册或通过 GitHub 注册:

Vrite 登录屏幕

进入后,您会看到一个看板仪表板。在这里,您可以管理所有内容:

Vrite 仪表板

此时,值得解释一下 Vrite 中的结构:

  • 工作区- 这是 Vrite 中最顶层的组织单位;它是您所有内容组、团队成员、编辑设置和 API 访问的控制点;系统会为您创建一个默认工作区,但您可以创建并受邀加入任意数量的工作区;
  • 内容组- 相当于看板仪表板中的列;它们基本上将所有内容片段分组在一个标签下,例如想法草稿已发布
  • 内容片段- 您的实际内容及其元数据(如描述、标签等)所在的位置;

假设您计划为 Dev.to 博客创建一个全新的工作区,用于发布独特的内容。要创建一个,请点击左下角的“切换工作区”按钮(六边形),然后点击“新建工作区”

创建新的 Vrite 工作区

您需要提供一个名称,以及可选的描述和徽标。然后点击“创建工作区”,并从列表中选择您创建的新工作区:

Vrite 工作区列表

回到仪表盘,您现在可以通过点击“新建组”来创建一些内容组来组织您的内容。完成后,您最终可以通过点击所选列底部的“新建内容片段”来创建新的内容片段。

在 Vrite 中创建新的内容片段

新建内容后,您可以在侧面板中查看和配置其所有元数据。在 Vrite 中,除了创建和管理内容之外,几乎所有操作都在这个可调整大小的视图中进行。这样,您可以在编辑元数据或配置设置的同时,始终关注内容。

现在,单击侧面板或看板中的内容块卡上的“在编辑器中打开” (您也可以使用侧面菜单按钮)以在编辑器中打开选定的内容块。

Vrite 编辑器

这就是奇迹发生的地方!在创作下一个精彩作品时,您可以随意探索编辑器。Vrite 实时同步所有更改,并支持现代所见即所得编辑器中的许多格式选项。除此之外,您还可以获得一个高级代码编辑器,用于编辑所有代码片段,并具有自动完成和支持语言的格式化等功能:

Vrite 中的代码片段编辑器

与 DEV 连接

写完下一篇作品后,就该发布了!为了方便起见,Vrite 编辑器提供了导出菜单,您可以将编辑器内容导出为 JSON、HTML 或 GitHub Flavored Markdown (GFM) 格式,以便轻松复制粘贴。但是,为了获得更流畅的自动发布体验,您可能需要使用 Vrite API 和 Webhook。

预期的工作流程如下:

  1. 将内容片段拖放到发布栏;
  2. 通过Webhooks向服务器发送消息;
  3. 通过Vrite API和JS SDK检索和处理内容;
  4. 在 Dev.to 上发布/更新博客文章;

在本教程中,我将使用Cloudflare Workers,因为它们非常快速且易于设置,但您也可以使用几乎任何其他支持 JS 的无服务器提供商。

首先创建一个新的 CF Worker 项目:

npm create cloudflare
Enter fullscreen mode Exit fullscreen mode

然后,cd进入脚手架项目wrangler login并安装Vrite JS SDK

wrangler login
npm i @vrite/sdk
Enter fullscreen mode Exit fullscreen mode

要与 SDK 交互,您需要一个 API 令牌。要从 Vrite 获取,请前往“设置”→“API”→“新建 API 令牌”

在 Vrite 中创建新的 API 令牌

建议将 API 令牌的权限保持在必要的最低限度,在本例中,这意味着仅对内容片段拥有写入权限(因为我们稍后会实际更新内容片段的元数据)。点击“创建新令牌”后,您将看到新创建的令牌。请妥善保管它——您只会看到一次

此外,要通过 Dev.to 的 API 发布内容,您还需要从中获取 API 密钥。为此,请转到DEV 帐户的设置底部,然后单击“生成 API 密钥”

在 Dev.to 中生成 API 密钥

现在,通过以下方式将两个令牌作为环境变量添加到 Worker wrangler.toml

name = "autopublishing"
main = "src/worker.ts"
compatibility_date = "2023-05-18"

[vars]
VRITE_API_TOKEN = "[YOUR_VRITE_API_TOKEN]"
DEV_API_KEY="[YOUR_DEV_API_KEY]"
Enter fullscreen mode Exit fullscreen mode

事件发生后,Vrite 会POST向 webhook 中已配置的目标 URL 发送一个包含 JSON 有效负载的请求。对于我们的用例来说,此有效负载中最重要的部分是刚刚添加到指定内容组(通过拖放或直接创建)的内容片段的 ID。

让我们最终创建我们的 Worker(内部src/worker.ts):

import { JSONContent, createClient } from '@vrite/sdk/api';
import { createContentTransformer, gfmTransformer } from '@vrite/sdk/transformers';

const processContent = (content: JSONContent): string => {
  // ...
};

export interface Env {
  VRITE_API_TOKEN: string;
  DEV_API_KEY: string;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const payload: { id: string } = await request.json();
    const client = createClient({ token: env.VRITE_API_TOKEN });
    const contentPiece = await client.contentPieces.get({
      id: payload.id,
      content: true,
      description: 'text',
    });
    const article = {
      title: contentPiece.title,
      body_markdown: processContent(contentPiece.content),
      description: contentPiece.description || undefined,
      tags: contentPiece.tags.map((tag) => tag.label?.toLowerCase().replace(/\s/g, '')).filter(Boolean),
      canonical_url: contentPiece.canonicalLink || undefined,
      published: true,
      series: contentPiece.customData?.devSeries || undefined,
      main_image: contentPiece.coverUrl || undefined,
    };

    if (contentPiece.customData?.devId) {
      try {
        const response = await fetch(`https://dev.to/api/articles/${contentPiece.customData.devId}`, {
          method: 'PUT',
          headers: {
            'api-key': env.DEV_API_KEY,
            Accept: 'application/json',
            'content-type': 'application/json',
            'User-Agent': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)',
          },
          body: JSON.stringify({
            article,
          }),
        });
        const data: { error?: string } = await response.json();

        if (data.error) {
          console.error('Error from DEV: ', data.error);
        }
      } catch (error) {
        console.error(error);
      }
    } else {
      try {
        const response = await fetch(`https://dev.to/api/articles`, {
          method: 'POST',
          body: JSON.stringify({ article }),
          headers: {
            'api-key': env.DEV_API_KEY,
            Accept: 'application/json',
            'content-type': 'application/json',
            'User-Agent': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)',
          },
        });
        const data: { error?: string; id?: string } = await response.json();

        if (data.error) {
          console.error(data.error);
        } else if (data.id) {
          await client.contentPieces.update({
            id: contentPiece.id,
            customData: {
              ...contentPiece.customData,
              devId: data.id,
            },
          });
        }
      } catch (error) {
        console.error(error);
      }
    }

    return new Response();
  },
};

Enter fullscreen mode Exit fullscreen mode

这里发生了什么?我们首先启动 Vrite API 客户端,并获取触发事件的内容片段的元数据和相关内容。然后,我们使用这些数据创建一个DEV APIarticle所需的对象,并用它来发出请求。

在 Vrite 中,除了标签或规范链接等严格定义的元数据外,您还可以提供基于 JSON 的自定义数据。这些数据既可以通过仪表盘配置,也可通过 API 进行配置,这使得它非常适合存储诸如本例中 DEV 文章 ID 之类的数据,它使我们能够决定是发布新文章还是更新现有文章(使用自定义devId属性)。同样的机制也适用于检索文章在 DEV 上应归入的系列名称,该名称可以通过 Vrite 仪表盘使用自定义devSeries属性进行配置。

值得注意的是,对于对 DEV API 的请求,我们传递了一个通用User-Agent标头 - 有必要发出成功的请求而没有403机器人检测错误。

内容转换器

您可能已经注意到,该body_markdown属性被设置为调用结果processContent()。这是因为 Vrite API 以 JSON 格式提供其内容。该格式源自Vrite 编辑器的ProseMirror库,可轻松适应各种需求,从而实现灵活的内容交付。

Vrite JS SDK 内置了用于转换此格式的工具,称为内容转换器。它们允许您轻松地将 JSON 处理为基于字符串的格式,例如 HTML 或 GFM(两者都在 SDK 中内置了专用的转换器)。

对于 DEV,大多数情况下使用 GFM 转换器即可。但是,此转换器会忽略 Vrite 编辑器和 DEV(例如 CodePen、CodeSandbox 和 YouTube)都支持的嵌入,因为它们不受 GFM 规范的支持。因此,让我们构建一个自定义转换器来扩展它,gfmTransformer以添加对这些嵌入的支持:

import { JSONContent, createClient } from '@vrite/sdk/api';
import { createContentTransformer, gfmTransformer } from '@vrite/sdk/transformers';

const processContent = (content: JSONContent): string => {
  const devTransformer = createContentTransformer({
    applyInlineFormatting(type, attrs, content) {
      return gfmTransformer({
        type,
        attrs,
        content: [
          {
            type: 'text',
            marks: [{ type, attrs }],
            text: content,
          },
        ],
      });
    },
    transformNode(type, attrs, content) {
      switch (type) {
        case 'embed':
          return `\n{% embed ${attrs?.src || ''} %}\n`;
        case 'taskList':
          return '';
        default:
          return gfmTransformer({
            type,
            attrs,
            content: [
              {
                type: 'text',
                attrs: {},
                text: content,
              },
            ],
          });
      }
    },
  });

  return devTransformer(content);
};

// ...

Enter fullscreen mode Exit fullscreen mode

内容转换遍历 JSON 树 - 从最低级到最高级节点 - 并处理每个节点,始终传递content从子节点生成的结果字符串。

在上面的函数中processContent(),我们将内联格式选项(如粗体、斜体等)的处理重定向到gfmTransformer,因为 GFM 和 DEV Markdown 都支持相同的格式选项。对于节点(如段落、图像、列表等),我们“过滤” taskLists (因为 DEV 不支持它们),并embeds使用 DEV 的 liquid 标签和作为节点属性提供的嵌入 URL来处理src

现在可以通过 Wrangler CLI 部署 Worker 了:

wrangler deploy
Enter fullscreen mode Exit fullscreen mode

部署完成后,您应该会在终端中获取调用 Worker 的 URL。现在,您可以使用它在 Vrite 中创建新的 Webhook:

转到“设置”→“Webhook”→“新建 Webhook”(全部在侧面板中)

在 Vrite 中创建新的 Webhook

对于事件选择New content piece added— 每次在给定的内容组内直接创建新内容片段(在本例中为已发布)或将其拖放到其中时,都会触发此事件。

现在,您只需拖放准备好的内容片段,即可看到它自动发布在 DEV 上!🎉

后续步骤

现在,Vrite 还有很多功能我在本文中没有介绍。以下是一些示例:

  • 仅限新添加到内容组并已发布/更新的内容。您可以考虑“锁定”此内容组,这样编辑这些内容时,需要先将文章移回“草稿”“编辑中”栏。如有必要,您可以为这些组设置专用的 Webhook,以便内容在 DEV 上自动取消发布。
  • 自推出 Workspaces 以来,Vrite 支持 Teams 和类似 Google Docs 的实时协作功能。这使其从标准 CMS 升级为真正优秀的编辑器,并允许您加快内容交付速度,无需手动复制粘贴。因此,您可以随时邀请其他协作者加入您的工作区,并通过角色权限控制他们的访问级别。
  • 由于 Vrite 支持各种格式选项和内容块,您可能需要限制可用的功能以更好地适应您的写作风格,尤其是在团队协作时。请尝试在设置中调整您的编辑体验,包括上述选项以及用于代码格式化的Prettier 配置
  • 最后,由于 Vrite 是一个外部 CMS,您可以自由地将其与任何其他内容交付前端(如您的个人博客或其他平台)连接起来,并轻松地交叉发布您的内容。

底线

现在,需要注意的是,Vrite 仍处于测试阶段。这意味着并非所有功能都已实现,您可能会遇到错误和其他问题。但这只是整个过程的一部分,希望您能耐心等待,因为我们正在不断改进技术写作领域!

鏂囩珷鏉ユ簮锛�https://dev.to/vrite/better-blogging-on-devto-with-vrite-headless-cms-for-technical-content-4i05
PREV
教程 - 企业模块联合指南
NEXT
Termux Useful Tweaks