<?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>Forem: rhymes</title>
    <description>The latest articles on Forem by rhymes (@rhymes).</description>
    <link>https://forem.com/rhymes</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%2Fuser%2Fprofile_image%2F2693%2Fbfd9a4a5-92b3-4ac3-a276-3ccb68d78203.jpg</url>
      <title>Forem: rhymes</title>
      <link>https://forem.com/rhymes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rhymes"/>
    <language>en</language>
    <item>
      <title>How to combine Rails's Ajax support and Stimulus</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Fri, 27 Aug 2021 15:03:18 +0000</pubDate>
      <link>https://forem.com/devteam/how-to-combine-rails-s-ajax-support-and-stimulus-3729</link>
      <guid>https://forem.com/devteam/how-to-combine-rails-s-ajax-support-and-stimulus-3729</guid>
      <description>&lt;p&gt;In this post I'm going to explain how we are exploring adding snappier SPA-like interactivity to the Admin and what we've learned so far. I am using the word "exploring" because this is ongoing work, that is not visible yet in Forems, so it many or may not reflect the final version but I think there are useful lessons to be learned nonetheless.&lt;/p&gt;

&lt;p&gt;I'm going to assume you're familiar with &lt;a href="https://rubyonrails.org/"&gt;Ruby on Rails&lt;/a&gt;, &lt;a href="https://stimulus.hotwired.dev/"&gt;Stimulus&lt;/a&gt; and the concept of &lt;a href="https://en.wikipedia.org/wiki/Component-based_software_engineering"&gt;componentization&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we wanted to accomplish
&lt;/h2&gt;

&lt;p&gt;Let's start with a video demo:&lt;/p&gt;



&lt;p&gt;The goal here is to give the user a perception of interactivity, and we want to do that without unleashing a full client side single page application. Forem's Admin interface is mostly server rendered and we wanted to explore a path to progressively enhance the experience, stopping short of a rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the status of the current Admin?
&lt;/h2&gt;

&lt;p&gt;Currently the Admin, on the backend, is a custom made collection of Rails controllers, for all intents and purposes a part of the Forem core app. It's not an external web app, it's also not generated by a third party gem. We think the Forem Creator (and their merry band of collaborators) experience is paramount and it has evolved from DEV's needs to now the larger Forem ecosystem's.&lt;/p&gt;

&lt;p&gt;Being a custom app, grown over the years, it's admittedly a mix of technologies that we're trying to streamline, hence the need for some good old exploratory software development. On the frontend it currently uses: jQuery, Bootstrap, vanilla JS, Stimulus, Preact, a few web components and our custom &lt;a href="https://storybook.forem.com/"&gt;Crayons design language&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did we explore an alternative?
&lt;/h2&gt;

&lt;p&gt;The end goal is to reduce that to Crayons, Stimulus and use Preact or Web Components when absolutely needed, to foster a nimbler architecture, with reuse between the "frontoffice" website and the admin, where possible.&lt;/p&gt;

&lt;p&gt;After discussing this with the team I set out to investigate the following assumption (not a direct quote): &lt;em&gt;"We want users actions to be interactive, minimizing page reloads, and because of that we're going to send blocks of server rendered HTML to them by injecting the markup into the page".&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If this sounds like a barebones version of notable frameworks like Elixir's &lt;a href="https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html"&gt;Phoenix LiveView&lt;/a&gt;, Rails's &lt;a href="https://github.com/stimulusreflex/stimulus_reflex"&gt;StimulusReflex&lt;/a&gt; or &lt;a href="https://turbo.hotwired.dev/"&gt;Hotwire Turbo&lt;/a&gt;, PHP's &lt;a href="https://laravel-livewire.com/"&gt;LiveWire&lt;/a&gt;, Django's &lt;a href="https://github.com/edelvalle/reactor"&gt;Reactor&lt;/a&gt;... well, you're right! (Bonus: my colleague &lt;a class="mentioned-user" href="https://dev.to/jgaskins"&gt;@jgaskins&lt;/a&gt; built a &lt;a href="https://github.com/jgaskins/live_view"&gt;LiveView clone for Crystal&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;You can sense a pattern in these frameworks, and the demand they fulfill.&lt;/p&gt;

&lt;p&gt;In our case, though, we used none of them. I wanted to explore how far we could go without adding a whole framework and by using the tools we had a bit more in depth. This to lessen the cognitive load on anyone that's going to further this exploration or adopt this pattern for the Admin as a whole.&lt;/p&gt;

&lt;p&gt;Aside from the obvious "why should I need a framework to send basic HTML to the client", we have plenty of frameworks and libraries on the client side already and frameworks usually take quite a while to be learned. Also, we're a small team.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;this is how I implented it&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Rails and HTML on the server side with a bit of JSON when needed. I cheated a bit the constraints I set for myself using GitHub's &lt;a href="https://viewcomponent.org/"&gt;ViewComponent&lt;/a&gt; but you can achieve similar results using builtin Rails partials and this post isn't going into depth about ViewComponent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rails's &lt;a href="https://guides.rubyonrails.org/working_with_javascript_in_rails.html"&gt;UJS&lt;/a&gt; (Unobtrusive JavaScript) and &lt;a href="https://stimulus.hotwired.dev/"&gt;Stimulus&lt;/a&gt; on the client side. UJS is a library builtin inside Rails that powers JavaScript interactions on the DOM via Rails special helpers, like &lt;code&gt;link_to&lt;/code&gt; or &lt;code&gt;button_to&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How does it all fit together?
&lt;/h2&gt;

&lt;p&gt;Let's start from the goal again: &lt;em&gt;a user clicks on a link, the client side sends a request to the server, some action is performed, some HTML is sent back, this HTML is injected in the page&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is what happens when the user clicks on one of the gray boxes for example:&lt;/p&gt;



&lt;p&gt;Clicking on "Emails", hits the &lt;code&gt;EmailsController&lt;/code&gt; which renders the &lt;code&gt;EmailsComponent&lt;/code&gt; (which, again, could just be a partial), the resulting HTML is sent to Stimulus which calls a JavaScript function injecting the HTML, thus finalizing the switch of the section.&lt;/p&gt;

&lt;p&gt;Let's look at the code, one step at a time:&lt;/p&gt;

&lt;h3&gt;
  
  
  Initiating the contact between client and server
&lt;/h3&gt;

&lt;p&gt;This is how the gray box titled "Emails" is defined in Rails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;admin_user_tools_emails_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;remote: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                 &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"ajax:success-&amp;gt;user#replacePartial"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                                                 &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"crayons-card box js-action"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h4&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"crayons-subtitle-3 mb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Emails&lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"color-base-70"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;pluralize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@emails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"past email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@emails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verified&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; - Verified&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;-%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and this is an example of the resulting HTML:&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;a&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"crayons-card box js-action"&lt;/span&gt;
  &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/admin/users/13/tools/emails"&lt;/span&gt;
  &lt;span class="na"&gt;data-remote=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
  &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"ajax:success-&amp;gt;user#replacePartial"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h4&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"crayons-subtitle-3 mb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Emails&lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"color-base-70"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; 7 past emails &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a bit going on in such a small snippet of code, let's unpack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;href="/admin/users/13/tools/emails"&lt;/code&gt; identifies this as a regular HTML link, if I were to visit it with my browser I would get the same response JavaScript is going to be sent when the user activates the click.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;data-remote="true"&lt;/code&gt; (the result of &lt;code&gt;remote: true&lt;/code&gt; in Ruby) is how Rails determines if the link should be handled by Ajax or not. Rails calles these &lt;a href="https://guides.rubyonrails.org/working_with_javascript_in_rails.html#remote-elements"&gt;remote elements&lt;/a&gt;, they can be links, forms or buttons.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;data-action="ajax:success-&amp;gt;user#replacePartial"&lt;/code&gt; is how we connect Rails UJS&lt;br&gt;
and Stimulus together. &lt;code&gt;data-action&lt;/code&gt; is a &lt;a href="https://stimulus.hotwired.dev/reference/actions"&gt;Stimulus action&lt;/a&gt; (the description of how to handle an event), &lt;code&gt;ajax:success&lt;/code&gt; is a &lt;a href="https://guides.rubyonrails.org/working_with_javascript_in_rails.html#rails-ujs-event-handlers"&gt;custom event triggered by Rails UJS&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what it all translates to: &lt;em&gt;on initiating the click on link, let Rails UJS fetch the response via Ajax and, upon a successful response, handle the &lt;code&gt;ajax:success&lt;/code&gt; event via the method &lt;code&gt;replacePartial&lt;/code&gt; in the Stimulus &lt;code&gt;UserController&lt;/code&gt; class&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is a lot of behavior in a few lines. It reads like declarative programming with a good abstraction, working well if one wants to minimize the amount of custom JavaScript to write and thus needs to describe behavior directly in the templates :-)&lt;/p&gt;

&lt;p&gt;The resource the link points to is a regular HTML snippet, this is what one sees if visited manually:&lt;/p&gt;



&lt;p&gt;The great thing (in my opinion), is that the whole behavior in question still works in isolation: it's server side rendered, it redirects upon submission as it should by default, it is essentially a regular HTML form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being able to play with these components in isolation definitely speeds up development&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The whole section (which I called &lt;code&gt;ToolsComponent&lt;/code&gt; on the server) works&lt;br&gt;
in isolation:&lt;/p&gt;


&lt;h3&gt;
  
  
  What happens on the server when this request is sent?
&lt;/h3&gt;

