<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vrite</title>
    <description>The latest articles on DEV Community by Vrite (@vrite).</description>
    <link>https://dev.to/vrite</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F6563%2F14039944-edd8-4367-97d9-50b5cd767adb.png</url>
      <title>DEV Community: Vrite</title>
      <link>https://dev.to/vrite</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vrite"/>
    <language>en</language>
    <item>
      <title>I Published This with Drag and Drop using Vrite</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Tue, 12 Mar 2024 17:33:07 +0000</pubDate>
      <link>https://dev.to/vrite/i-published-this-with-drag-and-drop-using-vrite-4b1e</link>
      <guid>https://dev.to/vrite/i-published-this-with-drag-and-drop-using-vrite-4b1e</guid>
      <description>&lt;p&gt;For many developers, blogging and technical writing play a key role in building their portfolio, sharing their projects, and for some — even in their day-to-day work. That’s why developer-centric platforms like &lt;a href="https://dev.to/"&gt;DEV&lt;/a&gt; and &lt;a href="https://hashnode.com/" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt;, and even more general ones, like &lt;a href="https://medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, are full of interesting technical content. The only problem with technical writing is in the actual &lt;em&gt;writing and publishing&lt;/em&gt; process. &lt;/p&gt;

&lt;p&gt;For blogs, the editing experience varies from platform to platform, and things like &lt;em&gt;cross-posting&lt;/em&gt; require a lot of effort (and copy-pasting) to adjust your content to each platform’s unique quirks.&lt;/p&gt;

&lt;p&gt;On the other hand, more complex technical writing (when working on e.g. product documentation, or knowledge bases) often involves &lt;em&gt;custom content&lt;/em&gt; that’s unique to your publication and isn’t supported in any kind of writing tool or CMS.&lt;/p&gt;

&lt;p&gt;You’re left with &lt;em&gt;Markdown&lt;/em&gt; which, while being pretty good for basic needs, is very limited, and usually requires various extensions and supersets to fit your needs (&lt;a href="https://mdxjs.com/" rel="noopener noreferrer"&gt;MDX&lt;/a&gt;, &lt;a href="https://markdoc.dev/" rel="noopener noreferrer"&gt;Markdoc&lt;/a&gt;, etc.). Also, MD might not be great for everyone (especially when writing longer pieces) and doesn’t provide the same experience as a modern WYSIWYG editor like &lt;a href="https://www.notion.so/" rel="noopener noreferrer"&gt;Notion&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These reasons (and many others) are why I decided to create &lt;a href="https://vrite.io/" rel="noopener noreferrer"&gt;Vrite&lt;/a&gt; - an &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;open-source&lt;/a&gt; &lt;em&gt;developer content platform&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vrite (v0.4)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FoHd_AKFsGqYTz8HixOI28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FoHd_AKFsGqYTz8HixOI28.png" alt="Vrite landing page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With tons of built-in features, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content management dashboard (with Kanban and Table views);&lt;/li&gt;
&lt;li&gt;Embedded &lt;a href="https://microsoft.github.io/monaco-editor/" rel="noopener noreferrer"&gt;Monaco Editor&lt;/a&gt; with &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt; formatter;&lt;/li&gt;
&lt;li&gt;Hybrid WYSIWYG editor with real-time collaboration;&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Vector search&lt;/em&gt; (with AI question-answering);&lt;/li&gt;
&lt;li&gt;Bi-directional &lt;em&gt;Git Sync&lt;/em&gt;;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vrite is meant to be the most versatile and extensible CMS-like platform for all your technical content needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extension System
&lt;/h3&gt;

&lt;p&gt;With the latest &lt;code&gt;v0.4.x&lt;/code&gt;, among many other features and improvements, the focus is on a new &lt;em&gt;extension system&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;An extension system is a key feature to make sure Vrite is able to handle pretty much any technical writing use case. From improving publishing (and cross-posting) to extending the capabilities of the WYSIWYG editor, a powerful extension system is the future of Vrite.&lt;/p&gt;

&lt;p&gt;The goal is to make creating extensions not much more difficult than creating e.g. a React component, while making sure the extensions are sandboxed and secure.&lt;/p&gt;

&lt;p&gt;To give you a sample of the extension API, here’s a snippet from a “first-party” &lt;em&gt;Dev.to extension&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @jsx createElement */&lt;/span&gt;
&lt;span class="cm"&gt;/** @jsxFrag createFragment */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Components&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createTemp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createRuntime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ExtensionConfigurationViewContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ExtensionContentPieceViewContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createFragment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vrite/sdk/extensions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ContentPieceData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;createRuntime&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;onUninstall&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;onConfigure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;configurationView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createView&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ExtensionConfigurationViewContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config.apiKey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config.organizationId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;autoPublish&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setAutoPublish&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config.autoPublish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;contentGroupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setContentGroupId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config.contentGroupId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;requireCanonicalLink&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config.requireCanonicalLink&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config.draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// ...&lt;/span&gt;

      &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Field&lt;/span&gt;
            &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contrast&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;API key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;API key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nx"&gt;Your&lt;/span&gt; &lt;span class="nx"&gt;Dev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;You&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="nx"&gt;one&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;
            &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="na"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//dev.to/settings/extensions), under **DEV Community API&lt;/span&gt;
            &lt;span class="nx"&gt;Keys&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;section&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Components.Field&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;contentPieceView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createView&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="nx"&gt;ExtensionContentPieceViewContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ContentPieceData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extensionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flush&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Speaking of which… this entire post was written in Vrite and published with a simple drag and drop using the Dev.to extension!&lt;/p&gt;

