With technical writing becoming increasingly popular - thanks in part to platforms like DEV or Hashnode, I found it interesting that the tooling in this niche is still lacking. You often have to write raw Markdown, jump between different editors and use many tools to support the content production process.
That’s why I decided to create Vrite - a new kind of headless CMS meant specifically for technical writing, with good developer experience in mind. From the built-in Kanban management dashboard to the advanced WYSIWYG editor with support for Markdown, real-time collaboration, embedded code editor, and Prettier integration - Vrite is meant to be a one-stop-shop for all your technical content.
With the release of the Public Beta earlier this week, Vrite is now open-source and accessible to everyone - to help guide the future roadmap and make the best tool for all technical writers!
DEV API
A CMS - especially a headless one - can only do so much without a connected publishing endpoint. In the case of Vrite, thanks to its API and flexible content format, it can be easily connected to anything from a personal blog to a GitHub repo or a platform like Dev.to.
Dev.to is an especially interesting option, since the API of the underlying platform - Forem - is well-documented and easily available. So, let’s see how to connect it with Vrite!
Getting Started With Vrite
Given that Vrite is open-source, you’ll soon be able to self-host it. That said, I’m still working on proper documentation and support for this process. For now, the best way to try out Vrite is through a free “cloud” version at app.vrite.io.
Start by signing up for an account - either directly or through GitHub:
When you’re in, you’ll be greeted by a Kanban dashboard. This is where you’ll be able to manage all your content:
At this point, it’s worth explaining how things are structured in Vrite:
- Workspace - this is the top-most organizational unit in Vrite; it’s where all your content groups, team members, editing settings, and API access are controlled; A default one is created for you, though you can create and be invited to as many as you want;
- Content Groups - the equivalents to columns in the Kanban dashboard; They basically group all the content pieces under one label, e.g. Ideas, Drafts, Published.
- Content Pieces - where your actual content and its metadata - like description, tags, etc live;
Let’s say you want a completely new workspace for your Dev.to blog as you plan to publish unique content there. To create one, click the Switch Workspace button in the bottom-left corner (hexagon) and then New Workspace.
You need to provide a name and optionally - a description and logo. Then click Create Workspace and select the new workspace you’ve created from the list:
Back in the dashboard, you can now create a few content groups to organize your content by clicking New group. When that’s done, you can finally create a new content piece by clicking New content piece at the bottom of the column of your choice.
With a new content piece, you can view and configure all its metadata in the side panel. In Vrite, almost everything, aside from creating and managing content happens in this resizable view. This way you can always keep an eye on the content while editing metadata or configuring settings.
Now, click Open in editor either in the side panel or on the content piece card in the Kanban (you can also use the side-menu button) to open the selected content piece in the editor.
This is where the magic happens! Feel free to explore the editor while writing your next great piece. Vrite synchronizes all the changes in real time and supports many of the formatting options available in modern WYSIWYG editors. On top of that, you also get an advanced code editor for all your snippets with features like autocompletion and formatting for supported languages:
Connecting with DEV
When you’ve finished writing your next piece, it’s time to publish it! For convenience, the Vrite editor provides an Export menu where you can get the contents of your editor in JSON, HTML, or GitHub Flavored Markdown (GFM) for easy copy-pasting. However, to get a more proper auto-publishing experience, you’ll likely want to use Vrite API and Webhooks.
The intended workflow looks like this:
- Drag and drop content pieces to the publishing column;
- Send a message to the server via Webhooks;
- Retrieve and process the content via the Vrite API and JS SDK;
- Publish/update a blog post on Dev.to;
For this tutorial, I’ll use Cloudflare Workers as they’re really fast and easy to set up, but you can use pretty much any other serverless provider with support for JS.
Start by creating a new CF Worker project:
npm create cloudflare
Then, cd
into the scaffolded project to wrangler login
and install Vrite JS SDK:
wrangler login
npm i @vrite/sdk
To interact with the SDK, you’ll need to have an API token. To get it from Vrite, go to Settings → API → New API token:
It’s recommended to keep the permissions of the API token to the necessary minimum, which in this case means only Write access to Content pieces (as we’ll actually be updating the content piece metadata later on). After clicking Create new token you’ll be presented with the newly-created token. Keep it safe and secure - you’ll only see it once!
Additionally, to publish the content on Dev.to via its API, you’ll need to get an API key from it as well. To do so, go to the bottom of the settings in your DEV account and click Generate API Key:
Now, add both tokens to the Worker as environment variables via 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]"
Upon the event, Vrite sends a POST
request to the configured target URL of the webhook with additional JSON payload. For our use case, the most important part of this payload will be the ID of a content piece that was just added to the given content group (either by drag and dropped or by being created directly)
Let’s finally create our Worker (inside 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();
},
};
What’s going on here? We start by initiating the Vrite API client and fetching metadata and the content related to the content piece that triggered the event. Then, we use this data to create an article
object that’s expected by the DEV API and use it to make a request.
In Vrite, in addition to strictly-defined metadata like tags or canonical links, you can also provide JSON-based custom data. It’s configurable both from the dashboard and through the API, making it a great storage for data like, in this case, DEV article ID, which allows us to determine whether to publish a new article or update an existing one (using a custom devId
property). The same mechanism is applied for retrieving the name of the series the article should be assigned to on DEV, which can be configured from the Vrite dashboard using a custom devSeries
property.
Worth noting is that, for requests to DEV API, we’re passing a generic User-Agent
header - it’s necessary to make a successful request without 403
bot-detection error.
Content Transformers
You might have noticed that the body_markdown
property is set to the result of processContent()
call. That’s because the Vrite API serves its content in a JSON format. Derived from the ProseMirror library powering Vrite editor, the format allows for versatile content delivery as it can be easily adapted to various needs.
The Vrite JS SDK has built-in tools for transforming this format called Content Transformers. They allow you to easily process the JSON to a string-based format, like HTML or GFM (both of which have dedicated transformers built into the SDK).
For DEV, using the GFM transformer would be fine in most cases. However, this transformer ignores embeds that are supported by both the Vrite editor and DEV (i.e. CodePen, CodeSandbox, and YouTube) as they aren’t supported in the GFM specification. Thus, let’s build a custom Transformer that extends the gfmTransformer
to add support for these embeds:
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);
};
// ...
A Content Transform goes through the JSON tree - from the lowest to the highest-level nodes - and processes every node, always passing in the resulting content
string generated from the child nodes.
In the processContent()
function above, we’re redirecting the processing of inline formatting options (like bold, italic, etc.) - to the gfmTransformer
, as both GFM and DEV Markdown support the same formatting options. In the case of nodes (like paragraphs, images, lists, etc.) we’re “filtering out” taskList
s (as DEV doesn’t support them) and handling processing for embeds
, using DEV’s liquid tags and embed URL available as a node attribute — src
.
Now the Worker is ready to be deployed via the Wrangler CLI:
wrangler deploy
When deployed, you should get the URL for calling the Worker in your terminal. You can now use it to create a new Webhook in Vrite:
Go to Settings → Webhooks → New Webhook (all in the side panel)
For an event select New content piece added
— this will trigger every time a new content piece is created directly within the given content group (in this case Published) or dragged and dropped into it.
Now you should be able to just drag and drop your ready content piece and see it be automatically published on DEV! 🎉
Next Steps
Now, there’s a lot you can do with Vrite even right now, that I haven’t covered in this article. Here are a few examples:
- Only content pieces that are newly added to the content group and getting published/updated. You might want to consider “locking” this content group so that editing these content pieces requires you to first move the article back to the Drafts or Editing column. If necessary, you can set up dedicated Webhooks for those groups, so that content pieces are automatically unpublished on DEV.
- Since the introduction of Workspaces, Vrite supports Teams and real-time collaboration like in e.g. Google Docs. This elevates it from a standard CMS to an actually good editor and allows you to speed up your content delivery with no need for manual copy-pasting. So feel free to invite other collaborators to join your workspace and control their access level through roles and permissions.
- With Vrite’s support for various formatting options and content blocks - you might want to limit the available features to better fit your writing style - especially when you’re working in a team. Try adjusting your Editing Experience in the settings, including mentioned options and a Prettier config for code formatting.
- Finally, as Vrite is an external CMS, you can freely connect it with any other content delivery frontend (like your personal blog or other platforms) and easily cross-post your content.
Bottom Line
Now, it’s worth remembering that Vrite is still in Beta. This means that not all features are implemented yet and you are likely to encounter bugs and other issues. But that’s just part of the process and I hope you’ll bear with me as we’re evolving the technical writing landscape!
- 🌟 Star Vrite on GitHub — https://github.com/vriteio/vrite
- 🐞 Report bugs — https://github.com/vriteio/vrite/issues
- 🐦 Follow on Twitter for latest updates — https://twitter.com/vriteio
- 💬 Join Vrite Discord — https://discord.gg/yYqDWyKnqE
- ℹ️ Learn more about Vrite — https://vrite.io
Top comments (20)
Lovely tool. I already coded a branch to migrate my blog to use it. My only concern before deploying this branch using Vrite is about the pricing methodology.
Did not see anything about this, which makes me a bit concerned about adopting it for now.
What's the costs? I see features like data storage, image uploading, HTTP requests, etc that surely cost something for you. So, where we add our CC to pay for it?
Although I love the enthusiasm, be warned that it's still in development - which is why bugs might occur and I haven't yet build a proper support for content imports/migrations as I plan to.
That's also why there's no payments built-in. I don't feel comfortable charging for something that might be buggy or unstable. Of course, the hosted version will have to cost something eventually and will play a huge part in making the development sustainable.
For now, for development purposes, I can manage the demand and pay the bills. Once we get to a stable version, I hope to create a pricing structure that will be reasonable and competitive with similar tools out there. If you will feel otherwise, there will be an option of self-hosting, with easy migration back and forth.
Awesome! That's the answer I was looking for. Hopefully it will not be expensive for me to support, since it will be for sure the tool I will use from now, so I will love to pay to support its development and growth ☺️
In any case, the self-hosted is a great alternative. The place I work today use this same exact business model, so I highly recommend you to go for it 👏
Really neat program for writing. I'm looking forward to exploring it more.
Thanks, glad you like it!
This looks rad! What would you say are the key advantages from a product perspective over Contentful? (Already loving that it's open source and that it's primarily Markdown-based!)
First off, it's definitely focused on a different audience, i.e. developers and technical writers. This is already visible with the built-in code editor and Prettier integration and I plan to build on that.
Secondly, I'm going for a simple, modern UI/UX. I feel like Contentful tries to do the same, but I did feel a bit lost when exploring their platform. With proper docs, I hope this won't be the case with Vrite - and that it'll scale with no matter how many features.
Lastly, I might be wrong but I think Vrite is pretty unique kind of CMS. Not only is it dev-focused, but also has:
All that in an open-source, soon-to-be self-hostable shell. There are more things to come that I feel will make it more special but that's pretty much it for now. Hope this answers your question.
I have been hoping for something like this, this looks awesome!
Thanks, more great stuff coming soon!
Nice tool, but first attempts to try the editor are strange. Did I do something wrong?
Compared to tools like TipTap or even the built-In dev-Editor, editing seems to be fairly complicated and limiting???
I don't think the comparison is the best one. DEV supports many options but doesn't provide a WYSIWYG experience, while TipTap is Justa framework to build on (one that Vrite is based on BTW). I usually look at Notion, Dropbox Paper or StackEdit (editor-wise). Vrite is still not fully-featured as those, but it’s getting there. The goal would be to support full GFM, with a few reasonable extensions (like embeds) to provide best experience for programming content and documentation writing in the future. Once this is achieved a “raw Markdown” view could also be added with all the input options (like in DEV but with e.g. syntax highlighting).
If you’re interested to see what you’re looking for prioritized, definitely open an issue or start a discussion on GitHub: github.com/vriteio/vrite/discussions
As mentioned, I really like the concept. But editing is currently limited to things you can do with every basic markdown. What kind of "technical content" do you think can be presented with super and subscript only?
Presenting content without any advanced formatting was the initial approach of HTML, wich included about the same options that the vrite editor provides. So, we already know this was not enough.
I'm not sure we're thinking about the same regarding the concept and idea for Vrite.
Basically, my intention is to create a headless CMS that provides good UI/UX for everything related to technical (programming-related to be specific) content (including writing and management).
From my experience, most often, documentation, blogging platforms, personal blogs, etc. are based heavily on Markdown with various extensions. So, the initial goal is to support the most popular specification - GFM.
Now, the editor isn't something people are meant to see. It's what you interact with when writing. After that you can access the content in JSON format mentioned in the post, and adjust it to your needs, converting it to Markdown, HTML, etc. as required.
For vast majority of target use-cases I think GFM with embeds is more than enough. This ensures that anything you create in Vrite can be adapted to most frontends easily, whether that's a blog, DEV, Hashnode, etc.
Given that Vrite is a headless CMS, I don't control where you publish content and how it's viewed. Thus, providing all kind of formatting options beyond GFM or popular extensions like embeds, LaTeX, etc. doesn't make much sense in my opinion. For example, Notion is one of my favorite tools and was an inspiration behind the editor. However, supporting all the content blocks or formatting options it provides doesn't make sense for a headless CMS, as the effort on the user side, to actually display those properly would be just too much. So, having too many options out-of-the-box, that require effort from the user to implement and display properly I think is confusing and thus a no-go.
Now, with all that said, GFM and some embeds will be the core of what the editor supports. However, I'm down for supporting other options with time - not built-in but through some kind of customisation. I haven't yet though about how exactly will this look, but it'd best if you could add the formatting options or content blocks you need, using the JSON format underneath, while providing a rendering mechanism for Vrite editor to display them while writing, and handle them accordingly on your frontend.
On the other hand, if Vrite ever goes beyond the "headless" part, then there's certainly a possibility to support much larger variety of options. However, to still maintain compatibility with other platforms and endpoints it'd be best implemented in a layer of customization, just like described above.
Hope this answers your doubts and hopefully clarifies what Vrite intends to achieve.
I love the idea of this tool. Blog content management, especially if cross-posting from say a personal site to dev.to (or platform of your choice) can get complicated very quickly. I find I spend more time jumping across sites to try and post and manage than I do creating content.
Thanks for making and sharing this, I am going to try it out as I really want to be posting more long-form content again without the overhead of data management.
I think the goal of self hosting is a good one, especially for a developer like me who often has a server/CMS or two laying around the room as a cloud backup.
Great work, looking forward to seeing this mature.
Thanks for kind words. I experienced something similar (both when writing on my personal blog and when working in larger content teams) while also not being satisfied with the features WYSIWYG editors provided for technical writing. Happy to have you on-board with the idea and hope you'll stay along for the ride!
I really like the idea and was already thinking to build something like this myself. Is there also some way of publishing to your own website via an SDK or is that maybe in planning? 😊
Certainly! You can adjust the mechanism in this post to your website, i.e. change how the Webhook publishes the content and how Content Transformers adjust it.
Right now the SDK also has a dedicated integration for Astro. Example usage can be found on the landing page for Vrite. Astro is pretty great so definitely check it and the integration out if you're interested.
Other than that I'm definitely look to make the publishing process easier (perhabs through an integration/extension system) and write more guides for popular platforms and tools. Will need more time for this though. 😉
P.S. Just read that you mention Astro in your bio, so I might be onto something 😂
Haha you are perfectly right!
I happen to also use Astro for my portfolio page, so it's the perfect fit! ✨
I am just testing your product and the code editor is the breeze. Code Formatting and commenting in the editor is a great feature!
Really looking forward to using this tool! 🥳
I have to say, I'm quite impressed with your choice to incorporate a kanban board into the writing process, I would very much benefit from that!
Glad to hear that! It's based on my past experiences as I've often seen Trello or similar tool used to manage content production in larger teams. It's great for technical blogs and marketing content but I also hope to make it more versatile for use-cases like documentation or help centers, in the future.