使用 Vrite 在 Dev.to 上更好地撰写博客 - 用于技术内容的无头 CMS
随着技术写作越来越受欢迎——部分原因在于DEV或Hashnode等平台的推动——我发现这个领域的工具仍然匮乏,这很有意思。你经常需要编写原始的 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 中最顶层的组织单位;它是您所有内容组、团队成员、编辑设置和 API 访问的控制点;系统会为您创建一个默认工作区,但您可以创建并受邀加入任意数量的工作区;
- 内容组- 相当于看板仪表板中的列;它们基本上将所有内容片段分组在一个标签下,例如想法、草稿、已发布。
- 内容片段- 您的实际内容及其元数据(如描述、标签等)所在的位置;
假设您计划为 Dev.to 博客创建一个全新的工作区,用于发布独特的内容。要创建一个,请点击左下角的“切换工作区”按钮(六边形),然后点击“新建工作区”。
您需要提供一个名称,以及可选的描述和徽标。然后点击“创建工作区”,并从列表中选择您创建的新工作区:
回到仪表盘,您现在可以通过点击“新建组”来创建一些内容组来组织您的内容。完成后,您最终可以通过点击所选列底部的“新建内容片段”来创建新的内容片段。
新建内容后,您可以在侧面板中查看和配置其所有元数据。在 Vrite 中,除了创建和管理内容之外,几乎所有操作都在这个可调整大小的视图中进行。这样,您可以在编辑元数据或配置设置的同时,始终关注内容。
现在,单击侧面板或看板中的内容块卡上的“在编辑器中打开” (您也可以使用侧面菜单按钮)以在编辑器中打开选定的内容块。
这就是奇迹发生的地方!在创作下一个精彩作品时,您可以随意探索编辑器。Vrite 实时同步所有更改,并支持现代所见即所得编辑器中的许多格式选项。除此之外,您还可以获得一个高级代码编辑器,用于编辑所有代码片段,并具有自动完成和支持语言的格式化等功能:
与 DEV 连接
写完下一篇作品后,就该发布了!为了方便起见,Vrite 编辑器提供了导出菜单,您可以将编辑器内容导出为 JSON、HTML 或 GitHub Flavored Markdown (GFM) 格式,以便轻松复制粘贴。但是,为了获得更流畅的自动发布体验,您可能需要使用 Vrite API 和 Webhook。
预期的工作流程如下:
- 将内容片段拖放到发布栏;
- 通过Webhooks向服务器发送消息;
- 通过Vrite API和JS SDK检索和处理内容;
- 在 Dev.to 上发布/更新博客文章;
在本教程中,我将使用Cloudflare Workers,因为它们非常快速且易于设置,但您也可以使用几乎任何其他支持 JS 的无服务器提供商。
首先创建一个新的 CF Worker 项目:
npm create cloudflare
然后,cd
进入脚手架项目wrangler login
并安装Vrite JS SDK:
wrangler login
npm i @vrite/sdk
要与 SDK 交互,您需要一个 API 令牌。要从 Vrite 获取,请前往“设置”→“API”→“新建 API 令牌”:
建议将 API 令牌的权限保持在必要的最低限度,在本例中,这意味着仅对内容片段拥有写入权限(因为我们稍后会实际更新内容片段的元数据)。点击“创建新令牌”后,您将看到新创建的令牌。请妥善保管它——您只会看到一次!
此外,要通过 Dev.to 的 API 发布内容,您还需要从中获取 API 密钥。为此,请转到DEV 帐户的设置底部,然后单击“生成 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]"
事件发生后,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();
},
};
这里发生了什么?我们首先启动 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);
};
// ...
内容转换遍历 JSON 树 - 从最低级到最高级节点 - 并处理每个节点,始终传递content
从子节点生成的结果字符串。
在上面的函数中processContent()
,我们将内联格式选项(如粗体、斜体等)的处理重定向到gfmTransformer
,因为 GFM 和 DEV Markdown 都支持相同的格式选项。对于节点(如段落、图像、列表等),我们“过滤” taskList
s (因为 DEV 不支持它们),并embeds
使用 DEV 的 liquid 标签和作为节点属性提供的嵌入 URL来处理src
。
现在可以通过 Wrangler CLI 部署 Worker 了:
wrangler deploy
部署完成后,您应该会在终端中获取调用 Worker 的 URL。现在,您可以使用它在 Vrite 中创建新的 Webhook:
转到“设置”→“Webhook”→“新建 Webhook”(全部在侧面板中)
对于事件选择New content piece added
— 每次在给定的内容组内直接创建新内容片段(在本例中为已发布)或将其拖放到其中时,都会触发此事件。
现在,您只需拖放准备好的内容片段,即可看到它自动发布在 DEV 上!🎉
后续步骤
现在,Vrite 还有很多功能我在本文中没有介绍。以下是一些示例:
- 仅限新添加到内容组并已发布/更新的内容。您可以考虑“锁定”此内容组,这样编辑这些内容时,需要先将文章移回“草稿”或“编辑中”栏。如有必要,您可以为这些组设置专用的 Webhook,以便内容在 DEV 上自动取消发布。
- 自推出 Workspaces 以来,Vrite 支持 Teams 和类似 Google Docs 的实时协作功能。这使其从标准 CMS 升级为真正优秀的编辑器,并允许您加快内容交付速度,无需手动复制粘贴。因此,您可以随时邀请其他协作者加入您的工作区,并通过角色和权限控制他们的访问级别。
- 由于 Vrite 支持各种格式选项和内容块,您可能需要限制可用的功能以更好地适应您的写作风格,尤其是在团队协作时。请尝试在设置中调整您的编辑体验,包括上述选项以及用于代码格式化的Prettier 配置。
- 最后,由于 Vrite 是一个外部 CMS,您可以自由地将其与任何其他内容交付前端(如您的个人博客或其他平台)连接起来,并轻松地交叉发布您的内容。
底线
现在,需要注意的是,Vrite 仍处于测试阶段。这意味着并非所有功能都已实现,您可能会遇到错误和其他问题。但这只是整个过程的一部分,希望您能耐心等待,因为我们正在不断改进技术写作领域!
- 🌟 GitHub 上的 Star Vrite — https://github.com/vriteio/vrite
- 🐞报告错误— https://github.com/vriteio/vrite/issues
- 🐦在 Twitter 上关注最新更新— https://twitter.com/vriteio
- 💬加入 Vrite Discord — https://discord.gg/yYqDWyKnqE
- ℹ️了解有关 Vrite 的更多信息— https://vrite.io