&lt;p&gt;Once again, let's start from the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Admin&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Users&lt;/span&gt;
    &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Tools&lt;/span&gt;
      &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Admin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ApplicationController&lt;/span&gt;
        &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
          &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

          &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;EmailsComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;content_type: &lt;/span&gt;&lt;span class="s2"&gt;"text/html"&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. We tell Rails not to embed the component (or partial) in a layout, we load the user object and we tell the framework to render the HTML sending it back to the client as HTML (this last tiny detail is important, as Rails's "remote mode" defaults to &lt;code&gt;text/javascript&lt;/code&gt; for the response, which is not very helpful to us in this case...).&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the frontend do when it receives the HTML?
&lt;/h3&gt;

&lt;p&gt;Let's look at the code once again:&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;a&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"crayons-card box js-action"&lt;/span&gt;
  &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/admin/users/13/tools/emails"&lt;/span&gt;
  &lt;span class="na"&gt;data-remote=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
  &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"ajax:success-&amp;gt;user#replacePartial"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h4&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"crayons-subtitle-3 mb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Emails&lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"color-base-70"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; 7 past emails &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've instructed the app to trigger &lt;code&gt;replacePartial&lt;/code&gt; inside the Stimulus&lt;br&gt;
&lt;code&gt;UserController&lt;/code&gt;, this is what it does:&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="nx"&gt;replacePartial&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preventDefault&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;stopPropagation&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="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;detail&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js-action&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolsComponentTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;replaceTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseText&lt;/span&gt;&lt;span class="p"&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;announceChangedSectionToScreenReader&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;This method:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;prevents the default behavior and stops propagation&lt;/li&gt;
&lt;li&gt;extracts the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest"&gt;XMLHttpRequest&lt;/a&gt; injected by Rails&lt;/li&gt;
&lt;li&gt;hides the section we're looking at and shows the new one&lt;/li&gt;
&lt;li&gt;announces the change to the screen reader, as we are neither changing the URL nor doing a full page reload.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How did we make this accessible?
&lt;/h3&gt;

&lt;p&gt;After discussing it with our resident accessibility guru, @suzanne, she suggested we use a "screen reader only" &lt;code&gt;aria-live&lt;/code&gt; element:&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;div&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"screen-reader-only"&lt;/span&gt;
  &lt;span class="na"&gt;data-user-target=&lt;/span&gt;&lt;span class="s"&gt;"activeSection"&lt;/span&gt;
  &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is managed by Stimulus, which at the end of the action, fetches the title of the new section, announces it to the screen reader and changes the focus so the section is ready to be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap so far
&lt;/h2&gt;

&lt;p&gt;So far we've seen quite a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using Rails builtin capabilities to connect client side code and the server side via Ajax but using server side HTML&lt;/li&gt;
&lt;li&gt;using Stimulus to listen in on the action and augment behavior as we see fit, keeping the code organized&lt;/li&gt;
&lt;li&gt;replacing a section of HTML with another, that's self contained in a component
that can be at least functional without JavaScript as well&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to send an email with Rails and Stimulus
&lt;/h2&gt;

&lt;p&gt;Here we're going to show how this "connection" works, using sending an email as an example.&lt;/p&gt;

&lt;p&gt;Let's start from the perspective of the user:&lt;/p&gt;



&lt;h2&gt;
  
  
  What does the email form do?
&lt;/h2&gt;

&lt;p&gt;Given we're in the domain of UJS and Stimulus combined, we have to look at how they are connected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt;
  &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"users--tools--ajax"&lt;/span&gt;
  &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"ajax:success@document-&amp;gt;users--tools--ajax#success ajax:error@document-&amp;gt;users--tools--ajax#error"&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="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;send_email_admin_user_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;-%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our "Emails" section declares it needs a Stimulus controller named &lt;code&gt;AjaxController&lt;/code&gt; and that it's going to dispatch to it the Rails UJS events &lt;code&gt;ajax:success&lt;/code&gt; and &lt;code&gt;ajax:error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the "Send Email" submit button is activated, Rails will send the form via Ajax to the server, which upon successful submission, will answer with data, in this case JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens on the server?
&lt;/h2&gt;

&lt;p&gt;Once again, code first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="c1"&gt;# email sent&lt;/span&gt;
  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Email sent!"&lt;/span&gt;

    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
      &lt;span class="n"&gt;redirect_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;fallback_location: &lt;/span&gt;&lt;span class="n"&gt;admin_users_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;js&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;content_type: &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the email is sent the server figures out if it was a regular form submission and thus invokes a redirect or if it was a submission via Ajax (our case), it sends back a feedback message in JSON.&lt;/p&gt;

&lt;p&gt;I am using JSON here because it fits well with the snackbar notifications, but we could send well styled HTML to inject for a richer interaction, same we did in the first part.&lt;/p&gt;

&lt;p&gt;Specifying the content type is important, because Rails defaults to &lt;code&gt;text/javascript&lt;/code&gt; for Ajax interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the client do once it receives a successful response?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;AjaxController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;success&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="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="o"&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;detail&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;data&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="c1"&gt;// close the panel and go back to the home view&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;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;CustomEvent&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:tools&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// display success info message&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;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;snackbar:add&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;detail&lt;/span&gt;&lt;span class="p"&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="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;The "success" event handler extracts the feedback message sent by the server and then dispatches two &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent"&gt;custom events&lt;/a&gt; that asynchronously communicate with two different areas of the page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;user:tools&lt;/code&gt; communicates with the Stimulus &lt;code&gt;UsersController&lt;/code&gt; telling it to initiate a navigation back to the initial screen, the "Tools" section. How? Via this line in the HTML of the container page:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;data-action="user:tools@document-&amp;gt;user#fetchAndOpenTools"
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;snackbar:add&lt;/code&gt; communicates with the Stimulus &lt;code&gt;SnackbarController&lt;/code&gt; telling it to add a new message to the stack of messages to show the user. &lt;a href="https://dev.to/devteam/how-to-wrap-a-preact-component-into-a-stimulus-controller-1bd0"&gt;I wrote a post if you're interested in how this part works&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the first event is received, the following function is invoked, which triggers an Ajax call, fetching the server side &lt;code&gt;ToolsComponent&lt;/code&gt;'s HTML and displaying it in the UI:&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="nx"&gt;fetchAndOpenTools&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preventDefault&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;stopPropagation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ajax&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&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;toolsComponentPathValue&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;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;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partial&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nx"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementsByClassName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js-component&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;outerHTML&lt;/span&gt;&lt;span class="p"&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;announceChangedSectionToScreenReader&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;&lt;code&gt;Rails.ajax&lt;/code&gt; is builtin in Rails UJS, not very different from using &lt;code&gt;window.fetch&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;There's quite a bit going on here, depending on your level of familiarity with the major parts: Rails and Stimulus.&lt;/p&gt;

&lt;p&gt;In my opinion Stimulus is really good to keep vanilla JS organized and to attach behavior to server side rendered HTML markup, in a declarative way.&lt;/p&gt;

&lt;p&gt;By leveraging Rails builtin Ajax support and thin layer you can add interactivity without having to rely on larger frameworks or having to switch to client side rendering.&lt;/p&gt;

&lt;p&gt;If this is something that fits your use case, only you know, but I hope this post showed you how to combine two frameworks to improve the user experience without a steep learning curve and thus increasing the level of developer productivity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;p&gt;Aside from countless DuckDuckGo searches (there's little documentation on how to fit all the pieces together) and source code reading, I mainly spent time here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://guides.rubyonrails.org/working_with_javascript_in_rails.html"&gt;Working with JavaScript in Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stimulus.hotwired.dev/reference/controllers"&gt;Stimulus's Reference documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.betterstimulus.com/"&gt;BetterStimulus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/stimulus-components/stimulus-remote-rails"&gt;stimulus-remote-rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/forem/forem/pull/14283"&gt;Initial Forem's pull request&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rails</category>
      <category>javascript</category>
      <category>stimulus</category>
    </item>
    <item>
      <title>How to wrap a Preact component into a Stimulus controller</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Thu, 24 Jun 2021 13:51:54 +0000</pubDate>
      <link>https://forem.com/devteam/how-to-wrap-a-preact-component-into-a-stimulus-controller-1bd0</link>
      <guid>https://forem.com/devteam/how-to-wrap-a-preact-component-into-a-stimulus-controller-1bd0</guid>
      <description>&lt;p&gt;In this post I'm going to illustrate the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wrapping a Preact component inside a Stimulus controller&lt;/li&gt;
&lt;li&gt;loading Preact and the component asynchronously on demand&lt;/li&gt;
&lt;li&gt;communicating with the wrapped component via JavaScript &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent" rel="noopener noreferrer"&gt;custom events&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is partly based on work &lt;a class="mentioned-user" href="https://dev.to/s_aitchison"&gt;@s_aitchison&lt;/a&gt; did &lt;a href="https://github.com/forem/forem/pull/12511" rel="noopener noreferrer"&gt;last February on Forem&lt;/a&gt;. Forem's public website uses Preact and vanilla JavaScript. Some of Forem's Admin views are using Stimulus. This is an example of how to recycle frontend components from one framework to another.&lt;/p&gt;

&lt;p&gt;I'm also assuming the reader has some familiarity with both Preact and Stimulus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping the component
&lt;/h2&gt;

&lt;p&gt;Yesterday I was working on some Admin interactions and I wanted to reuse &lt;a href="https://storybook.dev.to/?path=/story/app-components-snackbar-snackbar--simulate-adding-snackbar-items"&gt;Forem's &lt;code&gt;Snackbar&lt;/code&gt; component&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3oipmo4hf7jv0zo0ype.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3oipmo4hf7jv0zo0ype.png" alt="Example of Snackbar component in action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How it is implemented inside Preact is not important for our purposes and I haven't checked either, I just know its module exports &lt;code&gt;Snackbar&lt;/code&gt; and a function &lt;code&gt;addSnackbarItem&lt;/code&gt; to operate it.&lt;/p&gt;

&lt;p&gt;As the screenshot shows, it is similar to &lt;a href="https://material.io/components/snackbars" rel="noopener noreferrer"&gt;Material's &lt;code&gt;Snackbar&lt;/code&gt; component&lt;/a&gt;, as it provides &lt;em&gt;brief messages about app processes at the bottom of the screen&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With that in mind and with the groundwork laid by Suzanne Aitchison on &lt;a href="https://github.com/forem/forem/blob/main/app/javascript/admin/controllers/modal_controller.js" rel="noopener noreferrer"&gt;a different component&lt;/a&gt;, I wrote the following code:&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;Controller&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;stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Wraps the Preact Snackbar component into a Stimulus controller&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SnackbarController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&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;snackZone&lt;/span&gt;&lt;span class="dl"&gt;'&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;connect&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;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Snackbar&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="c1"&gt;// eslint-disable-next-line import/no-unresolved&lt;/span&gt;
      &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Snackbar&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;render&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;Snackbar&lt;/span&gt; &lt;span class="nx"&gt;lifespan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3&lt;/span&gt;&lt;span class="dl"&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snackZoneTarget&lt;/span&gt;&lt;span class="p"&gt;);&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;disconnect&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;render&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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&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;snackZoneTarget&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Any controller (or vanilla JS) can add an item to the Snackbar by dispatching a custom event.&lt;/span&gt;
  &lt;span class="c1"&gt;// Stimulus needs to listen via this HTML's attribute: data-action="snackbar:add@document-&amp;gt;snackbar#addItem"&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;addItem&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="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;addCloseButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;detail&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;addSnackbarItem&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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Snackbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;addSnackbarItem&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;addCloseButton&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;Let's go over it piece by piece.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining a container
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&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;snackZone&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;Most Preact components need a container to render in. In Stimulus lingo we need to define a "target", which is how the framework calls important HTML elements referenced inside its controller (the main class to organize code in).&lt;/p&gt;

&lt;p&gt;This is defined as a regular HTML &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; in the page:&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;div&lt;/span&gt; &lt;span class="na"&gt;data-snackbar-target=&lt;/span&gt;&lt;span class="s"&gt;"snackZone"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the controller, this element can be accessed as &lt;code&gt;this.snackZoneTarget&lt;/code&gt;. &lt;a href="https://stimulus.hotwire.dev/reference/targets" rel="noopener noreferrer"&gt;Stimulus documentation has more information on targets&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;(&lt;em&gt;snackZone&lt;/em&gt; is just how the &lt;code&gt;Snackbar&lt;/code&gt;'s container is called inside Forem's frontend code, I kept the name :D)&lt;/p&gt;

&lt;h3&gt;
  
  
  Mounting and unmounting the component
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Snackbar&lt;/code&gt; component, when initialized, doesn't render anything visible to the user. It waits for a message to be added to the stack of disappearing messages that are shown to the user after an action is performed. For this reason, we can use Stimulus lifecycle callbacks to mount it and unmount it.&lt;/p&gt;

&lt;p&gt;Stimulus &lt;a href="https://stimulus.hotwire.dev/reference/lifecycle-callbacks" rel="noopener noreferrer"&gt;provides two aptly named callbacks&lt;/a&gt;, &lt;code&gt;connect()&lt;/code&gt; and &lt;code&gt;disconnect()&lt;/code&gt;, that we can use to initialize and cleanup our Preact component.&lt;/p&gt;

&lt;p&gt;When the Stimulus controller is attached to the page, it will call the &lt;code&gt;connect()&lt;/code&gt; method, in our case we take advantage of this by loading Preact and the Snackbar component:&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;async&lt;/span&gt; &lt;span class="nf"&gt;connect&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;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Snackbar&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Snackbar&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;render&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;Snackbar&lt;/span&gt; &lt;span class="nx"&gt;lifespan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3&lt;/span&gt;&lt;span class="dl"&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snackZoneTarget&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;Here we accomplish the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;asynchronously load Preact, importing &lt;a href="https://preactjs.com/guide/v10/api-reference#render" rel="noopener noreferrer"&gt;its renderer function&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;asynchronously load &lt;a href="https://storybook.dev.to/?path=/story/app-components-snackbar-snackbar--simulate-adding-snackbar-items"&gt;Forem's &lt;code&gt;Snackbar&lt;/code&gt; component&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;rendering the component inside the container&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be "good citizens" we also want to clean up when the controller is disconnected:&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;async&lt;/span&gt; &lt;span class="nf"&gt;disconnect&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;render&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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&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;snackZoneTarget&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;This destroys Preact's component whenever Stimulus unloads its controller from the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Communicating with the component
&lt;/h3&gt;

&lt;p&gt;Now that we know how to embed Preact into Stimulus, how do we send messages? This is where the JavaScript magic lies :-)&lt;/p&gt;