&lt;p&gt;If you’re interested in trying it out for yourself, check out &lt;a href="https://app.vrite.io/" rel="noopener noreferrer"&gt;Vrite Cloud&lt;/a&gt; and the official &lt;a href="http://docs.vrite.io" rel="noopener noreferrer"&gt;docs&lt;/a&gt; (if you’re wondering, &lt;em&gt;yes&lt;/em&gt; — they were also written in Vrite, and published via &lt;em&gt;Git Sync&lt;/em&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Using The Dev.to Extension
&lt;/h2&gt;

&lt;p&gt;Start by signing into &lt;a href="https://app.vrite.io/" rel="noopener noreferrer"&gt;Vrite&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Ftrw29rPwiwBbGyYt9CBQ1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Ftrw29rPwiwBbGyYt9CBQ1.png" alt="Vrite login screen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then go to the &lt;em&gt;Extensions&lt;/em&gt; side panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F9O317hkWmi4ELPO-TQNkv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F9O317hkWmi4ELPO-TQNkv.png" alt="Navigating to the extensions side-panel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;From the &lt;em&gt;Extensions&lt;/em&gt; side panel, in the &lt;em&gt;Available&lt;/em&gt; section, find &lt;em&gt;Dev.to&lt;/em&gt; and click &lt;strong&gt;Install&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F65017ed7b0e627e259623b8a%2FEWm77fQwgi_tXL9Lj11dt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F65017ed7b0e627e259623b8a%2FEWm77fQwgi_tXL9Lj11dt.png" alt="Vrite Dev.to extension"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;The Dev.to extension has a few options you have to configure to fully activate it.&lt;/p&gt;

&lt;h4&gt;
  
  
  API Key
&lt;/h4&gt;

&lt;p&gt;To retrieve your Dev.to API key, go to the bottom of the &lt;em&gt;Extensions&lt;/em&gt; section in the DEV &lt;em&gt;Settings&lt;/em&gt;: &lt;a href="https://dev.to/settings/extensions"&gt;https://dev.to/settings/extensions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, add a description and &lt;em&gt;Generate API Key&lt;/em&gt;. Once the new key is available, copy and paste it into the extension’s configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F65017ed7b0e627e259623b8a%2FPglPIDqhQtIg-heHYdAF7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F65017ed7b0e627e259623b8a%2FPglPIDqhQtIg-heHYdAF7.png" alt="DEV API key generation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Organization ID (optional)
&lt;/h4&gt;

&lt;p&gt;If you want to publish the posts as an organization, you have to provide your &lt;em&gt;Organization ID&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You can find it by going to your DEV dashboard (&lt;a href="https://dev.to/dashboard"&gt;https://dev.to/dashboard&lt;/a&gt;) then switching from &lt;em&gt;Personal&lt;/em&gt; to your organization view using the selection menu above the posts list and copying the organization ID from the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://dev.to/dashboard/organization/[ID]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Content Group ID
&lt;/h4&gt;

&lt;p&gt;The content group you want to automatically publish from. Once a content piece is moved directly to this content group, it’ll trigger a Webhook that’ll auto-publish the content piece on Dev.to.&lt;/p&gt;

&lt;p&gt;You can copy the content group ID from the content group menu in either the dashboard or the explorer panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F65017ed7b0e627e259623b8a%2FN5l7xhJcxGW95WUf0PmKG.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F65017ed7b0e627e259623b8a%2FN5l7xhJcxGW95WUf0PmKG.png" alt="Copying the content group ID from Kanban dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Require Canonical Link
&lt;/h4&gt;

&lt;p&gt;When checked, the auto-publishing won’t trigger if the content piece doesn’t have a &lt;strong&gt;canonical link&lt;/strong&gt; assigned. Useful when cross-posting the content to e.g. Dev.to and your own blog.&lt;/p&gt;

&lt;h4&gt;
  
  
  Auto-publish
&lt;/h4&gt;

&lt;p&gt;Whether to enable auto-publishing for all content pieces by default.&lt;/p&gt;

&lt;h4&gt;
  
  
  Draft
&lt;/h4&gt;

&lt;p&gt;Whether the article should be marked as a draft on the Dev.to platform (not publically visible).&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;With auto-publish enabled, once ready, simply move (drag and drop) the content piece from e.g. &lt;em&gt;Drafts&lt;/em&gt; content group to the one configured e.g. &lt;em&gt;Published&lt;/em&gt;. You should see the article published on Dev.to shortly after.&lt;/p&gt;

&lt;p&gt;The extension also provides a content piece view (available from the &lt;em&gt;Extensions&lt;/em&gt; section of the &lt;em&gt;Content piece&lt;/em&gt; side panel.&lt;/p&gt;

&lt;p&gt;From here, you can customize the &lt;em&gt;Auto-publish&lt;/em&gt; and &lt;em&gt;Draft&lt;/em&gt; per the given content piece, and also set a &lt;em&gt;Series name&lt;/em&gt; to associate the article with, once it’s published on DEV.&lt;/p&gt;

&lt;p&gt;Finally, with or without the auto-publishing enabled, you can easily publish “manually” using the &lt;em&gt;Publish&lt;/em&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F65017ed7b0e627e259623b8a%2Fi6uXC8V_LHlhfvva3y-Y9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F65017ed7b0e627e259623b8a%2Fi6uXC8V_LHlhfvva3y-Y9.png" alt="Dev.to extension's content piece view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future of Vrite
&lt;/h2&gt;

&lt;p&gt;Currently, the extension system is only available on the Vrite Cloud, which is a &lt;a href="https://vrite.io/pricing/" rel="noopener noreferrer"&gt;paid product&lt;/a&gt; with a free trial available.&lt;/p&gt;

&lt;p&gt;The extensions will be opened for self-hosting once the API is stabilized and all (or most) of the necessary features are implemented (i.e. permission control, an official extension store, versioning, and development tools).&lt;/p&gt;

&lt;p&gt;Apart from the extension system, with features like sharing, and built-in front-ends in the pipeline, the future of Vrite looks exciting!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;Star on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔥 &lt;a href="https://app.vrite.io/" rel="noopener noreferrer"&gt;Try out Vrite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ℹ️ &lt;a href="https://docs.vrite.io/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://discord.gg/yYqDWyKnqE" rel="noopener noreferrer"&gt;Join Discord&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;a href="https://twitter.com/vriteio" rel="noopener noreferrer"&gt;Follow on Twitter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/company/vrite" rel="noopener noreferrer"&gt;Follow on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>writing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>WYSIWYG for MDX?! Introducing Vrite's Hybrid Editor</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Wed, 18 Oct 2023 16:01:57 +0000</pubDate>
      <link>https://dev.to/vrite/wysiwyg-for-mdx-introducing-vrites-hybrid-editor-4j13</link>
      <guid>https://dev.to/vrite/wysiwyg-for-mdx-introducing-vrites-hybrid-editor-4j13</guid>
      <description>&lt;p&gt;You might be familiar with so-called &lt;em&gt;What You See Is What You Get&lt;/em&gt; (WYSIWYG) or &lt;em&gt;Rich Text&lt;/em&gt; editors. They’re the core part of popular apps such as Notion, allowing users to both see and edit the formatting and content of the document.&lt;/p&gt;

&lt;p&gt;WYSIWYG editors are great for creating all kinds of content but, in some cases, they might be limiting, allowing you to use only a small set of formatting options and elements supported by the editor.&lt;/p&gt;

&lt;p&gt;That’s especially problematic in niche content categories, like technical/&lt;strong&gt;developer content&lt;/strong&gt;, where content requirements might differ, between companies, teams, and even individual projects.&lt;/p&gt;

&lt;p&gt;That’s why formats like &lt;strong&gt;Markdown&lt;/strong&gt; (MD) and &lt;a href="https://mdxjs.com/" rel="noopener noreferrer"&gt;MDX&lt;/a&gt; (MD with support for JSX) are so popular for use cases like documentation, knowledge bases, or technical blogs. They allow you to use any kind of custom formatting or elements and then process the content for publishing. On top of that, they’re great for implementing a &lt;strong&gt;docs-as-code&lt;/strong&gt; approach, where your documentation lives right beside your code (i.e. in a Git repo).&lt;/p&gt;

&lt;p&gt;However, writing Markdown files is a very different experience compared to the WYSIWYG approach. It can be challenging for beginners or less technical users, while also getting increasingly difficult to manage as the content base grows. My latest project — &lt;a href="https://vrite.io/" rel="noopener noreferrer"&gt;Vrite&lt;/a&gt; — is meant to solve this problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vrite Hybrid Editor
&lt;/h2&gt;

&lt;p&gt;Vrite is an &lt;a href="https://github.com/vriteio/vrite/stargazers" rel="noopener noreferrer"&gt;open-source&lt;/a&gt; developer content platform, featuring extensible editing experience, content management tools, and powerful APIs. It’s intended as an all-in-one, collaborative solution for product documentation, technical blogs, and knowledge bases.&lt;/p&gt;

&lt;p&gt;With a collaborative WYSIWYG editor being a core part of the experience, I was &lt;a href="https://x.com/areknawo/status/1703836534586151017" rel="noopener noreferrer"&gt;thinking for a long time&lt;/a&gt; about how to create an experience that still has the power and customizability of MDX when needed. That’s how I came up with Vrite’s new &lt;a href="https://editor.vrite.io/" rel="noopener noreferrer"&gt;&lt;em&gt;Hybrid Editor&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F55R-f3y9j5QmtG3inSChq.png%3Fw%3D1000" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F55R-f3y9j5QmtG3inSChq.png%3Fw%3D1000" alt="The Element block in Vrite editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To be fair, rather than being a whole new editor, in the latest v0.3, the Vrite editor was slightly redesigned and got a new block content type — &lt;em&gt;Element&lt;/em&gt; — which enables the use of JSX-like blocks right in the editor, leading to a part-markup, part-WYSIWYG editor.&lt;/p&gt;

&lt;p&gt;The Element block can have a &lt;em&gt;type&lt;/em&gt; (e.g. &lt;code&gt;Card&lt;/code&gt;) and a set of &lt;em&gt;props&lt;/em&gt; (like &lt;code&gt;title&lt;/code&gt; or &lt;code&gt;options&lt;/code&gt;). You can edit it by clicking the opening tag. When the block is active and editable, its syntax will be highlighted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FzRnbx1OOxFxuVBoXflKg7.png%3Fw%3D1000" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FzRnbx1OOxFxuVBoXflKg7.png%3Fw%3D1000" alt="Editing Element block in Vrite editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Elements can be either wrappers (containing content) or simple block nodes. You can switch between those two “modes”, similar to how it’s done in JSX/MDX — by changing the closing bracket of the first tag. &lt;code&gt;/&amp;gt;&lt;/code&gt; is for self-closing, while &lt;code&gt;&amp;gt;&lt;/code&gt; — for wrapping.&lt;/p&gt;

&lt;p&gt;Behind the scenes, Vrite processes the content and makes it accessible in &lt;a href="https://prosemirror.net/" rel="noopener noreferrer"&gt;ProseMirror-based&lt;/a&gt; JSON format, including the type and all the props of the Element block.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FjLyVtMY9hv75qg4kXU8CW.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FjLyVtMY9hv75qg4kXU8CW.png" alt="JSON content from the Element block."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This format makes it easy to further process the content — whether that’s for use with other features of Vrite or when accessing it through the API for publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  MDX and GitHub Sync
&lt;/h2&gt;

&lt;p&gt;With the Element block enabling the use of more custom content, it also empowers other features of Vrite — such as &lt;strong&gt;Git sync&lt;/strong&gt; — with new possibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git Sync
&lt;/h3&gt;

&lt;p&gt;When your existing documentation or technical content is stored in MD files in your Git repo, or you want to implement a docs-as-code approach, with your repo being the &lt;em&gt;single source of truth&lt;/em&gt; — Git sync allows you to sync the content from your repo with Vrite.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FdK6C3rvsjb5YwCKoS0pqb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FdK6C3rvsjb5YwCKoS0pqb.png" alt="Vrite Git sync - resolving conflicts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This way you can still use all of Vrite’s features (e.g. content management, publishing extensions, semantic search, etc.), while also being able to commit your changes to the repo, resolve conflicts, etc. Basically, Git sync turns Vrite into a technical content editor, with additional features on top.&lt;/p&gt;

&lt;p&gt;Currently (v0.3.2), Git sync only integrates with &lt;strong&gt;GitHub&lt;/strong&gt;, with other providers planned for later on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FEmqTRGmXcnypC2UzdcUFA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FEmqTRGmXcnypC2UzdcUFA.png" alt="Configuring Vrite's Git sync with GitHub"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Syncing MDX
&lt;/h3&gt;

&lt;p&gt;Git sync works on top of &lt;em&gt;Input&lt;/em&gt;/&lt;em&gt;Output Content Transformers&lt;/em&gt; — functions that can “transform” the content to and from the Vrite’s JSON format and whatever’s in your repo. This is quite versatile, and — in theory — should be able to support all kinds of formats, decoupling the format you use from the tooling and features you have available.&lt;/p&gt;

&lt;p&gt;Vrite comes with a &lt;a href="https://github.github.com/gfm/" rel="noopener noreferrer"&gt;GitHub-Flavored Markdown&lt;/a&gt; (GFM) transformer built-in. It supports basic Markdown formatting, with the addition of GFM extensions, like task lists or tables.&lt;/p&gt;

&lt;p&gt;To extend that, Vrite supports &lt;em&gt;“remote” transformers&lt;/em&gt; — essentially processing the content on the backend, communicating via batched &lt;code&gt;POST&lt;/code&gt; requests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Fvt9l9iL7Az3AUUJFrI3Ka.png%3Fw%3D1000" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Fvt9l9iL7Az3AUUJFrI3Ka.png%3Fw%3D1000" alt="Custom transformers section in Vrite's settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s how the new MDX extension works — it registers a new transformer, that processes your content through endpoints, such as &lt;code&gt;POST https://extensions.vrite.io/mdx/input&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FeAnKI0BAJ1ldf2Te9VR44.png%3Fw%3D1000" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FeAnKI0BAJ1ldf2Te9VR44.png%3Fw%3D1000" alt="Vrite extensions panel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, to use Git sync with your MDX content:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;From the &lt;em&gt;Extensions&lt;/em&gt; side panel → Install the MDX extension;&lt;/li&gt;
&lt;li&gt;From the &lt;em&gt;Source control&lt;/em&gt; side panel → Configure GitHub as a provider (selecting your repo and branch, path to sync, and &lt;em&gt;MDX&lt;/em&gt; as a transformer; Check out &lt;a href="https://vrite.io/blog/notion-like-experience-for-your-git-hub-content/" rel="noopener noreferrer"&gt;this blog post&lt;/a&gt; to learn more on this configuration;&lt;/li&gt;
&lt;li&gt;After the &lt;em&gt;Initial sync&lt;/em&gt;, check out your content and verify if it was synced correctly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;p&gt;The MDX transformer can handle much more than the built-in Markdown transformer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It supports YAML frontmatter, with properties like &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; or &lt;code&gt;slug&lt;/code&gt; being synced with Vrite’s metadata.&lt;/li&gt;
&lt;li&gt;It supports the Element block, converting it to MDX, as expected.&lt;/li&gt;
&lt;li&gt;It uses a special &lt;code&gt;&amp;lt;Import&amp;gt;&lt;/code&gt; Element to contain the &lt;code&gt;import&lt;/code&gt; statements from the MDX file (for better back-and-forth syncing).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That said, with both the Element block and MDX extension being new features in a project that’s still in Beta, there are also some limitations you have to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the Element block, you can only use values that are JSON-parseable. So, e.g. passing JSX element as a prop won’t work for now.&lt;/li&gt;
&lt;li&gt;Element is a block and there’s no support for custom inline formatting as of yet. Thus, if you use inline JSX components in your MDX (e.g &lt;code&gt;&amp;lt;Icon/&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;InlineBadge&amp;gt;...&amp;lt;/InlineBadge&amp;gt;&lt;/code&gt;, etc.) — they’ll be omitted.&lt;/li&gt;
&lt;li&gt;If you’re extending MDX for custom use cases, the Git sync isn’t guaranteed to work. The MDX transformers support only more popular customizations (such as &lt;code&gt;title&lt;/code&gt; or metadata for codeblocks);&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The end goal with MDX support is to extend the editor to be fully customizable (both inline and block-wise) and provide more documentation and details for users to create custom transformers, to support all kinds of use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;I fully intend to make Vrite a viable, go-to solution for all developer content needs — one that’s both open-source and extensible, while still providing an integrated, all-in-one experience.&lt;/p&gt;

&lt;p&gt;The Element block is an important milestone towards this goal. With custom inline content and an entire extension system — for extending the editor with WYSIWYG components built on top of custom elements — coming soon, there’s still a lot more in store.&lt;/p&gt;

&lt;p&gt;If you’re interested in the project, consider &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;leaving a star on GitHub&lt;/a&gt; 🌟.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>🤖 AI Search and Q&amp;A for Your Dev.to Content with Vrite</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Thu, 24 Aug 2023 16:45:58 +0000</pubDate>
      <link>https://dev.to/vrite/ai-search-and-qa-for-your-devto-content-with-vrite-4fch</link>
      <guid>https://dev.to/vrite/ai-search-and-qa-for-your-devto-content-with-vrite-4fch</guid>
      <description>&lt;p&gt;No one can deny that &lt;strong&gt;ChatGPT&lt;/strong&gt; brought Large Language Models (LLMs) into the public spotlight. While LLMs are not perfect, when you think about it, the ability to ask a wide variety of questions and get an answer in seconds is mind-blowing. 🤯&lt;/p&gt;

&lt;p&gt;The only thing left is to somehow connect it with your own data and provide a new context for the LLM to source answers from. This is where &lt;strong&gt;text embeddings&lt;/strong&gt;, &lt;strong&gt;vector databases&lt;/strong&gt;, and &lt;strong&gt;semantic search&lt;/strong&gt; come in. However, depending on your use case, implementing the entire stack necessary to power an “AI search” or a Question-and-Answer (Q&amp;amp;A) type of interface might be quite a challenge… but it doesn’t have to be.&lt;/p&gt;

&lt;p&gt;With the latest update, &lt;strong&gt;Vrite&lt;/strong&gt; — an &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;open-source technical content management platform&lt;/a&gt; I’m working on — now has a &lt;strong&gt;built-in search&lt;/strong&gt; and a Q&amp;amp;A feature to find answers to all the questions related to your content. Both inside Vrite — via a new &lt;strong&gt;command palette&lt;/strong&gt; — and outside — via an API. ✨&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FEiaM_RSqZBHHEGG5W2TLQ.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FEiaM_RSqZBHHEGG5W2TLQ.gif" alt="AI search and Q&amp;amp;A in Vrite's command palette"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This can be used to easily build a new kind of search experience for your blog or to provide answers to user’s questions in your product docs.&lt;/p&gt;

&lt;p&gt;To give you a fun example of that, we’ll go through a process of importing content from the &lt;strong&gt;Dev.to API&lt;/strong&gt; to Vrite to search through it, and then see how you can easily implement a semantic search on your own site using Vrite APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting-up
&lt;/h2&gt;

&lt;p&gt;Let’s start by getting into Vrite. You can use &lt;a href="http://app.vrite.io" rel="noopener noreferrer"&gt;the hosted version&lt;/a&gt; (free while Vrite is in Beta) or self-host Vrite from &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;the source code&lt;/a&gt; (with better self-hosting support &lt;a href="https://github.com/vriteio/vrite/issues/21" rel="noopener noreferrer"&gt;coming soon&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;To import your Dev.to content collection to Vrite, it’d be best to do so in a dedicated &lt;strong&gt;workspace&lt;/strong&gt;. In Vrite, you can use workspaces to separate different projects or teams. To create a new one, from the sidebar go to the &lt;em&gt;Workspace&lt;/em&gt; section.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FBpTJ0oetqDIlyeDtsr6wD.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FBpTJ0oetqDIlyeDtsr6wD.png" alt="Vrite workspace menu"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From here, you can both create and switch between different Workspaces. Create one for your Dev.to blog and switch to it.&lt;/p&gt;

&lt;p&gt;With the dedicated workspace ready, you’ll have to create a new &lt;strong&gt;API token&lt;/strong&gt; — both in Vrite and Dev.to — to use in the import script.&lt;/p&gt;

&lt;p&gt;To get one in Vrite, go to the &lt;em&gt;Settings&lt;/em&gt; side panel → &lt;em&gt;API&lt;/em&gt; section → click &lt;em&gt;New API token&lt;/em&gt;. From here you’ll have to configure the details and permissions for the new token. Make sure to select &lt;em&gt;Write&lt;/em&gt; permission for both &lt;em&gt;Content pieces&lt;/em&gt; and &lt;em&gt;Content groups&lt;/em&gt;, as these will be necessary to import the content.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FTCZhfE_qMyuLUbTnbuWNp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FTCZhfE_qMyuLUbTnbuWNp.png" alt="Creating a new API token in Vrite"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you create it, store the token in a safe place - you won’t be able to see it again.&lt;/p&gt;

&lt;p&gt;To get an API key from Dev.to, go to the &lt;a href="https://dev.to/settings/extensions"&gt;&lt;em&gt;Settings&lt;/em&gt;&lt;/a&gt;&lt;a href="https://dev.to/settings/extensions"&gt; → &lt;/a&gt;&lt;em&gt;&lt;a href="https://dev.to/settings/extensions"&gt;Extensions&lt;/a&gt;&lt;/em&gt; → &lt;em&gt;DEV Community API Keys&lt;/em&gt; section → provide a description and click &lt;em&gt;Generate API Key&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FomXIoRPXuSuLKidUDZpgJ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FomXIoRPXuSuLKidUDZpgJ.png" alt="Creating an API key in Dev.to"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be able to see your API Key at any time, though you should still keep it secure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Importing Content From Dev.to
&lt;/h2&gt;

&lt;p&gt;With API tokens ready, it’s time to prepare an import script.&lt;/p&gt;

&lt;p&gt;With Node.js (v18 or newer) and NPM installed, initialize a new project, install &lt;a href="https://www.npmjs.com/package/@vrite/sdk" rel="noopener noreferrer"&gt;Vrite SDK&lt;/a&gt;, and create the primary &lt;code&gt;import.mjs&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @vrite/sdk
&lt;span class="nb"&gt;touch &lt;/span&gt;import.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;import.mjs&lt;/code&gt;, let’s first create a function to fetch your articles from Dev.to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VRITE_API_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEV_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getDevArticles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;perPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://dev.to/api/articles/me/published?per_page=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.forem.api-v1+json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DEV_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From v18, Node.js provides a &lt;code&gt;fetch()&lt;/code&gt; API, similar to web browsers, which makes handling network requests much easier. Use it with the proper URL and headers to make a request to the &lt;a href="https://developers.forem.com/api/v1#tag/articles/operation/getUserPublishedArticles" rel="noopener noreferrer"&gt;User's published articles&lt;/a&gt; endpoint.&lt;/p&gt;

&lt;p&gt;The Dev.to API implements pagination, with the max value being &lt;code&gt;1000&lt;/code&gt;, so a single request should be enough to retrieve all the articles for most (if not all) users.&lt;/p&gt;

&lt;p&gt;To actually import the content to Vrite, let’s create a separate function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vrite/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gfmInputTransformer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vrite/sdk/transformers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;importToVrite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;numberOfArticles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getDevArticles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;numberOfArticles&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VRITE_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentGroupId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentGroups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Dev.to Articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;cover_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gfmInputTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentPieces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;contentGroupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;cover_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;canonicalLink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;canonical_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;coverUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cover_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Imported article: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Could not import article: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.mjs&lt;/code&gt; extension in newer versions of Node.js allows out-of-the-box use of ESM &lt;code&gt;import&lt;/code&gt; syntax, which we use to import Vrite SDK and &lt;code&gt;gfmInputTransformer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Vrite SDK provides a few built-in &lt;strong&gt;input&lt;/strong&gt; and &lt;strong&gt;output transformers&lt;/strong&gt;. These are functions, with standardized signatures to process the content from and into Vrite. In this case, &lt;code&gt;gfmInputTransformer&lt;/code&gt; is essentially a &lt;a href="https://github.github.com/gfm/" rel="noopener noreferrer"&gt;GitHub Flavored Markdown&lt;/a&gt; parser, using &lt;a href="https://marked.js.org/" rel="noopener noreferrer"&gt;Marked.js&lt;/a&gt; under the hood.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;importToVrite()&lt;/code&gt; function, we first retrieve articles from DEV using the mechanism discussed before and initialize the Vrite API client. From there, we create a new &lt;strong&gt;content group&lt;/strong&gt; for housing the content and loop over the imported articles using &lt;code&gt;for await&lt;/code&gt; to create new &lt;strong&gt;content pieces&lt;/strong&gt; from them.&lt;/p&gt;

&lt;p&gt;The created pieces include the transformed content and some additional metadata sourced from Dev.to, to easily identify individual pieces.&lt;/p&gt;

&lt;p&gt;With that, all you have to do is call the &lt;code&gt;importToVrite()&lt;/code&gt; function with the number of your latest Dev.to articles to import and watch it go! Here’s the entire script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vrite/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gfmInputTransformer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vrite/sdk/transformers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VRITE_API_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEV_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getDevArticles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;perPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://dev.to/api/articles/me/published?per_page=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.forem.api-v1+json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DEV_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;importToVrite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;numberOfArticles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getDevArticles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;numberOfArticles&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VRITE_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentGroupId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentGroups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Dev.to Articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;cover_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gfmInputTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentPieces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;contentGroupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;cover_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;canonicalLink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;canonical_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;coverUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cover_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Imported article: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Could not import article: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;importToVrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Search and Q&amp;amp;A in Vrite Dashboard
&lt;/h2&gt;

&lt;p&gt;With the content now in Vrite, let’s go back to the dashboard and see how to use the command palette to search right in Vrite.&lt;/p&gt;

&lt;p&gt;When coupled with Vrite support for collaboration, the built-in search and command palette can serve as a great tool when using Vrite as an internal knowledge base.&lt;/p&gt;

&lt;p&gt;To open the palette use &lt;code&gt;⌘K&lt;/code&gt; (on macOS), &lt;code&gt;Ctrl K&lt;/code&gt; (on Windows or Linux), or the search button in the dashboard’s toolbar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F6bNOww6WrVFDFBd-vkZn_.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F6bNOww6WrVFDFBd-vkZn_.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The command palette has 3 modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt; — the default, provides results as you type;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Command&lt;/strong&gt; — can be enabled by typing &lt;code&gt;&amp;gt;&lt;/code&gt; in empty search or by clicking the &lt;em&gt;Command&lt;/em&gt; button in the bottom-right corner; Allows quick access to various actions available in the current view; You can move back to the search mode by using &lt;code&gt;Backspace&lt;/code&gt; in empty input;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ask / Q&amp;amp;A&lt;/strong&gt; — can be enabled by clicking the &lt;em&gt;Ask&lt;/em&gt; button in the top-right corner; Type in your question and click &lt;code&gt;Enter&lt;/code&gt; to request an answer;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try searching for any term and see results from your various Dev.to blog posts appear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FO1GzklwCjFF6TO-J3VVgU.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FO1GzklwCjFF6TO-J3VVgU.png" alt="Vrite AI search in the command palette"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vrite indexes entire sections of your content pieces, identified by the title and a set of headings. This allows the LLM to extract the most semantic meaning from the content, which enables vector search to provide better search results for your queries. So, the better you structure your posts the better the search results will be.&lt;/p&gt;

&lt;p&gt;You can also try the Q&amp;amp;A mode, asking any question that there should be an answer for in your content. Upon submission, the prompt is sent together with the context, for GPT-3.5 to generate an answer, which is streamed back to the command palette.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Fv-u7aKjMsW94h4yLtkqI3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Fv-u7aKjMsW94h4yLtkqI3.png" alt="Vrite Q&amp;amp;A in the command palette"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Personally, I was quite impressed with how well the Q&amp;amp;A turned out. Even answers that would require reading through several pieces were generated accurately in seconds. Still, you should keep in mind that this won’t always be the case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search and Q&amp;amp;A via Vrite API
&lt;/h2&gt;

&lt;p&gt;Now, searching through Vrite’s command palette is nice, but the real fun begins when you get to implement this search and Q&amp;amp;A experience on your own blogs and docs via &lt;strong&gt;Vrite API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;First, you’ll have to “proxy” Vrite API searches via your own backend or serverless functions, due to CORS and security considerations (especially if your token has powerful permissions). To do so, you’ll have to access Vrite API from Node.js.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search
&lt;/h3&gt;

&lt;p&gt;First, make sure to have an API token with at least &lt;em&gt;Read&lt;/em&gt; access to &lt;em&gt;Content pieces&lt;/em&gt;. With that, you can use the &lt;code&gt;search()&lt;/code&gt; method of the API client to retrieve the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vrite/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VRITE_API_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VRITE_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dev.to&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The search result is an array of objects, each containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;contentPieceId&lt;/code&gt; — ID of the related content piece;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;breadcrumb&lt;/code&gt; — an array of the title and headings leading to the section;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;content&lt;/code&gt; — plain text content of the section;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can process or send these results directly to the frontend as a JSON array.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q&amp;amp;A
&lt;/h3&gt;

&lt;p&gt;Q&amp;amp;A is a bit more difficult. Due to the slow response associated with the time GPT-3.5 needs to generate an answer, the &lt;code&gt;/search/ask&lt;/code&gt; endpoint is implemented via &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events" rel="noopener noreferrer"&gt;Server Sent Events&lt;/a&gt; (SSEs) to stream the answer to the user, allowing them to see the first tokens as soon as they’re ready.&lt;/p&gt;

&lt;p&gt;Vrite SDK doesn’t support SSE streaming just yet, so, for now, you’ll have to implement this yourself. Use the &lt;a href="https://www.npmjs.com/package/eventsource" rel="noopener noreferrer"&gt;eventsource&lt;/a&gt; library or similar to connect with the endpoint and stream the answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;streamAnswer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.vrite.io/search/ask?query=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;VRITE_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;streamAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What is Dev.to?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example loads the entire answer and then resolves the &lt;code&gt;Promise&lt;/code&gt;. You can use this method directly, but the response time for each request will be counted in seconds.&lt;/p&gt;

&lt;p&gt;To provide a better User Experience (UX), you’ll likely want to forward the events coming from Vrite API, through your backend to the frontend, where the user will see the first tokens of the answer appear much faster. The implementation of this will depend on your backend framework, but the general approach is to write a &lt;code&gt;text/event-stream&lt;/code&gt; response as the data comes in. Here’s a &lt;a href="https://www.digitalocean.com/community/tutorials/nodejs-server-sent-events-build-realtime-app" rel="noopener noreferrer"&gt;good overview of the general process&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’m working to document and support this process better in the coming weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;While the AI search itself is really great, the best part about Vrite is that the search is only a small fraction of a greater whole. With the &lt;strong&gt;Kanban&lt;/strong&gt; content management, &lt;strong&gt;WYSIWYG&lt;/strong&gt; technical content &lt;strong&gt;editor&lt;/strong&gt;, &lt;a href="https://vrite.io/blog/notion-like-experience-for-your-git-hub-content/" rel="noopener noreferrer"&gt;Git sync&lt;/a&gt;, and &lt;strong&gt;extensions for publishing&lt;/strong&gt; to platforms like Dev.to with just drag and drop — we’ve only scratched the surface of what’s possible!&lt;/p&gt;

&lt;p&gt;Now, Vrite is currently in Beta and there are still bugs to be resolved, and new features to be added and fleshed out. If you want to help and support the project, &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;leave a star on GitHub&lt;/a&gt; and report any issues or bugs you encounter. With your support, I hope to make Vrite the go-to, open-source technical content platform 🔥&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 &lt;a href="https://app.vrite.io/" rel="noopener noreferrer"&gt;Try out Vrite&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ℹ️ &lt;a href="https://docs.vrite.io/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💬 &lt;a href="https://discord.gg/yYqDWyKnqE" rel="noopener noreferrer"&gt;Join Discord&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🐦 &lt;a href="https://twitter.com/vriteio" rel="noopener noreferrer"&gt;Follow on Twitter&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💼 &lt;a href="https://www.linkedin.com/company/vrite" rel="noopener noreferrer"&gt;Follow on LinkedIn&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>writing</category>
    </item>
    <item>
      <title>🔥✍️ Notion-like Experience for Your GitHub Content</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Tue, 22 Aug 2023 17:54:25 +0000</pubDate>
      <link>https://dev.to/vrite/notion-like-experience-for-your-github-content-5gk1</link>
      <guid>https://dev.to/vrite/notion-like-experience-for-your-github-content-5gk1</guid>
      <description>&lt;p&gt;A vast amount of technical content — technical blogs, product docs, and others — lives inside &lt;strong&gt;Git repos&lt;/strong&gt; — and for a good reason. Aside from Git’s version control, platforms like &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; provide tons of integrations and additional features, to easily host and automate various workflows. That’s especially important for efficient content delivery for blogs, or for docs to be kept in sync and right beside the documented codebase.&lt;/p&gt;

&lt;p&gt;That’s why, while working on &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;Vrite — an open-source technical content platform&lt;/a&gt; — I knew a good integration with Git is crucial for many workflows. It’s a must-have feature, that has to be implemented right for technical writers to benefit from it.&lt;/p&gt;

&lt;p&gt;So, after weeks of research and development, I’m excited to share with you what I’ve come up with, and how you can use Vrite to &lt;strong&gt;edit your GitHub content&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FLinVyQCjC_cf_f3Px3MQw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FLinVyQCjC_cf_f3Px3MQw.png" alt="Vrite WYSIWYG editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Various Types of Git Integrations
&lt;/h2&gt;

&lt;p&gt;Naturally, the idea of integrating Git into a Notion-like, modern, collaborative, WYSIWYG editing environment is nothing new. However, not all Git integrations are created equal.&lt;/p&gt;

&lt;p&gt;Some &lt;strong&gt;Content Management Systems&lt;/strong&gt; (CMSs) similar in functionality to Vrite are &lt;strong&gt;Git-based&lt;/strong&gt;, meaning they fundamentally rely on Git to work. While that implies great Git integration, it also comes with &lt;a href="https://strapi.io/blog/git-based-vs-api-first-cms" rel="noopener noreferrer"&gt;various limitations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Vrite leans more towards headless, &lt;strong&gt;API-first&lt;/strong&gt; CMS, storing its content in a database, and making it accessible via API for users to build custom frontends on top of. This means that Git becomes a separate layer that has to be integrated into existing tooling.&lt;/p&gt;

&lt;p&gt;CMSs and tools like knowledge bases approach this in various ways. Most commonly I’ve seen a one-way integration, meaning either your Single Source of Truth (SSOT) is a Git repo and every change has to be immediately committed to it or your SSOT is the actual app and you can commit changes in batches, with little to no regard for content or updates that happen in the repo. There seem to also be other solutions, like &lt;a href="https://github.com/vriteio/vrite/discussions/15" rel="noopener noreferrer"&gt;periodic sync&lt;/a&gt;, where a forceful sync happens every once in a while.&lt;/p&gt;

&lt;p&gt;To me, all these approaches seemed inferior to what developers have at their disposal in code editors and IDEs — including the ability to commit and pull at any time, resolve merge conflicts when necessary, switch between branches, stage certain changes, etc. I wanted to recreate at least a part of that in Vrite by integrating with various Git providers via their APIs — starting with GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vrite Git Sync
&lt;/h2&gt;

&lt;p&gt;So, how does Git sync currently work in Vrite? Let me take you through a quick overview.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Sync with GitHub
&lt;/h3&gt;

&lt;p&gt;You can use Vrite via the &lt;a href="https://app.vrite.io/" rel="noopener noreferrer"&gt;hosted version&lt;/a&gt; (that’s free while in Beta) or self-host it from the open-source repo (though &lt;a href="https://github.com/vriteio/vrite/issues/21" rel="noopener noreferrer"&gt;good support for self-hosting&lt;/a&gt; is still in the works).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FhEnQh6iIlxgxMFC7lxZ34.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FhEnQh6iIlxgxMFC7lxZ34.png" alt="Signing in to Vrite"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When inside the dashboard, open the &lt;em&gt;Source control&lt;/em&gt; side panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FrUtWjl3ZXTZzZte-LXqT6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FrUtWjl3ZXTZzZte-LXqT6.png" alt="Source control side panel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From here, you’ll have to first select and configure a provider. For now, only GitHub is supported, with other major providers (GitLab, BitBucket, and Gitea) to come later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F6nx_WQZI2_1V3XHpkwGr1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F6nx_WQZI2_1V3XHpkwGr1.png" alt="Configuring GitHub provider"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To configure GitHub you’ll first have to select a repo and authorize access to it. To do so, first, authenticate with GitHub, and &lt;a href="https://github.com/apps/vrite-io/installations/select_target" rel="noopener noreferrer"&gt;install the Vrite GitHub app&lt;/a&gt;. Only accounts and repos authorized in the GitHub App’s installation will be available for selection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FEAjgcm0sVxTnSGOy_xVBw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FEAjgcm0sVxTnSGOy_xVBw.png" alt="Configuring mapping settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After you’ve selected the repo and branch to sync, you can optionally configure the mapping settings. These dictate how to map the repository’s content to Vrite.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Base directory&lt;/strong&gt; — indicates which directory to sync with, so you can only sync e.g. &lt;code&gt;/docs&lt;/code&gt; directory. It points to the root (&lt;code&gt;/&lt;/code&gt;) directory by default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Match pattern&lt;/strong&gt; — a glob match pattern that, applied relative to the base directory, will only sync files with matched filenames. By default, it matches all &lt;code&gt;.md&lt;/code&gt; files.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FtuFiP_74lFxUG12f7k6SG.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FtuFiP_74lFxUG12f7k6SG.png" alt="Initial sync section"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you configure GitHub provider, and &lt;em&gt;Save&lt;/em&gt; the configuration, you’ll have to perform the &lt;strong&gt;initial sync&lt;/strong&gt;. That’s when the existing content from GitHub will be synced into Vrite, creating content groups for all the folders, content pieces for synced files, and indexing it all. Depending on the amount of content you have, this can take a while.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing Content In Vrite
&lt;/h3&gt;

&lt;p&gt;Once the sync is complete, you’ll see a new content group appear, with a name corresponding to your repo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FKXj-VcM_ELvWIgPKo3jCV.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FKXj-VcM_ELvWIgPKo3jCV.png" alt="After initial sync"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, a word on managing your Git content in Vrite.&lt;/p&gt;

&lt;p&gt;In Vrite, your content is organized into &lt;strong&gt;content pieces&lt;/strong&gt; — containing the actual content and related metadata, and &lt;strong&gt;content groups&lt;/strong&gt; — grouping multiple content pieces together. In the Vrite dashboard’s primary view — Kanban — these are displayed as individual draggable columns and cards inside them.&lt;/p&gt;

&lt;p&gt;Kanban is great for managing the content production process, but it doesn't necessarily represent the way your Git repo is structured — with nested folders and all. That’s why, to provide a good experience for the new workflow with Git, Vrite now supports &lt;strong&gt;nested content groups&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To navigate those, you can use the &lt;strong&gt;folder icon&lt;/strong&gt;, next to the content group’s name. Click it to get inside the content group, and drag it onto the other folder icon to move one group into the other.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F7J2RcTaik0TSmNi7rYABM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F7J2RcTaik0TSmNi7rYABM.png" alt="Folder icon and breadcrumb in use"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same dragging and clicking mechanism works with the &lt;strong&gt;breadcrumb&lt;/strong&gt; that will appear in the toolbar. Simply click a content group (or the workspace icon) to get back to the certain nesting level, or drag a folder icon from the Kanban onto one part of the breadcrumb to move it.&lt;/p&gt;

&lt;p&gt;Why use the folder icon for that? I think it’s a more intuitive way to manage group nesting while dragging entire columns is still reserved for rearranging the kanban.&lt;/p&gt;

&lt;p&gt;But that’s not all. If you prefer a more folder-like approach to managing your content, you can always switch to the new &lt;strong&gt;list view&lt;/strong&gt;, similar to the one you’ve experienced in GitHub. The same rules apply here, with the additional option to also drag individual content pieces onto the groups’ folder icons to move them. This feature is to-be-added in the Kanban view.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FoiCNWQ7hVnIyEkeSZD0aL.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FoiCNWQ7hVnIyEkeSZD0aL.png" alt="List view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One more thing to note is in regard to filename mapping. Vrite supports various metadata for content pieces, including &lt;strong&gt;title&lt;/strong&gt; and &lt;strong&gt;slug&lt;/strong&gt;, but these didn’t seem appropriate to use for filename mapping. What if you want to use the title for something meaningful, or the slug for publishing on your blog, docs, etc.? That’s why Vrite added a new metadata field called &lt;strong&gt;Filename&lt;/strong&gt;, which is specifically meant to aid in use cases like Git integration, assigning a meta filename to your piece.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FeTkRO1-KWjz720M-6BZZ_.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FeTkRO1-KWjz720M-6BZZ_.png" alt="Content piece metadata"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, with the increased number of metadata fields, I’ve decided to keep some (including filename) disabled by default, to allow you to better focus on the metadata that matters to you. So, if you use Git, you’ll likely want to go to the &lt;em&gt;Settings&lt;/em&gt; → &lt;em&gt;Metadata&lt;/em&gt; section and enable the fields that matter to you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F64RZUv_Pt-HqbxV7sHjn_.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F64RZUv_Pt-HqbxV7sHjn_.png" alt="Metadata settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pull, Commit &amp;amp; Resolve Changes
&lt;/h3&gt;

&lt;p&gt;Knowing how to manage content in Vrite, let’s go back to the Source control panel and see how to sync your edits with Git.&lt;/p&gt;

&lt;p&gt;First — &lt;strong&gt;commits&lt;/strong&gt;. Go to the editor and edit a piece or two. You should see the changed files reflected in the &lt;em&gt;Changes&lt;/em&gt; section shortly after. In case you want to commit the changes, simply provide a &lt;em&gt;Commit message&lt;/em&gt; and commit. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FBSpq1yBj9epLlXHck_tYI.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FBSpq1yBj9epLlXHck_tYI.png" alt="Commiting changes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The commit should show up in the repo as verified and authored by &lt;strong&gt;vrite-io[bot]&lt;/strong&gt;. I decided against attributing changes to individual users, as Vrite enables you to add team members and collaborate on the content in real time, making it difficult to differentiate who actually made the change.&lt;/p&gt;

&lt;p&gt;Now, when you try to commit after a change has been done to the repo from outside your Vrite account, you’ll receive a &lt;em&gt;“Pull required before committing changes”&lt;/em&gt; error notification. To resolve this, you’ll have to first &lt;strong&gt;pull the latest changes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To do so, simply use the button in the &lt;em&gt;Pull&lt;/em&gt; section. Vrite will fetch all commits made since the last pull (or the initial sync) and sync the content of files changed in those commits. It can be done entirely automatically, or there can be a &lt;strong&gt;conflict&lt;/strong&gt;. In this case, you’ll have to resolve it first, before you can sync the changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FB4ui6Tvprkm1nG3iMK198.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FB4ui6Tvprkm1nG3iMK198.png" alt="Pull and resolve conflicts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All conflicted files will be listed in the Pull section. Click on one of them to start resolving the conflict.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Fm3gZ62zN4nAq0VhirdrVH.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Fm3gZ62zN4nAq0VhirdrVH.png" alt="Resolving conflicts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll see a &lt;a href="https://microsoft.github.io/monaco-editor/" rel="noopener noreferrer"&gt;Monaco Editor&lt;/a&gt;-powered change editor. The content incoming from the Git repo is on the left, while the current content in Vrite is on the right. You can make changes in the editor on the right - this will ultimately become the result content. Once you’re done, click &lt;em&gt;Resolve&lt;/em&gt;. If there are no other conflicts, you should now be able to pull the latest changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;p&gt;With that, you now know pretty much all there is to know about GitHub sync in Vrite. It’s quite simple and still not as feature-rich as you can find in code editors or IDEs, but it gets the job done, better than quite a few other CMS and knowledge-base tools.&lt;/p&gt;

&lt;p&gt;That said, there’s definitely room to improve. I’d like Vrite to be the “go-to” editor/CMS for blogs and docs, and this necessitates some additional features. In the short term, I’m looking to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom remote transformers&lt;/strong&gt; — to allow developers to provide custom functions for processing content in and out of Vrite;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stage&lt;/strong&gt; and &lt;strong&gt;undo&lt;/strong&gt; — to better organize the commits from Vrite;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom blocks&lt;/strong&gt; — to extend the content that can be synced with and edited in Vrite, to support more use cases, like complex, custom docs;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the long-term:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Support for other providers;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy switching between branches;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Custom WYSIWYG editor for conflict resolution;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Access to your Git version history right from Vrite;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Syncing the content from GitHub with Vrite allows you to have a great WYSIWYG editing experience for your technical content, while still having access to all the features of Git and GitHub. In addition to that, you get to benefit from all the features of Vrite, like easy content management, powerful API with Webhooks, and… an &lt;strong&gt;AI search&lt;/strong&gt;, though that’s a story for another day.&lt;/p&gt;

&lt;p&gt;In case you’re interested in Vrite, &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;leave a 🌟 on GitHub&lt;/a&gt;, as well as any feature requests or issues you may have, to support the project. ❤️&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 &lt;a href="https://app.vrite.io/" rel="noopener noreferrer"&gt;Try out Vrite&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ℹ️ &lt;a href="https://docs.vrite.io/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💬 &lt;a href="https://discord.gg/yYqDWyKnqE" rel="noopener noreferrer"&gt;Join Discord&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🐦 &lt;a href="https://twitter.com/vriteio" rel="noopener noreferrer"&gt;Follow on Twitter&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💼 &lt;a href="https://www.linkedin.com/company/vrite" rel="noopener noreferrer"&gt;Follow on LinkedIn&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>writing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Vrite Editor: Open-Source WYSIWYG Markdown Editor</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Tue, 18 Jul 2023 17:58:10 +0000</pubDate>
      <link>https://dev.to/vrite/vrite-editor-open-source-wysiwyg-markdown-editor-6o5</link>
      <guid>https://dev.to/vrite/vrite-editor-open-source-wysiwyg-markdown-editor-6o5</guid>
      <description>&lt;p&gt;I’ve created an &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;open-source&lt;/a&gt;, &lt;a href="https://editor.vrite.io/" rel="noopener noreferrer"&gt;minimalistic WYSIWYG Markdown editor&lt;/a&gt; to easily create and export Markdown and HTML content on the go, without any sign-up required!&lt;/p&gt;

&lt;p&gt;Here’s “Why?”, “How?”, and other questions answered!&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Why?”
&lt;/h2&gt;

&lt;p&gt;In software development, Markdown is basically omnipresent. You can find it in READMEs, powering software blogs, and serving as one of the primary content formatting methods when &lt;strong&gt;Rich Text Editing&lt;/strong&gt; (RTE) is required.&lt;/p&gt;

&lt;p&gt;Markdown is awesome! But, when writing 1000 words+ articles, I quickly feel the need for a better experience. For years, I’ve used &lt;a href="https://stackedit.io/" rel="noopener noreferrer"&gt;StackEdit&lt;/a&gt; — an open-source, in-browser Markdown editor — for editing all kinds of long-format Markdown text. That said, given my recent experience with WYSIWYG editors, I thought I could do something better.&lt;/p&gt;

&lt;p&gt;That’s how I created &lt;strong&gt;Vrite Editor&lt;/strong&gt; — an open-source Rich Text Editor, focused on good user experience and technical writing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Fm4XKgoSC1csabmRO-suPy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2Fm4XKgoSC1csabmRO-suPy.png" alt="Vrite Editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;No good tool is built without using good tools, and Vrite Editor is no different. Before getting into WYSIWYG editors, I &lt;a href="https://vrite.io/blog/best-js-rich-text-editors-for-2023/" rel="noopener noreferrer"&gt;extensively researched available RTE frameworks&lt;/a&gt;, that could provide the tooling and functionality I was looking for. Ultimately, I picked &lt;a href="https://tiptap.dev/" rel="noopener noreferrer"&gt;TipTap&lt;/a&gt; and underlying &lt;a href="https://prosemirror.net/" rel="noopener noreferrer"&gt;ProseMirror&lt;/a&gt; — IMO, the best tools currently available for all kinds of WYSIWYG editors.&lt;/p&gt;

&lt;p&gt;Aside from that, I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://solidjs.com/" rel="noopener noreferrer"&gt;Solid.js&lt;/a&gt; — the super-fast UI framework;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://unocss.dev/" rel="noopener noreferrer"&gt;UnoCSS&lt;/a&gt; — for styling with Tailwind-like atomic CSS;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://vitejs.dev/" rel="noopener noreferrer"&gt;Vite&lt;/a&gt; — build tool for putting it all together;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The results came out pretty nicely! 🌟&lt;/p&gt;

&lt;h2&gt;
  
  
  Features &amp;amp; Behind the Scenes
&lt;/h2&gt;

&lt;p&gt;Vrite Editor comes with many features to make editing a breeze! You can check them all out in &lt;a href="https://docs.vrite.io/content-editor/" rel="noopener noreferrer"&gt;the official documentation&lt;/a&gt;. That said, there are a few unique ones that I feel are worth diving deeper into.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Editor
&lt;/h3&gt;

&lt;p&gt;Something that I always felt was lacking in WYSIWYG editors (even the top ones, like &lt;a href="https://notion.so/" rel="noopener noreferrer"&gt;Notion&lt;/a&gt;) is good support for &lt;strong&gt;code editing&lt;/strong&gt;. Granted, technical writing is a niche use-case and you can get by with simple syntax highlighting and some copy-pasting. That said, I wanted to try something else — I wanted to integrate a full-blown code editor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FNLQ2piOpD-6jOZEtv67gT.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FNLQ2piOpD-6jOZEtv67gT.png" alt="Integrated code editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By referencing &lt;a href="https://prosemirror.net/examples/codemirror/" rel="noopener noreferrer"&gt;the ProseMirror docs&lt;/a&gt;, forwarding the editor state back and forth, and adjusting the layout, I managed to integrate &lt;a href="https://microsoft.github.io/monaco-editor/" rel="noopener noreferrer"&gt;Monaco Editor&lt;/a&gt; — the web editor extracted from VS Code — together with &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt; (for code formatting) right into the Vrite Editor (I know, that’s a lot of editors in one place 😅).&lt;/p&gt;

&lt;p&gt;Granted, this is not VS Code in your content editor, but this way you get great syntax highlighting for many languages, while also having formatting and auto-completion for at least a few  — like JavaScript or HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table Editor
&lt;/h3&gt;

&lt;p&gt;The “whole grail of WYSIWYG editing” (as stated in &lt;a href="https://tiptap.dev/api/nodes/table#introduction" rel="noopener noreferrer"&gt;the TipTap docs&lt;/a&gt;) took a while to get right. While TipTap provides extensions for supporting Table formatting, the UI is a different story. Table formatting UIs are notoriously difficult to get right. That’s why I was quite happy when I finally felt like I built something that was… alright.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FCThF1-P0HI4iWTIWlvQ0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FCThF1-P0HI4iWTIWlvQ0s.png" alt="Table editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When your cursor is inside the table, you’ll see a bottom menu appear, which you can use e.g. insert and remove rows, columns, and headers. On the other hand, when you select some cells, you’ll see a menu to e.g. merge, split, or delete them. It’s not Excel-level but, for an inline table, I think it’s pretty good.&lt;/p&gt;

&lt;h3&gt;
  
  
  Markdown Pasting
&lt;/h3&gt;

&lt;p&gt;Handling Markdown pasting was a surprising challenge. Naturally, you’d expect an editor that works with Markdown to be able to parse and properly load the Markdown you pasted, so some work had to go into that.&lt;/p&gt;

&lt;p&gt;TipTap provides &lt;a href="https://tiptap.dev/guide/custom-extensions/#input-rules" rel="noopener noreferrer"&gt;input rules&lt;/a&gt; to handle so-called “Markdown shortcuts” while typing, and &lt;a href="https://tiptap.dev/guide/custom-extensions/#paste-rules" rel="noopener noreferrer"&gt;paste rules&lt;/a&gt; for offering the same parsing functionality while pasting. The problem is, paste rules work line-by-line, which means parsing something like a blockquote, codeblock, or a list, just isn’t possible.&lt;/p&gt;

&lt;p&gt;To handle pasting block Markdown content like this, I had to tap into ProseMirror and implement a custom mechanism (though somewhat based on TipTap’s paste rules), detecting starting and ending points of the blocks and parsing them with &lt;a href="https://marked.js.org/" rel="noopener noreferrer"&gt;Marked.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thankfully, with this done, most Markdown can be pasted freely into the editor and should be parsed just fine!&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile Support
&lt;/h3&gt;

&lt;p&gt;The other thing that's quite hard to get right in a web-based WYSIWYG editor, is &lt;strong&gt;mobile support&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Initially, I thought that a good editing experience on the mobile platform can only be achieved with a native mobile app, so why bother trying to get it right on the mobile web?&lt;/p&gt;

&lt;p&gt;Well, ultimately I tried to add mobile support to Vrite Editor anyway, and, to my surprise, it turned out pretty good!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FJ8WjrDuhI5xLdG6zwjGny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FJ8WjrDuhI5xLdG6zwjGny.png" alt="Vrite on Mobile"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There were many things that needed optimization — and some that still do. From the general layout, toolbar, and the UI of block elements to formatting and block content menus — everything had to be in some way optimized or completely changed for mobile.&lt;/p&gt;

&lt;p&gt;Now, there’s still some work to be done here. Monaco editor &lt;a href="https://github.com/microsoft/monaco-editor/issues/246" rel="noopener noreferrer"&gt;doesn’t work well for mobile&lt;/a&gt;, and some “optional” features weren’t yet optimized (like link previews or comments).&lt;/p&gt;

&lt;p&gt;Still, with the work already done, Vrite Editor is usable and actually quite comfortable on mobile.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By the way, I did write this section on my phone. 😊&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Markdown Exports
&lt;/h3&gt;

&lt;p&gt;Now, with the editor working, the last important part was to output Markdown. TipTap provides methods to retrieve both HTML and ProseMirror JSON representations of the content. I decided to use the JSON (since it’s easier to work with) and created functions to transform it into various outputs. I called these &lt;em&gt;“Content Transformers”&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FTi3gDzFHWvSRgu0X4UgS7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FTi3gDzFHWvSRgu0X4UgS7.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using Content Transformers I created an “Export” menu, outputting the content in HTML, GitHub Flavored Markdown (GFM), and ProseMirror JSON (e.g. for custom processing) to a Monaco Editor, with options to download or copy it.&lt;/p&gt;

&lt;p&gt;While there might be better ways to do this, this one provides a lot of flexibility and makes sure your content can be properly processed to Markdown or any other format you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vrite CMS
&lt;/h2&gt;

&lt;p&gt;In case you enjoy using Vrite Editor, you might be interested in a larger project that it’s a part of — “Vrite CMS”, or simply “Vrite”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vrite.io/" rel="noopener noreferrer"&gt;Vrite is a headless CMS for technical content&lt;/a&gt;, providing editing and management tools to work with content like technical product documentation, programming blogs, etc. Basically, it’s my answer to (what I consider) a lack of good tooling focused on this kind of content.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F2vZXejDOz1ef-GxPUeDWl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F2vZXejDOz1ef-GxPUeDWl.png" alt="Vrite dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Beyond just the Vrite Editor, you get additional features, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Kanban dashboard for content management;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API and Webhooks;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Extension system for easy publishing to popular platforms;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Real-time collaboration;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and more!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since Vrite (and Vrite Editor for that matter) is currently in &lt;strong&gt;Public Beta&lt;/strong&gt;, new features and improvements are in active development. The best way to try it out right now is through the hosted version at &lt;a href="https://app.vrite.io/" rel="noopener noreferrer"&gt;app.vrite.io&lt;/a&gt; (free while in Beta) with better self-hosting support in the works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;In any case, if you’re interested in technical writing, definitely check out &lt;a href="https://editor.vrite.io/" rel="noopener noreferrer"&gt;Vrite Editor&lt;/a&gt; and &lt;a href="https://vrite.io/" rel="noopener noreferrer"&gt;Vrite&lt;/a&gt; as a whole. Also, &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;leave a 🌟 on GitHub&lt;/a&gt;, as well as any feature requests or issues you may have, to support the project. ❤️&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 &lt;a href="https://app.vrite.io/" rel="noopener noreferrer"&gt;Try out Vrite&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ℹ️ &lt;a href="https://docs.vrite.io/" rel="noopener noreferrer"&gt;Usage guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🚀 &lt;a href="https://vrite.io/blog" rel="noopener noreferrer"&gt;Blog&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📝 &lt;a href="https://github.com/vriteio/vrite/issues" rel="noopener noreferrer"&gt;Report a bug&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🙋‍♀️ &lt;a href="https://github.com/vriteio/vrite/discussions" rel="noopener noreferrer"&gt;Request a feature&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💬 &lt;a href="https://discord.gg/yYqDWyKnqE" rel="noopener noreferrer"&gt;Join Discord&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🐦 &lt;a href="https://twitter.com/vriteio" rel="noopener noreferrer"&gt;Follow on Twitter&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💼 &lt;a href="https://www.linkedin.com/company/vrite" rel="noopener noreferrer"&gt;Follow on LinkedIn&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>productivity</category>
      <category>writing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I put ChatGPT into a WYSIWYG editor</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Mon, 19 Jun 2023 20:44:18 +0000</pubDate>
      <link>https://dev.to/vrite/how-i-put-chatgpt-into-a-wysiwyg-editor-ndf</link>
      <guid>https://dev.to/vrite/how-i-put-chatgpt-into-a-wysiwyg-editor-ndf</guid>
      <description>&lt;p&gt;With all the hype going on, AI (or rather &lt;em&gt;Machine Learning&lt;/em&gt; (ML) and &lt;em&gt;Large Language Models&lt;/em&gt; (LLMs) are everywhere. Personally, I might not use ChatGPT (and similar alternatives) much, but I sure do rely on likes of &lt;a href="https://github.com/features/copilot"&gt;GitHub Copilot&lt;/a&gt; (for intelligent autocompletion in VS Code), or &lt;a href="https://www.grammarly.com/"&gt;Grammarly&lt;/a&gt; (for editing my blog posts) every day.&lt;/p&gt;

&lt;p&gt;I think we’re still quite a few breakthroughs away from AGI and current technology won’t be enough to get us there (thankfully or not). That said, we’re already deep into the times of &lt;em&gt;“AI-enhanced”&lt;/em&gt; apps, where the apps at the top may not have the best AI systems, but they integrate them in the best possible way.&lt;/p&gt;

&lt;p&gt;That’s why it was an interesting process, exploring OpenAI’s API and trying to integrate it into the Rich Text Editor (RTE) of Vrite — my open-source headless CMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending the WYSIWYG Editor
&lt;/h2&gt;

&lt;p&gt;For those unfamiliar Vrite, in a nutshell, is a headless CMS for technical content, like programming blogs or software docs. It can be viewed as two apps in one — Kanban dashboard for content management and WYSIWYG editor for writing, with additional dev-friendly features like embedded code snippet editor and formatter.&lt;/p&gt;

&lt;p&gt;The latest big addition to Vrite is an early extension system, to easily build integrations and extend what Vrite can do. This, to me, seemed like the perfect way to introduce ChatGPT into the editor — as an &lt;strong&gt;extension&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Block Actions
&lt;/h3&gt;

&lt;p&gt;To be able to use the extension system for integrating ChatGPT into the editor, a new API had to be introduced. I called it &lt;em&gt;Block Action API&lt;/em&gt; since its specifically meant for adding quick actions to the editor, that operate on top-level content blocks, like paragraphs, headings, or images, as is highlighted below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--v8Ge9ZSn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/D8nLJPBuGdH4Ry2W7VYlU.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--v8Ge9ZSn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/D8nLJPBuGdH4Ry2W7VYlU.png" alt="Content blocks in Vrite editor - highlighted" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With Block Actions API, extensions can read the JSON content of the active block and update it with HTML-formatted content, just like it’s done in Vrite API (on one end, parsing JSON output is easier while, on the other, HTML is more suitable to transform content into).&lt;/p&gt;

&lt;p&gt;From the UI side, Block Actions are displayed as buttons on the side of the actively-selected block. They can either invoke an action directly on click or — like with ChatGPT — open a dropdown menu to prompt the user for more details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DtZzym9Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/RBV_yv3nf8nfem3iFcNDq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DtZzym9Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/RBV_yv3nf8nfem3iFcNDq.png" alt="GPT-3.5 extension's Block Action dropdown" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The buttons had to be absolutely positioned, which required both a custom &lt;a href="https://tiptap.dev/"&gt;TipTap&lt;/a&gt; extension and tapping deeper into the underlying &lt;a href="https://prosemirror.net/"&gt;ProseMirror&lt;/a&gt; (both libraries powering the Vrite editor).&lt;/p&gt;

&lt;p&gt;The process basically came down to figuring out the position and size of the block node, given a selection of an entire top-level node or just its child node (&lt;a href="https://github.com/vriteio/vrite/blob/5acb789dc2a92a6da75daf062065ee5c164b4608/apps/web/src/lib/editor/extensions/block-action-menu/plugin.tsx"&gt;source code&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BlockActionMenuPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Extension&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="nx"&gt;onSelectionUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isTextSelection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;TextSelection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeAfter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;selectedNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeDOM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeDOM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentOffset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domAtPos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blockParent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getBlockParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parentPos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pm-container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;childPos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;blockParent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parentPos&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;childPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;relativePos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;childPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;parentPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;childPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;parentPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;childPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;parentPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;childPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;parentPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rangeFrom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rangeTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;relativePos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;relativePos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;parentPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isTextSelection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;findParentAtDepth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;rangeFrom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;rangeTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Replacing Editor Content
&lt;/h3&gt;

&lt;p&gt;The second part involved handling the actual process of replacing the block’s content with the newly provided one. The trickiest thing to figure out here was to get the correct range (the start and end position in ProseMirror) of the block node. This was necessary to then use TipTap’s commands to properly replace the range.&lt;/p&gt;

&lt;p&gt;If you’ve taken a closer look at the last code snippets — the code for that was already there. The block’s range was updated, together with the Block Action UI positioning, on every selection update.&lt;/p&gt;

&lt;p&gt;The actual replacement of the range with new content was much easier to do. All there was to it was converting the HTML to Schema-adherent JSON and involving proper commands (&lt;a href="https://github.com/vriteio/vrite/blob/5acb789dc2a92a6da75daf062065ee5c164b4608/apps/web/src/lib/editor/extensions/block-action-menu/component.tsx"&gt;source code&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;replaceContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodeOrFragment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createNodeFromContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodeOrFragment&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;PMNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodeOrFragment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodeOrFragment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insertContentAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;generateJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extensionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;setRange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;computeDropdownPosition&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;replaceContent()&lt;/code&gt; function could then be called remotely, from the extension’s sandbox, by sending a proper message to the main frame.&lt;/p&gt;

&lt;p&gt;To enable use-cases like ChatGPT integrations, where the content will be updated (i.e. replaced) multiple times in a row before the process is finished, the function also locked the editor for a short time of the function being called and updated the range, and UI positioning on every call. But why was this required?&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating With OpenAI’s API
&lt;/h2&gt;

&lt;p&gt;The process of integrating OpenAI’s API is pretty well-documented in &lt;a href="https://platform.openai.com/docs/api-reference/chat/create"&gt;its official docs&lt;/a&gt;. Given that an official SDK is provided, the entire process can be done in just a few lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;configuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_ORGANIZATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;OpenAIApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createChatCompletion&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, all that is true, but only if you’re willing to wait what often is &lt;strong&gt;+20s&lt;/strong&gt; for a single response! That’s a lot for a single request. Nothing from changing the server location to optimizing the request by limiting &lt;code&gt;max_tokens&lt;/code&gt; or customizing other parameters worked. It all comes down to the fact that current-day LLMs (those on the level of GPT-3 at least) are still rather slow.&lt;/p&gt;

&lt;p&gt;With that said, the ChatGPT app still manages to be perceived as fairly fast and responsive. That’s thanks to the use of streaming and &lt;strong&gt;Server-Sent Events&lt;/strong&gt; (SSEs).&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming ChatGPT Response
&lt;/h3&gt;

&lt;p&gt;The chat completion and other endpoints of OpenAI’s API support streaming through Server-Sent Events, essentially maintaining an open connection through which the new tokens are sent as soon as they’re available.&lt;/p&gt;

&lt;p&gt;Unfortunately, the official Node.js SDK &lt;a href="https://github.com/openai/openai-node/issues/18"&gt;doesn’t support streaming&lt;/a&gt; and requires you to use workarounds to get it working, resulting in much more code required, just to connect with the API (&lt;a href="https://github.com/vriteio/vrite/blob/5acb789dc2a92a6da75daf062065ee5c164b4608/apps/backend/extensions/src/routes/gpt.ts"&gt;source code&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;configuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_ORGANIZATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;OpenAIApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createChatCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;responseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getHeaders&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responseData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;responseData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^data: /&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[DONE]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Could not JSON parse stream message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that, you also have to support streaming on your end, between your API server and web client which, in the case of Vrite, meant integrating SSEs with &lt;a href="https://www.fastify.io/"&gt;Fastify&lt;/a&gt; and &lt;a href="https://trpc.io/"&gt;tRPC&lt;/a&gt;. Not the cleanest solution, but pretty stable nonetheless.&lt;/p&gt;

&lt;p&gt;From the frontend (the extension sandbox to be precise), a connection with the new streaming endpoint has to be established and incoming data — correctly processed (&lt;a href="https://github.com/vriteio/vrite/blob/5acb789dc2a92a6da75daf062065ee5c164b4608/packages/extensions/src/gpt-3.5/functions/generate.ts"&gt;source code&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fetchEventSource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@microsoft/fetch-event-source&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExtensionBlockActionViewContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;includeContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includeContext&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setTemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentRequestController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentRequestController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abort&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setTemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetchEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://extensions.vrite.io/gpt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;includeContext&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gfmTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentRequestController&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;onopen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;onerror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setTemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error while generating content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;onmessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partOfContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;partOfContent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;onclose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setTemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource"&gt;EventSource Web API&lt;/a&gt; for handling SSEs (built into most modern browsers) unfortunately supports only &lt;code&gt;GET&lt;/code&gt; requests, which was quite limiting when a &lt;code&gt;POST&lt;/code&gt; request with larger &lt;code&gt;body&lt;/code&gt; JSON data was required. As an alternative, you can use the Fetch API or a ready library like &lt;a href="https://github.com/Azure/fetch-event-source"&gt;Microsoft’s Fetch Event Source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Again, with streaming enabled, you’ll now receive new tokens as soon as they’re available. Given that OpenAI’s API uses Markdown in its response format, a full message will need to be put together from the incoming tokens and parsed to HTML, as accepted by the &lt;code&gt;replaceContent&lt;/code&gt; function. For this purpose, I’ve used the &lt;a href="https://marked.js.org/"&gt;Marked.js parser&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, with each new token, the larger response is being built up. Every time a new token comes, the full Markdown is parsed and the content updated, making for a nice “typing-like effect”.&lt;/p&gt;

&lt;p&gt;While this process does have some overhead, it’s not noticeable in use, while the Markdown simply has to be parsed with each new token, as it may contain e.g. the closing of the code block, or the end of the formatted segment. So, while this process could potentially be optimized, it wouldn’t lead to any recognizable performance improvement in the majority of cases.&lt;/p&gt;

&lt;p&gt;Finally, worth noting is the use of &lt;code&gt;AbortController&lt;/code&gt;, which can be used to stop the stream at any time the user chooses to. That’s especially great for longer responses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3M-pr0NK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/jNJ8Hp3l8JgU9gffhrbZZ.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3M-pr0NK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/jNJ8Hp3l8JgU9gffhrbZZ.gif" alt="GPT-3.5 Block Action... in action" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;In general, I’m very happy with how this turned out. Data streaming, nice typing effect, and good integration with the editor’s existing content blocks thanks to Markdown parsing — all came together to create a compelling User Experience.&lt;/p&gt;

&lt;p&gt;Now, there’s certainly room for improvement. The Block Actions API, as well as the Vrite Extensions as a whole still have a lot of development work ahead of them before they can be created by other users. Other UI/UX improvements to consider, like operating on multiple blocks at once (e.g. for additional context for ChatGPT) and displaying UI inline (much like Notion AI) not to obscure the view are just a few examples of what I was considering. That said, it’ll take some more time to implement these ideas well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3t4Q_z2_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/yx9-Te-MULUbOF9p8Tk__.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3t4Q_z2_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/yx9-Te-MULUbOF9p8Tk__.png" alt="Vrite Kanban dashboard" width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vrite is much more than just a GPT-enhanced editor. It’s a full, open-source CMS focused on technical content like programming blogs, with a code editor, API, Kanban management dashboard included and easy publishing integrations included. So, if you’re interested in trying it out and possibly using it to power your blog, definitely check it out!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;Star Vrite on GitHub&lt;/strong&gt; — &lt;a href="https://github.com/vriteio/vrite"&gt;https://github.com/vriteio/vrite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐞 &lt;strong&gt;Report bugs&lt;/strong&gt; — &lt;a href="https://github.com/vriteio/vrite/issues"&gt;https://github.com/vriteio/vrite/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;strong&gt;Follow on Twitter&lt;/strong&gt; — &lt;a href="https://twitter.com/vriteio"&gt;https://twitter.com/vriteio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Join Vrite Discord&lt;/strong&gt; — &lt;a href="https://discord.gg/yYqDWyKnqE"&gt;https://discord.gg/yYqDWyKnqE&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ℹ️ &lt;strong&gt;Learn more about Vrite&lt;/strong&gt; — &lt;a href="https://vrite.io"&gt;https://vrite.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📕 &lt;strong&gt;Vrite documentation&lt;/strong&gt; — &lt;a href="https://docs.vrite.io"&gt;https://docs.vrite.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Next-Level Technical Blogging with Dev.to API</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Tue, 13 Jun 2023 19:49:08 +0000</pubDate>
      <link>https://dev.to/vrite/next-level-technical-blogging-with-devto-api-12jl</link>
      <guid>https://dev.to/vrite/next-level-technical-blogging-with-devto-api-12jl</guid>
      <description>&lt;p&gt;Having a platform like DEV at the center of your technical blog is a great option. With over 1 million users, Dev.to is one of the largest developer communities there is, welcoming technical writers and readers at all levels.&lt;/p&gt;

&lt;p&gt;That said, things are complicated when it comes to writing and delivering content on DEV. With its minimal editor, you’ll likely have to do a lot of copy-pasting and manual adjustments to get your post from your editor to DEV and other platforms you might want to cross-post to, like &lt;a href="https://hashnode.com/" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thankfully, DEV’s underlying platform — Forem — has a great API you can leverage to both publish and fetch blog posts from DEV with ease. Let me show you how…&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started With Forem API
&lt;/h2&gt;

&lt;p&gt;Currently, there are 2 versions of the Forem API available — version 0 and version 1. They differ a bit in the request format but provide similar endpoints. You can use either one of them (both are &lt;a href="https://developers.forem.com/api" rel="noopener noreferrer"&gt;documented in the official docs&lt;/a&gt;), though it’s recommended to use the newer v1.&lt;/p&gt;

&lt;p&gt;To switch between different versions, you have to provide proper HTTP headers. For v1, these are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API-Key: [your-api-key]
Accept: application/vnd.forem.api-v1+json
Content-Type: application/json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While for the v0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API-Key: [your-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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key difference is in the &lt;code&gt;Accept&lt;/code&gt; header — you have to specify &lt;code&gt;application/vnd.forem.api-v1+json&lt;/code&gt; to enable the newer version. On top of that, for v0, you have to provide a valid &lt;code&gt;User-Agent&lt;/code&gt; header, as otherwise your request will result in &lt;code&gt;403 Forbidden&lt;/code&gt; error. The provided value is only an example, that I verified to work.&lt;/p&gt;

&lt;p&gt;From this point on, I’ll be referencing only v1, though all the endpoints described are the same in both versions.&lt;/p&gt;

&lt;p&gt;With all this made clear, all you need now is an API key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Your API Key
&lt;/h3&gt;

&lt;p&gt;To get your API key you have to go to the very bottom of the &lt;a href="https://dev.to/settings/extensions"&gt;&lt;strong&gt;Extensions&lt;/strong&gt;&lt;/a&gt;&lt;a href="https://dev.to/settings/extensions"&gt; tab of your Dev.to account’s &lt;/a&gt;&lt;strong&gt;&lt;a href="https://dev.to/settings/extensions"&gt;Settings&lt;/a&gt;&lt;/strong&gt;&lt;a href="https://dev.to/settings/extensions"&gt; page&lt;/a&gt;. There you’ll see the &lt;strong&gt;DEV Community API Keys&lt;/strong&gt; section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FP5TCUOaTlAx8BiAormvIz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FP5TCUOaTlAx8BiAormvIz.png" alt="DEV Community API Keys section"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter a custom description and &lt;em&gt;Generate API Key&lt;/em&gt;. You should see a new API key appear on the list, from where you can copy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching Articles From DEV
&lt;/h2&gt;

&lt;p&gt;Now, there’s a lot you can do with this API and its endpoints, which are all &lt;a href="https://developers.forem.com/api/v1" rel="noopener noreferrer"&gt;pretty well-documented&lt;/a&gt;. So, rather than serving as the second documentation, I’d like to discuss two of probably the biggest use cases for this API.&lt;/p&gt;

&lt;p&gt;The first one is to use Dev.to as your content hub, making DEV the center of your programming publication, while also serving the content on your own website, like a custom blog or portfolio page.&lt;/p&gt;

&lt;p&gt;The most important endpoint for this use case would be &lt;code&gt;GET /api/articles/me&lt;/code&gt;, which allows you to retrieve all of your published articles. Here’s how you can use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DEVArticle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type_of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;comments_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;public_reactions_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;page_views_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;published_timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;positive_reactions_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cover_image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tag_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reading_time_minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;twitter_username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;github_username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;website_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;profile_image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;profile_image_90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;profile_image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;profile_image_90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GetArticlesOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getArticles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;GetArticlesOptions&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DEVArticle&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DEVArticle&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;perPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;perPage&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://dev.to/api/articles/me?per_page=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;page=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.forem.api-v1+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;getArticles&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;perPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API is CORS-enabled, meaning you’ll have to use the &lt;code&gt;getArticles()&lt;/code&gt; functions from your backend. For making the actual request, you can use the &lt;code&gt;fetch()&lt;/code&gt; function, available since Node.js v18. For older versions of Node.js, you can use a &lt;code&gt;fetch()&lt;/code&gt;-compatible library like &lt;a href="https://www.npmjs.com/package/node-fetch" rel="noopener noreferrer"&gt;node-fetch&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Converting Markdown to HTML
&lt;/h3&gt;

&lt;p&gt;Once you have the article data, you’ll likely have to process its &lt;code&gt;body_markdown&lt;/code&gt; to a format required by your website, like HTML. There are many tools you can do this with — here’s an example using &lt;a href="https://marked.js.org/" rel="noopener noreferrer"&gt;Marked.js&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markdownToHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^.*{%&lt;/span&gt;&lt;span class="se"&gt;\s?(&lt;/span&gt;&lt;span class="sr"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt; &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;?)\s?&lt;/span&gt;&lt;span class="sr"&gt;%}.*&lt;/span&gt;&lt;span class="se"&gt;(?:\n&lt;/span&gt;&lt;span class="sr"&gt;|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getEmbedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;embedType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;youtube&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://www.youtube.com/embed/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;codepen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;pen&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/embed/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;codesandbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://codesandbox.io/embed/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;// etc...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedExtension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;embedExtension&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^.*{%/&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tokenizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;embedExtension&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;embedType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="na"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;iframe src="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getEmbedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embedType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
      &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;&amp;lt;/iframe&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;gfm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;embedExtension&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the most part, DEV uses a standard Markdown format, which is easily handled by Marked.js and its built-in GitHub Flavored Markdown support. For embeds (i.e. liquid tags), you can easily extend Marked.js using its &lt;a href="https://marked.js.org/using_pro#extensions" rel="noopener noreferrer"&gt;Custom Extension&lt;/a&gt; system. Above is an example of an extension converting liquid tags to &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; elements, creating proper URLs for embeds like YouTube, CodePen, or CodeSandbox. You should be able to easily extend it to handle the embeds you use.&lt;/p&gt;

&lt;p&gt;If you want to go beyond fetching articles, Forem API provides various other endpoints I encourage you to explore. Here are a few examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieve &lt;a href="https://developers.forem.com/api/v1#tag/users/operation/getUserMe" rel="noopener noreferrer"&gt;your profile details&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.forem.com/api/v1#tag/comments/operation/getCommentsByArticleId" rel="noopener noreferrer"&gt;Fetch comments&lt;/a&gt; for the article;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.forem.com/api/v1#tag/articles/operation/getArticles" rel="noopener noreferrer"&gt;Filter articles&lt;/a&gt; based on tags;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Publishing on DEV via API
&lt;/h2&gt;

&lt;p&gt;An arguably more popular use case for an API like Forem’s is simplified or automatic publishing.&lt;/p&gt;

&lt;p&gt;In general, there’s a lot of copy-pasting when writing a technical article — between various code and text editors, publications like DEV or Hashnode, and potentially even a CMS powering your custom blog. Going through this whole process for every single article can be tiring.&lt;/p&gt;

&lt;p&gt;That said, with some automation and different APIs, you can simplify this process to a high degree. In the case of Forem API, you can use the &lt;code&gt;POST /api/articles&lt;/code&gt; to first publish an article on DEV and then update it as needed with &lt;code&gt;PUT /api/articles/{id}&lt;/code&gt;. Here’s an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DEVArticleInput&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;published&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;series&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;main_image&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publishArticle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DEVArticleInput&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DEVArticle&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://dev.to/api/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.forem.api-v1+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateArticle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;articleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;articleUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DEVArticleInput&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DEVArticle&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://dev.to/api/articles/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;articleId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.forem.api-v1+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;articleUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these two functions, you can easily publish and update your articles as needed, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;publishArticle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello, world!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;# Hello World&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;This is my first post published through Forem API!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Later on&lt;/span&gt;
&lt;span class="nf"&gt;updateArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1234567&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;javascript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typescript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;updatedArticle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updatedArticle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you’ll need to convert your content input to a DEV-compatible Markdown format, doing a somewhat inverse operation to what was demonstrated earlier with Marked.js. This will likely require some custom processing to best fit your use case.&lt;/p&gt;

&lt;p&gt;Now, when properly implemented, such publishing automation can greatly improve your publishing experience. Still, this likely won’t help you escape copy-pasting between your editors.&lt;/p&gt;

&lt;p&gt;That said, if you’re looking for something that could provide a great experience across your entire technical writing process — from writing and coding to content management and publishing — then you might be interested in my latest project — &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;Vrite&lt;/a&gt; — &lt;strong&gt;open-source headless CMS for technical content&lt;/strong&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Easy Publishing With Vrite
&lt;/h2&gt;

&lt;p&gt;Vrite combines multiple tools and concepts that are familiar to all developers, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kanban dashboard for content management;&lt;/li&gt;
&lt;li&gt;Modern rich text editor with built-in &lt;a href="https://microsoft.github.io/monaco-editor/" rel="noopener noreferrer"&gt;code editor&lt;/a&gt; and &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt; code formatter;&lt;/li&gt;
&lt;li&gt;Content delivery API with &lt;a href="https://www.npmjs.com/package/@vrite/sdk" rel="noopener noreferrer"&gt;dedicated JavaScript SDK&lt;/a&gt;;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;in the form of headless CMS. It’s a unique combination that, in my opinion, has the potential to provide the best experience across the entire technical writing space. For more details, you can &lt;a href="https://docs.vrite.io/" rel="noopener noreferrer"&gt;check out the official guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F09ofHBxoNyXu-4HVPhwOb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F09ofHBxoNyXu-4HVPhwOb.png" alt="Vrite editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Publishing via Dev.to Extension
&lt;/h3&gt;

&lt;p&gt;Staying on the topic of Dev.to, Vrite’s latest feature — &lt;strong&gt;Extensions&lt;/strong&gt; — make integrating Vrite with DEV as easy as clicking a few buttons, allowing you to easily publish and update content from Vrite to DEV both manually and &lt;strong&gt;automatically&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Right now, Vrite Extensions enable easy integration with platforms like DEV, Hashnode or Medium and extend the capabilities of Vrite editor with the power of OpenAI’s GPT-3.5, with a lot more to come.&lt;/p&gt;

&lt;p&gt;Setting up an extension is easy and can be done from the &lt;em&gt;Extensions&lt;/em&gt; side panel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FsG5Fm9bi6_VvTOYVm_rkp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FsG5Fm9bi6_VvTOYVm_rkp.png" alt="Configuring Dev.to extension"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://vrite.io/blog/building-an-extension-system-on-the-web/" rel="noopener noreferrer"&gt;check out this blog post&lt;/a&gt; for a more detailed introduction to Vrite Extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;There’s a lot you can do with Dev.to’s API. Between fetching and publishing content it’s really versatile and can be used to handle a lot of custom use cases. &lt;/p&gt;

&lt;p&gt;That said, if you’re interested in an even better technical writing experience, definitely give Vrite a try and let me know what you think about it and how I can make it better!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;Star Vrite on GitHub&lt;/strong&gt; — &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;https://github.com/vriteio/vrite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐞 &lt;strong&gt;Report bugs&lt;/strong&gt; — &lt;a href="https://github.com/vriteio/vrite/issues" rel="noopener noreferrer"&gt;https://github.com/vriteio/vrite/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;strong&gt;Follow on Twitter&lt;/strong&gt; — &lt;a href="https://twitter.com/vriteio" rel="noopener noreferrer"&gt;https://twitter.com/vriteio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Join Vrite Discord&lt;/strong&gt; — &lt;a href="https://discord.gg/yYqDWyKnqE" rel="noopener noreferrer"&gt;https://discord.gg/yYqDWyKnqE&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ℹ️ &lt;strong&gt;Learn more about Vrite&lt;/strong&gt; — &lt;a href="https://vrite.io" rel="noopener noreferrer"&gt;https://vrite.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📕 &lt;strong&gt;Vrite documentation&lt;/strong&gt; — &lt;a href="https://docs.vrite.io" rel="noopener noreferrer"&gt;https://docs.vrite.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>writing</category>
    </item>
    <item>
      <title>Start programming blog in minutes with Astro and Vrite</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Tue, 23 May 2023 20:02:35 +0000</pubDate>
      <link>https://dev.to/vrite/start-programming-blog-in-minutes-with-astro-and-vrite-e5l</link>
      <guid>https://dev.to/vrite/start-programming-blog-in-minutes-with-astro-and-vrite-e5l</guid>
      <description>&lt;p&gt;Running a programming blog is a great way to hone your skills, build a personal brand and expand your portfolio. However, it certainly requires a lot of effort to both get started and to keep going. You not only need to have the programming knowledge and writing skills but also the ability to make quick and sure decisions.&lt;/p&gt;

&lt;p&gt;Developers (being developers) will often spend a lot of time picking their tech stacks, rather than building the blog or creating content. Now, while this is certainly beneficial for learning about new tools, sometimes you just need to ship! 🚢&lt;/p&gt;

&lt;p&gt;Thus, more recently, for any kind of static or content-heavy websites, like a landing page, blog, etc. I’ve defaulted to &lt;a href="https://astro.build/"&gt;Astro&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Astro
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AG8pkxeP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/4CWtHta2QjoUdtXCHDjgI.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AG8pkxeP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/4CWtHta2QjoUdtXCHDjgI.png" alt="Astro landing page" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, if you haven’t used Astro before - you’re missing out. It’s a web framework for building really fast SSG or SSR-powered websites.&lt;/p&gt;

&lt;p&gt;It does so through &lt;strong&gt;island architecture&lt;/strong&gt;, by only loading the JS code of your components when required — e.g. when the component becomes visible or the browser is idle — and shipping static pre- or server-rendered HTML in all other cases.&lt;/p&gt;

&lt;p&gt;On top of that, it integrates really well with your existing stack, including various UI frameworks, like React, Vue, or my recent favorite — &lt;a href="https://solidjs.com"&gt;Solid.js&lt;/a&gt; — for building the interactive parts of your UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vrite — CMS for Technical Content
&lt;/h2&gt;

&lt;p&gt;With Astro handling the front end, you still need some kind of data source for your blog. While a few Markdown files can do the job, I don’t consider that the most scalable or comfortable setup.&lt;/p&gt;

&lt;p&gt;Unfortunately, most CMSs won’t be ideal either, as writing about code is a very specific, rather niche use case. Goodies like code highlighting, formatting, or proper Markdown and Keyboard shortcuts support aren’t guaranteed. Not to mention, the additional requirements you might have when working in teams, like real-time collaboration or proper content management tools.&lt;/p&gt;

&lt;p&gt;All these reasons taken together are essentially why I created &lt;strong&gt;Vrite&lt;/strong&gt;, which is basically a &lt;strong&gt;CMS for technical content&lt;/strong&gt;. With everything above and more included — like code editor, &lt;a href="https://prettier.io/"&gt;Prettier&lt;/a&gt; integration, Kanban dashboard, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vrite and Astro
&lt;/h2&gt;

&lt;p&gt;Now, Vrite is &lt;a href="https://github.com/vriteio/vrite"&gt;open-source&lt;/a&gt; and currently in &lt;strong&gt;Public Beta&lt;/strong&gt;. This means that, while there might be bugs, you can already do quite a lot with it. For example, &lt;a href="https://vrite.io/blog/better-blogging-on-dev-to-with-vrite-headless-cms-for-technical-content/"&gt;you can easily integrate it with the DEV platform via its API&lt;/a&gt;. However, by combining it with Astro, you can take it a step further!&lt;/p&gt;

&lt;p&gt;With its great performance and support for Solid.js (the framework Vrite is built with), Astro is already powering the Vrite &lt;a href="https://vrite.io/"&gt;landing page&lt;/a&gt; and &lt;a href="https://vrite.io/blog"&gt;blog&lt;/a&gt; — and does so really well!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--14_aOcC_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/LAizvPhZSbINasew4aUne.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--14_aOcC_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/LAizvPhZSbINasew4aUne.png" alt="Vrite landing page" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This great pairing inspired me to create Vrite’s first dedicated integration — &lt;a href="https://github.com/vriteio/sdk-js#astro-integration"&gt;one for Astro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, with Astro, Vrite, and easy integration between the two, it’s possible to get a blog up and running in minutes! Let me show you how…&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Blog With Astro and Vrite
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Source code here: &lt;a href="https://github.com/areknawo/start-programming-blog-in-minutes-with-astro-and-vrite"&gt;https://github.com/areknawo/start-programming-blog-in-minutes-with-astro-and-vrite&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since Vrite is open-source and based on Node.js, you’ll soon be able to self-host it pretty easily if you want to. However, until I document this process and stabilize Vrite, the best way to try it out is through a hosted instance at &lt;a href="http://app.vrite.io"&gt;app.vrite.io&lt;/a&gt;. So, start by signing in for an account:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gZbGSx8u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/bacwnXsRwCQj1bGJj_4eT.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gZbGSx8u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/bacwnXsRwCQj1bGJj_4eT.png" alt="Signing in to Vrite" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you’re in, you’ll see a Kanban dashboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ICEhf8Sr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/L30UxjhVDpOTSyar76PEy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ICEhf8Sr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/L30UxjhVDpOTSyar76PEy.png" alt="Vrite Kanban dashboard" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can read more about how the content is organized in Vrite in &lt;a href="https://vrite.io/blog/better-blogging-on-dev-to-with-vrite-headless-cms-for-technical-content/"&gt;my previous blog post&lt;/a&gt;. However, for now, all you need to know is that individual columns are called &lt;strong&gt;content groups&lt;/strong&gt; — meant for organizing your content — while the cards inside — &lt;strong&gt;content pieces&lt;/strong&gt; — are containing the actual content with related metadata.&lt;/p&gt;

&lt;p&gt;Create a few content groups to represent your content production process (e.g. &lt;em&gt;Ideas&lt;/em&gt;, &lt;em&gt;Drafts&lt;/em&gt;, &lt;em&gt;Published&lt;/em&gt;) by clicking &lt;em&gt;New group&lt;/em&gt;, and then create your first content piece by clicking &lt;em&gt;New content piece&lt;/em&gt; in the content group of choice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WmEyJO4V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/VPGBFWXAUGPlfdckbc-bs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WmEyJO4V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/VPGBFWXAUGPlfdckbc-bs.png" alt="Content piece side panel" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With a content piece now created and selected, you’ll see a &lt;strong&gt;side panel&lt;/strong&gt;, containing all its metadata. Inspired by code editors like VS Code, with which most developers are quite familiar, the resizable side panel is where you’ll edit metadata, configure all available settings, manage your team, and more.&lt;/p&gt;

&lt;p&gt;Once the content piece is selected (opened in the side panel and highlighted), you can move to the editor by clicking the &lt;em&gt;Editor&lt;/em&gt; button in the side toolbar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MJvrjbeA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/7zfAmzzUOHjzFsrSGhXhZ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MJvrjbeA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/7zfAmzzUOHjzFsrSGhXhZ.png" alt="Vrite editor" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vrite editor&lt;/strong&gt; is focused on providing the best technical writing experience possible. Thanks to many features, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modern, clean UI/UX with WYSIWYG editing experience;&lt;/li&gt;
&lt;li&gt;Markdown and keyboard shortcuts;&lt;/li&gt;
&lt;li&gt;Built-in code editor with code highlighting, autocompletio,n and Prettier formatting (for supported languages);&lt;/li&gt;
&lt;li&gt;Support for most of the GFM (GitHub-Flavored Markdown) formatting options and content blocks;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;it’s easy to get started with and can cover vast majority of use cases in technical blogging.&lt;/p&gt;

&lt;p&gt;To customize the editor to the needs of your blog, you can go to &lt;em&gt;Settings → Editing experience&lt;/em&gt; to enable/disable various formatting options and content blocks and provide your own &lt;a href="https://prettier.io/docs/en/configuration.html"&gt;Prettier config&lt;/a&gt; for code formatting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5gzacgxV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/KwjiChkmy-4KAYTxx4Y92.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5gzacgxV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/KwjiChkmy-4KAYTxx4Y92.png" alt="Customizing the editing experience in Vrite" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, to integrate with Vrite you’ll need an API token. To get it, head to &lt;em&gt;Settings → API → New API token&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--svzr9JAk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/HGX803GyU7s06cu2_Vkf0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--svzr9JAk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/HGX803GyU7s06cu2_Vkf0.png" alt="Creating a new API token in Vrite" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, you can customize the token name, description, and permissions. It’s recommended to only use necessary permissions, which for a personal blog would likely mean &lt;strong&gt;read&lt;/strong&gt; access to &lt;em&gt;content pieces&lt;/em&gt; and &lt;em&gt;content groups&lt;/em&gt; (to retrieve the content), &lt;em&gt;tags,&lt;/em&gt; and &lt;em&gt;profile&lt;/em&gt; to build tag-based lists and provide some profile info about you (which you can configure in &lt;em&gt;Settings → Profile&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Save your API token and keep it safe. Now it’s time to create an Astro-powered blog!&lt;/p&gt;

&lt;p&gt;Start by creating a new Astro project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create astro@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted, choose &lt;em&gt;"Use blog template”&lt;/em&gt; as this will get you started the fastest.&lt;/p&gt;

&lt;p&gt;Now, &lt;code&gt;cd&lt;/code&gt; into the new projects and install &lt;a href="https://github.com/vriteio/sdk-js"&gt;Vrite JS SDK&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @vrite/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK provides all the tools you need to interact with Vrite’s API, including the API client, and Content Transformers. I’ve already covered those in &lt;a href="https://vrite.io/blog/better-blogging-on-dev-to-with-vrite-headless-cms-for-technical-content/"&gt;a previous blog post&lt;/a&gt;. However, you don’t actually have to think about them when working with Astro — thanks to the dedicated integration!&lt;/p&gt;

&lt;p&gt;First, create a &lt;code&gt;.env&lt;/code&gt; file to house your Vrite API token and an ID of the content group to publish content from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VRITE_ACCESS_TOKEN=
VRITE_CONTENT_GROUP_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get the ID of the content group, in the Kanban dashboard, open the context menu and &lt;em&gt;Copy ID&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ia47ce-D--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/Xm6Q2dmplu3OsLQbM16eu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ia47ce-D--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/Xm6Q2dmplu3OsLQbM16eu.png" alt="Copying content group ID" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, in the &lt;code&gt;astro.config.mjs&lt;/code&gt; import and configure the integration, by first loading the ENV variables and then providing the Vrite plugin in the &lt;code&gt;integrations&lt;/code&gt; array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mdx&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@astrojs/mdx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vritePlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vrite/sdk/astro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sitemap&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@astrojs/sitemap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loadEnv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;VRITE_ACCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VRITE_CONTENT_GROUP_ID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;loadEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="dl"&gt;''&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;mdx&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;vritePlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VRITE_ACCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;contentGroupId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VRITE_CONTENT_GROUP_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, for the best experience, add the Vrite SDK types to your &lt;code&gt;tsconfig.json&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"astro/tsconfigs/strict"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strictNullChecks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@vrite/sdk/types"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"astro/client"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the integration is configured and ready to go! It’s time to use it in the code.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;src/pages/blog/index.astro&lt;/code&gt; file let’s display a full list of existing blog posts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;---
// ...
import { getContentPieces } from "virtual:vrite";

const posts = await getContentPieces({ limit: "all" });
---

&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Header&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
          {
            posts.map((post) =&amp;gt; (
              &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
                {post.date &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;FormattedDate&lt;/span&gt; &lt;span class="na"&gt;date=&lt;/span&gt;&lt;span class="s"&gt;{new&lt;/span&gt; &lt;span class="na"&gt;Date&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;post.date&lt;/span&gt;&lt;span class="err"&gt;)}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;}
                &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;{`/blog/${post.slug}/`}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{post.title}&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
            ))
          }
        &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Footer&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All it takes is a &lt;code&gt;getContentPieces()&lt;/code&gt; function call to retrieve all the content pieces from the configured content group, in the order seen in the Kanban dashboard. Here we retrieve all the available content pieces (by providing &lt;code&gt;{ limit: “all” }&lt;/code&gt;, though, for bigger blogs, you’ll likely want to use pagination, which is also supported by this utility.&lt;/p&gt;

&lt;p&gt;The function itself comes from the &lt;code&gt;virtual:vrite&lt;/code&gt; module that provides a few other utilities like this, plus a fully-configured API client to make your work with Vrite and Astro a breeze. Additionally, (if you configured your &lt;code&gt;tsconfig.json&lt;/code&gt;), it’s fully typed, providing great DX!&lt;/p&gt;

&lt;p&gt;With that, you should now see the list of the content pieces on the website:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7gN0NfzT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/4GrdjXEe7_85ySRJIq1C4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7gN0NfzT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/4GrdjXEe7_85ySRJIq1C4.png" alt="List of blog posts" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What about the actual content? Well, for this you should move to &lt;code&gt;src/pages/blog/[…slug].astro&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;---
import BlogPost from "../../layouts/BlogPost.astro";
import { getStaticPaths, ContentPiece, Content } from "virtual:vrite";

export { getStaticPaths };
type Props = ContentPiece;

const contentPiece = Astro.props;
---

&lt;span class="nt"&gt;&amp;lt;BlogPost&lt;/span&gt;
  &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;{contentPiece.title}&lt;/span&gt;
  &lt;span class="na"&gt;description=&lt;/span&gt;&lt;span class="s"&gt;{contentPiece.description&lt;/span&gt; &lt;span class="err"&gt;||&lt;/span&gt; &lt;span class="err"&gt;""}&lt;/span&gt;
  &lt;span class="na"&gt;pubDate=&lt;/span&gt;&lt;span class="s"&gt;{contentPiece.date&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="na"&gt;new&lt;/span&gt; &lt;span class="na"&gt;Date&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;contentPiece.date&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="na"&gt;:&lt;/span&gt; &lt;span class="na"&gt;new&lt;/span&gt; &lt;span class="na"&gt;Date&lt;/span&gt;&lt;span class="err"&gt;()}&lt;/span&gt;
  &lt;span class="na"&gt;heroImage=&lt;/span&gt;&lt;span class="s"&gt;{contentPiece.coverUrl}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Content&lt;/span&gt; &lt;span class="na"&gt;contentPieceId=&lt;/span&gt;&lt;span class="s"&gt;{contentPiece.id}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/BlogPost&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Astro, you can use &lt;a href="https://docs.astro.build/en/core-concepts/routing/#dynamic-routes"&gt;dynamic routes&lt;/a&gt;, together with rest parameters, i.e. &lt;code&gt;[…slug]&lt;/code&gt; to generate pages from an external source, like a CMS. The parameter can be used to match and then fetch the correct data in the &lt;code&gt;getStaticPaths()&lt;/code&gt; function (when in the SSG mode).&lt;/p&gt;

&lt;p&gt;This is such a common use case that the Vrite integration implements this function for you! Simply make sure your route has a &lt;code&gt;slug&lt;/code&gt; parameter, re-export the function from &lt;code&gt;virtual:vrite&lt;/code&gt; module and — you’re done!&lt;/p&gt;

&lt;p&gt;The rest of the file is used to render the blog post. All the data of the content piece is available via &lt;code&gt;Astro.props&lt;/code&gt; (which you can strongly-typed by adding &lt;code&gt;type Props = ContentPiece;&lt;/code&gt;). You can then supply this data to a &lt;code&gt;BlogPost&lt;/code&gt; layout provided by the template. Finally, to render the content, use the &lt;code&gt;Content&lt;/code&gt; component provided by the integration, and supply it with the content piece ID.&lt;/p&gt;

&lt;p&gt;With that, your blog post should now be visible under the generated URL:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--x_ee4q8V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/BBujxgmrn4mgw6pFp2SB6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--x_ee4q8V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/BBujxgmrn4mgw6pFp2SB6.png" alt="Blog post from Vrite" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Content&lt;/code&gt; component automatically renders your blog post to HTML and uses Astro’s &lt;code&gt;Code&lt;/code&gt; component for &lt;a href="https://docs.astro.build/en/reference/api-reference/#code-"&gt;displaying and highlighting the code snippets&lt;/a&gt;, though you’ll have to &lt;code&gt;npm install shiki&lt;/code&gt; for it to work:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--a8R7ZyfL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/qz1rjQUgnnVmQd2WPy1lw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--a8R7ZyfL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://assets.vrite.io/6409e82d7dfc74cef7a72e0d/qz1rjQUgnnVmQd2WPy1lw.png" alt="Auto-highlighted code blocks" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you have a blog basically ready to go. If you want to customize the integration further, you can always tap into the underlying &lt;strong&gt;Vrite API client&lt;/strong&gt; to e.g. retrieve your Vrite profile. In &lt;code&gt;src/pages/about.astro&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;---
import Layout from "../layouts/BlogPost.astro";
import { client } from "virtual:vrite";

const profile = await client.profile.get();
---

&lt;span class="nt"&gt;&amp;lt;Layout&lt;/span&gt;
  &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;{profile.fullName&lt;/span&gt; &lt;span class="err"&gt;||&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;About&lt;/span&gt; &lt;span class="na"&gt;me&lt;/span&gt;&lt;span class="err"&gt;"}&lt;/span&gt;
  &lt;span class="na"&gt;description=&lt;/span&gt;&lt;span class="s"&gt;"Lorem ipsum dolor sit amet"&lt;/span&gt;
  &lt;span class="na"&gt;pubDate=&lt;/span&gt;&lt;span class="s"&gt;{new&lt;/span&gt; &lt;span class="na"&gt;Date&lt;/span&gt;&lt;span class="err"&gt;("&lt;/span&gt;&lt;span class="na"&gt;May&lt;/span&gt; &lt;span class="err"&gt;23&lt;/span&gt; &lt;span class="err"&gt;2023")}&lt;/span&gt;
  &lt;span class="na"&gt;updatedDate=&lt;/span&gt;&lt;span class="s"&gt;{new&lt;/span&gt; &lt;span class="na"&gt;Date&lt;/span&gt;&lt;span class="err"&gt;("&lt;/span&gt;&lt;span class="na"&gt;May&lt;/span&gt; &lt;span class="err"&gt;23&lt;/span&gt; &lt;span class="err"&gt;2023")}&lt;/span&gt;
  &lt;span class="na"&gt;heroImage=&lt;/span&gt;&lt;span class="s"&gt;"/placeholder-about.jpg"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{profile.bio}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Layout&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That said, it’s worth noting that the profile data in Vrite is a bit limited right now and you’ll likely want to add some more information right into the page.&lt;/p&gt;

&lt;p&gt;When you’re done, simply run &lt;code&gt;npm run build&lt;/code&gt; and have Astro generate a super-fast static blog for you!&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;While I might be a bit biased, I think this is one of the fastest and easiest CMS integrations out there. Unless you’ve experienced some bugs — mind you Vrite is still in beta and you should &lt;a href="https://github.com/vriteio/vrite"&gt;report those on GitHub&lt;/a&gt; — with some styling, you’ll have a blog in no time! Additionally, if you ever need to scale, the Vrite and Astro combo easily supports &lt;strong&gt;pagination&lt;/strong&gt; and &lt;strong&gt;SSR&lt;/strong&gt;, with little to no additional setup!&lt;/p&gt;

&lt;p&gt;Now, there’s still a lot more you can do with Vrite even now, that wasn’t covered in this blog post. For example — you can set up a Webhook to automatically rebuild and redeploy your static blog when you add a new content piece to the group (you can get some inspiration for that from &lt;a href="https://vrite.io/blog/better-blogging-on-dev-to-with-vrite-headless-cms-for-technical-content/"&gt;this blog post&lt;/a&gt;). You can also invite your team to collaborate on content in real-time or cross-post using Webhooks and Content Transformers. Documentation and more content on all of that is coming soon.&lt;/p&gt;

&lt;p&gt;Overall, if you’re running a programming blog or are a technical writer in need of better tools, I think Vrite is (or will soon become) what you were looking for. Either way, if you’re interested, definitely check out Vrite at &lt;a href="https://app.vrite.io"&gt;app.vrite.io&lt;/a&gt; and follow me on this journey for better technical tooling!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;Star Vrite on GitHub&lt;/strong&gt; — &lt;a href="https://github.com/vriteio/vrite"&gt;https://github.com/vriteio/vrite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐞 &lt;strong&gt;Report bugs&lt;/strong&gt; — &lt;a href="https://github.com/vriteio/vrite/issues"&gt;https://github.com/vriteio/vrite/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;strong&gt;Follow on Twitter for the  latest updates&lt;/strong&gt; — &lt;a href="https://twitter.com/vriteio"&gt;https://twitter.com/vriteio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Join Vrite Discord&lt;/strong&gt; — &lt;a href="https://discord.gg/yYqDWyKnqE"&gt;https://discord.gg/yYqDWyKnqE&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ℹ️ &lt;strong&gt;Learn more about Vrite&lt;/strong&gt; — &lt;a href="https://vrite.io"&gt;https://vrite.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>astro</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Better blogging on Dev.to with Vrite - headless CMS for technical content</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Fri, 19 May 2023 17:06:08 +0000</pubDate>
      <link>https://dev.to/vrite/better-blogging-on-devto-with-vrite-headless-cms-for-technical-content-4i05</link>
      <guid>https://dev.to/vrite/better-blogging-on-devto-with-vrite-headless-cms-for-technical-content-4i05</guid>
      <description>&lt;p&gt;With technical writing becoming increasingly popular - thanks in part to platforms like &lt;a href="https://dev.to/"&gt;DEV&lt;/a&gt; or &lt;a href="https://hashnode.com/" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;That’s why I decided to create &lt;a href="https://vrite.io/" rel="noopener noreferrer"&gt;Vrite&lt;/a&gt; - a new kind of headless CMS meant specifically for technical writing, with good developer experience in mind. From the built-in &lt;strong&gt;Kanban management&lt;/strong&gt; dashboard to the advanced WYSIWYG editor with support for &lt;strong&gt;Markdown&lt;/strong&gt;, real-time collaboration, embedded &lt;strong&gt;code editor&lt;/strong&gt;, and &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt; integration - Vrite is meant to be a one-stop-shop for all your technical content.&lt;/p&gt;

&lt;p&gt;With the release of the Public Beta earlier this week, &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;Vrite is now open-source&lt;/a&gt; and accessible to everyone - to help guide the future roadmap and make the best tool for all technical writers!&lt;/p&gt;

&lt;h2&gt;
  
  
  DEV API
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Dev.to is an especially interesting option, since the API of the underlying platform - Forem - is &lt;a href="https://developers.forem.com/api" rel="noopener noreferrer"&gt;well-documented&lt;/a&gt; and easily available. So, let’s see how to connect it with Vrite!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started With Vrite
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="http://app.vrite.io" rel="noopener noreferrer"&gt;app.vrite.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Start by &lt;a href="https://app.vrite.io/auth" rel="noopener noreferrer"&gt;signing up for an account&lt;/a&gt; - either directly or through GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FbacwnXsRwCQj1bGJj_4eT.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FbacwnXsRwCQj1bGJj_4eT.png" alt="Vrite login screen"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you’re in, you’ll be greeted by a Kanban dashboard. This is where you’ll be able to manage all your content:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FL30UxjhVDpOTSyar76PEy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FL30UxjhVDpOTSyar76PEy.png" alt="Vrite dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, it’s worth explaining how things are structured in Vrite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workspace&lt;/strong&gt; - 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;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Groups&lt;/strong&gt; - the equivalents to columns in the Kanban dashboard; They basically group all the content pieces under one label, e.g. &lt;em&gt;Ideas&lt;/em&gt;, &lt;em&gt;Drafts&lt;/em&gt;, &lt;em&gt;Published&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Pieces&lt;/strong&gt; - where your actual content and its metadata - like description, tags, etc live;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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 &lt;em&gt;Switch Workspace&lt;/em&gt; button in the bottom-left corner (hexagon) and then &lt;em&gt;New Workspace&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F6h5fAlJxUjw0trYnXTvU2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F6h5fAlJxUjw0trYnXTvU2.png" alt="Creating a new Vrite workspace"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need to provide a name and optionally - a description and logo. Then click &lt;em&gt;Create Workspace&lt;/em&gt; and select the new workspace you’ve created from the list:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F9EDxj55kQrvceGAkGjwDy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F9EDxj55kQrvceGAkGjwDy.png" alt="Vrite workspaces list"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back in the dashboard, you can now create a few content groups to organize your content by clicking &lt;em&gt;New group&lt;/em&gt;. When that’s done, you can finally create a new content piece by clicking &lt;em&gt;New content piece&lt;/em&gt; at the bottom of the column of your choice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FVPGBFWXAUGPlfdckbc-bs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FVPGBFWXAUGPlfdckbc-bs.png" alt="Creating a new content piece in Vrite"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With a new content piece, you can view and configure all its metadata in the &lt;strong&gt;side panel&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;Now, click &lt;em&gt;Open in editor&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F-rSHnt6bh74jSHx9uNaCn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F-rSHnt6bh74jSHx9uNaCn.png" alt="Vrite editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FdOXCQGclUgWJHm3Y8tzYr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FdOXCQGclUgWJHm3Y8tzYr.png" alt="Snippet editor in Vrite"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting with DEV
&lt;/h2&gt;

&lt;p&gt;When you’ve finished writing your next piece, it’s time to publish it! For convenience, the Vrite editor provides an &lt;em&gt;Export&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;The intended workflow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Drag and drop content pieces to the publishing column;&lt;/li&gt;
&lt;li&gt;Send a message to the server via Webhooks;&lt;/li&gt;
&lt;li&gt;Retrieve and process the content via the Vrite API and JS SDK;&lt;/li&gt;
&lt;li&gt;Publish/update a blog post on Dev.to;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For this tutorial, I’ll use &lt;a href="https://workers.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt; as they’re really fast and easy to set up, but you can use pretty much any other serverless provider with support for JS.&lt;/p&gt;

&lt;p&gt;Start by creating a new CF Worker project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create cloudflare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, &lt;code&gt;cd&lt;/code&gt; into the scaffolded project to &lt;code&gt;wrangler login&lt;/code&gt; and install &lt;a href="https://github.com/vriteio/sdk-js" rel="noopener noreferrer"&gt;Vrite JS SDK&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wrangler login
npm i @vrite/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To interact with the SDK, you’ll need to have an API token. To get it from Vrite, go to &lt;em&gt;Settings → API → New API token&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FK4hXEwb2q97bUPYZDeXjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FK4hXEwb2q97bUPYZDeXjl.png" alt="Creating new API token in Vrite"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s recommended to keep the permissions of the API token to the necessary minimum, which in this case means only &lt;em&gt;Write&lt;/em&gt; access to &lt;em&gt;Content pieces&lt;/em&gt; (as we’ll actually be updating the content piece metadata later on). After clicking &lt;em&gt;Create new token&lt;/em&gt; you’ll be presented with the newly-created token. Keep it safe and secure - &lt;strong&gt;you’ll only see it once&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://dev.to/settings/extensions"&gt;bottom of the settings in your DEV account&lt;/a&gt; and click &lt;em&gt;Generate API Key&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F5SD7-x8GEhTbLD5VOny4G.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2F5SD7-x8GEhTbLD5VOny4G.png" alt="Generate API key in Dev.to"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, add both tokens to the Worker as environment variables via &lt;code&gt;wrangler.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"autopublishing"&lt;/span&gt;
&lt;span class="py"&gt;main&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src/worker.ts"&lt;/span&gt;
&lt;span class="py"&gt;compatibility_date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2023-05-18"&lt;/span&gt;

&lt;span class="nn"&gt;[vars]&lt;/span&gt;
&lt;span class="py"&gt;VRITE_API_TOKEN&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"[YOUR_VRITE_API_TOKEN]"&lt;/span&gt;
&lt;span class="py"&gt;DEV_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"[YOUR_DEV_API_KEY]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon the event, Vrite sends a &lt;code&gt;POST&lt;/code&gt; 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)&lt;/p&gt;

&lt;p&gt;Let’s finally create our Worker (inside &lt;code&gt;src/worker.ts&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JSONContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vrite/sdk/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createContentTransformer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gfmTransformer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vrite/sdk/transformers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSONContent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;VRITE_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;DEV_API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VRITE_API_TOKEN&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentPiece&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentPieces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;processContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canonicalLink&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;devSeries&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;main_image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coverUrl&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;devId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://dev.to/api/articles/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEV_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User-Agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error from DEV: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://dev.to/api/articles`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEV_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User-Agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentPieces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;customData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;contentPiece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;devId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;article&lt;/code&gt; object that’s expected by &lt;a href="https://developers.forem.com/api" rel="noopener noreferrer"&gt;the DEV API&lt;/a&gt; and use it to make a request.&lt;/p&gt;

&lt;p&gt;In Vrite, in addition to strictly-defined metadata like tags or canonical links, you can also provide JSON-based &lt;em&gt;custom data&lt;/em&gt;. 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 &lt;code&gt;devId&lt;/code&gt; 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 &lt;code&gt;devSeries&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;Worth noting is that, for requests to DEV API, we’re passing a generic &lt;code&gt;User-Agent&lt;/code&gt; header - it’s necessary to make a successful request without &lt;code&gt;403&lt;/code&gt; bot-detection error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content Transformers
&lt;/h3&gt;

&lt;p&gt;You might have noticed that the &lt;code&gt;body_markdown&lt;/code&gt; property is set to the result of &lt;code&gt;processContent()&lt;/code&gt; call. That’s because the Vrite API serves its content in a JSON format. Derived from the &lt;a href="https://prosemirror.net/" rel="noopener noreferrer"&gt;ProseMirror&lt;/a&gt; library powering Vrite editor, the format allows for versatile content delivery as it can be easily adapted to various needs.&lt;/p&gt;

&lt;p&gt;The Vrite JS SDK has built-in tools for transforming this format called &lt;em&gt;Content Transformers&lt;/em&gt;. 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).&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;gfmTransformer&lt;/code&gt; to add support for these embeds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JSONContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vrite/sdk/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createContentTransformer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gfmTransformer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vrite/sdk/transformers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSONContent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;devTransformer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContentTransformer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;applyInlineFormatting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;gfmTransformer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
            &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;transformNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;embed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`\n{% embed &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; %}\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;taskList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;gfmTransformer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
                &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;devTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;content&lt;/code&gt; string generated from the child nodes.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;processContent()&lt;/code&gt; function above, we’re redirecting the processing of inline formatting options (like bold, italic, etc.) - to the &lt;code&gt;gfmTransformer&lt;/code&gt;, 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” &lt;code&gt;taskList&lt;/code&gt;s (as DEV doesn’t support them) and handling processing for &lt;code&gt;embeds&lt;/code&gt;, using DEV’s liquid tags and embed URL available as a node attribute — &lt;code&gt;src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now the Worker is ready to be deployed via the Wrangler CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wrangler deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;Go to &lt;em&gt;Settings → Webhooks → New Webhook&lt;/em&gt; (all in the side panel) &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FdnQHZhWfIQ_C9xp3E2YON.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.vrite.io%2F6409e82d7dfc74cef7a72e0d%2FdnQHZhWfIQ_C9xp3E2YON.png" alt="Creating a new Webhook in Vrite"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For an event select &lt;code&gt;New content piece added&lt;/code&gt; — this will trigger every time a new content piece is created directly within the given content group (in this case &lt;em&gt;Published&lt;/em&gt;) or dragged and dropped into it.&lt;/p&gt;

&lt;p&gt;Now you should be able to just drag and drop your ready content piece and see it be automatically published on DEV! 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only content pieces that are newly added to the content group and getting published/updated. You might want to consider &lt;strong&gt;“locking” this content group&lt;/strong&gt; so that editing these content pieces requires you to first move the article back to the &lt;em&gt;Drafts&lt;/em&gt; or &lt;em&gt;Editing&lt;/em&gt; column. If necessary, you can set up dedicated Webhooks for those groups, so that content pieces are automatically unpublished on DEV.&lt;/li&gt;
&lt;li&gt;Since the introduction of Workspaces, Vrite supports Teams and &lt;strong&gt;real-time collaboration&lt;/strong&gt; 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 &lt;strong&gt;roles&lt;/strong&gt; and &lt;strong&gt;permissions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;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 &lt;em&gt;Editing Experience&lt;/em&gt; in the settings, including mentioned options and a &lt;strong&gt;Prettier config&lt;/strong&gt; for code formatting.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Now, it’s worth remembering that Vrite is still in &lt;strong&gt;Beta&lt;/strong&gt;. 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!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;Star Vrite on GitHub&lt;/strong&gt; — &lt;a href="https://github.com/vriteio/vrite" rel="noopener noreferrer"&gt;https://github.com/vriteio/vrite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐞 &lt;strong&gt;Report bugs&lt;/strong&gt; — &lt;a href="https://github.com/vriteio/vrite/issues" rel="noopener noreferrer"&gt;https://github.com/vriteio/vrite/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;strong&gt;Follow on Twitter for latest updates&lt;/strong&gt; — &lt;a href="https://twitter.com/vriteio" rel="noopener noreferrer"&gt;https://twitter.com/vriteio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Join Vrite Discord&lt;/strong&gt; — &lt;a href="https://discord.gg/yYqDWyKnqE" rel="noopener noreferrer"&gt;https://discord.gg/yYqDWyKnqE&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ℹ️ &lt;strong&gt;Learn more about Vrite&lt;/strong&gt; — &lt;a href="https://vrite.io" rel="noopener noreferrer"&gt;https://vrite.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>writing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Separation of concerns slows you down</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Fri, 27 Jan 2023 20:52:21 +0000</pubDate>
      <link>https://dev.to/vrite/separation-of-concerns-slows-you-down-5eag</link>
      <guid>https://dev.to/vrite/separation-of-concerns-slows-you-down-5eag</guid>
      <description>&lt;p&gt;&lt;strong&gt;Separation of concerns&lt;/strong&gt; has long been regarded as a good design principle for organizing your codebase. Personally, in my quest for writing clean code, for a long time, I &lt;a href="https://areknawo.com/separation-of-concerns-with-custom-react-hooks/"&gt;tried to adhere to this principle&lt;/a&gt; when creating frontend web apps, while also trying to keep my development experience at a consistently high level.&lt;/p&gt;

&lt;p&gt;Fast forward to today and it’s been a while since I decided to no longer follow this principle. I’m still focused on writing clean code, I now value more development experience and code that’s easy to scale, maintain and evolve over time. I’ve come to a difficult and controversial conclusion - separation of concerns actually slows you down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separation of concerns in frontend web development
&lt;/h2&gt;

&lt;p&gt;As a web developer, the most common and well-known example of separation of concerns is in the UI - HTML for markup, CSS for styles, and JS for interactivity. This “golden standard” of concern separation was heavily shaken by modern JS toolings such as UI frameworks and compilers.&lt;/p&gt;

&lt;p&gt;React through &lt;strong&gt;JSX&lt;/strong&gt; brought markup right into your JS code. This was and still is a big reason why many developers are against it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might argue that other frameworks, such as Vue, did the same thing, just in a different way. However, Vue’s &lt;strong&gt;&lt;a href="https://vuejs.org/guide/scaling-up/sfc.html"&gt;Single File Components&lt;/a&gt;&lt;/strong&gt; (SFCs) still adhere to this principle. While the code for all elements of the component resides in the same file, styling, markup, and JS logic are still separated into clear sections. It’s important to remember that separation of concerns doesn’t necessarily equal separation of code into separate files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ content }}&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#333&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, a similar process has been happening with CSS. Thanks to &lt;strong&gt;CSS-in-JS&lt;/strong&gt; and, more recently, &lt;strong&gt;utility CSS&lt;/strong&gt; solutions like &lt;a href="https://tailwindcss.com/"&gt;Tailwind CSS&lt;/a&gt;, the process of styling also moved into JS code. Combined with JSX, your entire component can now be a single file, where all elements of a component intertwine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
      &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-gray-200 hover:bg-gray-300 text-gray-800 px-2 py-1 rounded-lg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pros and cons of concern separation
&lt;/h3&gt;

&lt;p&gt;Some might look at tools like JSX or Tailwind CSS and say it’s a highway to code spaghetti. They wouldn’t be wrong. Forgoing the separation of concerns completely and building giant-size components can quickly lead to code that’s hard to read and difficult to understand, building up technical debt over time.&lt;/p&gt;

&lt;p&gt;However, just because it’s easy to do it wrong, doesn’t mean it can’t be done right. I’d say these modern tools force us to look at web development differently. To see components as complete building blocks, rather than just groups of styles, markup, and code.&lt;/p&gt;

&lt;p&gt;When done right, I’d argue JSX and utility CSS (or CSS-in-JS) can speed up development, improve the developer experience and even lead to more organized code. You no longer have to switch “contexts” in your mind or files in your editor to go between markup or styles. You can see components in a new light, optimizing them to serve their intended purpose. But, over all else - you don’t have to name things as often.&lt;/p&gt;

&lt;p&gt;If you’ve worked long enough as a software developer long enough you’ll know this job comes down to two things - &lt;strong&gt;solving problems&lt;/strong&gt; and &lt;strong&gt;naming things&lt;/strong&gt; - where the latter is the most difficult. Combining JSX with utility CSS means dramatically less naming required for CSS classes, element IDs, etc. This translates to even further increased developer productivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different kind of separation of concerns
&lt;/h2&gt;

&lt;p&gt;Now, forgoing “standard” separation of concerns doesn’t mean putting everything in a single file and calling it a day. I’ve already touched on why it’s the wrong path and how it can lead you to completely unmaintainable code. Instead, you should approach your code organization from a point of components and business logic.&lt;/p&gt;

&lt;p&gt;Extract the smallest parts of your UI - especially ones that can be reused - and organize them correctly. I like to think it’s a more natural way to organize your app’s codebase. Thinking about components, different parts of your UI, their responsibilities, and functions seems more natural than artificially separating styles and markup just for the sake of it.&lt;/p&gt;

&lt;p&gt;In addition to that, any complex logic that’s not directly related to the component (i.e. doesn’t change the state of the UI), such as API client, data processing, utility functions, etc., should also be separated whenever possible. In general, any business logic that doesn’t need to directly interact with the UI should be extracted and properly modularized.&lt;/p&gt;

&lt;p&gt;Following these two, rather simple rules should lead you to a different kind of concern separation. One where concerns are separated based on their &lt;strong&gt;functions&lt;/strong&gt; and &lt;strong&gt;responsibilities&lt;/strong&gt; in relation to your codebase. One that should come to you more naturally and should improve your productivity rather than slow you down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Now, the fact is, if you already have strong opinions or work with a framework that imposes a specific approach to the separation of concerns, this post likely won’t make you change your mind. However, if you obsess over clean code, and are trying many different approaches, frameworks, etc., then maybe this convinced you to try something different.&lt;/p&gt;

&lt;p&gt;For the time being, this is how I approach web development on pretty much every project I have “architectural control” over. That’s how I worked with &lt;a href="https://www.solidjs.com/"&gt;Solid.js&lt;/a&gt; and Tailwind CSS for the past 2 years. That’s how &lt;a href="https://vrite.io"&gt;Vrite&lt;/a&gt; is being built. Has worked pretty well so far…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vrite.io"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8hPcEmHy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fru77wusds91dhadprnz.png" alt="Vrite - Join waitlist" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>react</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>Solid.js, React, and Vue - reactivity systems compared</title>
      <dc:creator>Arek Nawo</dc:creator>
      <pubDate>Thu, 26 Jan 2023 23:17:27 +0000</pubDate>
      <link>https://dev.to/vrite/solidjs-react-and-vue-reactivity-systems-compared-30od</link>
      <guid>https://dev.to/vrite/solidjs-react-and-vue-reactivity-systems-compared-30od</guid>
      <description>&lt;p&gt;Working on many projects over the years, I’ve used multiple JavaScript UI frameworks. React, Vue, and now Solid.js are the frameworks that I’ve worked with and enjoyed the most.&lt;/p&gt;

&lt;p&gt;Recently, especially when working with Solid.js, I started to notice and appreciate small details like how every framework’s &lt;strong&gt;reactivity system&lt;/strong&gt; differs. It’s an important thing to keep in mind while jumping between frameworks as, even though the APIs keep getting more similar, how the framework works underneath influences performance, software architecture as well as how you think about your app in general.&lt;/p&gt;

&lt;p&gt;The thing is understanding the framework’s reactivity system to the full extent takes time and requires deep knowledge of its architecture. That’s a lot - especially when working with multiple frameworks. That’s why I wanted to simplify these concepts and provide you a “good enough” starting point so that you both understand the major differences and have a solid entry point to exploring this topic deeper on your own…&lt;/p&gt;

&lt;h2&gt;
  
  
  Reactivity Systems
&lt;/h2&gt;

&lt;p&gt;If you’ve only ever worked with or are laser-focused on a single framework you might not have thought about its reactivity system or rendering model too much - especially in comparison to other frameworks. How the component state is created and managed, what triggers a re-render, what parts of the UI are being updated, the inner workings of Virtual DOM, and how it all impacts performance - these are questions that you usually don’t consider when building UIs. However, sometimes it’s worth taking a step back and considering how these things impact your entire codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  React
&lt;/h3&gt;

&lt;p&gt;In React, when using Functional Components with Hooks, your entire “component function” is what gets executed on every re-render. Take a look at this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Example&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;someData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSomeData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;re-render&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`You clicked &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; times`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setSomeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;You&lt;/span&gt; &lt;span class="nx"&gt;clicked&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;times&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
        &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Count not updated here yet: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;Click&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the entire component function is re-run on every re-render, you use React Hooks to both define the state of the component (&lt;code&gt;useState()&lt;/code&gt;), as well as to “filter out” pieces of code that should run only when specific state properties are updated (&lt;code&gt;useEffect()&lt;/code&gt;). That’s the simplest way to think about the re-renders here - “everything runs by default, you use Hooks to filter out what should not”. That’s by design.&lt;/p&gt;

&lt;p&gt;Few other things to note here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Updating &lt;code&gt;someData&lt;/code&gt; (in an interval) triggers re-renders even though the value isn’t used in the view. Now, due to the way React works, it’s required to keep the value in the component up-to-date, and, thanks to Virtual DOM, no DOM operation will be performed. However, it’s still a re-render that can be costly in complex components which you need to keep in mind. Changing &lt;code&gt;someData&lt;/code&gt; to a ref using &lt;code&gt;useRef()&lt;/code&gt; would be the solution here.&lt;/li&gt;
&lt;li&gt;  You can only be sure that &lt;code&gt;count&lt;/code&gt; was updated, after a re-render, in the &lt;code&gt;useEffect()&lt;/code&gt; callback. The &lt;code&gt;console.log()&lt;/code&gt; right after &lt;code&gt;setCount()&lt;/code&gt; will still display the old value. It’s not that problematic when you have other ways to access the new value or have adapted your mental model to the way React works. If you’re new to React, or you’re constantly switching between different frameworks, or work on complex components with complex event handlers - this might be an issue. If you want to both update the component as well as have the latest value available right away, you might have to combine &lt;code&gt;useState()&lt;/code&gt; with &lt;code&gt;useRef()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Things that aren’t as important but also stand out when compared to other frameworks: having to explicitly define effect dependencies (no auto-tracking) and no direct “on mount” lifecycle callback (&lt;code&gt;useEffect(() =&amp;gt; {}, [])&lt;/code&gt; serves as an alternative).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To battle, these and other “issues” React established “Rules of Hooks” and recommends splitting your code into smaller components. The more granular the component and its state, the lesser the potential overhead for every re-render.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vue
&lt;/h3&gt;

&lt;p&gt;Here’s an equivalent example for Vue 3 (note: even though it’s less popular in Vue, I’ve used JSX for a closer visual comparison with other frameworks):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;watchEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onMounted&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Example&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineComponent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;someData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Count updated here already: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;watchEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`You clicked &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; times`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;onMounted&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;someData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;re-render&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;You&lt;/span&gt; &lt;span class="nx"&gt;clicked&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;times&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Click&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Vue, every component has an entry &lt;code&gt;setup()&lt;/code&gt; function. It’s here that you use Composition API to set up your component’s logic and finally, return a rendering function. You see a difference between what runs only once and what runs on every re-render right away. If you want a piece of code to run when certain state property changes, you have to use watchers to “filter them into” the update cycle, as opposed to “filtering out” in React.&lt;/p&gt;

&lt;p&gt;On top of that, there are a few advantages to Vue’s reactivity system. First off, Vue does provide actual lifecycle hooks like &lt;code&gt;onMounted()&lt;/code&gt;. On top of that, the &lt;code&gt;setup()&lt;/code&gt; function itself serves as a great entry point for any logic that doesn’t require the component to be already rendered.&lt;/p&gt;

&lt;p&gt;Secondly, thanks to reactive objects and refs based on JS proxies, Vue can automatically detect when a certain effect or re-render needs to be triggered. Thus, you don’t need to provide explicit dependencies in &lt;code&gt;watchEffect()&lt;/code&gt; (though you can with the &lt;code&gt;watch()&lt;/code&gt;), and setting &lt;code&gt;someData&lt;/code&gt; ref in an interval won’t trigger a re-render, since it’s not used in the view.&lt;/p&gt;

&lt;p&gt;Finally, when updating a ref, you can be sure that when you read the value again, it’ll already be changed. Keep in mind though that the re-render triggered by that change likely hasn’t happened yet and the UI isn’t up-to-date.&lt;/p&gt;

&lt;p&gt;With all that and a lot of optimization on the Virtual DOM and other parts of the framework, Vue has much better performance when compared to React and arguably, a better development experience. The &lt;code&gt;setup()&lt;/code&gt; function makes for a great starting point for your components, not having to worry as much about too many re-renders, while Composition API provides similar ergonomics as React Hooks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solid
&lt;/h3&gt;

&lt;p&gt;To me, Solid often feels like the best of React and Vue combined. However, this line of thinking doesn’t give me a complete picture of the framework. Once you step beyond basic components, you quickly see that Solid is much different than other frameworks. When working with Solid, you’ll have to forget a lot of what you’ve learned about components and reactivity from other frameworks.&lt;/p&gt;

&lt;p&gt;Consider the example below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSignal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onMount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;solid-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Example&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;someData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSomeData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;createEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`You clicked &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; times`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;onMount&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setSomeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;You&lt;/span&gt; &lt;span class="nx"&gt;clicked&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="nx"&gt;times&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
        &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Count updated here already: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;Click&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Solid.js components exist only to organize your code. That’s why a component function is run only once. It’s similar to a component function in React, with properties of the &lt;code&gt;setup()&lt;/code&gt; function of Vue. However, once it’s run, the component vanishes, while all that’s left are the JSX elements, reactive primitives, and effects. That’s very different from e.g. Vue, where every component has an instance you can access. Still, Solid does provide some lifecycle hooks like &lt;code&gt;onMount()&lt;/code&gt; but they’re more focused on reactive scope rather than the actual lifecycle of the component, like in Vue.&lt;/p&gt;

&lt;p&gt;Another big part of Solid.js is its fine-grained reactivity system. While the API might seem similar to React Hooks, it’s completely different underneath. Solid’s reactivity is built on signals that are automatically tracked to appropriately trigger effects and UI updates. Thus, similarly to Vue, you don’t need to pass explicit dependencies to &lt;code&gt;createEffect()&lt;/code&gt; (even though you can with &lt;code&gt;on()&lt;/code&gt;), while updating data that’s not in the view (like &lt;code&gt;someData&lt;/code&gt;), won’t trigger a UI update.&lt;/p&gt;

&lt;p&gt;Solid is the only framework of the three not to have a Virtual DOM. Thanks to fine-grained reactivity, and compiler optimizations, Solid is able to quickly update just the right part of the UI - synchronously. This results in charts-topping performance and reassures you that your UI is always up-to-date. Thus, after calling the &lt;code&gt;setCount()&lt;/code&gt; function you can be sure that both the value and the UI have already been updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As you can see, even though the frameworks seem similar on the outside, many underlying differences make or break performance and developer experience.&lt;/p&gt;

&lt;p&gt;Personally, over the last few years, Solid has been my favorite. Thanks to its top performance, reactivity system, and API, it’s been a development experience like no other. That’s why it’s powering this blog, Vrite landing page, and soon - Vrite itself. If you’re interested, give Solid a look!&lt;/p&gt;

</description>
      <category>independent</category>
      <category>ai</category>
      <category>career</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