&lt;p&gt;Generally, good software design teaches us to avoid coupling components of any type, regardless if we're talking about JavaScript modules, Ruby classes, entire software subsystems and so on.&lt;/p&gt;

&lt;p&gt;JavaScript's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events#adding_custom_data_%E2%80%93_customevent" rel="noopener noreferrer"&gt;CustomEvent Web API&lt;/a&gt; comes to the rescue.&lt;/p&gt;

&lt;p&gt;With it it's possible to lean in the standard pub/sub architecture that JavaScript developers are familiar with: an element listens to an event, handles it with a handler and an action on another element triggers an event. The first element is the subscriber, the element triggering the event is the publisher.&lt;/p&gt;

&lt;p&gt;With this is mind: what are Stimulus controllers if not also global event subscribers, reacting to changes?&lt;/p&gt;

&lt;p&gt;First we need to tell Stimulus to listen to a custom event:&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;body&lt;/span&gt;
  &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"snackbar"&lt;/span&gt;
  &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"snackbar:add@document-&amp;gt;snackbar#addItem"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;data-controller="snackbar"&lt;/code&gt; attaches Stimulus &lt;code&gt;SnackbarController&lt;/code&gt;, defined in the first section of this post, to the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; of the page.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;data-action="snackbar:add@document-&amp;gt;snackbar#addItem"&lt;/code&gt; instructs the framework to listen to the custom event &lt;code&gt;snackbar:add&lt;/code&gt; on &lt;code&gt;window.document&lt;/code&gt; and when received to send it to the &lt;code&gt;SnackbarController&lt;/code&gt; by invoking its &lt;code&gt;addItem&lt;/code&gt; method acting as en event handler.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;addItem&lt;/code&gt; is defined as:&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;async&lt;/span&gt; &lt;span class="nf"&gt;addItem&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="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;addCloseButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;detail&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;addSnackbarItem&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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Snackbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;addSnackbarItem&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;addCloseButton&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 handler extracts, from the event custom payload, the message and a boolean that, if true, will display a button to dismiss the message. It then imports the method &lt;code&gt;addSnackbarItem&lt;/code&gt; and invokes it with the correct arguments, to display a message to the user.&lt;/p&gt;

&lt;p&gt;The missing piece in our "pub/sub" architecture is the published, that is given us for free via the Web API &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent" rel="noopener noreferrer"&gt;&lt;code&gt;EventTarget.dispatchEvent&lt;/code&gt;&lt;/a&gt; method:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;snackbar:add&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;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;snackbar:add&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;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MESSAGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;addCloseButton&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;snackbar:add&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;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MESSAGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;addCloseButton&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="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The great advantage is that the publisher doesn't need to inside Stimulus at all, it can be any JavaScript function reacting to an action: the network, the user or any DOM event.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;CustomEvent&lt;/code&gt; interface is straightforward and flexible enough that can be used to create more advanced patterns like the, now defunct, &lt;a href="https://v3.vuejs.org/guide/migration/events-api.html#events-api" rel="noopener noreferrer"&gt;Vue Events API&lt;/a&gt; which provided a global event bus in the page, out of scope for this post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo
&lt;/h3&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqerjfpqnqg5ekushl114.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqerjfpqnqg5ekushl114.gif" alt="Video demo of Snackbar wrapped in Stimulus and invoked via dispatchEvent"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I hope this showed you a strategy of reuse when you're presented with multiple frameworks that have to interact with each other on a page.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>preact</category>
      <category>stimulus</category>
    </item>
    <item>
      <title>Changelog: API updates</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Mon, 02 Nov 2020 10:20:27 +0000</pubDate>
      <link>https://forem.com/devteam/changelog-api-updates-13e9</link>
      <guid>https://forem.com/devteam/changelog-api-updates-13e9</guid>
      <description>&lt;p&gt;We recently shipped a series of improvements to the Forem API thanks to our open source contributors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetching articles by their slugs&lt;/li&gt;
&lt;li&gt;Fetching podcast episodes comments&lt;/li&gt;
&lt;li&gt;Retrieving a list of followed tags&lt;/li&gt;
&lt;li&gt;Getting the list of articles in the reading list&lt;/li&gt;
&lt;li&gt;Finding organizations details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read on to find more about these changes!&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://dev.to/Oghenebrume50"&gt;Oghenebrume50&lt;/a&gt; we can fetch an article by its slug:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/forem/forem/pull/8929"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Fetch article by slug
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#8929&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/Oghenebrume50"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s---pcTV4dJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://avatars3.githubusercontent.com/u/37092867%3Fv%3D4" alt="Oghenebrume50 avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/Oghenebrume50"&gt;Oghenebrume50&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/forem/forem/pull/8929"&gt;&lt;time&gt;Jun 26, 2020&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;What type of PR is this? (check all applicable)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Refactor&lt;/li&gt;
&lt;li&gt;[X] Feature&lt;/li&gt;
&lt;li&gt;[ ] Bug Fix&lt;/li&gt;
&lt;li&gt;[ ] Optimization&lt;/li&gt;
&lt;li&gt;[ ] Documentation Update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Description&lt;/h2&gt;
&lt;p&gt;According to this issue #8313 the user suggested that articles should be searchable by slug and not only by id. My implement included converting the param input to an integer, so if a string that is not a valid number is passed it returns 0. So if the value is not zero then I find with the id else I find by the slug,  in both cases, the error is well handled by rails, this is why I used &lt;code&gt;find_by!&lt;/code&gt; instead of &lt;code&gt;find_by&lt;/code&gt; because the former return an &lt;code&gt; ActiveRecord::ActiveRecordError&lt;/code&gt; when nothing is found&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;QA Instructions, Screenshots, Recordings&lt;/h2&gt;
&lt;p&gt;This is an example of a working slug
&lt;a href="https://user-images.githubusercontent.com/37092867/85867764-40165d00-b7c1-11ea-9bd6-9d9ebea37c66.png" rel="nofollow"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZlUTUuOy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://user-images.githubusercontent.com/37092867/85867764-40165d00-b7c1-11ea-9bd6-9d9ebea37c66.png" alt="workingslug"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Example of a wrong slug that is not found
&lt;a href="https://user-images.githubusercontent.com/37092867/85867802-51f80000-b7c1-11ea-824e-1b0ce6242eb7.png" rel="nofollow"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--44uAbKLM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://user-images.githubusercontent.com/37092867/85867802-51f80000-b7c1-11ea-824e-1b0ce6242eb7.png" alt="wrongslug"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Also, the id works fine
&lt;a href="https://user-images.githubusercontent.com/37092867/85868232-ebbfad00-b7c1-11ea-8fd6-43beb0e04796.png" rel="nofollow"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZKHuZaki--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://user-images.githubusercontent.com/37092867/85868232-ebbfad00-b7c1-11ea-8fd6-43beb0e04796.png" alt="id"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I tested everything, all possible endpoints and they work fine.&lt;/p&gt;
&lt;p&gt;I am opening this as a draft PR to check if this is a good approach, if this is okay I can update the documentation.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Please replace this line with instructions on how to test your changes, as well
as any relevant images for UI changes.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added tests?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] yes&lt;/li&gt;
&lt;li&gt;[x] no, because they aren't needed&lt;/li&gt;
&lt;li&gt;[ ] no, because I need help&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added to documentation?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] docs.dev.to&lt;/li&gt;
&lt;li&gt;[ ] readme&lt;/li&gt;
&lt;li&gt;[x] no documentation needed&lt;/li&gt;
&lt;/ul&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/forem/forem/pull/8929"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;You can find more information &lt;a href="https://docs.dev.to/api/#operation/getArticleByPath"&gt;in the documentation&lt;/a&gt; and try it here: &lt;a href="https://dev.to/api/articles/devteam/the-7-most-popular-dev-posts-from-the-past-week-4fhi"&gt;https://dev.to/api/articles/devteam/the-7-most-popular-dev-posts-from-the-past-week-4fhi&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://dev.to/jkrsn98"&gt;jkrsn98&lt;/a&gt; we can retrieve a podcast episode’s comments:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/forem/forem/pull/9677"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        API: retrieve podcast episodes comments
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#9677&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/jkrsn98"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--c10tGVpC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://avatars3.githubusercontent.com/u/30130633%3Fv%3D4" alt="jkrsn98 avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/jkrsn98"&gt;jkrsn98&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/forem/forem/pull/9677"&gt;&lt;time&gt;Aug 07, 2020&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;What type of PR is this? (check all applicable)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Refactor&lt;/li&gt;
&lt;li&gt;[x] Feature&lt;/li&gt;
&lt;li&gt;[ ] Bug Fix&lt;/li&gt;
&lt;li&gt;[ ] Optimization&lt;/li&gt;
&lt;li&gt;[ ] Documentation Update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Description&lt;/h2&gt;
&lt;p&gt;Modified the index in comments controller which previously only handled look for a_id parameter, to having it look for a_id or p_id parameter which allow for accessing podcast episode comments. If a_id key has value, it looks for article with that id, if a_id has no value, then it must be that user is trying to query with p_id, so it looks for podcast episode.&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Related Tickets &amp;amp; Documents&lt;/h2&gt;
&lt;p&gt;Fixes #6526&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;QA Instructions, Screenshots, Recordings&lt;/h2&gt;
&lt;p&gt;Currently you can only request comments related to articles with parameter a_id
&lt;a href="https://camo.githubusercontent.com/b4950d8a57d47c29296722879c043cfa9d81dd75/68747470733a2f2f692e696d6775722e636f6d2f764e74376c4c522e706e67" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/b4950d8a57d47c29296722879c043cfa9d81dd75/68747470733a2f2f692e696d6775722e636f6d2f764e74376c4c522e706e67" alt="requesting with a id"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With the line I modified it now uses parameter p_id as well, which will return all the comments related to the podcast episode with that id, same behaviour as was for articles
For example, with the following podcast episode with id 1, I comment on it then try to query with that id, and it works as follows
&lt;a href="https://camo.githubusercontent.com/87a53cd6ed0a8a06571ab9b5a263a6530144df5d/68747470733a2f2f692e696d6775722e636f6d2f5a43715a4831522e706e67" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/87a53cd6ed0a8a06571ab9b5a263a6530144df5d/68747470733a2f2f692e696d6775722e636f6d2f5a43715a4831522e706e67" alt="requesting with p id"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added tests?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] yes&lt;/li&gt;
&lt;li&gt;[ ] no, because they aren't needed&lt;/li&gt;
&lt;li&gt;[ ] no, because I need help&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added to documentation?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] docs.forem.com&lt;/li&gt;
&lt;li&gt;[ ] readme&lt;/li&gt;
&lt;li&gt;[ ] no documentation needed&lt;/li&gt;
&lt;/ul&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/forem/forem/pull/9677"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;You can find more information &lt;a href="https://docs.forem.com/api/#operation/getCommentsByArticleId"&gt;in the documentation&lt;/a&gt; and try it here: &lt;a href="https://dev.to/api/comments?p_id=15564"&gt;https://dev.to/api/comments?p_id=15564&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/jkrsn98"&gt;jkrsn98&lt;/a&gt; also added the missing &lt;code&gt;created_at&lt;/code&gt; field in comments that will help determine when a comment was posted on the website:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/forem/forem/pull/9829"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        API: add created_at datetime to comments
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#9829&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/jkrsn98"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--c10tGVpC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://avatars3.githubusercontent.com/u/30130633%3Fv%3D4" alt="jkrsn98 avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/jkrsn98"&gt;jkrsn98&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/forem/forem/pull/9829"&gt;&lt;time&gt;Aug 17, 2020&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;What type of PR is this? (check all applicable)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Refactor&lt;/li&gt;
&lt;li&gt;[x] Feature&lt;/li&gt;
&lt;li&gt;[ ] Bug Fix&lt;/li&gt;
&lt;li&gt;[ ] Optimization&lt;/li&gt;
&lt;li&gt;[ ] Documentation Update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Description&lt;/h2&gt;
&lt;p&gt;Added created_at field for comments in api response&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Related Tickets &amp;amp; Documents&lt;/h2&gt;
&lt;p&gt;Fixes #6095&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;QA Instructions, Screenshots, Recordings&lt;/h2&gt;
&lt;p&gt;Currently when user requests api/comment they do not see when each comment was created&lt;/p&gt;
&lt;p&gt;&lt;a href="https://camo.githubusercontent.com/3a18e48e38e0b8bff6c84d87520267198677e9aa/68747470733a2f2f692e696d6775722e636f6d2f41446b6151664f2e706e67" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/3a18e48e38e0b8bff6c84d87520267198677e9aa/68747470733a2f2f692e696d6775722e636f6d2f41446b6151664f2e706e67" alt="before"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now that information is included:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://camo.githubusercontent.com/b817219a924e0ad89e933ecb93da8e2cae9d245d/68747470733a2f2f692e696d6775722e636f6d2f794f306f6667702e706e67" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/b817219a924e0ad89e933ecb93da8e2cae9d245d/68747470733a2f2f692e696d6775722e636f6d2f794f306f6667702e706e67" alt="after"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added tests?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] yes&lt;/li&gt;
&lt;li&gt;[ ] no, because they aren't needed&lt;/li&gt;
&lt;li&gt;[ ] no, because I need help&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added to documentation?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] docs.forem.com&lt;/li&gt;
&lt;li&gt;[ ] readme&lt;/li&gt;
&lt;li&gt;[ ] no documentation needed&lt;/li&gt;
&lt;/ul&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/forem/forem/pull/9829"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Thanks to &lt;a href="https://dev.to/danascheider"&gt;danascheider&lt;/a&gt;, a user can retrieve a list of the tags they follow:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/forem/forem/pull/9108"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        API: Add route for fetching a user's followed tags
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#9108&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/danascheider"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--x7ymGEUt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://avatars1.githubusercontent.com/u/5115928%3Fv%3D4" alt="danascheider avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/danascheider"&gt;danascheider&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/forem/forem/pull/9108"&gt;&lt;time&gt;Jul 03, 2020&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;What type of PR is this? (check all applicable)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Refactor&lt;/li&gt;
&lt;li&gt;[x] Feature&lt;/li&gt;
&lt;li&gt;[ ] Bug Fix&lt;/li&gt;
&lt;li&gt;[ ] Optimization&lt;/li&gt;
&lt;li&gt;[ ] Documentation Update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Description&lt;/h2&gt;
&lt;p&gt;This PR adds the &lt;code&gt;/api/tags/me&lt;/code&gt; route, which returns the authenticated user's followed tags as JSON.&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Related Tickets &amp;amp; Documents&lt;/h2&gt;
&lt;p&gt;Closes #5111&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;QA Instructions, Screenshots, Recordings&lt;/h2&gt;
&lt;p&gt;I've added request specs for requests with and without valid authentication.&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added tests?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] yes&lt;/li&gt;
&lt;li&gt;[ ] no, because they aren't needed&lt;/li&gt;
&lt;li&gt;[ ] no, because I need help&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added to documentation?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] docs.dev.to&lt;/li&gt;
&lt;li&gt;[ ] readme&lt;/li&gt;
&lt;li&gt;[x] no documentation needed&lt;/li&gt;
&lt;/ul&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/forem/forem/pull/9108"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;You can find more information &lt;a href="https://docs.forem.com/api/#operation/getFollowedTags"&gt;in the documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://dev.to/bhacaz"&gt;bhacaz&lt;/a&gt; a user can retrieve a list of articles in their reading list.&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/forem/forem/pull/10540"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        API - New endpoint to retrieve the articles in the reading list of the authenticated user
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#10540&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/Bhacaz"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--V4juBCrI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://avatars1.githubusercontent.com/u/7858787%3Fv%3D4" alt="Bhacaz avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/Bhacaz"&gt;Bhacaz&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/forem/forem/pull/10540"&gt;&lt;time&gt;Oct 02, 2020&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;What type of PR is this? (check all applicable)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Refactor&lt;/li&gt;
&lt;li&gt;[x] Feature&lt;/li&gt;
&lt;li&gt;[ ] Bug Fix&lt;/li&gt;
&lt;li&gt;[ ] Optimization&lt;/li&gt;
&lt;li&gt;[x] Documentation Update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Description&lt;/h2&gt;
&lt;p&gt;Add a new endpoint in the DEV API to retrieve the reading list articles for the authenticated user.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GET api/articles/me/readinglist&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Related Tickets &amp;amp; Documents&lt;/h2&gt;
&lt;p&gt;This feature was requested in Issue #6755.&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;QA Instructions, Screenshots, Recordings&lt;/h2&gt;
&lt;p&gt;You can try the new API endpoint via a &lt;code&gt;curl&lt;/code&gt; command with a valid &lt;em&gt;api-key&lt;/em&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell js-code-highlight"&gt;
&lt;pre&gt;curl -H &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;api-key: API-KEY&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; http://localhost:3000/api/readinglist &lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added tests?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] yes&lt;/li&gt;
&lt;li&gt;[ ] no, because they aren't needed&lt;/li&gt;
&lt;li&gt;[ ] no, because I need help&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added to documentation?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] docs.forem.com/api&lt;/li&gt;
&lt;li&gt;[ ] readme&lt;/li&gt;
&lt;li&gt;[ ] no documentation needed&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;What gif best describes this PR or how it makes you feel?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://camo.githubusercontent.com/fde07889c7499ded2f4f0262c75eaa87e83f4b86/68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f315467454346306d4e566972432f67697068792e676966" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/fde07889c7499ded2f4f0262c75eaa87e83f4b86/68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f315467454346306d4e566972432f67697068792e676966" alt="Cat reading"&gt;&lt;/a&gt;&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/forem/forem/pull/10540"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;You can find more information &lt;a href="https://docs.forem.com/api/#operation/getReadinglist"&gt;in the documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://dev.to/diogoosorio"&gt;diogoosorio&lt;/a&gt; we can retrieve the profile image of a user or an organization.&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/forem/forem/pull/10547"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Introduce /api/profile_images/:username endpoint
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#10547&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/diogoosorio"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--jj-GPzb1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://avatars3.githubusercontent.com/u/1484824%3Fv%3D4" alt="diogoosorio avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/diogoosorio"&gt;diogoosorio&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/forem/forem/pull/10547"&gt;&lt;time&gt;Oct 04, 2020&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;What type of PR is this? (check all applicable)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Refactor&lt;/li&gt;
&lt;li&gt;[x] Feature&lt;/li&gt;
&lt;li&gt;[ ] Bug Fix&lt;/li&gt;
&lt;li&gt;[ ] Optimization&lt;/li&gt;
&lt;li&gt;[ ] Documentation Update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Description&lt;/h2&gt;
&lt;p&gt;This PR aims to introduce the operation &lt;code&gt;GET /api/profile_images/:username&lt;/code&gt; to the API.&lt;/p&gt;
&lt;p&gt;I struggled a bit with what the contract for the operation should be (as this isn't a "resource" per se), I ended up keeping it simple and re-using the structure I saw throughout the rest of the API, when dealing with profile images:&lt;/p&gt;
&lt;div class="highlight highlight-source-json js-code-highlight"&gt;
&lt;pre&gt;{
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;type_of&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;profile_image&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;profile_image&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;/uploads/organization/profile_image/be5e87e9-5b77-40ea-81e8-dd10c6d01146.png&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;profile_image_90&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;/uploads/organization/profile_image/be5e87e9-5b77-40ea-81e8-dd10c6d01146.png&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
}&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Having said that, I'm happy to refactor the contract to whatever the maintainers believe it to be the correct way to represent a profile image in this endpoint - I just ask for concrete example of what the response's payload should look like 😄 .&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Related Tickets &amp;amp; Documents&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/forem/forem/issues/10132"&gt;https://github.com/forem/forem/issues/10132&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;QA Instructions, Screenshots, Recordings&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;curl "http://localhost:3000/api/profile_images/:username"&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added tests?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] yes&lt;/li&gt;
&lt;li&gt;[ ] no, because they aren't needed&lt;/li&gt;
&lt;li&gt;[ ] no, because I need help&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added to documentation?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] docs.forem.com&lt;/li&gt;
&lt;li&gt;[ ] readme&lt;/li&gt;
&lt;li&gt;[ ] no documentation needed&lt;/li&gt;
&lt;/ul&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/forem/forem/pull/10547"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;You can find more information &lt;a href="https://docs.forem.com/api/#operation/getProfileImage"&gt;in the documentation&lt;/a&gt; and try it here &lt;a href="https://dev.to/api/profile_images/devteam"&gt;https://dev.to/api/profile_images/devteam&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://dev.to/twinsfan421"&gt;twinsfan421&lt;/a&gt; we’re one step closer to having API for organizations:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/forem/forem/pull/10931"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        API: Endpoint to get an organization's details
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#10931&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/twinsfan421"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--ImwN0-Bd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://avatars1.githubusercontent.com/u/25860670%3Fv%3D4" alt="twinsfan421 avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/twinsfan421"&gt;twinsfan421&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/forem/forem/pull/10931"&gt;&lt;time&gt;Oct 19, 2020&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;What type of PR is this? (check all applicable)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Refactor&lt;/li&gt;
&lt;li&gt;[x] Feature&lt;/li&gt;
&lt;li&gt;[ ] Bug Fix&lt;/li&gt;
&lt;li&gt;[ ] Optimization&lt;/li&gt;
&lt;li&gt;[ ] Documentation Update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Description&lt;/h2&gt;
&lt;p&gt;This would provide an api show endpoint &lt;code&gt;dev.to/api/organizations/{org_username}&lt;/code&gt; to search organizations by their username.&lt;br&gt;
As discussed in issue 9212, This is the first of 4 related endpoints that will include:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dev.to/api/organizations/{org_username}/users
dev.to/api/organizations/{org_username}/articles
dev.to/api/organizations/{org_username}/listings
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Related Tickets &amp;amp; Documents&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/forem/forem/issues/9212"&gt;https://github.com/forem/forem/issues/9212&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added tests?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] yes&lt;/li&gt;
&lt;li&gt;[ ] no, because they aren't needed&lt;/li&gt;
&lt;li&gt;[ ] no, because I need help&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;Added to documentation?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] docs.forem.com&lt;/li&gt;
&lt;li&gt;[ ] readme&lt;/li&gt;
&lt;li&gt;[ ] no documentation needed&lt;/li&gt;
&lt;/ul&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/forem/forem/pull/10931"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;You can find more information &lt;a href="https://docs.forem.com/api/#operation/getOrganization"&gt;in the documentation&lt;/a&gt; and try it here: &lt;a href="https://dev.to/api/organizations/devteam"&gt;https://dev.to/api/organizations/devteam&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We hope you’ll find these updates to the Forem API useful!&lt;/p&gt;

</description>
      <category>changelog</category>
    </item>
    <item>
      <title>Is accidental complexity inevitable?</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Mon, 17 Aug 2020 12:27:32 +0000</pubDate>
      <link>https://forem.com/rhymes/is-accidental-complexity-inevitable-58i9</link>
      <guid>https://forem.com/rhymes/is-accidental-complexity-inevitable-58i9</guid>
      <description>&lt;p&gt;I'll start with two questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is complexity inevitable in software development?&lt;/li&gt;
&lt;li&gt;How much of this complexity is our failure to curb "accidental complexity" which we are the ones to introduce to begin with?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let us use a couple of popular tools as examples: Rust and React.&lt;/p&gt;

&lt;p&gt;For what I've gathered Rust has has been developed out of need. The need to manage systems programming, manual memory management and the development of software that sits very close to the operating system. Its complexity is intrinsic to the problems it had to solve initially.&lt;/p&gt;

&lt;p&gt;React has been developed out of need as well. The need to manage a massive frontend code base at Facebook. Its complexity is intrinsic to the problems it had to solve initially.&lt;/p&gt;

&lt;p&gt;As any software developer knows, tools intended for a purpose are easily going to be used for anything else but that initial purpose. It's normal, we're humans. It's not like Rust or React creators have a direct line with all the developers out there.&lt;/p&gt;

&lt;p&gt;This goes back to the second of my initial questions: is a tool developed and managed by a company, worth several hundreds of billions of dollars, to handle a massive frontend code base going to be accidental complexity when applied to the 99.99% of projects which are infinitely smaller? &lt;/p&gt;

&lt;p&gt;If you're not familiar with the difference between essential/intrinsic complexity and accidental complexity I suggest you this post:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/stereobooster" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--66IMIZ2X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--zvC6uajv--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/94664/dd5f88a0-3b21-4a84-8dac-95a1f8d1fbb5.png" alt="stereobooster"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/stereobooster/complexity-5d62" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Complexity&lt;/h2&gt;
      &lt;h3&gt;stereobooster ・ Dec 28 '18 ・ 9 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#programming&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;Is the push towards React's adoption only a positive or could it have effectively hurt some companies business over the years?&lt;/p&gt;

&lt;p&gt;Is using Rust for your average web application a good idea? Is using Rust for anything other than systems programming a good idea? Is using React for anything other than a complex frontend a good idea? &lt;/p&gt;

&lt;p&gt;We all know how the wrong tool chosen at the wrong time can affect a company's prospects. So, when people choose a tool before having written any lines of code, are they betting they will be able to manage the complexity it brings or are they really making an informed choice?&lt;/p&gt;

&lt;p&gt;Evidence tells us you can build successful products with or without either Rust or React, so that's a given but the problem, as always, isn't in the tool per se.&lt;/p&gt;

&lt;p&gt;I think Nat Allison here has a point:&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--PB_CaVcE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/1231468297649979392/t_7jReNz_normal.jpg" alt="Nat Alison profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Nat Alison
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        @tesseralis
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ir1kO05j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      I've talked with Dan about React's philosophy. React is built for *massive scale*. Facebook-level scale. Sure, it's perfectly fine for your blog or cute app, but it's a tool for megacorporations to influence the world at scale.
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      00:24 AM - 15 Aug 2020
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1294429688782176257" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fFnoeFxk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-reply-action-238fe0a37991706a6880ed13941c3efd6b371e4aefe288fe8e0db85250708bc4.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1294429688782176257" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k6dcrOn8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-retweet-action-632c83532a4e7de573c5c08dbb090ee18b348b13e2793175fea914827bc42046.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/like?tweet_id=1294429688782176257" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SRQc9lOp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-like-action-1ea89f4b87c7d37465b0eb78d51fcb7fe6c03a089805d7ea014ba71365be5171.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


&lt;p&gt;Are we software developers simply bad at making choices for a thousand different reasons?&lt;/p&gt;

</description>
      <category>healthydebate</category>
    </item>
    <item>
      <title>Happy birthday Django! 🎂</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Mon, 20 Jul 2020 07:48:57 +0000</pubDate>
      <link>https://forem.com/rhymes/happy-birthday-django-2876</link>
      <guid>https://forem.com/rhymes/happy-birthday-django-2876</guid>
      <description>&lt;p&gt;Django is 15 years old!&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--DVaYM1AO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/378800000261649705/be9cc55e64014e6d7663c50d7cb9fc75_normal.jpeg" alt="Simon Willison profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Simon Willison
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        &lt;a class="mentioned-user" href="https://dev.to/simonw"&gt;@simonw&lt;/a&gt;
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ir1kO05j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      15 years ago today on my blog: Introducing Django &lt;a href="https://t.co/fABuSKQjKC"&gt;simonwillison.net/2005/Jul/17/dj…&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      13:57 PM - 17 Jul 2020
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1284125169229787136" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fFnoeFxk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-reply-action-238fe0a37991706a6880ed13941c3efd6b371e4aefe288fe8e0db85250708bc4.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1284125169229787136" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k6dcrOn8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-retweet-action-632c83532a4e7de573c5c08dbb090ee18b348b13e2793175fea914827bc42046.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/like?tweet_id=1284125169229787136" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SRQc9lOp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-like-action-1ea89f4b87c7d37465b0eb78d51fcb7fe6c03a089805d7ea014ba71365be5171.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


&lt;p&gt;Do you have a Django story to share?&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>news</category>
    </item>
    <item>
      <title>How we decreased our memory usage with jemalloc</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Fri, 05 Jun 2020 08:21:45 +0000</pubDate>
      <link>https://forem.com/devteam/how-we-decreased-our-memory-usage-with-jemalloc-4d5n</link>
      <guid>https://forem.com/devteam/how-we-decreased-our-memory-usage-with-jemalloc-4d5n</guid>
      <description>&lt;p&gt;DEV is a Ruby on Rails application deployed on Heroku servers.&lt;/p&gt;

&lt;p&gt;The other day &lt;a href="https://dev.to/molly_struve/"&gt;Molly Struve&lt;/a&gt;, our resident SRE sorceress, noticed we were having some memory troubles. They didn't look like memory leaks, since they plateaued over time. But they were obviously worth investigating.&lt;/p&gt;

&lt;p&gt;After investigating a bit I remembered how in the past I happened to switch a Rails app from the standard memory allocator to &lt;a href="https://github.com/jemalloc/jemalloc" rel="noopener noreferrer"&gt;jemalloc&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The jemalloc library is an alternative memory allocator that can be used by apps (Redis ships with it) which works better with memory fragmentation in a multithreading environment.&lt;/p&gt;

&lt;p&gt;We researched that, activated it and this was the result:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fojmqori6h632620pnios.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%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fojmqori6h632620pnios.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The slight decrease you see before the red bar is due to deployments which for obvious reasons free some occupied memory.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I do the same in my Rails app(s)?
&lt;/h2&gt;

&lt;p&gt;The prerequisite is that you're using multiple threads with Rails, otherwise it won't help much.&lt;/p&gt;

&lt;p&gt;If you're using Heroku you can follow &lt;a href="https://devcenter.heroku.com/articles/ruby-memory-use#excess-memory-use-due-to-malloc-in-a-multi-threaded-environment" rel="noopener noreferrer"&gt;these instructions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If not, you can take inspiration from &lt;a href="https://www.41studio.com/blog/2019/how-to-install-ruby-on-rails-6-with-jemalloc-on-linux-ubuntu-19-04-2/" rel="noopener noreferrer"&gt;this tutorial&lt;/a&gt; and recompile your Ruby and activate &lt;code&gt;jemalloc&lt;/code&gt; support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this happen?
&lt;/h2&gt;

&lt;p&gt;Nate Berkopec has an extensive explanation in his post &lt;a href="https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html" rel="noopener noreferrer"&gt;Malloc Can Double Multi-threaded Ruby Program Memory Usage&lt;/a&gt; but long story short: the Ruby virtual machine and the default memory allocator (&lt;code&gt;malloc&lt;/code&gt;) aren't great at talking to each other when multithreading is involved because of the way they are both designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are there any downsides?
&lt;/h2&gt;

&lt;p&gt;Potentially yes, as it would make your app less portable (it doesn't work on Windows) and there are options with a smaller footprint that are being explored by Ruby core, like using &lt;a href="https://bugs.ruby-lang.org/issues/15667" rel="noopener noreferrer"&gt;malloc_trim()&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My suggestion is to test it :-)&lt;/p&gt;

</description>
      <category>rails</category>
      <category>performance</category>
      <category>changelog</category>
    </item>
    <item>
      <title>How to build unique indexes in PostgreSQL on large text</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Thu, 28 May 2020 12:15:24 +0000</pubDate>
      <link>https://forem.com/rhymes/how-to-build-unique-indexes-in-postgresql-on-large-text-3e6d</link>
      <guid>https://forem.com/rhymes/how-to-build-unique-indexes-in-postgresql-on-large-text-3e6d</guid>
      <description>&lt;p&gt;As I believe a relational database schema should be as independent as possible from the apps using it, &lt;a href="https://github.com/thepracticaldev/dev.to/pulls?q=is%3Apr+author%3Arhymes+%22unique+indexes%22"&gt;I've been trying to strengthen the DEV's database a bit lately&lt;/a&gt; (there are exceptions to this rule but they are not for this post).&lt;/p&gt;

&lt;p&gt;One way to do that is to make sure that Rails model statements like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;uniqueness: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;correspond to actual &lt;a href="https://www.postgresql.org/docs/11/indexes-unique.html"&gt;unique indexes&lt;/a&gt; in PostgreSQL.&lt;/p&gt;

&lt;p&gt;Two reasons for that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;let the DBMS do its job, it was built to check constraints&lt;/li&gt;
&lt;li&gt;data can "get in" from all sort of ways (throwaway SQL scripts for example)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if today your database is used only by a single app, you might have more than one in the future and adding indexes on existing tables or having to clean duplicate rows in large tables is always a bit of a pain (because of locking, I might write another article about that..).&lt;/p&gt;

&lt;h3&gt;
  
  
  What happened then?
&lt;/h3&gt;

&lt;p&gt;It seems straigtforward, right? List the column(s) you need the index for, write a Rails migration for them, run the migration, forget about it.&lt;/p&gt;

&lt;p&gt;That's where a random test literally saved me from an oversight.&lt;/p&gt;

&lt;p&gt;We have a test in our codebase that imports 20+ items from a RSS feed, transforms them into articles and inserts them in the DB, then checks the count to make sure it matches.&lt;/p&gt;

&lt;p&gt;They are all different articles, but the database is going to check they are unique anyway (for obvious reasons).&lt;/p&gt;

&lt;p&gt;The counts weren't matching and after some very serious debugging magic (aka setting a breakpoint and printing stuff) I came across this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&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="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="c1"&gt;#&amp;lt;RssReader&amp;gt;)&amp;gt; p e&lt;/span&gt;
&lt;span class="c1"&gt;#&amp;lt;ActiveRecord::StatementInvalid: PG::ProgramLimitExceeded: ERROR:  index row size 7280 exceeds btree version 4 maximum 2704 for index "index_articles_on_body_markdown_and_user_id_and_title"&lt;/span&gt;
&lt;span class="no"&gt;DETAIL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="no"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="n"&gt;references&lt;/span&gt; &lt;span class="n"&gt;tuple&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&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="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;relation&lt;/span&gt; &lt;span class="s2"&gt;"articles"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="no"&gt;HINT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="no"&gt;Values&lt;/span&gt; &lt;span class="n"&gt;larger&lt;/span&gt; &lt;span class="n"&gt;than&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;cannot&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;indexed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="no"&gt;Consider&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="no"&gt;MD5&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="n"&gt;indexing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;INSERT&lt;/span&gt; &lt;span class="no"&gt;INTO&lt;/span&gt; &lt;span class="s2"&gt;"articles"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"body_markdown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"boost_states"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cached_tag_list"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cached_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cached_user_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cached_user_username"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"feed_source_url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"processed_html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"published_from_feed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"reading_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"updated_at"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait, what!?&lt;/p&gt;

&lt;p&gt;After a bit of digging I realized my oversight: if the text to be indexed is too large and doesn't fit PostgreSQL buffer page, indexing is not going to work. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;PostgreSQL buffer page size can be enlarged but that's beside the point and also not a great idea.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  So, what's the solution?
&lt;/h3&gt;

&lt;p&gt;The solution is to create a hash of the column and index that instead of the column itself.&lt;/p&gt;

&lt;p&gt;There are many ways to go about this but this is what I chose for our particular situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="nv"&gt;"index_articles_on_digest_body_markdown_and_user_id_and_title"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="nv"&gt;"articles"&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"body_markdown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sha512'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CREATE UNIQUE INDEX&lt;/code&gt; is self explanatory: creates an index on a column, making sure you can't insert the same value twice&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CONCURRENTLY&lt;/code&gt; is a huge change in PostgreSQL land. In short: it adds the index asynchronously in the background. Basically it doesn't block operations on the table while the index is being built.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;btree&lt;/code&gt; is the &lt;a href="https://www.postgresql.org/docs/11/indexes-types.html"&gt;standard default index for PostgreSQL&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;digest("body_markdown", 'sha512'::text)&lt;/code&gt; is where the magic happens: we tell PostgreSQL to build a SHA512 hash (go away MD5 😅) and use that for comparison of the index&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"user_id", "title"&lt;/code&gt; are there because this is not an index on a single column, but a multi column index&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what happens when you try to add the value twice in the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pgcli PracticalDeveloper_development
PracticalDeveloper_development&amp;gt; insert into articles &lt;span class="o"&gt;(&lt;/span&gt;body_markdown, user_id, title, created_at, updated_at&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;select &lt;/span&gt;body_markdown, user_id, title, now&lt;span class="o"&gt;()&lt;/span&gt;, now&lt;span class="o"&gt;()&lt;/span&gt; from articles order by random&lt;span class="o"&gt;()&lt;/span&gt; limit 1&lt;span class="p"&gt;;&lt;/span&gt;
duplicate key value violates unique constraint &lt;span class="s2"&gt;"index_articles_on_digest_body_markdown_and_user_id_and_title"&lt;/span&gt;
DETAIL:  Key &lt;span class="o"&gt;(&lt;/span&gt;digest&lt;span class="o"&gt;(&lt;/span&gt;body_markdown, &lt;span class="s1"&gt;'sha512'&lt;/span&gt;::text&lt;span class="o"&gt;)&lt;/span&gt;, user_id, title&lt;span class="o"&gt;)=(&lt;/span&gt;&lt;span class="se"&gt;\x&lt;/span&gt;1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c652bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75, 10,  The Curious Incident of the Dog &lt;span class="k"&gt;in &lt;/span&gt;the Night-Time Voluptas quia&lt;span class="o"&gt;)&lt;/span&gt; already exists.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;bonus tip for &lt;a href="https://www.pgcli.com/"&gt;pgcli&lt;/a&gt; which I use instead of the regular psql&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The result of this investigation is &lt;a href="https://github.com/thepracticaldev/dev.to/pull/8072/commits/bcd6912a98329a8cba65e70cee9a3dddd7bedfe3"&gt;this commit&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>rails</category>
      <category>database</category>
    </item>
    <item>
      <title>A couple of thoughts from the Python survey results - 2019</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Wed, 20 May 2020 11:35:09 +0000</pubDate>
      <link>https://forem.com/rhymes/a-couple-of-thoughts-from-the-python-survey-results-2019-2hj4</link>
      <guid>https://forem.com/rhymes/a-couple-of-thoughts-from-the-python-survey-results-2019-2hj4</guid>
      <description>&lt;p&gt;&lt;em&gt;the screenshots and data come from &lt;a href="https://www.jetbrains.com/lp/python-developers-survey-2019"&gt;Python Developers Survey 2019 Results&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python's usage in data analysis is more or less the same as last year but its usage for web development is decreasing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A4ohWqaC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/i21rlt43xu3x6oufy37s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A4ohWqaC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/i21rlt43xu3x6oufy37s.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9EtdqIKa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/fn2ygnwo0eahkov9xrg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9EtdqIKa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/fn2ygnwo0eahkov9xrg8.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flask usage share is strong! I thought Django was much more used. Interesting how their communities have roughly the same size (though Django is much more popular for prototypes I guess). This would be unthinkable in Ruby land. I believe Rails has much higher usage share by Ruby web developers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ch3ESnmv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/m0dmu8oavfzdzuyisq37.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ch3ESnmv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/m0dmu8oavfzdzuyisq37.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A special mention to that 26% (!!) of people who do web development in Python with no frameworks.&lt;/p&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>Redis 6 is out</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Fri, 01 May 2020 10:47:19 +0000</pubDate>
      <link>https://forem.com/rhymes/redis-6-is-out-5db2</link>
      <guid>https://forem.com/rhymes/redis-6-is-out-5db2</guid>
      <description>&lt;p&gt;Huge fan of Redis here :-)&lt;/p&gt;

&lt;p&gt;Its latest version adds the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://redis.io/commands/acl-setuser"&gt;Access control lists (ACL)&lt;/a&gt; so that clients/users can be barred from performing dangerous operations&lt;/li&gt;
&lt;li&gt;Multithreaded I/O (Redis until now has been largely single threaded with async) which is bound to make Redis even faster&lt;/li&gt;
&lt;li&gt;Client side caching: this is really cool in my opinion, reminds me of the evergreen article about &lt;a href="https://nickcraver.com/blog/2019/08/06/stack-overflow-how-we-do-app-caching/#layers-of-cache-at-stack-overflow"&gt;how StackOverflow does layered caching&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redis.io/commands/stralgo"&gt;Longest common substring (LCS)&lt;/a&gt; command which works both on text and binary data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href="https://redislabs.com/blog/diving-into-redis-6/"&gt;Redis Labs: Diving Into Redis 6.0&lt;/a&gt; and the &lt;a href="https://raw.githubusercontent.com/antirez/redis/6.0/00-RELEASENOTES"&gt;official release notes&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>news</category>
    </item>
    <item>
      <title>My laptop is 8 years old</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Tue, 28 Apr 2020 20:00:21 +0000</pubDate>
      <link>https://forem.com/rhymes/my-laptop-is-8-years-old-5h98</link>
      <guid>https://forem.com/rhymes/my-laptop-is-8-years-old-5h98</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kjpkDB0b--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/q6hlc0wa3mu9mhc7nt8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kjpkDB0b--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/q6hlc0wa3mu9mhc7nt8v.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I bought this laptop in Paris in 2012: it has the French keyboard layout (&lt;code&gt;AZERTY&lt;/code&gt; instead of &lt;code&gt;QWERTY&lt;/code&gt;) even though I use it with the Italian layout by memory (👀).&lt;/p&gt;

&lt;p&gt;It's probably one of the last models Apple made that you could open up and upgrade yourself. I did, a long time ago, with 16 GB of RAM and a 512GB SSD.&lt;/p&gt;

&lt;p&gt;It's starting to worry me: the fan makes weird noises sometimes.&lt;/p&gt;

&lt;p&gt;I can say that &lt;a href="https://github.com/thepracticaldev/dev.to"&gt;daily programming with Rails&lt;/a&gt; activates the fan quite often (damn you Rails system tests 😂), it didn't when &lt;a href="https://dev.to/rhymes/what-ive-learned-in-7-days-of-go-5e3"&gt;a couple of years ago I was writing a web app with Go&lt;/a&gt; (not a fair comparison, I know).&lt;/p&gt;

&lt;p&gt;Having held on to a "slow" computer (well, it wasn't the first few years 😝) also had the side effect of me becoming really interested in performance optimization in programming 🔥&lt;/p&gt;

&lt;p&gt;It's also the only computer I own.&lt;/p&gt;

&lt;p&gt;Eight years with a portable computer is pretty amazing in my opinion. It was great hardware or I got super lucky or both!&lt;/p&gt;

&lt;p&gt;Here's to another 8 years. Just kidding 🤣&lt;/p&gt;

</description>
      <category>watercooler</category>
      <category>hardware</category>
    </item>
    <item>
      <title>A semi technical explainer of all known Zoom issues</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Sun, 05 Apr 2020 15:17:58 +0000</pubDate>
      <link>https://forem.com/rhymes/a-semi-technical-explainer-of-all-known-zoom-issues-1bab</link>
      <guid>https://forem.com/rhymes/a-semi-technical-explainer-of-all-known-zoom-issues-1bab</guid>
      <description>&lt;p&gt;(or &lt;em&gt;Zoom choices are what got them in trouble&lt;/em&gt;)&lt;/p&gt;

&lt;p&gt;(opinions are mine and &lt;em&gt;not&lt;/em&gt; DEV's)&lt;/p&gt;

&lt;h2&gt;
  
  
  A (mild) defense of Zoom Inc.'s troubles
&lt;/h2&gt;

&lt;p&gt;Let me start by quoting &lt;a href="https://citizenlab.ca/2020/04/move-fast-roll-your-own-crypto-a-quick-look-at-the-confidentiality-of-zoom-meetings/"&gt;Citizenlab's report&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For those using Zoom to keep in touch with friends, hold social events, or organize courses or lectures that they might otherwise hold in a public or semi-public venue, our findings should not necessarily be concerning.&lt;/p&gt;

&lt;p&gt;For those who have no choice but to use Zoom, including in contexts where secrets may be shared, we speculate that the browser plugin may have some marginally better security properties, as data transmission occurs over TLS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unfortunately a few hours later, on the same day, &lt;a href="https://devforum.zoom.us/t/zoom-web-client-is-down/10829/8"&gt;the web client was put under maintenance&lt;/a&gt; and thus disabled for the time being, hopefully not for long. &lt;strong&gt;update:&lt;/strong&gt; &lt;em&gt;the web client &lt;a href="https://devforum.zoom.us/t/zoom-web-client-is-down/10829/11"&gt;has been restored&lt;/a&gt;, multiple sources confirmed. Please enable &lt;a href="https://support.zoom.us/hc/en-us/articles/115005666383"&gt;join from your browser&lt;/a&gt; as a default setting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Zoom probably didn't anticipate going from 0 to 100 (actually + 1126% of increased usage according to some estimates) in the span of a few weeks and the fact that their network hasn't melted down and rendered all calls impossible to make is a testament to the quality of the underlying technology.&lt;/p&gt;

&lt;p&gt;As we all can imagine there are challenges in scaling that big that fast, but most of the problems that have been identified up until now don't really have much to do with scalability or reliability per se, but with questionable software design choices and bad privacy or marketing decisions made by the company.&lt;/p&gt;

&lt;p&gt;To be fair, sometimes shortcuts seem a great idea when you're in the heat of the moment and have a booming product, but the more people use it, the higher the likelihood that these shortcuts will come back to haunt it. Also, Zoom devs are humans and like all humans, sometimes they just make bad decisions without malice. What worries me is how the company management, also entirely human AFAIK 😃, decided to handle the response, more on that later.&lt;/p&gt;

&lt;p&gt;Last but not least: the media piled up on them quite extensively and some security flaws (except maybe "zoombombing") aren't an inherent problem for those meetings that would otherwise be held in public if we weren't quarantined even though the extent of the problem with user generated content is that its severity differs case by case and here there would be millions of cases (each Zoom call) to analyze.&lt;/p&gt;

&lt;p&gt;Zoom is used by everyone: individuals, institutions, therapists, teachers, doctors, religious and secular organizations, goverment officials and even heads of state.&lt;/p&gt;

&lt;p&gt;Zoom, as of April 1st 2020, halted all feature development and vowed to fix privacy and security issues over the following 90 days.&lt;/p&gt;

&lt;p&gt;What went wrong is also a cautionary tale about the importance of implementing secure practices and caring about users privacy from day one because at the scale they are now it's quite understandable they are trying to put out fires left and right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is or was wrong it with
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;TLDR;&lt;/code&gt; Zoom has numerous known security holes. Some of those have since been fixed, some haven't.&lt;/p&gt;

&lt;p&gt;The company also made (and in some cases, still makes) questionable decisions related to privacy.&lt;/p&gt;

&lt;p&gt;(&lt;em&gt;I'm going to use the past tense where I reasonably verified the issue has been fixed&lt;/em&gt;)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoom has a security issue in its "waiting room" feature&lt;/strong&gt;. The issue is currently unknown as the security researchers correctly disclosed it only to Zoom Inc. granting them time to get it fixed lest it gets in the hands of malicious actors. Security researchers are advising people to use passwords and not the waiting room feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recordings are easily findable on the web&lt;/strong&gt;: Zoom saves recordings with a guessable name pattern, thus it's quite trivial to find them if they are uploaded to the open web. Search engines are literally built to find public data on the web. Again, "security through obscurity" is not a good practice if the content is sensible, and it was: the Washington Post was able to watch other people's therapy sessions and elementary school online classes by scanning the web (!!!!!).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The ID of a meeting room is numeric&lt;/strong&gt;, which means that people can guess it (manually or with scripts) and thus, being openness the default, people can hijack meetings, thus "zoombombing". The meeting ID has 9 to 11 digits, not even "obscured". Security researchers did actually find meetings and with scripts had up to 14% of a success rate guessing correct meetings URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recurring Zoom meetings links can be found&lt;/strong&gt;: they also contain info of the meeting organizer and whatever info the organizers disclosed as topic or description. Security researchers found meetings of large banks, government contractors and other companies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Screensharing by any participant is on by default&lt;/strong&gt;, which means that people can stream whatever they want without oversight. Very handy for private and regulated meetings, not great if meetings rooms are open by default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File transfer is on by default&lt;/strong&gt;: don't think this needs explaining in an app where a meeting is public. You can literally send to dozens or hundreds of people malware hoping at least one of them will click on it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The app has too many settings&lt;/strong&gt;: I went through the configuration panel on both the app and the web version (before it was disabled) and I didn't understand half of the options and got bored after a few minutes (minutes!!!). As we all know as creators of sofware the default matters (most users don't even look at apps settings), and by default you should respect the user's privacy and be secure, otherwise hell can break loose when enough people come knocking with the receipts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The company lied about being end to end encrypted&lt;/strong&gt;, that's it. They said they use end to end encryption (e2e) but they don't. They also own decryption keys for what is encrypted on the wire, which is definitely &lt;strong&gt;not *end to end encryption&lt;/strong&gt;&lt;em&gt;. *FYI&lt;/em&gt;: true end to end encryption means that only the participants in a communication exchange can actually see the data in the clear. Not the company providing the service, not any goverment, not anyone except who's invited.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoom uses weakish encryption&lt;/strong&gt;: even though the service is not e2e encrypted, calls don't travel in the clear on the transport network. They are encrypted using a central encrypting server which holds the keys. The issue is that they use a single AES 128 bit key in ECB mode which is definitely deprecated and has security holes. Security researchers were able to decrypt video and audio frames from "encrypted" calls.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoom encryption and security protocols are not independently audited&lt;/strong&gt;, which means that they most certainly contain flaws. We all know how hard is to pull off encryption done correctly, I can't imagine how hard it is to do it with video, audio, text and generic media. "Roll your own encryption" is 99.99% of the time a bad idea, it's monumentally bad in this instance. As &lt;a href="https://www.schneier.com/blog/archives/2020/04/security_and_pr_1.html"&gt;Bruce Scheiner wrote&lt;/a&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm okay with AES-128, but using ECB (electronic codebook) mode indicates that there is no one at the company who knows anything about cryptography.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoom encryption keys are occasionally on servers under the jurisdiction of the Chinese goverment&lt;/strong&gt;: under Chinese law the goverment of China can require companies to disclose their encryption keys and tools for oversight. This has happened also if all participants were outside the country. Likely a data routing problem (Zoom tries to keep calls local to the participants) but not reassuring nonetheless. Also makes me thing of sci fi scenario in which governments or attackers decrypt all of these calls and use facial recognition to create mass surveillance tools. 👀&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The company sent data to Facebook unbeknownst to users&lt;/strong&gt;: this is probably quite common in apps that embed the Facebook SDK without tweaking it (and it only applied to the iOS app which might mean it was truly unintentional), but it's not great anyway. Facebook already knows a lot about users (both those using it and those who don't). It has been fixed since discovery.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoom's privacy policy was all encompassing&lt;/strong&gt;. Basically it stated that all personal data (including recordings, chats and uploaded files) could be shared with third parties. It has been since amended.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoom allowed hosts to monitor participant's attention&lt;/strong&gt;. This is 1984-the-book kind of stuff 😱. It was removed since discovery, on April 1st.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The app bypassed the usual installation process&lt;/strong&gt;. This was probably done to be friendler to the user in the very common scenario in which a user gets a link, doesn't have the app on their computer and wants to be in the videocall as fast as possible. It's exactly what malware does. It has been fixed since the discovery.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoom shares your contact info to everyone within certain email domains&lt;/strong&gt;. What happened was that thousands of users whose emails belonged to a Dutch email provider where pooled together and their personal info shared with each other.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The app let Windows users click on anything resembling a link&lt;/strong&gt;. Basically you could automatically open a file on a shared drive sending your network credentials to an attacker. It has been fixed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoom sent your contact data to Linkedin&lt;/strong&gt;. In some situations, if they could match you to a Linkedin Profile somehow, they did, without telling you and sharing your Linkedin data to other people. This feature has since been removed on April 1st.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;0 day vulnerabilities leading to hardware take over were discovered&lt;/strong&gt;. The likelihood of those happening was very low (the vulnerability window span the length of the installation process and just that). Fixed as well on April 1st.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Records of private messages with the host are available in the export&lt;/strong&gt;. This is not a huge problem in theory as the export doesn't contain private messages between other participants than those with the host.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zoom tracks lots of data about its users and has many third party trackers on its website&lt;/strong&gt;. This to me feels like a very intentional choice. Part of those were amended when the new privacy policy was published the other day.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What has happened in the "aftermath"
&lt;/h2&gt;

&lt;p&gt;So far:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Privacy and advocacy groups started to notice&lt;/li&gt;
&lt;li&gt;Class actions are starting to mount&lt;/li&gt;
&lt;li&gt;The US government and the FBI have started to notice 👀&lt;/li&gt;
&lt;li&gt;NASA, SpaceX, Disney and other companies moved away from Zoom&lt;/li&gt;
&lt;li&gt;New York City has banned Zoom from its schools&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;I'm sure you've noticed how this article doesn't talk at all about alternatives. The question of alternatives really depends on what the users requirements are and if the entire globe is your users, then it's hard to evaluate on the spot. It also takes a lot of time to evaluate all options and I think it'll take a few days before deeply researched articles about pros and cons of the alternatives start to appear. You also need a group of people distributed all over the world for thorough testing of each app.&lt;/p&gt;

&lt;p&gt;Let's also not forget that we mostly felt okay with Zoom until tens of millions of people started using it overnight and it got on experts's radars. Alternatives might be just as flawed, simply less popular right now.&lt;/p&gt;

&lt;p&gt;Trust in the company is important so I do understand why regular people are rushing to find alternatives.&lt;/p&gt;

&lt;p&gt;I'm also not a tech columnist nor a security expert, so I can't claim "X is better than Zoom for everything and for everyone".&lt;/p&gt;

&lt;p&gt;I'll see if I can find a reasonably well done comparison of alternatives in the next few days with what I think should be generic requirements: great video and audio call performance, secure by default with indipendently reviewed encryption and protocols, and absolutely no adtech on all of this sensible data (which wouldn't be possible anyway if they had e2e, though technically you can still sell metadata about users...).&lt;/p&gt;

&lt;p&gt;Echoing other people's sentiments I read online: Apple is sitting on a gold mine if they open Facetime, get it audited and make sure their e2e encryption has no "backdoors".&lt;/p&gt;

&lt;p&gt;I'm leaving last a long list of links, with some excerpts, which is what I've read to write this summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Media (and other sources) coverage (in chronological order)
&lt;/h2&gt;

&lt;p&gt;Although there are past issues (like the "open web server" debacle from 2019), I've focused only on recent media coverage from March-April 2020.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;20200317&lt;/code&gt; - (Techcrunch) - &lt;a href="https://techcrunch.com/2020/03/17/zoombombing/"&gt;Beware of ‘ZoomBombing’: screensharing filth to video calls&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;20200326&lt;/code&gt; - (Motherboard, Vice) - &lt;a href="https://www.vice.com/en_us/article/k7e599/zoom-ios-app-sends-data-to-facebook-even-if-you-dont-have-a-facebook-account"&gt;Zoom iOS App Sends Data to Facebook Even if You Don’t Have a Facebook Account&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;20200330&lt;/code&gt; - (Doc Searls, digital privacy expert) - &lt;a href="https://blogs.harvard.edu/doc/2020/03/30/zooms-new-privacy-policy/"&gt;Zoom’s new privacy policy&lt;/a&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;There will be no need for Zoom to disambiguate services and websites if neither is involved with adtech at all. And Zoom will be in a much better position to trumpet their commitment to privacy.&lt;/p&gt;

&lt;p&gt;That said, this privacy policy rewrite is a big help. So thank you, Zoom, for listening.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;20200331&lt;/code&gt; - (Motherboard, Vice) - &lt;a href="https://www.vice.com/en_us/article/pke4vb/zoom-faces-class-action-lawsuit-for-sharing-data-with-facebook"&gt;Zoom Faces Class Action Lawsuit for Sharing Data with Facebook&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;20200401&lt;/code&gt; - (Motherboard, Vice) - &lt;a href="https://www.vice.com/en_us/article/k7e95m/zoom-leaking-email-addresses-photos"&gt;Zoom is Leaking Peoples' Email Addresses and Photos to Strangers&lt;/a&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;"I was shocked by this! I subscribed (with an alias, fortunately) and I saw 995 people unknown to me with their names, images and mail addresses."&lt;/p&gt;

&lt;p&gt;"I just had a look at the free for private use version of Zoom and registered with my private email. I now got 1000 names, email addresses and even pictures of people in the company Directory. Is this intentional?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200401&lt;/code&gt; - (webrtcH4cKS, WebRTC technologists) - &lt;a href="https://webrtchacks.com/you-dont-have-end-to-end-encryption-e2ee/"&gt;Does your video call have End-to-End Encryption? Probably not...&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;So yes, Zoom does not have end-to-end encryption. Quite often, WebRTC doesn’t either – not yet at least. If you are using a WebRTC service check their terms of service and privacy policy and make sure that you understand what they are saying about this. Hopefully we will see this change soon as WebRTC Insertable Streams matures.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200402&lt;/code&gt; - (Fight for the future, digital rights group) - &lt;a href="https://tumblr.fightforthefuture.org/post/614318663706820608/new-campaign-calls-for-zoom-to-actually"&gt;New campaign calls for Zoom to (actually) implement end to end encryption to keep people safe&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Digital rights group Fight for the Future, known for organizing massive online protests for net neutrality and Internet privacy, has launched a new campaign calling for video conferencing service Zoom to implement default end-to-end encryption on all video, audio, and chat content.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200402&lt;/code&gt; - (Steven Bellovin, security researcher and professor) - &lt;a href="https://www.cs.columbia.edu/~smb/blog/2020-04/2020-04-02.html"&gt;Zoom Security: The Good, the Bad, and the Business Model&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;There is, though, a class of problems that worries me: security shortcuts in the name of convenience or usability. Consider the first widely known flaw in Zoom: a design decision that allowed “any website to forcibly join a user to a Zoom call, with their video camera activated, without the user's permission.” Why did it work that way? It was intended as a feature&lt;/p&gt;

&lt;p&gt;I'm optimistic that things are heading in the right direction. Still, it's the shortcuts that worry me the most. Those aren't just problems that they can fix, they make me fear for the attitudes of the development team towards security. I'm not convinced that they get it—and that's bad. Fixing that is going to require a CISO office with real power, as well as enough education to make sure that the CISO doesn't have to exercise that power very often. They also need a privacy officer, again with real power; many of their older design decisions seriously impact privacy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200402&lt;/code&gt; - (Krebs on Security, security expert) - &lt;a href="https://krebsonsecurity.com/2020/04/war-dialing-tool-exposes-zooms-password-problems/"&gt;‘War Dialing’ Tool Exposes Zoom’s Password Problems&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;according to data gathered by a new automated Zoom meeting discovery tool dubbed “zWarDial,” a crazy number of meetings at major corporations are not being protected by a password.&lt;/p&gt;

&lt;p&gt;Lo said zWarDial evades Zoom’s attempts to block automated meeting scans by routing the searches through multiple proxies in Tor, a free and open-source software that lets users browse the Web anonymously.&lt;/p&gt;

&lt;p&gt;“Having a password enabled on the meeting is the only thing that defeats it,” he said.&lt;/p&gt;

&lt;p&gt;Lo shared the output of one day’s worth of zWarDial scanning, which revealed information about nearly 2,400 upcoming or recurring Zoom meetings. That information included the link needed to join each meeting; the date and time of the meeting; the name of the meeting organizer; and any information supplied by the meeting organizer about the topic of the meeting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200402&lt;/code&gt; - (Reuters) - &lt;a href="https://www.reuters.com/article/us-spacex-zoom-video-commn/elon-musks-spacex-bans-zoom-over-privacy-concerns-memo-idUSKBN21J71H"&gt;Elon Musk's SpaceX bans Zoom over privacy concerns -memo&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;In an email dated March 28, SpaceX told employees that all access to Zoom had been disabled with immediate effect.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200403&lt;/code&gt; - (NYTimes) - &lt;a href="https://www.nytimes.com/2020/04/03/technology/zoom-harassment-abuse-racism-fbi-warning.html"&gt;‘Zoombombing’ Becomes a Dangerous Organized Effort&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Zoom raiders often employ shocking imagery, racial epithets and profanity to derail video conferences.&lt;/p&gt;

&lt;p&gt;Harassers have begun to leverage every feature of Zoom’s platform for abuse. They have used the app’s custom background feature to project a GIF of a person drinking to participants in an Alcoholics Anonymous meeting, and its annotation feature to write racist messages in a meeting of the American Jewish Committee in Paris.&lt;/p&gt;

&lt;p&gt;The frequency and reach of the incidents on Zoom prompted the F.B.I. to issue a warning on Tuesday, singling out the app&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200403&lt;/code&gt; - (TidBITS) - &lt;a href="https://tidbits.com/2020/04/03/every-zoom-security-and-privacy-flaw-so-far-and-what-you-can-do-to-protect-yourself/"&gt;Every Zoom Security and Privacy Flaw So Far, and What You Can Do to Protect Yourself&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;As detailed as this article is, I fear that this list of problems and choices will be far from the last we hear about Zoom’s security and privacy troubles. In fact, while writing and editing this article over the last 48 hours, we had to add six additional exploits, design-choice errors, and privacy concerns.&lt;/p&gt;

&lt;p&gt;Zoom has gone into what’s known as “technical debt.” The company’s developers made a lot of poor decisions in the past, which are likely difficult and costly to fix. The longer it takes Zoom to address the core problems, the harder and more costly future fixes will be, as additional code is built upon that weak foundation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200403&lt;/code&gt; - (Washington Post) - &lt;a href="https://www.washingtonpost.com/technology/2020/04/03/thousands-zoom-video-calls-left-exposed-open-web/"&gt;Thousands of Zoom video calls left exposed on open Web&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Videos viewed by The Washington Post included one-on-one therapy sessions; a training orientation for workers doing telehealth calls that included people’s names and phone numbers; small-business meetings that included private company financial statements; and elementary school classes, in which children’s faces, voices and personal details were exposed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200403&lt;/code&gt; - (Citizenlab) - &lt;a href="https://citizenlab.ca/2020/04/move-fast-roll-your-own-crypto-a-quick-look-at-the-confidentiality-of-zoom-meetings/"&gt;Move Fast &amp;amp; Roll Your Own Crypto: A Quick Look at the Confidentiality of Zoom Meetings&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Until a few weeks ago, it would have been uncommon for high stakes business negotiations, high level diplomacy, political strategy conferences, and cabinet meetings to be conducted over platforms whose security properties are unknown.&lt;/p&gt;

&lt;p&gt;Zoom has not publicly disclosed information such as statistics of requests for data by governments, and what Zoom has done in response to these requests. Zoom’s policies concerning notifications to users over breaches or the handing-over of data to governments are also unknown&lt;/p&gt;

&lt;p&gt;During our analysis, we also identified a security issue with Zoom’s Waiting Room feature. Assessing that the issue presented a risk to users, we have initiated a responsible vulnerability disclosure process with Zoom. We are not currently providing public information about the issue to prevent it from being abused.&lt;/p&gt;

&lt;p&gt;As a result of these troubling security issues, we discourage the use of Zoom at this time for use cases that require strong privacy and confidentiality&lt;/p&gt;

&lt;p&gt;For those who have no choice but to use Zoom, including in contexts where secrets may be shared, we speculate that the browser plugin may have some marginally better security properties, as data transmission occurs over TLS.&lt;/p&gt;

&lt;p&gt;In the meantime, we advise Zoom users who desire confidentiality to not use Zoom Waiting Rooms. Instead, we encourage users to use Zoom’s password feature, which appears to offer a higher level of confidentiality than waiting rooms&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;20200403&lt;/code&gt; - (Politico) - &lt;a href="https://www.politico.com/news/2020/04/03/multiple-state-ags-looking-into-zooms-privacy-practices-162743"&gt;Multiple state AGs looking into Zoom’s privacy practices&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;20200403&lt;/code&gt; - (Bruce Scheiner, legendary security researcher) - &lt;a href="https://www.schneier.com/blog/archives/2020/04/security_and_pr_1.html"&gt;Security and Privacy Implications of Zoom&lt;/a&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Privacy first: Zoom spies on its users for personal profit. It seems to have cleaned this up somewhat since everyone started paying attention, but it still does it.&lt;/p&gt;

&lt;p&gt;I'm sure lots more of these bad security decisions, sloppy coding mistakes, and random software vulnerabilities are coming.&lt;/p&gt;

&lt;p&gt;But it gets worse. Zoom's encryption is awful. First, the company claims that it offers end-to-end encryption, but it doesn't. It only provides link encryption, which means everything is unencrypted on the company's servers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200404&lt;/code&gt; - (Steven Bellovin, security researcher and professor) - &lt;a href="https://www.cs.columbia.edu/~smb/blog/2020-04/2020-04-04.html"&gt;Zoom Cryptography and Authentication Problems&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;When companies roll their own crypto, I expect it to have flaws. I don't expect those flaws to be errors I'd find unacceptable in an introductory undergraduate class, but that's what happened here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200404&lt;/code&gt; - (Chalkbeat, organization related to American schools) - &lt;a href="https://chalkbeat.org/posts/ny/2020/04/04/nyc-forbids-schools-from-using-zoom-for-remote-learning-after-privacy-concerns-emerge/"&gt;NYC forbids schools from using Zoom for remote learning due to privacy and security concerns&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Instead, the guidance says, schools should switch to Microsoft Teams “as soon as possible,” which the education department suggests has similar functionality and is more secure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200404&lt;/code&gt; - (Techcrunch) - &lt;a href="https://techcrunch.com/2020/04/03/zoom-calls-routed-china/"&gt;Zoom admits some calls were routed through China by mistake&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Zoom said in February that “rapidly added capacity” to its Chinese regions to handle demand was also put on an international whitelist of backup data centers, which meant non-Chinese users were in some cases connected to Chinese servers when data centers in other regions were unavailable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200410&lt;/code&gt; - (The Verge) - &lt;a href="https://www.theverge.com/2020/4/8/21213978/google-zoom-ban-security-risks-hangouts-meet"&gt;Google bans its employees from using Zoom over security concerns&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;“Recently, our security team informed employees using Zoom Desktop Client that it will no longer run on corporate computers as it does not meet our security standards for apps used by our employees. Employees who have been using Zoom to stay in touch with family and friends can continue to do so through a web browser or via mobile.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;20200412&lt;/code&gt; - (NY Times) - &lt;a href="https://www.nytimes.com/2020/04/12/business/media/disney-ceo-coronavirus.html"&gt;Bob Iger Thought He Was Leaving on Top. Now, He’s Fighting for Disney’s Life.&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;After a few weeks of letting Mr. Chapek take charge, Mr. Iger smoothly reasserted control, BlueJeans video call by BlueJeans video call. (Disney does not use Zoom for its meetings for security reasons.)&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>healthydebate</category>
      <category>zoom</category>
      <category>security</category>
    </item>
    <item>
      <title>Changelog: updated API docs!</title>
      <dc:creator>rhymes</dc:creator>
      <pubDate>Fri, 13 Mar 2020 17:57:32 +0000</pubDate>
      <link>https://forem.com/devteam/changelog-updated-api-docs-np</link>
      <guid>https://forem.com/devteam/changelog-updated-api-docs-np</guid>
      <description>&lt;p&gt;We recently updated the &lt;a href="https://docs.dev.to/api/"&gt;documentation&lt;/a&gt; of the &lt;a href="https://docs.dev.to/api/"&gt;DEV API (beta)&lt;/a&gt; to add detailed information on the following resources: comments, followers, listings, podcast episodes, tags, users and video articles.&lt;/p&gt;

&lt;p&gt;The API documentation portal is generated from an &lt;a href="https://spec.openapis.org/oas/v3.0.3"&gt;OpenAPI 3 specification&lt;/a&gt; file, built with &lt;a href="https://github.com/Redocly/redoc"&gt;Redoc&lt;/a&gt; and deployed on &lt;a href="https://www.netlify.com/"&gt;Netlify&lt;/a&gt;. ❤️&lt;/p&gt;

</description>
      <category>changelog</category>
    </item>
  </channel>
</rss>
