<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Magnus Skog</title>
    <description>The latest articles on DEV Community by Magnus Skog (@mskog).</description>
    <link>https://dev.to/mskog</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%2F100362%2F369227dd-1dc2-441f-8d76-cb31bbef67cc.jpeg</url>
      <title>DEV Community: Magnus Skog</title>
      <link>https://dev.to/mskog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mskog"/>
    <language>en</language>
    <item>
      <title>Trello to Notion for free</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Tue, 01 Jun 2021 18:03:40 +0000</pubDate>
      <link>https://dev.to/mskog/trello-to-notion-for-free-2549</link>
      <guid>https://dev.to/mskog/trello-to-notion-for-free-2549</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rzAXHK0a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/06/photo-1601933470096-0e34634ffcde.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rzAXHK0a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/06/photo-1601933470096-0e34634ffcde.jpeg" alt="Trello to Notion for free"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.notion.so/"&gt;Notion&lt;/a&gt; recently make &lt;a href="https://developers.notion.com/"&gt;their API&lt;/a&gt; public. This has been a long awaited thing that opens up a lot of possibilities for Notion users like myself. My current setup is using &lt;a href="https://www.trello.com"&gt;Trello&lt;/a&gt; as an inbox of sorts and then I move the cards manually to Notion as part of my daily routine. This is of course boring and can now be automated using the new Notion API.&lt;/p&gt;

&lt;p&gt;My first version of this used &lt;a href="https://zapier.com/"&gt;Zapier&lt;/a&gt;. Zapier is a no-code solution for things like this. It can connect pretty much anything to everything. You can get a notification to Slack when someone subscribes to your Twitter account, automatically share posts in an RSS feed to your LinkedIn profile, and so on.&lt;/p&gt;

&lt;p&gt;They have a great interface for this and it makes things very easy to use. The free version comes with 100 tasks a month. A task a single thing that Zapier does. Moving a Trello card to Notion is a single task for example. However, I also want Zapier to delete the card when it has been moved. This means that I now need two tasks to do this and the free version will not be enough.&lt;/p&gt;

&lt;p&gt;The cheapest plan costs $19.99 a month billed annually and includes 750 tasks a month. This would be enough but since I don't use Zapier for other things then this is quite a bit of money just for this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://automate.io/"&gt;Automate.io&lt;/a&gt; is another option and includes 600 monthly actions for $9.99 a month.&lt;/p&gt;

&lt;p&gt;I eventually decided to just use &lt;a href="https://www.mskog.com/posts/self-hosted-tools-for-web-development"&gt;Huginn&lt;/a&gt; instead. It turned out that the Notion API is pretty easy to use with Huginn but there is a lot of nested data in it. It took quite some time to get it working so I thought that I'd share my Huginn scenario here so that you can get started quicker.&lt;/p&gt;

&lt;p&gt;Click &lt;a href="https://gist.github.com/mskog/e676a711a31d200a2ebed9bfd3612907"&gt;here&lt;/a&gt; to get it. It is shared though Github gists.&lt;/p&gt;

&lt;p&gt;You will need to fill in the following:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTION_DATABASE_ID&lt;/strong&gt;. The ID of the database to create tasks in. Simply browse to the database in Notion to find it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTION_API_SECRET.&lt;/strong&gt; &lt;a href="https://developers.notion.com/docs"&gt;Sign up for the Notion API&lt;/a&gt; to find it. Do note that you have "share" the database with the API to make it work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TRELLO_LIST_ID&lt;/strong&gt;. The list to pull cards from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TRELLO_API_KEY&lt;/strong&gt; and &lt;strong&gt;TRELLO_API_TOKEN&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My scenario is pulling cards from two different Trello boards to two different Notion databases: one for personal use and one for work. Simply delete the Agents you don't want.&lt;/p&gt;

&lt;p&gt;Enjoy your free API integration!&lt;/p&gt;

</description>
      <category>productivity</category>
    </item>
    <item>
      <title>Automated morning music with Pipedream, Huginn, and Mailbrew</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Mon, 29 Mar 2021 08:32:00 +0000</pubDate>
      <link>https://dev.to/mskog/automated-morning-music-with-pipedream-huginn-and-mailbrew-36ph</link>
      <guid>https://dev.to/mskog/automated-morning-music-with-pipedream-huginn-and-mailbrew-36ph</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--31Fltqdw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/photo-1454916286212-0ea211dc68d6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--31Fltqdw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/photo-1454916286212-0ea211dc68d6.jpeg" alt="Automated morning music with Pipedream, Huginn, and Mailbrew"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Get a great music album delivered to the inbox every morning, with a &lt;a href="https://www.spotify.com/"&gt;Spotify&lt;/a&gt; link so that you don't have to find the album yourself. Sounds good? Let's make it happen.&lt;/p&gt;

&lt;p&gt;We will break this down into these parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get music albums&lt;/li&gt;
&lt;li&gt;Finding out if said music album is any good&lt;/li&gt;
&lt;li&gt;Get the Spotify link to the album&lt;/li&gt;
&lt;li&gt;Deliver this link every morning. By email or chat.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of my finds after using this for a while&lt;/p&gt;

&lt;p&gt;We can solve the first problem in many ways. We can use the &lt;a href="https://www.reddit.com/r/Music/"&gt;/r/music subreddit&lt;/a&gt; on Reddit or even Pitchfork Medias &lt;a href="https://pitchfork.com/best/"&gt;"best new music"&lt;/a&gt;. What I settled on is &lt;a href="https://www.metacritic.com/music"&gt;Metacritic&lt;/a&gt;. They have a simple feed of albums that is easy to develop for. There is also little risk ending up with Led Zeppelin IV in your inbox like you would on Reddit (although that would be good too).&lt;/p&gt;

&lt;p&gt;To do this we are going to use &lt;a href="https://pipedream.com/workflows"&gt;Pipedream&lt;/a&gt;. It is a more developer oriented &lt;a href="https://zapier.com/"&gt;Zapier&lt;/a&gt; alternative for automating workflows. One of the benefits compared to Zapier is that you can run any Node.js code you like as a step. I've written about &lt;a href="https://www.mskog.com/posts/self-hosted-tools-for-web-development"&gt;Huginn&lt;/a&gt; in the past as a self-hosted Zapier alternative. You can do everything in Huginn if you wish but I wanted an excuse to give Pipedream a try.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5NawzR6X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/Screenshot-from-2021-03-13-13-46-27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5NawzR6X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/Screenshot-from-2021-03-13-13-46-27.png" alt="Automated morning music with Pipedream, Huginn, and Mailbrew"&gt;&lt;/a&gt;The pitch for Pipedream&lt;/p&gt;

&lt;p&gt;So what you need to do is sign up for Pipedream first. They have a very generous free tier which will be more than enough for this project. Once you've that you go to the sources tab and create a new source. The easiest way to get data out of Metacritic is to use their official RSS feeds, so create a new source and select RSS.&lt;/p&gt;

&lt;p&gt;All you have to do now is name your feed and put &lt;a href="https://www.metacritic.com/rss/music"&gt;https://www.metacritic.com/rss/music&lt;/a&gt; in the Feed URL. Pipedream will now emit events for every new item in the feed.&lt;/p&gt;

&lt;p&gt;Step 1 is now complete. We have a way to get music albums. Step 2 will be a bit more complicated. The album review score is not available in the RSS feed and Metacritic does not make this available through any official means. You can solve this problem by scraping the Metacritic site, but there is a better way. Some awesome person has wrapped a GraphQL API around Metacritic. The thing is available for free over at &lt;a href="https://mcgqlapi.com/api"&gt;https://mcgqlapi.com/api&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://graphql.org/"&gt;GraphQL&lt;/a&gt; we can now get album metadata using a query like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;album&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;album&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ignorance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The Weather Station"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;productImage&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;criticScore&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fantastic! Now we can get critic scores. This works for games, movies, and tv shows as well so you can use this for all kinds of projects. There is a problem though. The RSS feed does not make a distinction between Album and Artist, and we need both to be able to use the API. To solve this we need to scrape Metacritic.&lt;/p&gt;

&lt;p&gt;To create a scraper we will once again make use of Pipedream. Select the workflow tab and create a new workflow. Select  "USE ONE OF YOUR EXISTING SOURCES" and choose the RSS source we created earlier. Then click the "+"-icon to add an action and select "Run Node.js code" to create a new custom action.&lt;/p&gt;

&lt;p&gt;There are many ways to fetch the needed data. I chose to use &lt;a href="https://www.npmjs.com/package/cheerio"&gt;Cheerio&lt;/a&gt;. Add 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cheerio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cheerio&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="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;fetchHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cheerio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetchHTML&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;link&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;album&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.product_title:first&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;artist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.band_name:first&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;album&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should do the trick. The return value will be usable in future steps in Pipedream.&lt;/p&gt;

&lt;p&gt;Now, you might have noticed that we could grab the score as well using Cheerio. We don't have to use the GraphQL API for this, so why do it? I did it to learn how to do GraphQL with Pipedream. Feel free to change the Cheerio part above to also include score and then skip the next step. There is also more data available in the GraphQL so have a look around if you want to add something else.&lt;/p&gt;

&lt;p&gt;We now have an album. Time to figure out if it's any good! Making a GraphQL request with Pipedream is easy. Choose "Run Node.js code" again and use the following snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isomorphic-fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://mcgqlapi.com/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
query {
  album(input: { album: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metacritic_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$return_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;album&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;", artist: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metacritic_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$return_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" }) {
    url
    criticScore
    album
    artist
    productImage
  }
}`&lt;/span&gt; 
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsonData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;jsonData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;album&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;$end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;criticScore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;album&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productImage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;album&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;criticScore&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
  &lt;span class="nx"&gt;$end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;criticScore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;album&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productImage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can access the data from previous steps through the &lt;code&gt;${steps.metacritic_data.$return_value.album}&lt;/code&gt; variables. I decided that I only want albums with a Metacritic score of 75 or above. &lt;code&gt;$end()&lt;/code&gt;will end the workflow in Pipedream and it will make sure that we do not proceed to the next step.&lt;/p&gt;

&lt;p&gt;Next up: I only want albums that are available on &lt;a href="https://www.spotify.com/se/"&gt;Spotify&lt;/a&gt;, because I intend to listen to the album that morning. This is another one of those cases where Pipedream is great since it has built-in support for the Spotify API. Simply select Spotify for the next step and search for "Search". You are looking for the &lt;code&gt;GET /search&lt;/code&gt; endpoint. You have to authorize with Spotify to use this.&lt;/p&gt;

&lt;p&gt;This step takes the parameter Q. Add the following: &lt;code&gt;{{steps.metacritic_score.$return_value.album}} {{steps.metacritic_score.$return_value.artist}}&lt;/code&gt;. Also, make sure you add the "Type" optional parameter as well and put "album" in the field.&lt;/p&gt;

&lt;p&gt;Right! Now we have a good album with an artist name, album name as well as a Spotify URL and a nice image of the album. What we want to do now is have this delivered to us every morning. There are so many ways to do this. You can send it  by email or you can add it to Trello/Slack/Asana/Monday/Twitter. I chose to have it delivered in my morning newsletter. I like to read that while having my morning coffee so that is perfect.&lt;/p&gt;

&lt;p&gt;To do this I use an app called &lt;a href="https://mailbrew.com/"&gt;Mailbrew&lt;/a&gt;. It can create custom newsletters for you from Twitter data, Reddit subreddits, RSS feeds, and much more. I find it handy for setting up summaries of subreddits so that I don't have to refresh Reddit all day. If you want to try Mailbrew and want to support this blog then feel free to use my &lt;a href="https://mailbrew.com/?aff=Mskog"&gt;referral code&lt;/a&gt; to sign up. I would really appreciate it! Now, of course you don't have to use Mailbrew for this. This example will assume that we are using Mailbrew or something similar.&lt;/p&gt;

&lt;p&gt;This is unfortunately where we run into an issue with Pipedream. Since we need to output the data as an RSS feed to use Mailbrew then we need to turn the album list into RSS. As far as I know you cannot do that with Pipedream since there is no way to create an "API endpoint" of sorts. Luckily, I got just the thing for this: &lt;a href="https://www.mskog.com/posts/self-hosted-tools-for-web-development"&gt;Huginn&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--51fA43iJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/68747470733a2f2f7261772e6769746875622e636f6d2f687567696e6e2f687567696e6e2f6d61737465722f6d656469612f687567696e6e2d6c6f676f2e706e67.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--51fA43iJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/68747470733a2f2f7261772e6769746875622e636f6d2f687567696e6e2f687567696e6e2f6d61737465722f6d656469612f687567696e6e2d6c6f676f2e706e67.png" alt="Automated morning music with Pipedream, Huginn, and Mailbrew"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first thing you'll need is a Webhook agent to accept incoming albums from Pipedream. Create one and then create another step in Pipedream to post the albums to Huginn. It will be another Node.js step an will look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8HHCs4cw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/Screenshot-from-2021-03-08-20-49-48.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8HHCs4cw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/Screenshot-from-2021-03-08-20-49-48.png" alt="Automated morning music with Pipedream, Huginn, and Mailbrew"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you've set this up you have completed the Pipedream part.&lt;/p&gt;

&lt;p&gt;Of course, we don't want all the albums at once. We want one album every day. So use a Delay agent in Huginn. Set "max_emitted_events" to 1 and it will emit a single event every time it runs.&lt;/p&gt;

&lt;p&gt;Finally, add an output agent to add an RSS feed.  The full Huginn scenario is available here: &lt;a href="https://gist.github.com/mskog/4504fbc4e52df04f2221103bb8306c88"&gt;https://gist.github.com/mskog/4504fbc4e52df04f2221103bb8306c88&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can add this RSS feed to Mailbrew. It looks like this for me:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---lJasdVn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/Screenshot-from-2021-03-08-20-53-20.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---lJasdVn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/03/Screenshot-from-2021-03-08-20-53-20.png" alt="Automated morning music with Pipedream, Huginn, and Mailbrew"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There you! A lot of effort to save 1 minute every morning. But it's cool is it not? This is what automation is for me! You learn something and you get a little treat every morning in your inbox. Of course this is just the beginning.&lt;/p&gt;

&lt;p&gt;Now there are ways to improve this of course. Here are a couple of ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make sure it doesn't throw an error if the Album is not available on Spotify.&lt;/li&gt;
&lt;li&gt;Use the GraphQL API to extract genres for the albums and then only select the genres you like.&lt;/li&gt;
&lt;li&gt;Count the number of songs in the album (the data is available in the API response from Spotify) and then don't include albums with only one song&lt;/li&gt;
&lt;li&gt;Add a deduplicate agent in Huginn to remove duplicate albums. Just in case something goes wrong and Pipedream sends the same album twice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this was just a single example of what you can do with automation tools. Since then I've also done something similar with Movies and TV Shows. I get a newsletter every week with all this so that I don't have to check Metacritic. I find it very relaxing to receive emails instead and read them on my terms.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Ruby on Rails stack for side projects in 2021</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Mon, 01 Mar 2021 14:32:00 +0000</pubDate>
      <link>https://dev.to/mskog/my-ruby-on-rails-stack-for-side-projects-in-2021-1jkn</link>
      <guid>https://dev.to/mskog/my-ruby-on-rails-stack-for-side-projects-in-2021-1jkn</guid>
      <description>&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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fvaggelis-karofilakis-5tW8SwF6trE-unsplash.jpg" 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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fvaggelis-karofilakis-5tW8SwF6trE-unsplash.jpg" alt="My Ruby on Rails stack for side projects in 2021"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why would you start a new side project in Ruby on Rails in 2021? Shouldn't you use something like &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; or even &lt;a href="https://github.com/blitz-js/blitz" rel="noopener noreferrer"&gt;Blitz&lt;/a&gt;? Well, I find that Ruby on Rails is still one of the most productive ways to create an application. It is &lt;a href="http://boringtechnology.club/" rel="noopener noreferrer"&gt;safe and boring&lt;/a&gt; and it gets the job done.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://m.signalvnoise.com/html-over-the-wire/" rel="noopener noreferrer"&gt;HTML over the wire&lt;/a&gt; becoming popular with tools like &lt;a href="https://github.com/phoenixframework/phoenix_live_view" rel="noopener noreferrer"&gt;Phoenix LiveView&lt;/a&gt; and &lt;a href="https://laravel-livewire.com/" rel="noopener noreferrer"&gt;Laravel Livewire&lt;/a&gt;, you can't go wrong with good old Ruby on Rails. You can even try &lt;a href="https://hotwire.dev/" rel="noopener noreferrer"&gt;Hotwire&lt;/a&gt; to stream HTML over web sockets.&lt;/p&gt;

&lt;p&gt;With all that said I thought I'd share my recommended stack for Ruby on Rails development in 2021.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;p&gt;This is long, so if you don't want to read the whole thing then here is a quick summary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ruby and Rails versions&lt;/strong&gt; : Ruby 3 and Ruby on Rails 6&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Architecture&lt;/strong&gt; : Use "the Rails way"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Background jobs&lt;/strong&gt; : &lt;a href="https://sidekiq.org/" rel="noopener noreferrer"&gt;Sidekiq&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Database&lt;/strong&gt; : &lt;a href="https://www.postgresql.org/" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Asset Management&lt;/strong&gt; : &lt;a href="https://github.com/rails/webpacker" rel="noopener noreferrer"&gt;Webpacker&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;CSS&lt;/strong&gt; : &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Javascript&lt;/strong&gt; : &lt;a href="https://github.com/alpinejs/alpine" rel="noopener noreferrer"&gt;Alpine.js&lt;/a&gt;, &lt;a href="https://stimulus.hotwire.dev/" rel="noopener noreferrer"&gt;Stimulus&lt;/a&gt;, and &lt;a href="https://vuejs.org/" rel="noopener noreferrer"&gt;Vue&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Pagination&lt;/strong&gt; : &lt;a href="https://github.com/ddnexus/pagy" rel="noopener noreferrer"&gt;Pagy&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Testing&lt;/strong&gt; : &lt;a href="https://rspec.info/" rel="noopener noreferrer"&gt;RSpec&lt;/a&gt; or &lt;a href="https://github.com/seattlerb/minitest" rel="noopener noreferrer"&gt;Minitest&lt;/a&gt; with &lt;a href="https://github.com/vcr/vcr" rel="noopener noreferrer"&gt;VCR&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Periodic jobs&lt;/strong&gt; : &lt;a href="https://github.com/Rykian/clockwork" rel="noopener noreferrer"&gt;Clockwork&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Code formatting&lt;/strong&gt; : &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt; for Javascript. &lt;a href="https://github.com/rubocop/rubocop" rel="noopener noreferrer"&gt;Rubocop&lt;/a&gt; for Ruby&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Exception management&lt;/strong&gt; : &lt;a href="https://rollbar.com/" rel="noopener noreferrer"&gt;Rollbar&lt;/a&gt; or &lt;a href="https://www.honeybadger.io/" rel="noopener noreferrer"&gt;Honeybadger&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Sending email&lt;/strong&gt; : &lt;a href="https://postmarkapp.com/" rel="noopener noreferrer"&gt;Postmark&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Slugs&lt;/strong&gt; : &lt;a href="https://github.com/norman/friendly_id" rel="noopener noreferrer"&gt;friendly_id&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Full Text Search&lt;/strong&gt; : &lt;a href="https://github.com/Casecommons/pg_search" rel="noopener noreferrer"&gt;pg_search&lt;/a&gt; or &lt;a href="https://github.com/ankane/searchkick" rel="noopener noreferrer"&gt;searchkick&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Deployment&lt;/strong&gt; : &lt;a href="https://www.heroku.com/" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt; or &lt;a href="https://github.com/dokku/dokku" rel="noopener noreferrer"&gt;Dokku&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Self-hosted tools&lt;/strong&gt; : &lt;a href="https://github.com/thumbor/thumbor" rel="noopener noreferrer"&gt;Thumbor&lt;/a&gt;, &lt;a href="https://github.com/huginn/huginn" rel="noopener noreferrer"&gt;Huginn&lt;/a&gt;, &lt;a href="https://www.openfaas.com/" rel="noopener noreferrer"&gt;OpenFaaS&lt;/a&gt; and &lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Editor tools&lt;/strong&gt; : &lt;a href="https://github.com/castwide/solargraph" rel="noopener noreferrer"&gt;Solargraph&lt;/a&gt;, &lt;a href="https://github.com/jemmyw/vscode-rails-fast-nav" rel="noopener noreferrer"&gt;Rails fast nav&lt;/a&gt;, &lt;a href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt;, &lt;a href="https://marketplace.visualstudio.com/items?itemName=misogi.ruby-rubocop" rel="noopener noreferrer"&gt;ruby-rubocop&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; : &lt;a href="https://www.influxdata.com/" rel="noopener noreferrer"&gt;InfluxDB&lt;/a&gt;, &lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt; and &lt;a href="https://github.com/influxdata/influxdb-rails" rel="noopener noreferrer"&gt;influxdb-rails&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Templating engine&lt;/strong&gt; : ERB&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Admin tool&lt;/strong&gt; : &lt;a href="https://activeadmin.info/" rel="noopener noreferrer"&gt;activeadmin&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Ruby and Rails versions
&lt;/h3&gt;

&lt;p&gt;Use the latest Ruby and the latest Rails versions. This is currently Ruby 3.0 and Rails 6.1. They both work well together and I haven't found any reason to stay on the 2.x Ruby versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rails architecture
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Caveat&lt;/strong&gt; : This is just my opinion and if you do things differently then that is great too!&lt;/p&gt;

&lt;p&gt;If you are building a side project with Ruby on Rails then I think that you should stick to the Rails way of doing things. Do not reach for fancy things like &lt;a href="https://en.wikipedia.org/wiki/Domain-driven_design" rel="noopener noreferrer"&gt;DDD&lt;/a&gt; or &lt;a href="https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)" rel="noopener noreferrer"&gt;hexagonal architecture&lt;/a&gt;, especially not if your app is small. There is a very real &lt;a href="https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it" rel="noopener noreferrer"&gt;YAGNI&lt;/a&gt; factor when it comes to software architecture and you can waste a lot of time designing something that you don't need.&lt;/p&gt;

&lt;p&gt;Don't introduce &lt;a href="https://github.com/drapergem/draper" rel="noopener noreferrer"&gt;decorators and view models&lt;/a&gt;. Use helpers instead.&lt;br&gt;&lt;br&gt;
Don't extract domain models. Put the code in the ActiveRecord models and the controllers.&lt;br&gt;&lt;br&gt;
Don't reach for &lt;a href="https://mkdev.me/en/posts/a-couple-of-words-about-interactors-in-rails" rel="noopener noreferrer"&gt;interactors&lt;/a&gt; to model your domain logic.&lt;br&gt;&lt;br&gt;
Don't try to &lt;a href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction" rel="noopener noreferrer"&gt;avoid duplication&lt;/a&gt; too early.&lt;/p&gt;

&lt;p&gt;I have done multiple apps that need all of the above, but those were huge business critical projects with tens of thousands of lines of code. I've started out with an advanced design from the start so many times and then realized that I didn't even need it.&lt;/p&gt;

&lt;p&gt;Once you see a pattern emerging in your app you can start thinking about these things. Perhaps your views got all cluttered with presentation logic that don't really belong in your model. Then, and only then, should you think about perhaps introducing decorators or view components.&lt;/p&gt;

&lt;p&gt;Your codebase will eventually tell you what it wants to be and once you are experienced enough you will be able to tell when you need to restructure things.&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%2Fthumbs.mskog.com%2Fhttps%3A%2F%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fphoto-1487958449943-2429e8be8625.jpeg" 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%2Fthumbs.mskog.com%2Fhttps%3A%2F%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fphoto-1487958449943-2429e8be8625.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Background jobs
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://sidekiq.org/" rel="noopener noreferrer"&gt;Sidekiq&lt;/a&gt;. I've been using that for many years now and it has been rock solid both for side projects and work projects. I also recommend using &lt;a href="https://guides.rubyonrails.org/active_job_basics.html" rel="noopener noreferrer"&gt;ActiveJob&lt;/a&gt; with Sidekiq just to make things a bit easier and rails-like. It will make &lt;a href="https://github.com/mperham/sidekiq/wiki/Active-Job#performance" rel="noopener noreferrer"&gt;things a bit slower&lt;/a&gt; but that shouldn't matter for side projects.&lt;/p&gt;

&lt;p&gt;One issue with Sidekiq is that you won't have access to rate limiting and scheduled jobs unless you pay for the enterprise version. If you are running a business then this is probably a good idea but we are going for dirt cheap here. The rate limit part can be solved using something like &lt;a href="https://github.com/Shopify/limiter" rel="noopener noreferrer"&gt;ruby-limiter&lt;/a&gt; for global rate limiting and &lt;a href="https://github.com/brainopia/sidekiq-limit_fetch" rel="noopener noreferrer"&gt;sidekiq_limit_fetch&lt;/a&gt; to limit concurrency per queue. It doesn't have official support for modern Sidekiq versions but in my experience it works fine anyway. You should probably not use that for business critical things however.&lt;/p&gt;

&lt;p&gt;If you want to get fancy you can also take a look at &lt;a href="http://contribsys.com/faktory/" rel="noopener noreferrer"&gt;Faktory&lt;/a&gt;, which is a polyglot job processing framework from the creator of Sidekiq. Then you can write your jobs in any language you want as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database
&lt;/h3&gt;

&lt;p&gt;Use a relational database such as PostgreSQL or MySQL. I do not recommend using document databases such as MongoDB because there is really no reason for it in my opinion. PostgreSQL is plenty fast and you probably won't need things like horizontal scaling for a side project. You can of course try something fancy like &lt;a href="https://fauna.com/" rel="noopener noreferrer"&gt;FaunaDB&lt;/a&gt;, &lt;a href="https://www.cockroachlabs.com/" rel="noopener noreferrer"&gt;CockroachDB&lt;/a&gt;, or any of the many database flavors.&lt;/p&gt;

&lt;p&gt;This might come off as old school, but when I develop side projects I need something that &lt;a href="http://cryto.net/~joepie91/blog/2015/07/19/why-you-should-never-ever-ever-use-mongodb/" rel="noopener noreferrer"&gt;stores my data safely&lt;/a&gt; and is easy to backup with rock solid performance. PostgreSQL checks all those boxes and it has always served me well. Let me put it his way: There is no reason to ponder "is my data safe" when using PostgreSQL.&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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2F1_PY24xlr4TpOkXW04HUoqrQ.jpeg" 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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2F1_PY24xlr4TpOkXW04HUoqrQ.jpeg" alt="My Ruby on Rails stack for side projects in 2021"&gt;&lt;/a&gt;Even the logo is amazing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Asset Management
&lt;/h3&gt;

&lt;p&gt;Two ways: &lt;a href="https://guides.rubyonrails.org/asset_pipeline.html" rel="noopener noreferrer"&gt;Asset Pipeline&lt;/a&gt; or &lt;a href="https://edgeguides.rubyonrails.org/webpacker.html" rel="noopener noreferrer"&gt;Webpacker&lt;/a&gt;. I always use Webpacker. It is modern, you have access to the full NPM ecosystem, and it has all the modern features such as tree shaking and hot module replacement.&lt;/p&gt;

&lt;p&gt;Webpacker itself is just a Rails style wrapper on top of Webpack. You probably don't have to worry about &lt;a href="https://webpack.js.org/configuration/" rel="noopener noreferrer"&gt;configuring webpack&lt;/a&gt; if you use Webpacker so that is great.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS
&lt;/h3&gt;

&lt;p&gt;OK I realize that this is highly opinionated so I'm just going to give you my recommendation: I'm a big fan of &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt;. I find that its utility-based approach is great for getting things done. I like it so much that I even paid for the premium component package &lt;a href="https://tailwindui.com/" rel="noopener noreferrer"&gt;TailwindUI&lt;/a&gt;. It is just one of those things that you either like or don't like. Since you already use Webpacker(right?) you can use whatever you want so find something you like, learn everything about it, and then stick to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Javascript
&lt;/h3&gt;

&lt;p&gt;I have divided this into three sections to make it easier to recommend things.&lt;br&gt;&lt;br&gt;
Javascript is one of those things where you can really screw yourself over by choosing the wrong thing. I recommend starting very small and only expand when you know that you need it.&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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fphoto-1583484963886-cfe2bff2945f.jpeg" 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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fphoto-1583484963886-cfe2bff2945f.jpeg" alt="My Ruby on Rails stack for side projects in 2021"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just a little Javascript&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This is when you need things like "show this element when I click this button" and "show the value of this input field in this h2-tag as I type it". For this I recommend using &lt;a href="https://github.com/alpinejs/alpine" rel="noopener noreferrer"&gt;Alpine.js&lt;/a&gt;. You don't need to juggle script tags and jQuery with Alpine and it gets the job done when you need something fast and simple. It can however gunk up your HTML quite a bit so when you get to that point you might want to move on to the next section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some Javascript&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Once you need to introduce NPM packages and need something more organized then Alpine might get messy. At this point I would introduce &lt;a href="https://stimulus.hotwire.dev/" rel="noopener noreferrer"&gt;Stimulus&lt;/a&gt;. It is made by the creators of Ruby of Rails. It will clean up a lot of gunk introduced by Alpine and leave you with reusable controllers. Here you can import NPM packages and it works very well with Webpacker. You can of course keep using Alpine as well and use Stimulus for the more advanced things if you wish since they work very well together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A lot of Javascript&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You might not reach this stage and you should be very careful about choosing to go down this path prematurely since it is quite a bit of work. If you need advanced state management, high performance and complicated client side logic then Stimulus might no longer cut it. You may have to reach for a declarative, component based library like &lt;a href="https://reactjs.org/" rel="noopener noreferrer"&gt;React&lt;/a&gt;, &lt;a href="https://vuejs.org/" rel="noopener noreferrer"&gt;Vue&lt;/a&gt;, or &lt;a href="https://svelte.dev/" rel="noopener noreferrer"&gt;Svelte&lt;/a&gt;. All of these, and others, work very well with Webpacker and there are wrappers to make things easier for you. For example we have &lt;a href="https://github.com/shakacode/react_on_rails" rel="noopener noreferrer"&gt;React on Rails&lt;/a&gt; for React integration.&lt;/p&gt;

&lt;p&gt;Again, you need to be careful about taking this step because once you start using something like React you have to &lt;em&gt;replace your HTML&lt;/em&gt; with React components to use it. You cannot simply have React "use your HTML" as a template. You have to rewrite everything from that DOM node and "down" to be able to use React properly. However, if you use Vue then you can &lt;a href="https://gorails.com/forum/rails-6-with-full-vue-frontend-vs-sprinkled-vue-components" rel="noopener noreferrer"&gt;reuse your existing HTML&lt;/a&gt; as the template in Vue. This might make the transition easier and much like Stimulus did Vue will become "Javascript sprinkles" on top of your existing HTML. I don't think that something similar can be done with React. Hit me up on Twitter if you know of a way!&lt;/p&gt;




&lt;p&gt;But Magnus, what about &lt;a href="https://hotwire.dev/" rel="noopener noreferrer"&gt;Hotwire&lt;/a&gt; or &lt;a href="https://docs.stimulusreflex.com/" rel="noopener noreferrer"&gt;StimulusReflex&lt;/a&gt;? If you send HTML over the wire then you won't need to add any advanced Javascript at all right? The simple answer is that I don't have enough experience with these tools to be able to recommend them. They seem &lt;em&gt;very&lt;/em&gt; promising however and I will absolutely write an article about this technology once I have worked with them some more.&lt;/p&gt;

&lt;p&gt;Bottom line: Do not reach for something advanced unless you know that you need it. Stick with simple things like Alpine.js until you are absolutely sure that you need something else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching
&lt;/h3&gt;

&lt;p&gt;Since you already use &lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; to get Sidekiq going then you might as well &lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#activesupport-cache-rediscachestore" rel="noopener noreferrer"&gt;use that&lt;/a&gt; for your cache as well.&lt;/p&gt;

&lt;p&gt;For the actual caching part you should use &lt;a href="https://blog.appsignal.com/2018/04/03/russian-doll-caching-in-rails.html" rel="noopener noreferrer"&gt;Russian Doll Caching&lt;/a&gt;, also known as Fragment Caching. This is the preferred way of caching things in Rails. I also would like you recommend using &lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#action-caching" rel="noopener noreferrer"&gt;Action Caching&lt;/a&gt; if you can get away with it. It was removed back in Ruby on Rails 4 but there is a &lt;a href="https://github.com/rails/actionpack-action_caching" rel="noopener noreferrer"&gt;gem that you can use&lt;/a&gt;. It was removed from Rails because it can be a hassle to get the cache expiration to work properly and such. I try to use it whenever I can however because it is so much faster.&lt;/p&gt;

&lt;p&gt;I wrote more about caching in my article about &lt;a href="https://www.mskog.com/posts/42-performance-tips-for-ruby-on-rails/#page-and-action-caching" rel="noopener noreferrer"&gt;Ruby on Rails performance&lt;/a&gt; so you might want to check that out as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pagination
&lt;/h3&gt;

&lt;p&gt;I've been using &lt;a href="https://github.com/kaminari/kaminari" rel="noopener noreferrer"&gt;Kaminari&lt;/a&gt; in the past but I've since switched to P&lt;a href="https://github.com/ddnexus/pagy" rel="noopener noreferrer"&gt;agy&lt;/a&gt;. It has all the bells and whistles that you can think of like &lt;a href="https://ddnexus.github.io/pagy/api/countless.html" rel="noopener noreferrer"&gt;countless pagination&lt;/a&gt;, pagination using AJAX, and built-in helpers for many CSS frameworks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing
&lt;/h3&gt;

&lt;p&gt;This should probably be a full article. I could write about what to test, when to test it and how much to test. I will probably make this into an article of its own so I'll just stick to writing about which frameworks and tools I recommend here.&lt;/p&gt;

&lt;p&gt;This will be a bit of a boring one since you really can't go wrong with either &lt;a href="https://rspec.info/" rel="noopener noreferrer"&gt;RSpec&lt;/a&gt; or &lt;a href="https://github.com/seattlerb/minitest" rel="noopener noreferrer"&gt;Minitest&lt;/a&gt; and it is a matter of preference. If you want something that is close to bare-bones Ruby then use Minitest. If you want something declarative and like having DSLs for things then use RSpec. I find that RSpec is a bit easier to organize and its helpers are nice. Personally I use them both but I've been leaning towards Minitest lately since that is the Rails default. It also has parallel tests out of the box while you have to use something like &lt;a href="https://github.com/grosser/parallel_tests" rel="noopener noreferrer"&gt;parallel_tests&lt;/a&gt; to get that going in RSpec. If you test suite is large then having built-in parallelization is fantastic.&lt;/p&gt;

&lt;p&gt;For test setup I use &lt;a href="https://github.com/thoughtbot/factory_bot" rel="noopener noreferrer"&gt;Factorybot&lt;/a&gt; instead of fixtures since I find that to be much easier to reason about.&lt;/p&gt;

&lt;p&gt;Finally, a word about mocking external APIs. You can do this in many ways from setting up manual mocks using &lt;a href="https://github.com/bblimke/webmock" rel="noopener noreferrer"&gt;Webmock&lt;/a&gt;, to using fake servers with &lt;a href="https://github.com/sinatra/sinatra" rel="noopener noreferrer"&gt;Sinatra&lt;/a&gt; or &lt;a href="https://github.com/betterment/webvalve" rel="noopener noreferrer"&gt;WebValve&lt;/a&gt;, all the way to replaying previous calls using &lt;a href="https://github.com/vcr/vcr" rel="noopener noreferrer"&gt;VCR&lt;/a&gt;. They all have their pros and cons, but for fast development I'm a big fan of VCR. You don't have to muck around with setting up mocks with fake data loaded from fixtures. VCR however can be messy too since you literally replaying past requests in your tests. The fixtures created by VCR can also become large and slow. For very fast development however, it is what I recommend. Once you run into enough problems I would take a look at the fake server approach instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Periodic jobs
&lt;/h3&gt;

&lt;p&gt;There are many tools for this. For Sidekiq you can use its &lt;a href="https://github.com/mperham/sidekiq/wiki/Ent-Periodic-Jobs" rel="noopener noreferrer"&gt;built-in periodic jobs feature&lt;/a&gt;. That is only for the enterprise version however. There are a number of gems with similar functionality like &lt;a href="https://github.com/ondrejbartas/sidekiq-cron" rel="noopener noreferrer"&gt;sidekiq-cron&lt;/a&gt; and &lt;a href="https://github.com/moove-it/sidekiq-scheduler" rel="noopener noreferrer"&gt;sidekiq-scheduler&lt;/a&gt;. There is also the &lt;a href="https://github.com/Rykian/clockwork" rel="noopener noreferrer"&gt;Clockwork&lt;/a&gt; gem that is not tied to Sidekiq. Personally I use Clockwork, since that is not tied to Sidekiq and it has been working well for many years. This will however start another Rails process to do the scheduling so there will be extra RAM use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code formatting
&lt;/h3&gt;

&lt;p&gt;You should absolutely use code formatting tools that run every single time you save your files. They take a lot of work out of coding so that you can focus on implementing features.&lt;/p&gt;

&lt;p&gt;For Javascript code I use &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt;. It comes with reasonable defaults and a non-approach to settings. It is opinionated, gets the job done and there isn't a million things to configure.&lt;/p&gt;

&lt;p&gt;For Ruby I use &lt;a href="https://github.com/rubocop/rubocop" rel="noopener noreferrer"&gt;Rubocop&lt;/a&gt;. You can set this up so that it runs using &lt;a href="https://github.com/rails/spring" rel="noopener noreferrer"&gt;Spring&lt;/a&gt; by generating a binstub using &lt;a href="https://github.com/toptal/spring-commands-rubocop" rel="noopener noreferrer"&gt;spring-commands-rubocop&lt;/a&gt;. It will make it run a bit faster. Rubocop also has plugins for your testing framework.&lt;/p&gt;

&lt;p&gt;For ERB I use &lt;a href="https://marketplace.visualstudio.com/items?itemName=aliariff.vscode-erb-beautify" rel="noopener noreferrer"&gt;ERB Formatter/Beautify&lt;/a&gt;. It works very well if you use Visual Studio Code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performing HTTP Requests
&lt;/h3&gt;

&lt;p&gt;I used to use &lt;a href="https://github.com/lostisland/faraday" rel="noopener noreferrer"&gt;Faraday&lt;/a&gt; for this but I've switched to the &lt;a href="https://github.com/httprb/http" rel="noopener noreferrer"&gt;HTTP&lt;/a&gt; gem. I find it to be a bit cleaner and easier to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exception management
&lt;/h3&gt;

&lt;p&gt;I use &lt;a href="https://rollbar.com/pricing/" rel="noopener noreferrer"&gt;Rollbar&lt;/a&gt; to capture errors in production. It has a very generous free tier and it starts at $1 a month once you go past that. &lt;a href="https://www.honeybadger.io/plans/" rel="noopener noreferrer"&gt;Honeybadger&lt;/a&gt; is also a great option.&lt;/p&gt;

&lt;p&gt;In development mode you should make sure to use &lt;a href="https://github.com/BetterErrors/better_errors" rel="noopener noreferrer"&gt;better errors&lt;/a&gt; to get nicer error pages when things go wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Continuous integration
&lt;/h3&gt;

&lt;p&gt;If you run your tests and deployment on an external service then you have a lot of options. Personally I use &lt;a href="https://circleci.com/" rel="noopener noreferrer"&gt;Circle CI&lt;/a&gt; for all my projects, Ruby or not. It has a very generous free tier as well as extra stuff for open source projects. The paid tier starts at $30 a month which is reasonable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending email
&lt;/h3&gt;

&lt;p&gt;This is a bit of an odd one to be included perhaps but since getting the emails to arrive safely is pretty then this is quite important. I've used &lt;a href="https://sendgrid.com/" rel="noopener noreferrer"&gt;Sendgrid&lt;/a&gt; and &lt;a href="https://mailgun.com/" rel="noopener noreferrer"&gt;Mailgun&lt;/a&gt; in the past but I have found them both to be unreliable. Messages don't arrive correctly sometimes or they get stuck in Microsoft spam filters. I would recommend everyone to use &lt;a href="https://postmarkapp.com/pricing" rel="noopener noreferrer"&gt;Postmark&lt;/a&gt; for all your email needs. They get the job done and you never have to worry about deliverability. I'm not even getting paid to say that; I just like them that much. They are not free however and start at $10 a month for 10000 emails.&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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fphoto-1603791440384-56cd371ee9a7.jpeg" 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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fphoto-1603791440384-56cd371ee9a7.jpeg" alt="My Ruby on Rails stack for side projects in 2021"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Slugs
&lt;/h3&gt;

&lt;p&gt;Sometimes you want a nice URL and perhaps not "myapp.com/movies/1234". Instead you want "myapp.com/movies/parasite". For this you should use the &lt;a href="https://github.com/norman/friendly_id" rel="noopener noreferrer"&gt;friendly_id&lt;/a&gt; gem. It handles every single conceivable feature for slugging such as generating slugs, multiple languages as well as slug history if you need that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full Text Search
&lt;/h3&gt;

&lt;p&gt;I think there are two reasonable options here. You can either use a service such as &lt;a href="https://www.elastic.co/" rel="noopener noreferrer"&gt;Elasticsearch&lt;/a&gt; or use built-in functionality in PostgreSQL with the &lt;a href="https://github.com/Casecommons/pg_search" rel="noopener noreferrer"&gt;pg_search&lt;/a&gt;. I find that pg_search is better if you can get away with it because it is simply using PostgreSQL. You will need to think carefully about indexes if you have a lot of data though since full text search can be very slow.&lt;/p&gt;

&lt;p&gt;If you decide to use Elasticsearch then you should use the &lt;a href="https://github.com/ankane/searchkick" rel="noopener noreferrer"&gt;searchkick&lt;/a&gt; gem to make it a breeze.&lt;/p&gt;

&lt;p&gt;Finally, you can also use an externally hosted service such as &lt;a href="https://www.algolia.com/" rel="noopener noreferrer"&gt;Algolia&lt;/a&gt;. Don't immediately reach for that however since you will need to manage indexing yourself and it can become expensive if you have a lot of queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment
&lt;/h3&gt;

&lt;p&gt;If you are ok with paying a bit for hosting then just use &lt;a href="https://www.heroku.com/" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt; and don't look back. It is a "push code to deploy" with hosted databases and everything. They have all you need to deploy and run your app, but it can get quite expensive. I went into this in greater detail in &lt;a href="https://www.mskog.com/posts/heroku-vs-self-hosted-paas/" rel="noopener noreferrer"&gt;this article I wrote&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want the cheapest way possible but still easy then you should use &lt;a href="https://github.com/dokku/dokku" rel="noopener noreferrer"&gt;Dokku&lt;/a&gt;. It is a self-hosted "mini Heroku". All you need is a cheap server, an hour to install it and then you are off to the races. I went into Dokku in greater detail in the &lt;a href="https://www.mskog.com/posts/heroku-vs-self-hosted-paas/" rel="noopener noreferrer"&gt;Heroku post mentioned earlier&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you need a cheap and solid server then feel free to use my &lt;a href="https://hetzner.cloud/?ref=GjnpYTNYtgRU" rel="noopener noreferrer"&gt;referral code&lt;/a&gt; to get €20 in credit at Hetzner. Read my &lt;a href="https://www.mskog.com/posts/hetzner-cloud-review-revisited-in-2020/" rel="noopener noreferrer"&gt;review&lt;/a&gt; for more information.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-hosted tools
&lt;/h3&gt;

&lt;p&gt;These are not Rails-specific but they are very nice to have for any app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/thumbor/thumbor" rel="noopener noreferrer"&gt;Thumbor&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Self-hosted image transformation and optimization tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://huginn.mskog.com/" rel="noopener noreferrer"&gt;Huginn&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Self-hosted Zapier/IFTTT alternative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenFAAS&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Self-hosted serverless functions. See &lt;a href="https://www.mskog.com/posts/self-hosting-serverless-with-openfaas/" rel="noopener noreferrer"&gt;this article&lt;/a&gt; for more details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;InfluxDB + Telegraf + Grafana&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Self-hosted monitoring and visualization stack.&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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fkjc8zTZo.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%2Fs3-eu-central-1.amazonaws.com%2Fmskog-cms%2F2021%2F02%2Fkjc8zTZo.png" alt="My Ruby on Rails stack for side projects in 2021"&gt;&lt;/a&gt;A Grafana server monitoring dashboard&lt;/p&gt;

&lt;p&gt;I wrote more about self-hosted tools in &lt;a href="https://www.mskog.com/posts/self-hosted-tools-for-web-development/" rel="noopener noreferrer"&gt;this article&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Editor tools
&lt;/h3&gt;

&lt;p&gt;There are more examples in my article about &lt;a href="https://www.mskog.com/posts/visual-studio-code-plugins-for-ruby/" rel="noopener noreferrer"&gt;Visual Studio Code plugins for Rub&lt;/a&gt;y. These are the essentials:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=rebornix.Ruby" rel="noopener noreferrer"&gt;Ruby&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Well...you'll need this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/castwide/solargraph" rel="noopener noreferrer"&gt;Solargraph&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
"Go to definition"  in your editor. Code navigation&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=jemmyw.rails-fast-nav" rel="noopener noreferrer"&gt;&lt;strong&gt;Rails fast nav&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Navigate your Rails apps faster&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode" rel="noopener noreferrer"&gt;&lt;strong&gt;Prettier&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Run and display prettier from within Visual Studio Code&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=misogi.ruby-rubocop" rel="noopener noreferrer"&gt;&lt;strong&gt;rubo-rubocop&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Run your Rubocop cops from within VSCode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring
&lt;/h3&gt;

&lt;p&gt;If you're ok with paying some money then use &lt;a href="https://www.datadoghq.com/pricing/" rel="noopener noreferrer"&gt;Datadog&lt;/a&gt;. It has server monitoring, log management, and application monitoring. If you want something self-hosted then use the InfluxDB+Grafana stack mentioned earlier and then use &lt;a href="https://github.com/influxdata/influxdb-rails" rel="noopener noreferrer"&gt;influxdb-rails&lt;/a&gt; to push metrics. It works really well and Grafana can give you some really solid visualizations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Templating engine
&lt;/h3&gt;

&lt;p&gt;I used to be a huge fan of &lt;a href="https://haml.info/" rel="noopener noreferrer"&gt;HAML&lt;/a&gt;, but now we have things like &lt;a href="https://emmet.io/" rel="noopener noreferrer"&gt;Emmet&lt;/a&gt; and autoformatting. I also write HTML in React and Vue so the jump to HAML is not great. I highly recommend that you just stick to ERB which is the Rails default. If you have an app that is full of HAML templates then you can use &lt;a href="https://haml2erb.org/" rel="noopener noreferrer"&gt;https://haml2erb.org/&lt;/a&gt; to convert them to ERB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Admin tool
&lt;/h3&gt;

&lt;p&gt;I've tried most of the the &lt;a href="https://www.ruby-toolbox.com/categories/rails_admin_interfaces" rel="noopener noreferrer"&gt;admin tool&lt;/a&gt;s out there and I find that &lt;a href="https://activeadmin.info/index.html" rel="noopener noreferrer"&gt;activeadmin&lt;/a&gt; is the best choice. It has a very powerful, yet daunting, &lt;a href="https://activeadmin.info/documentation.html" rel="noopener noreferrer"&gt;DSL&lt;/a&gt; for generating admin tools. You can have a very decent admin section with little effort using activeadmin, so I highly recommend trying that before writing your own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Misc gems
&lt;/h3&gt;

&lt;p&gt;In no particular order:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/janko/down" rel="noopener noreferrer"&gt;&lt;strong&gt;Down&lt;/strong&gt;&lt;/a&gt;: Safe file downloads&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/jhawthorn/discard" rel="noopener noreferrer"&gt;&lt;strong&gt;Discard&lt;/strong&gt;&lt;/a&gt;: Soft-delete.&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/feedjira/feedjira" rel="noopener noreferrer"&gt;&lt;strong&gt;Feedjira&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;.&lt;/strong&gt; RSS management&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/kjvarga/sitemap_generator" rel="noopener noreferrer"&gt;&lt;strong&gt;sitemap_generator&lt;/strong&gt;&lt;/a&gt; Generate sitemaps with ease&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/kpumuk/meta-tags" rel="noopener noreferrer"&gt;&lt;strong&gt;meta-tags&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;.&lt;/strong&gt; Create SEO tags, Twitter cards and opengraph sharing attributes.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/gjtorikian/html-pipeline" rel="noopener noreferrer"&gt;html-pipeline&lt;/a&gt;.&lt;/strong&gt; Tools for doing auto-linking of URLs inside text blocks and more.&lt;/p&gt;

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

&lt;p&gt;Wow that was a long one. Thank you so much for making all the way to the end and I hope that your future Ruby on Rails projects will be successful.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What do you use for a Team Knowledgebase?</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Thu, 25 Feb 2021 15:33:13 +0000</pubDate>
      <link>https://dev.to/mskog/what-do-you-use-for-a-team-knowledgebase-4n3l</link>
      <guid>https://dev.to/mskog/what-do-you-use-for-a-team-knowledgebase-4n3l</guid>
      <description>&lt;p&gt;I've been looking for something to use for a team knowledgebase for a while now. This is going to be used for everything from technical documentation to onboarding and code standards.&lt;/p&gt;

&lt;p&gt;Currently we are using the Hugo static site generator with a nice theme but it is quite clunky to have to check out the code every time we need to change something.&lt;/p&gt;

&lt;p&gt;We are currently looking at &lt;a href="https://www.getoutline.com/"&gt;Outline&lt;/a&gt; to solve this and so far it seems pretty decent, but it lacks comments and permissions.&lt;/p&gt;

&lt;p&gt;Feature wish list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to update, preferably with markdown support&lt;/li&gt;
&lt;li&gt;File attachments&lt;/li&gt;
&lt;li&gt;Support for discussions/comments&lt;/li&gt;
&lt;li&gt;Integration with tools to make it easy to find things like Slack and Alfred&lt;/li&gt;
&lt;li&gt;Versioning&lt;/li&gt;
&lt;li&gt;Admin tools to be able to lock documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Can you recommend something?&lt;/p&gt;

</description>
      <category>help</category>
    </item>
    <item>
      <title>LQIP in Rails using Thumbor and base64</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Tue, 02 Feb 2021 18:30:35 +0000</pubDate>
      <link>https://dev.to/mskog/lqip-in-rails-using-thumbor-and-base64-104m</link>
      <guid>https://dev.to/mskog/lqip-in-rails-using-thumbor-and-base64-104m</guid>
      <description>&lt;p&gt;There are &lt;a href="https://jmperezperez.com/svg-placeholders/"&gt;many&lt;/a&gt; &lt;a href="https://cloudinary.com/blog/low_quality_image_placeholders_lqip_explained"&gt;ways&lt;/a&gt; to do low quality image placeholders for applications. This is a simple way using Thumbor and Ruby to generate blurry base64 thumbnails. This way the image can be loaded in the first HTML load and there is no need for additional requests to fetch the  thumbnails while the original images are loading.&lt;/p&gt;

&lt;p&gt;You will need &lt;a href="https://github.com/thumbor/thumbor"&gt;Thumbor&lt;/a&gt; to use this method. If you're unfamiliar with Thumbor then I wrote an introduction &lt;a href="https://www.mskog.com/posts/self-hosted-tools-for-web-development"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The idea is to take the original image and then drop the quality as low we can and then make it small with a blur effect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HQ7ghyrh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thumbs.mskog.com/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/01/pexels-photo-5733127.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HQ7ghyrh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thumbs.mskog.com/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/01/pexels-photo-5733127.jpeg" alt="Original image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's start by applying the quality filter and drop the quality to say 5&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NuAuD19o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thumbs.mskog.com/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/01/quality-5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NuAuD19o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thumbs.mskog.com/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/01/quality-5.jpeg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we take the image size down to say 100 pixels wide and keep the aspect ratio.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vR3Bq9tw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thumbs.mskog.com/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/01/small.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vR3Bq9tw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thumbs.mskog.com/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/01/small.jpeg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, if we blow this image up to a reasonable size it will look real bad. So what I like to do is play with the quality and size a bit and add a bit of blur. In this example I changed the size to 200 pixels wide, the quality to 25 and added 5 blur:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TjzKh1_S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thumbs.mskog.com/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/01/final.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TjzKh1_S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thumbs.mskog.com/https://s3-eu-central-1.amazonaws.com/mskog-cms/2021/01/final.jpeg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The size is a bit larger now but I think the trade off is worth it. Here is the URL for the final image if you want to have a look at the settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://thumbs.mskog.com/200x/filters:format:(jpeg):quality(25):blur(5)/https://images.pexels.com/photos/5733127/pexels-photo-5733127.jpeg?auto=compress&amp;amp;cs=tinysrgb&amp;amp;dpr=2&amp;amp;h=750&amp;amp;w=1260"&gt;https://thumbs.mskog.com/200x/filters:format:(jpeg):quality(25):blur(5)/https://images.pexels.com/photos/5733127/pexels-photo-5733127.jpeg?auto=compress&amp;amp;cs=tinysrgb&amp;amp;dpr=2&amp;amp;h=750&amp;amp;w=1260&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can now use this excellent &lt;a href="https://www.base64-image.de/"&gt;Base64 Image Encoder&lt;/a&gt; site to convert the image to base64. The final image will thus be about 3.26kB. Keep in mind that this is text and can be compressed. Gzip compression should be able to take this down to about 2kB once we add it to a website.&lt;/p&gt;

&lt;p&gt;Ok, so now we have settings that we like. Next up we need to write some Ruby code to convert the image to base64. This is very easy to do:&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="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"net/http"&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://thumbs.mskog.com/200x/filters:format:(jpeg):quality(25):blur(5)/https://images.pexels.com/photos/5733127/pexels-photo-5733127.jpeg?auto=compress&amp;amp;cs=tinysrgb&amp;amp;dpr=2&amp;amp;h=750&amp;amp;w=1260"&lt;/span&gt;

&lt;span class="n"&gt;base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all you need. We now have a base64 string representation of the image. I like to save this result to a database field on the model with the image. You can also use the Rails cache and generate these base64 images on the fly if you want. This will however be very slow and that is probably not what you want. Instead, just use &lt;a href="https://guides.rubyonrails.org/active_job_basics.html"&gt;ActiveJob&lt;/a&gt; to create these in the background once the original image URL is saved. Then save them to the model. Another idea is to use &lt;a href="https://edgeguides.rubyonrails.org/active_storage_overview.html"&gt;Active Storage&lt;/a&gt; and hook into the metadata analysis to add your base64 strings this way.&lt;/p&gt;

&lt;p&gt;Right, time to put the base64 images to use. What you want to do is to use these as placeholders while the full image is loading. I usually use &lt;a href="https://github.com/verlok/vanilla-lazyload"&gt;vanilla-lazyload&lt;/a&gt; for this. It is a simple and tiny project that gets the job done. Do mind that if you use Turbolinks you have to make sure that vanilla-lazyload does its thing after Turbolinks is done. If you use Webpacker it will look something like this in your application.js file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;LazyLoad&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vanilla-lazyload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lazyLoad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;LazyLoad&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="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbolinks:load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;lazyLoad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can use this in your &lt;a href="https://en.wikipedia.org/wiki/Data_URI_scheme#Syntax"&gt;HTML&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And there we go. Lazy loaded base64 placeholders that will be replaced by the full image once it is done loading. If you want to see how this looks in practice then I made a &lt;a href="https://upcomingvideogames.mskog.com/"&gt;test site&lt;/a&gt; for it&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>RSS Bridge and Huginn: Feeds for everything</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Mon, 05 Oct 2020 18:24:35 +0000</pubDate>
      <link>https://dev.to/mskog/rss-bridge-and-huginn-feeds-for-everything-4ec9</link>
      <guid>https://dev.to/mskog/rss-bridge-and-huginn-feeds-for-everything-4ec9</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TSR2m1nT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/09/photo-1439996822684-81bde34914e4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TSR2m1nT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/09/photo-1439996822684-81bde34914e4.jpeg" alt="RSS Bridge and Huginn: Feeds for everything"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/RSS-Bridge/rss-bridge"&gt;RSS Bridge&lt;/a&gt; is my latest find in the automation space. While I already had &lt;a href="https://www.google.com/search?q=huginn&amp;amp;oq=huginn&amp;amp;aqs=chrome..69i57j35i39l2j0j69i60l4.1185j0j1&amp;amp;sourceid=chrome&amp;amp;ie=UTF-8"&gt;Huginn&lt;/a&gt; installed as well as &lt;a href="https://github.com/openfaas/faasd"&gt;FAAS&lt;/a&gt; for creating and running serverless functions, RSS Bridge has been a great addition to this stack. What it is is a simple way to turn anything into a feed. You can get tweets by a specific user, trending projects on Github, popular shots from &lt;a href="https://dribbble.com/"&gt;Dribble&lt;/a&gt;, and much much more. RSS Bridge can output the results as an RSS/Atom feed, HTML, or as JSON. The latter is the real killer feature when combining it with more automation using Huginn for exmaple. RSS bridge will cache the feed so no risk of hitting the underlying site too much either. It can be hosted on just about anything as it is a PHP application with no database. You can easily host it on &lt;a href="https://heroku.com/"&gt;Heroku&lt;/a&gt; or use &lt;a href="https://github.com/dokku/dokku"&gt;Dokku&lt;/a&gt; like I did. On my VPS it uses about 50 mb of RAM so it is a real tiny little thing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you need a cheap and solid server then feel free to use my &lt;a href="https://hetzner.cloud/?ref=GjnpYTNYtgRU"&gt;referral code&lt;/a&gt; to get €20 in credit at Hetzner. Read my &lt;a href="https://www.mskog.com/posts/hetzner-cloud-review-revisited-in-2020/"&gt;review&lt;/a&gt; for more information.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each resulting bridge is a simple URL that you can use directly or pass it along for further processing.&lt;/p&gt;

&lt;p&gt;One of the issues is that the agents sometimes don't work. Many sites don't want this kind of scraping done to them. Instagram is a great example. RSS Bridge has a bridge to grab images from a user but it does not currently work. There is an issue to resolve it on Github but currently no go. It can be frustrating to see a bridge you want to use and then it doesn't work at all once you use it. However, what works works really well.&lt;/p&gt;

&lt;p&gt;RSS bridge shines when combined with &lt;a href="https://www.mskog.com/posts/self-hosted-tools-for-web-development/"&gt;Huginn&lt;/a&gt; as I will demonstrate with a couple of examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example #1: Dilbert strips to Slack
&lt;/h3&gt;

&lt;p&gt;Say that you want to get the latest &lt;a href="https://dilbert.com/"&gt;Dilbert&lt;/a&gt; strip posted to your private Slack channel. To do this you'll need Huginn, RSS Bridge and a FAAS function to extract the largest image from the page.&lt;/p&gt;

&lt;p&gt;The Huginn scenario for this consists of three agents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Website agent to fetch posts from the feed from RSS Bridge. The feed URL will be something like &lt;a href="https://yourssbridge.com?action=display&amp;amp;bridge=Dilbert&amp;amp;format=Json"&gt;https://yourssbridge.com?action=display&amp;amp;bridge=Dilbert&amp;amp;format=Json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A Website agent to receive events from 1 and use the &lt;a href="https://github.com/mskog/faas-functions/tree/master/largest-image"&gt;serverless function&lt;/a&gt; extract the largest image from the page.&lt;/li&gt;
&lt;li&gt;A Slack agent to post the images to Slack.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've added a gist with the scenario below. Do note that you have to use your own serverless function and your own RSS Bridge and Slack URLs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/mskog/092f655ea7013fcd59db7d9c3c1fbe9f"&gt;https://gist.github.com/mskog/092f655ea7013fcd59db7d9c3c1fbe9f&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this you get the latest Dilbert strip image posted to your Slack channel of choice every day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example #2: Reddit digest
&lt;/h3&gt;

&lt;p&gt;If you want to avoid refreshing Reddit all day every day then perhaps you would prefer a simple email with the content from your favorite subreddit? It is very easy to do with Huginn and RSSBridge. You do need to add an SMTP service to Huginn to use it. &lt;a href="https://sendgrid.com/pricing/"&gt;Sendgrid&lt;/a&gt; is free for up to 100 emails a day which should be plenty unless you need hundreds of subreddit digests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/mskog/b361447167e0eb7a66ee67f1f1633eb4"&gt;https://gist.github.com/mskog/b361447167e0eb7a66ee67f1f1633eb4&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus Example: Myfitnesspal dashboard
&lt;/h3&gt;

&lt;p&gt;I use &lt;a href="https://www.myfitnesspal.com/"&gt;Myfitnesspal&lt;/a&gt; to track my macro nutrients and caloric intake. I wanted a nice dashboard to show me this over time and some completely necessary graphs to track things. Myfitnesspal has an API that can do this but it is only for registered partners. Exporting data is only available for paying users and I don't want to do that manually. So, enter our good friend the web scraper.&lt;/p&gt;

&lt;p&gt;To do this you need Huginn, FAAS with a Puppeteer function, Influxdb and Grafana. I wrote about the latter &lt;a href="https://www.mskog.com/posts/self-hosted-tools-for-web-development/"&gt;here&lt;/a&gt;. Feel free to use &lt;a href="https://github.com/mskog/faas-functions/tree/master/myfitnesspal"&gt;my function&lt;/a&gt; to scrape the data from Myfitnesspal. It is a real hack job but it gets the job done every day. Then all you need to do is to use a Website agent to execute the function and a Post agent to add the data to Influxdb. Then you can use Grafana to make a nice graph like so:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kErSpg6q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/09/myfitnesspal_grafana.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kErSpg6q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/09/myfitnesspal_grafana.png" alt="RSS Bridge and Huginn: Feeds for everything"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus Example #2: Withings dashboard
&lt;/h3&gt;

&lt;p&gt;Let us stick with the fitness stuff shall we. I own a &lt;a href="https://www.withings.com/se/en/"&gt;Withings&lt;/a&gt; WIFI enabled smart scale for some reason. I weigh myself daily and the scale will upload the data to the cloud. I would like to have this data in Grafana as well. Withings does have an API but it is an oauth2 enabled atrocity that is quite painful to use for such a simple thing as &lt;em&gt;getting my data&lt;/em&gt;. I spent a solid weekend trying to get this working before giving up and resorted to scraping again. An hour later I had my data in Grafana just like with Myfitnesspal. Once again you can use &lt;a href="https://github.com/mskog/faas-functions/tree/master/withings"&gt;my FAAS function&lt;/a&gt; if you wish!&lt;/p&gt;

&lt;p&gt;The procedure to get this going is identical to the Myfitnesspal one. Simply use a Website agent to get the data from the FAAS function and then use a Post agent to add it to Influx. I will not post an example graph because I don't want to scare anyone.&lt;/p&gt;

&lt;p&gt;Now all I need is to get the data out of my &lt;a href="https://play.google.com/store/apps/details?id=com.sarasoft.es.fivethreeonebasic&amp;amp;hl=en"&gt;weight&lt;/a&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.sarasoft.es.fivethreeonebasic&amp;amp;hl=en"&gt;lifting tracker app&lt;/a&gt; and we're in business.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced Example: Weightlifting stats dashboard
&lt;/h3&gt;

&lt;p&gt;Time for something a bit more involved. My current &lt;a href="https://play.google.com/store/apps/details?id=com.sarasoft.es.fivethreeonebasic&amp;amp;hl=en"&gt;lifting tracker app&lt;/a&gt; can export the data to CSV and share it using the default Android share. Huginn has several agents that can deal with files so as long as we can get the file to Huginn we are golden. I would love to have my own data available so that I can graph it next to the other stuff above.&lt;/p&gt;

&lt;p&gt;To do this we need to do three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get the CSV data into Huginn&lt;/li&gt;
&lt;li&gt;Format the data and remove duplicates&lt;/li&gt;
&lt;li&gt;Import the data in InfluxDB&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To solve the first problem we will use Dropbox. Huginn has Dropbox integration built in so this step will not be too hard. You do need to register an application in the &lt;a href="https://www.dropbox.com/developers/apps?_tk=pilot_lp&amp;amp;_ad=topbar4&amp;amp;_camp=myapps"&gt;Dropbox App Console&lt;/a&gt; and make sure the token is set to permanent. You can use a scoped app if you wish. Then you need to setup a Dropbox Watch Agent to watch for changes in the directory as well as some trigger agents to avoid running the workflow when a file is deleted. Finally we use a Dropbox File Url Agent to create a link to the raw file that we can read in step 2.&lt;/p&gt;

&lt;p&gt;It gets much easier now since we have a URL for the file itself. Simply use a Website agent to read the file, a CSV agent to format the data, a Trigger agent to select the correct rows, and a De duplication agent to get rid of duplicates. Finally we use a Post agent to get the data to Influx. Ok maybe not that simple.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QlhOwNKi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/09/Screenshot-from-2020-09-30-20-37-56.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QlhOwNKi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/09/Screenshot-from-2020-09-30-20-37-56.png" alt="RSS Bridge and Huginn: Feeds for everything"&gt;&lt;/a&gt;The diagram for the scenario&lt;/p&gt;

&lt;p&gt;This was a lot of work but now I have a fully automated workflow to get the data out of my app and into my own data store. All I have to do now is hit Export in the app and save the file to Dropbox. Do note that we did not need to use anything but Huginn and Dropbox to do all of this.&lt;/p&gt;

&lt;p&gt;The complete scenario is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/mskog/88a246000d1db4c28753ac76d0481627"&gt;https://gist.github.com/mskog/88a246000d1db4c28753ac76d0481627&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I am getting more and more excited about using things like Huginn, RSS Bridge, and FAAS to create complicated automated workflows. I have even started building applications that take advantage of this to do things like scraping websites and processing data. Do you have any good ideas for what to do next? Post a comment!&lt;/p&gt;

</description>
      <category>openfaas</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Prevent blank and initial search with Algolia</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Thu, 03 Sep 2020 19:12:47 +0000</pubDate>
      <link>https://dev.to/mskog/prevent-blank-and-initial-search-with-algolia-19ae</link>
      <guid>https://dev.to/mskog/prevent-blank-and-initial-search-with-algolia-19ae</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--e87cujHo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/09/photo-1528911104572-560677f3996b.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--e87cujHo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/09/photo-1528911104572-560677f3996b.jpeg" alt="Prevent blank and initial search with Algolia"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.algolia.com/"&gt;Algolia&lt;/a&gt; recent updated their &lt;a href="https://www.algolia.com/pricing/"&gt;pricing&lt;/a&gt;. You no longer have to pay a hefty monthly fee to get search going. It is now entirely based on your search volume and you get 10000 search requests for free every month. This makes it very attractive for blogs like this one.&lt;/p&gt;

&lt;p&gt;I follow the &lt;a href="https://www.gatsbyjs.com/docs/adding-search-with-algolia/"&gt;official Gatsby guide&lt;/a&gt; to get search going which was easy enough, but I ran into a problem. Algolia will by default make a search request on every single page load &lt;em&gt;and another&lt;/em&gt; when you focus the search bar. The reasoning for this is that firing off a blank request make the next request faster. However, for people who are trying to limit the number of requests this is a problem. For a popular blog you'd very easily hit the 10k limit even if no one is using your search.&lt;/p&gt;

&lt;p&gt;There is no official way to skip the initial or blank search using the &lt;a href="https://github.com/algolia/react-instantsearch"&gt;Algolia React client&lt;/a&gt; so we have to do it ourselves. You probably have something like this in your code if you use the official client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;algoliaClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;algoliasearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rootRef&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InstantSearch&lt;/span&gt;
        &lt;span class="nx"&gt;searchClient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;algoliaClient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;indexName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;indices&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;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;onSearchStateChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{({&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="nx"&gt;onFocus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setFocus&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="nx"&gt;hasFocus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hasFocus&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&lt;/span&gt;
          &lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;hasFocus&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;indices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/InstantSearch&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To fix the issue all you have to is to create your own search client and hook into the search method like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;algoliaClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;algoliasearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requests&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;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;algoliaClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rootRef&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InstantSearch&lt;/span&gt;
        &lt;span class="nx"&gt;searchClient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchClient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;indexName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;indices&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;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;onSearchStateChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{({&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="nx"&gt;onFocus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setFocus&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="nx"&gt;hasFocus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hasFocus&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&lt;/span&gt;
          &lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;hasFocus&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;indices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/InstantSearch&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There you go! No more searches being fired on initial page load and no more blank searches.&lt;/p&gt;

</description>
      <category>gatsby</category>
      <category>tips</category>
      <category>beginners</category>
    </item>
    <item>
      <title>My experience when changing from macOS to Ubuntu 20.04</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Sun, 30 Aug 2020 08:38:00 +0000</pubDate>
      <link>https://dev.to/mskog/my-experience-when-changing-from-macos-to-ubuntu-20-04-fp5</link>
      <guid>https://dev.to/mskog/my-experience-when-changing-from-macos-to-ubuntu-20-04-fp5</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J5nFzUdG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/photo-1517783999520-f068d7431a60.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J5nFzUdG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/photo-1517783999520-f068d7431a60.jpeg" alt="My experience when changing from macOS to Ubuntu 20.04"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've been using a Macbook Pro for work for about 7 years now. My first one was a 2013 model and my current one is the 2018 model. In the beginning it was a breath of fresh air compared to the old Ubuntu based desktop computers we were using for work in the past. However, while I think that the Mac experience was great back in 2013 it has become worse over time. From excessive &lt;a href="https://www.theverge.com/2018/7/24/17605652/macbook-pro-thermal-throttling-apple-software-fix"&gt;thermal throttling&lt;/a&gt; to &lt;a href="https://www.tomsguide.com/opinion/why-is-apple-still-selling-macbooks-with-bad-butterfly-keyboards"&gt;bad keyboards&lt;/a&gt; and dangerous OS updates, it has been going downhill for a while now.&lt;/p&gt;

&lt;p&gt;During the Covid 19 epidemic I've had some spare time on my hands so I decided to give Ubuntu 20.04 a try on my home gaming computer. Here is what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bad Docker performance
&lt;/h2&gt;

&lt;p&gt;Once of the major problems I have with development on a Mac is the poor Docker performance. I like to use Docker for development, especially for databases. I keep a docker compose file around that launches the containers I need like Postgres, Redis, and so on. I don't run the actual code in the Docker container but I guess I should be doing that too. Doing this on a mac has some of the benefits but the poor performance is just awful. We are talking about something like a 50% performance loss when running in Docker containers. Not to mention that the docker virtual machine on mac is memory hungry and takes a while to start. We used to run the same setup in docker at work for our development but the poor performance was finally too much to bear. We even tried to move the code to Docker containers as well but that made things even slower.&lt;/p&gt;

&lt;p&gt;Once you go back to Linux things will improve dramatically. No more memory bloat, no slow startup, and huge performance improvements. We are talking about native performance here so everything works perfectly,. That was a huge breath of fresh air for me and that alone would probably had been enough to make the switch permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance improvements
&lt;/h2&gt;

&lt;p&gt;My Macbook Pro is the 2018 "premium" model. It has a an Intel &lt;a href="https://ark.intel.com/content/www/us/en/ark/products/134903/intel-core-i9-8950hk-processor-12m-cache-up-to-4-80-ghz.html"&gt;I9-8950HK&lt;/a&gt; CPU with 6 cores in it. My desktop computer has a &lt;a href="https://ark.intel.com/content/www/us/en/ark/products/88195/intel-core-i7-6700k-processor-8m-cache-up-to-4-20-ghz.html"&gt;6700k&lt;/a&gt;. While the laptop CPU has better specs in general that is not the case in real life use. I assume this is because it is a..well...a laptop CPU and of course the thermals. The 2018 i9 Macbook Pro is notorious for having &lt;a href="https://www.techradar.com/news/is-the-macbook-pro-2018-subject-to-excessive-thermal-throttling"&gt;very poor thermals&lt;/a&gt;. Of course, there is only so much you can do with a high performance CPU in such a small body like the Macbook but still, it is quite awful. Upon any kind of realistic load it will hit 100 degrees Celsius and start throttling. This is to be expected but it will also do this when running turbo boost on a single core. My desktop computer has a "gaming grade" cooler in it so it runs nice and cool. It will handle full load on all cores no problem. In general use when running tests for example the desktop computer is about 25% faster in. It really does make me want to upgrade the CPU though, perhaps to a Ryzen &lt;a href="https://www.amd.com/en/products/cpu/amd-ryzen-9-3900x"&gt;3900x&lt;/a&gt; or even a &lt;a href="https://www.amd.com/en/products/cpu/amd-ryzen-9-3950x"&gt;3950x&lt;/a&gt;. Not sure why but It seems like a good idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware issues
&lt;/h2&gt;

&lt;p&gt;I have been using a Macbook as my daily work computer since 2013 and in my experience the hardware on them works well enough(apart from the &lt;a href="https://www.theverge.com/2020/5/4/21246223/macbook-keyboard-butterfly-magic-pro-apple-design"&gt;keyboard&lt;/a&gt;) but if you connect non-Apple hardware to it it might have problems. For starters, finding a docking station that does the job well was not fun. I have two 1440p monitors connected to my Macbook at home and the main issue I'm having is that they keep turning themselves off every hour or so. To fix it I have to reconnect them and sometimes even open the lid on the Macbook itself. I've tried with several different cables and nothing has worked. Extremely annoying.&lt;/p&gt;

&lt;p&gt;Another issue is that both monitors have a refresh rate of 144hz which the Macbook cannot handle. If I do set anything about 60hz in the display settings the screen goes black and then I have to reboot to fix it for some reason.&lt;/p&gt;

&lt;p&gt;I'm happy to say that Ubuntu 20.04 recognized everything in my desktop computer out of the box with zero problems hardware wise. The drivers for my Nvidia GPU are included in the latest release of Ubuntu, no separate install needed, and worked perfectly. Both monitors work in 1440p with 144hz support. No issues at all. We've made some progress for everyday Linux use for sure!&lt;/p&gt;

&lt;h2&gt;
  
  
  Linux on the desktop?
&lt;/h2&gt;

&lt;p&gt;Everything was going well until the installation of Ubuntu was complete and I rebooted the computer. I got a login screen even though I set the thing to auto-login. When I logged in it kicked me back to the login screen again in an endless loop. This is a &lt;a href="http://ubuntuhandbook.org/index.php/2020/01/login-loop-auto-login-enabled-in-ubuntu-19-10-with-nvidia-driver/"&gt;known issue&lt;/a&gt; and as far as I know it has not been solved. I do wonder how that made it past testing though...&lt;/p&gt;

&lt;h2&gt;
  
  
  Software use: Gnome vs macOS
&lt;/h2&gt;

&lt;p&gt;As I mentioned earlier I've been using a Macbook for development for about 7 years now. I don't think that I can remember a single significant upgrade to the everyday use for me in a macOS upgrade since then. Except perhaps dark mode...&lt;/p&gt;

&lt;p&gt;Every single system upgrade has broken something in a horrible way for me on the Macbook so every upgrade is dreaded.&lt;/p&gt;

&lt;p&gt;I find that the everyday use experience is fine on Ubuntu. The file manager has everything you need and I haven't noticed any problems yet. We'll see what happens next time I upgrade though.&lt;/p&gt;

&lt;h2&gt;
  
  
  Missing apps
&lt;/h2&gt;

&lt;p&gt;So far Ubuntu seems like a great deal! I do miss some of my favorite apps however. Like &lt;a href="https://www.alfredapp.com/"&gt;Alfred&lt;/a&gt;. I've build so many little automation scripts and such for it and I really miss Alfred. I know that there is a replacement called &lt;a href="https://github.com/albertlauncher/albert"&gt;Albert&lt;/a&gt; but I haven't looked into it yet. It looks like a good option though!&lt;/p&gt;

&lt;p&gt;I also miss &lt;a href="https://kapeli.com/dash"&gt;Dash&lt;/a&gt;. Having all the documentation in the world available from anywhere is a great thing. I know that the &lt;a href="https://github.com/zealdocs/zeal"&gt;Zeal&lt;/a&gt; project is a replacement and I will give that a try for sure.&lt;/p&gt;

&lt;p&gt;Other than that I don't think I miss any apps except perhaps &lt;a href="https://panic.com/transmit/"&gt;Transmit&lt;/a&gt; for FTP. I don't use FTP that much though so that's fine.&lt;/p&gt;

&lt;p&gt;Even though my employer pays for the apps I need I still find it refreshing that every single good application is not behind a paywall. Free software and all that you know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminal emulators
&lt;/h2&gt;

&lt;p&gt;On my Mac I like to use &lt;a href="https://iterm2.com/version3.html"&gt;Iterm&lt;/a&gt; with the sliding-from-the-top feature to access my terminal with a single keyboard shortcut. Turns out that in the Linux world there are plenty of alternatives to solve the same problem. I'm currently using &lt;a href="https://ostechnix.com/tilda-highly-configurable-gtk-based-drop-terminal-unix-like-systems/"&gt;Tilda&lt;/a&gt; for this. It has the same slide-down feature as Iterm and seems to be an overall great terminal emulator. The only issue for me so far is that Tilda doesn't seem to have a "normal window" feature. It is all about the slide thing. Iterm has both. This means that I use both Tilda and the Gnome terminal. A bit annoying but it works.&lt;/p&gt;

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

&lt;p&gt;I've been using the Ubuntu setup for about a month now and so far it has been a pleasant time. It has everything I need for my day to day development needs and the performance has been rock solid. I'm not quite ready to get a "Linux laptop" yet though but we'll see where the Macbooks go once they start using their own ARM CPUs. Perhaps I'll switch once I need to upgrade my space heater to something better.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>ubuntu</category>
      <category>mac</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Autocomplete in Ruby on Rails using Stimulus</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Mon, 10 Aug 2020 15:00:00 +0000</pubDate>
      <link>https://dev.to/mskog/autocomplete-in-ruby-on-rails-using-stimulus-do7</link>
      <guid>https://dev.to/mskog/autocomplete-in-ruby-on-rails-using-stimulus-do7</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rKxkwg2U--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/photo-1534278931827-8a259344abe7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rKxkwg2U--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/photo-1534278931827-8a259344abe7.jpeg" alt="Autocomplete in Ruby on Rails using Stimulus"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are going to build a simple application in Ruby on Rails to autocomplete Reddit subreddits.  If you want to view the completed application first then it is available on &lt;a href="https://autocomplete-stimulus-example.herokuapp.com/"&gt;here&lt;/a&gt;.  The code is available on &lt;a href="https://github.com/mskog/autocomplete_stimulus_example"&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The finished application will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8FWYKyj7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-27_at_21.24.31-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8FWYKyj7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-27_at_21.24.31-1.png" alt="Autocomplete in Ruby on Rails using Stimulus"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will be using &lt;a href="https://stimulusjs.org/"&gt;Stimulus&lt;/a&gt; for this example. Stimulus is a simple Javascript framework that uses the HTML in the application. It is not a virtual DOM framework like React or Vue. I think it works very well when you need something more advanced than say Alpine.js but don't want to move to React or Vue. It works very well with Turbolinks as well so its a great fit for many applications.&lt;/p&gt;

&lt;p&gt;Generate a new Rails application using &lt;a href="https://github.com/rails/webpacker"&gt;webpacker&lt;/a&gt;. Make sure you have Rails 6 installed(&lt;code&gt;rails -v&lt;/code&gt;). As this application will not use a database then we can skip it as well.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rails new autocomplete_stimulus --webpack --skip-active-record&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Install Stimulus:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn add stimulus&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We also need autocomplete.js and axios:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn add autocomplete.js axios&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;There are many libraries out there for doing simple autocomplete and typeahead stuff. I am a big fan of &lt;a href="https://github.com/algolia/autocomplete.js"&gt;autocomplete.js by Algolia&lt;/a&gt;. You don't need to use &lt;a href="https://www.algolia.com/"&gt;Algolia&lt;/a&gt; to use autocomplete.js however and it comes with all the bells and whistles like debounce support.&lt;/p&gt;

&lt;p&gt;Add the following to &lt;code&gt;app/javascript/packs/application.js&lt;/code&gt; to get Stimulus going. Make sure that you don't delete any of the other content.&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;Application&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;definitionsFromContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stimulus/webpack-helpers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;application&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&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;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../stimulus/controllers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;.js$/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;definitionsFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Go into &lt;code&gt;config/routes.rb&lt;/code&gt; and make sure it looks something 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="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="ss"&gt;:home&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:show&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'home#show'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;We will be using &lt;code&gt;home#show&lt;/code&gt; for our main page so create &lt;code&gt;app/controllers/home_controller.rb&lt;/code&gt; and add the following code to it:&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;class&lt;/span&gt; &lt;span class="nc"&gt;HomeController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&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;Finally we'll need a template for the HTML. Create &lt;code&gt;app/views/home/show.html.erb&lt;/code&gt; and add the following:&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;style=&lt;/span&gt;&lt;span class="s"&gt;"margin: auto; width: 25%; padding: 10px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Autocomplete&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;data-target=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete.field"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Here you can see Stimulus in action. The &lt;code&gt;data-controller&lt;/code&gt; is where Stimulus will start working. The value of the data attribute is important as that corresponds to a controller named &lt;code&gt;autocomplete_controller&lt;/code&gt;. Let's create it now! Create the directory tree and file structure like so: &lt;code&gt;app/javascript/stimulus/controllers/autocomplete_controller.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Mind that the naming of the controller is very important in Stimulus so make sure you get it right.&lt;/p&gt;

&lt;p&gt;Lets start with some boilerplate code in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="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="s2"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&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="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="s2"&gt;field&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;targets&lt;/code&gt; array contains all the targets of this Stimulus controller. As you can see it is what we added to the &lt;code&gt;input&lt;/code&gt; tag in the template. Now we need to setup the Autocomplete part in the controller. The controller should now look something like this:&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="s2"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autocomplete.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&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="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="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nx"&gt;connect&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;ac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&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;fieldTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hint&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;source&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;search&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;connect&lt;/code&gt; method is run whenever the controller is hooked up to the view. To access the field target we use &lt;code&gt;this.fieldTarget&lt;/code&gt;. Mind that &lt;code&gt;this.search()&lt;/code&gt; doesn't exist yet. Lets create it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="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="s2"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autocomplete.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&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="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="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}])&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;connect&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;ac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&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;fieldTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hint&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;source&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;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Mind that we also imported Axios to perform HTTP requests. &lt;code&gt;source&lt;/code&gt; expects a function so the search-function will return a function.&lt;/p&gt;

&lt;p&gt;We are now ready to give this a try. Fire up the Rails server with &lt;code&gt;rails server&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you go to localhost:3000 you should now see something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZMgLZNDA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-25_at_12.03.28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZMgLZNDA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-25_at_12.03.28.png" alt="Autocomplete in Ruby on Rails using Stimulus"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you type something in the input field you should see "Hello" as a suggestion. Stimulus is now working. Excellent!&lt;/p&gt;

&lt;p&gt;Now lets move on to actually wiring this up with the backend.&lt;/p&gt;

&lt;p&gt;Go to &lt;code&gt;config/routes.rb&lt;/code&gt; and make it look 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="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="ss"&gt;:home&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:show&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="ss"&gt;:autocomplete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:show&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'home#show'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;We now need to create a controller for the route. Create the file &lt;code&gt;app/controllers/autocompletes_controller.rb&lt;/code&gt; and add the following to it:&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;class&lt;/span&gt; &lt;span class="nc"&gt;AutocompletesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'Foo'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'Bar'&lt;/span&gt; &lt;span class="p"&gt;}&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="n"&gt;data&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;We now have a placeholder controller. Now all we have to do is add this to our Stimulus controller to make it request the autocomplete from the server. Change your Stimulus controller to look like this:&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="s2"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autocomplete.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&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="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="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/autocomplete&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;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;connect&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;ac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&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;fieldTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hint&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;source&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;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autocomplete:selected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;ac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autocomplete&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setVal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;We have imported &lt;code&gt;axios&lt;/code&gt; and changed the &lt;code&gt;search&lt;/code&gt; function to perform requests instead of having placeholder data.&lt;/p&gt;

&lt;p&gt;If you try this in the browser it should now always return "Foo" and "Bar". Now lets do it for real and autocomplete subreddit names shall we. I like to use the &lt;code&gt;HTTP&lt;/code&gt; gem for http requests in my Rails application so install that by adding the following to your Gemfile:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gem "http"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Don't forget to restart your rails server after adding the gem.&lt;/p&gt;

&lt;p&gt;Change your &lt;code&gt;AutocompleteController&lt;/code&gt; to perform requests to Reddit:&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;class&lt;/span&gt; &lt;span class="nc"&gt;AutocompletesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://www.reddit.com/subreddits/search.json?q=&lt;/span&gt;&lt;span class="si"&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;:query&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;limit=10"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;subreddits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;"children"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&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;subreddit&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;subreddit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"display_name"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;subreddit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;fetch&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="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;subreddit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"public_description"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;subreddits&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;This should be it. If you try this in your browser it should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tRw8hJQr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-25_at_12.24.51.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tRw8hJQr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-25_at_12.24.51.png" alt="Autocomplete in Ruby on Rails using Stimulus"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It works! Looks real bad though. Lets add some styling to fix it.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;app/assets/stylesheets/style.scss&lt;/code&gt; and add the following to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.algolia-autocomplete&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.algolia-autocomplete&lt;/span&gt; &lt;span class="nc"&gt;.aa-input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.algolia-autocomplete&lt;/span&gt; &lt;span class="nc"&gt;.aa-hint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.algolia-autocomplete&lt;/span&gt; &lt;span class="nc"&gt;.aa-hint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.algolia-autocomplete&lt;/span&gt; &lt;span class="nc"&gt;.aa-dropdown-menu&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="mh"&gt;#999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.algolia-autocomplete&lt;/span&gt; &lt;span class="nc"&gt;.aa-dropdown-menu&lt;/span&gt; &lt;span class="nc"&gt;.aa-suggestion&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.algolia-autocomplete&lt;/span&gt; &lt;span class="nc"&gt;.aa-dropdown-menu&lt;/span&gt; &lt;span class="nc"&gt;.aa-suggestion.aa-cursor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#b2d7ff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.algolia-autocomplete&lt;/span&gt; &lt;span class="nc"&gt;.aa-dropdown-menu&lt;/span&gt; &lt;span class="nc"&gt;.aa-suggestion&lt;/span&gt; &lt;span class="nt"&gt;em&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now try again!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--S5pMBC-4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-27_at_21.02.02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--S5pMBC-4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-27_at_21.02.02.png" alt="Autocomplete in Ruby on Rails using Stimulus"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A bit better! Of course, this still looks bad and will need some more love to be used for real.&lt;/p&gt;

&lt;p&gt;As an added bonus you can easily change the Stimulus controller so that it gets its API URL from a data attribute instead of having it hard coded in the controller itself. This way you can reuse the Stimulus controller for autocompletes all over the place!&lt;/p&gt;

&lt;p&gt;All it takes is to change the HTML to this:&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;style=&lt;/span&gt;&lt;span class="s"&gt;"margin: auto; width: 25%; padding: 10px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Autocomplete&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete"&lt;/span&gt; &lt;span class="na"&gt;data-autocomplete-url=&lt;/span&gt;&lt;span class="s"&gt;"/autocomplete"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;data-target=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete.field"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Note that data attribute that holds the URL. It is also a &lt;a href="https://stimulusjs.org/reference/data-maps"&gt;Stimulus thing&lt;/a&gt; so make note of it. Now change the Stimulus controller like so:&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="s2"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autocomplete.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&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="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="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="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="nx"&gt;connect&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;ac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&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;fieldTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hint&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;source&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;source&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autocomplete:selected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;ac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autocomplete&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setVal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;But what if you want to have some other rendering of the suggestions? Perhaps you would like to add some images or a nice description or something? There are a number of ways to do this. For example, you can use a simple Javascript template string with some HTML in the Stimulus controller and return that in the &lt;code&gt;suggestion&lt;/code&gt; function. This works but its real dirty to deal with it once the string gets &lt;a href="http://complicated.It"&gt;c&lt;/a&gt;omplicated. You could also add some kind of Javascript template system like Mustache in separate files and import those in your controller using a metaprogrammed data attribute. I prefer to keep the suggestion next to the rest of the autocomplete form, so I will show you a way to do it using &lt;a href="https://github.com/janl/mustache.js"&gt;Mustache&lt;/a&gt; and script tags. Mind that you don't really need Mustache for this but it makes things a bit cleaner.&lt;/p&gt;

&lt;p&gt;Start by installing Mustache:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn add mustache&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now, change your HTML to this:&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;style=&lt;/span&gt;&lt;span class="s"&gt;"margin: auto; width: 25%; padding: 10px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Autocomplete&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete"&lt;/span&gt; &lt;span class="na"&gt;data-autocomplete-url=&lt;/span&gt;&lt;span class="s"&gt;"/autocomplete"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;data-target=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete.suggestionTemplate"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/template"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h2&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;data-target=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete.field"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Note the new &lt;code&gt;script&lt;/code&gt; tag. The browser will not render that so it's safe to use. The curly braces are Mustache tags which will correspond to the JSON data from our autocomplete controller on the Ruby side. We also added a new target data attribute to be able to access it from Stimulus. Lets do that now:&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="s2"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autocomplete.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Mustache&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mustache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&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="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="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suggestionTemplate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="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="nx"&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="nx"&gt;suggestionTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;suggestionTemplateTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&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;ac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;autocomplete&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;fieldTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hint&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;source&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;source&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Mustache&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="nx"&gt;suggestionTemplate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;suggestion&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="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autocomplete:selected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;ac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autocomplete&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setVal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;We've added Mustache, the new target and a new way to render suggestions using our new template. It will look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2vyrn0Jn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-27_at_21.24.31.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2vyrn0Jn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/08/Screenshot_2020-07-27_at_21.24.31.png" alt="Autocomplete in Ruby on Rails using Stimulus"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ok that looks terrible but you get the idea. Do note that you can still reuse the Stimulus controller for other autocomplete fields. Simply add a new template and URL!&lt;/p&gt;

&lt;p&gt;There you have it! A simple way to add a reusable Stimulus component to your Rails applications. The complete application application is available on &lt;a href="https://autocomplete-stimulus-example.herokuapp.com/"&gt;Heroku&lt;/a&gt; and the code is available on &lt;a href="https://github.com/mskog/autocomplete_stimulus_example"&gt;Github&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>stimulus</category>
      <category>beginners</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Break bad habits by making them annoying</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Wed, 01 Jul 2020 17:44:08 +0000</pubDate>
      <link>https://dev.to/mskog/break-bad-habits-by-making-them-annoying-3mn5</link>
      <guid>https://dev.to/mskog/break-bad-habits-by-making-them-annoying-3mn5</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NXhF1y6A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/06/filip-mishevski-c9-ESkwMzvo-unsplash.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NXhF1y6A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/06/filip-mishevski-c9-ESkwMzvo-unsplash.jpg" alt="Break bad habits by making them annoying"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Breaking bad habits can be really hard. &lt;a href="https://www.youtube.com/watch?v=k2Wcu6aGyz8&amp;amp;t=99s"&gt;Willpower alone is often not enough&lt;/a&gt; to get the job done or it simply takes too much of it. Willpower is also often regarded as finite. If you spend all of it on forcing yourself to not go on Twitter then you won't have enough of it left to make yourself to go to the gym later. I often fall into this trap myself. I can quite easily make myself avoid going on social media first thing in the morning but then I am rested and my mind is fresh. After a long day of work it is a completely different story. It's so easy to reach for my phone while waiting for my dinner to get ready(those pizza rolls take forever!). I'm tired, hungry and the work day has taken all of my effort.&lt;/p&gt;

&lt;p&gt;I've illustrated this in this graph that I made myself:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AvWdEcNV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/06/energy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AvWdEcNV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/06/energy.png" alt="Break bad habits by making them annoying"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For me willpower alone only works when there is some kind of reward or tangible thing waiting at the end of the road. "Stop visiting Twitter in excess and you'll get more things done" is too abstract for me. Yesterday-me is not the boss of me; I'm going on Twitter! Since most of the bad habits don't have any immediate rewards for breaking them then I had to think of something else.&lt;/p&gt;

&lt;p&gt;What I like to do instead is to make the bad habits harder.  Solving the phone problem after work for example is easy. Just turn the phone off and put it on the top of a bookcase or something. Now I have to go get a stool AND wait for the phone to boot. (Do not climb Billy bookcases kids!).  Now it's suddenly annoying to get to my phone to check social media or something so now it is much easier to avoid it.&lt;/p&gt;

&lt;p&gt;This article is mostly about how to break the habit of visiting "bad" sites while on your computer but some of these also work for your phone. Let us start with a couple of generic tips shall we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not keep your phone next to you while working. &lt;a href="https://hbr.org/2018/03/having-your-smartphone-nearby-takes-a-toll-on-your-thinking"&gt;Research has shown&lt;/a&gt; that simply having your phone within reach is enough to drain your energy. Keep your phone in another room or use the Billy idea from earlier.&lt;/li&gt;
&lt;li&gt;Do not keep your phone next to your bed if you can avoid it. Buy an alarm clock or a Google Nest and use that instead to wake you up. You can also have your spouse pour cold water on you if you prefer the human touch. I recommend the Google Nest because it has the added benefit of listening to you while you sleep.&lt;/li&gt;
&lt;li&gt;Do not go on social media first thing in the morning. This can ruin the whole day. It can also make you reactive rather than proactive. You don't want to spend your morning angry about something on Twitter while you are trying to plan your day.&lt;/li&gt;
&lt;li&gt;If you know in your heart that you will eventually cave and check your phone then make a game of it. Challenge yourself to stay off your phone for as long as possible during the day. If you can get to say 4 pm then you have done good and probably got a lot of productive hours out of it. Now break that record tomorrow! Once you check your phone it becomes a lot more likely that you will do it again later so try your best to stay away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now on to the tips I have about making bad habits annoying. The idea is that if you make the bad habits annoying enough then you will train your brain to avoid them. If you know that you have to jump through hoops just to go on Reddit then you might just skip it. I use a couple of tools and tricks to make this easier:&lt;/p&gt;

&lt;h2&gt;
  
  
  Motion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.inmotion.app/"&gt;Motion&lt;/a&gt; is a Chrome extension that nag you whenever you visit a "bad" site. If you visit Twitter for example a modal will pop up telling you to cut it out already. You can then choose to have access to the site for a number of minutes and then the modal will pop up again. Motion also has special functionality to hide Twitter recommendations, Youtube recommended videos, and more.  You can also set work hours so that Motion is only active when you are trying to get things done.&lt;/p&gt;

&lt;p&gt;I like Motion and it is my primary way to nag me to stop visiting bad sites but there are a couple of issues. My brain has gotten used to clicking away the nag screen so the annoyance factor has faded. Motion will also have the popup on your screen but you can still see the site behind it. This means that you can easily spot something interesting behind the nag screen that will make you want to use the site.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use Blocksite to block sites on Chrome and on mobile
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://chrome.google.com/webstore/detail/block-site-website-blocke/eiimnmioipafcokbfikbljfdeojpcgbh?hl=en"&gt;Blocksite&lt;/a&gt; is a Chrome extension that blocks any websites you want. It can also be synced with your phone so that the same sites are blocked there as well. You can put in password protection to keep your from disabling it whenever you want to go on Twitter. It works well to keep you off the worst offending sites.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dKR4joID--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/07/Screenshot-2020-07-01-at-19.33.35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dKR4joID--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/07/Screenshot-2020-07-01-at-19.33.35.png" alt="Break bad habits by making them annoying"&gt;&lt;/a&gt;Blocksite blocking a site. I like the picture. Has a real "come on, wtf are you doing?" vibe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improvement: Store the blocksite password in a place that is hard to get to
&lt;/h3&gt;

&lt;p&gt;I found that it was much too easy for me to just punch in the Blocksite admin password and unblock Twitter if I wanted to go there. What I did to solve this was to create a new database in my &lt;a href="https://keepass.info/"&gt;password manager&lt;/a&gt; and put the password in there. I then put the password manager master password in a text file and put that on one of my servers. This way I had to be on my work computer to access the Blocksite settings since that is the only one with SSH access to the servers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use a Pi-hole to block the sites
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pi-hole.net/"&gt;Pi-hole is an ad-blocking DNS server&lt;/a&gt; that you can run on a tiny computer inside your own network such as a Raspberry Pi. Its main feature is network-wide ad blocking but it can also be used to block sites in general. The latest version of it also added functionality to block sites on only some devices. Now you can use it to block things on your own devices only while it will leave other devices alone. This is my go-to method for blocking distracting sites since it works on both my computers and my phone as long as they are connected to my home network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improvement: Store the admin password for the Pi-hole in a place that is hard to get to
&lt;/h3&gt;

&lt;p&gt;This is the same deal as the blocksite one. Store the admin password on a server or something to keep you from getting to it easily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improvement: Use a throwaway Google account to store the password
&lt;/h3&gt;

&lt;p&gt;This one is for emergency use only! I created a new Google account and put the password in there. I then changed the password for the account to some random garbage string and didn't write it down. I now have no access to the account at all. The only way to get access back is to use Google account recovery which I connected to my main email account.  The magic part is that Google has a 24-48 hour quarantine on account recovery. It will thus take that long to reset your account and get access. This was so annoying to me that I completely stopped using the bad sites.&lt;/p&gt;




&lt;h2&gt;
  
  
  Miscellaneous tips
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use a habit chain
&lt;/h3&gt;

&lt;p&gt;Made famous by comedian Jerry Seinfeld. It is a technique he used to make sure that he was writing every single day. He tried to make an endless chain of days on which he worked on his material. He found that if there was a long streak of days behind him then it made it much easier to force himself to work on it today as well. This can be adapted for negative habits. You can use a simple calendar or an app to track it. There a multiple such apps, just search for "don't break the chain app" and you'll find plenty. Simply put in "Don't visit Twitter during productive hours" and then keep that chain going. Making yourself accountable to &lt;em&gt;something&lt;/em&gt; is often a useful technique.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge yourself
&lt;/h3&gt;

&lt;p&gt;30 day challenge: Don't visit Reddit at all for 30 days. This can be kind of fun but to me it is especially effective if you combine it with journaling. Write down your thoughts about the site you want to avoid every day in a journal. Are you happy that you can't go on Reddit? Do you miss the cats? Once again I find that making this a challenge only in your mind is not very effective. You need something tangible like a journal.&lt;/p&gt;

</description>
      <category>productivity</category>
    </item>
    <item>
      <title>Hetzner Cloud Review: Revisited in 2020</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Thu, 18 Jun 2020 12:00:00 +0000</pubDate>
      <link>https://dev.to/mskog/hetzner-cloud-review-revisited-in-2020-2jii</link>
      <guid>https://dev.to/mskog/hetzner-cloud-review-revisited-in-2020-2jii</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Z_kjR54Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/06/science-in-hd-3kgzvab8SMg-unsplash.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Z_kjR54Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/06/science-in-hd-3kgzvab8SMg-unsplash.jpg" alt="Hetzner Cloud Review: Revisited in 2020"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It has now been well over two years since my &lt;a href="https://www.mskog.com/posts/hetzner-cloud-a-quick-review/"&gt;Hetzner Cloud Review&lt;/a&gt; and I thought it was time to post an updated one. I am still a happy customer at Hetzner. There has been quite a few updates since then. Hetzner Cloud now has an even nicer interface, block storage, private networks, dedicated CPU offerings and a brand new server line with AMD EPYC processors. They also have a data center in Finland now.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to give Hetzner a try then feel free to use my &lt;a href="https://hetzner.cloud/?ref=GjnpYTNYtgRU"&gt;referral code&lt;/a&gt; to get €20 in credit to use for servers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Back in 2018 Hetzner Cloud was in my humble opinion the best value VPS provider out there. I believe that this might still be true today. There are a couple of providers now which come close price wise but overall I still think Hetzner is #1.&lt;/p&gt;

&lt;p&gt;My current main VPS is a CX21 with 2 CPU cores, 8GB RAM and 80GB of NVM disk. It is running quite a few things and has been chugging along now for a long time. I've had no serious downtime and only a couple of reboots here and there. Overall it has been rock solid and performance has been excellent. One of the fears of 2018 was that since Hetzner Cloud was new it would quickly get oversold and slow down considerably. I'm happy to say that this is not the case and there has been no evidence of any noisy neighbours on any of my servers.&lt;/p&gt;

&lt;p&gt;I like to compare things to &lt;a href="https://www.digitalocean.com/?refcode=457e363d283d&amp;amp;utm_campaign=Referral_Invite&amp;amp;utm_medium=Referral_Program&amp;amp;utm_source=CopyPaste"&gt;DigitalOcean&lt;/a&gt; since they are really the servers to beat. They are simple, have great features and lots of goodies like Block storage, Hosted databases, and managed Kubernetes.&lt;/p&gt;

&lt;p&gt;Let's compare the cheapest servers available to get started. I ran a couple of quick &lt;a href="https://www.geekbench.com/"&gt;Geekbench 5&lt;/a&gt; benchmarks to gauge the performance of each server as well.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Hetzner&lt;/th&gt;
&lt;th&gt;DigitalOcean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CX11&lt;/td&gt;
&lt;td&gt;Standard 1GB Droplet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Intel Skylake&lt;/td&gt;
&lt;td&gt;Intel Skylake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVM&lt;/td&gt;
&lt;td&gt;SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU MHz&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2100&lt;/td&gt;
&lt;td&gt;2300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Cores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk space&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20 GB&lt;/td&gt;
&lt;td&gt;25 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traffic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20 TB&lt;/td&gt;
&lt;td&gt;1 TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geographic location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Germany, Finland&lt;/td&gt;
&lt;td&gt;USA, Europe, Asia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$2.71&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geekbench (single core)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;648&lt;/td&gt;
&lt;td&gt;713&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geekbench (multi core)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;682&lt;/td&gt;
&lt;td&gt;710&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So what are we looking at here? With Hetzner you get twice the RAM, 20 times the transfer, slightly lower clock speed at almost half the price of the Digital Ocean droplet. Now, DigitalOcean has a lot more to offer like different kind of server types, managed databases, object storage, load balancers and so on. Hetzner only has their servers, block storage and private networking. However, it is bang-for-buck unquestionable that Hetzners offering remains exceptionally good.&lt;/p&gt;

&lt;h2&gt;
  
  
  AMD EPYC
&lt;/h2&gt;

&lt;p&gt;Hetzner has a new kind of VPS with &lt;a href="https://www.amd.com/en/products/epyc"&gt;AMD EPYC&lt;/a&gt; CPUs. These are brand new and became available in early 2020. These are essentially "souped up" versions of the other servers. Each of the previous servers has an EPYC counterpart. For example, here is a comparison between the CX31 and its EPYC sibling the CPX31. I also added a similar DigitalOcean droplet for comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CX31&lt;/th&gt;
&lt;th&gt;CPX31&lt;/th&gt;
&lt;th&gt;DigitalOcean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Intel Skylake&lt;/td&gt;
&lt;td&gt;AMD EPYC&lt;/td&gt;
&lt;td&gt;Intel Skylake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Cores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk space&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80 GB&lt;/td&gt;
&lt;td&gt;160 GB&lt;/td&gt;
&lt;td&gt;160 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traffic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20 TB&lt;/td&gt;
&lt;td&gt;20 TB&lt;/td&gt;
&lt;td&gt;5 TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$9.59&lt;/td&gt;
&lt;td&gt;$13.37&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geekbench(single core)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;729&lt;/td&gt;
&lt;td&gt;608&lt;/td&gt;
&lt;td&gt;605&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geekbench(multi core)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1431&lt;/td&gt;
&lt;td&gt;1822&lt;/td&gt;
&lt;td&gt;2077&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So what you get is twice the CPU cores and twice the disk space for less than 50% additional money. The performance per core seem to be lower on the EPYC architecture however. I did one final benchmark to compare the CPX31 to the next level of Skylake VPS:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CPX31&lt;/th&gt;
&lt;th&gt;CX41&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AMC EPYC&lt;/td&gt;
&lt;td&gt;Intel Skylake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Cores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk space&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;160 GB&lt;/td&gt;
&lt;td&gt;160 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$13.37&lt;/td&gt;
&lt;td&gt;$17.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geekbench (single core)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;608&lt;/td&gt;
&lt;td&gt;649&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geekbench (multi core)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1822&lt;/td&gt;
&lt;td&gt;2168&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So what we are looking at here is about 30% more money for 15% additional multi core performance and twice the RAM. This looks like a great upgrade path to me when you do run out of RAM on your EPYC server.&lt;/p&gt;

&lt;p&gt;What about other alternatives then? DigitalOcean is not the only VPS provider out there. I did a couple of other comparisons to popular VPS platforms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Hetzner&lt;/th&gt;
&lt;th&gt;&lt;a href="https://upcloud.com/"&gt;UpCloud&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://aws.amazon.com/lightsail/"&gt;Amazon Lightsail&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AMD EPYC&lt;/td&gt;
&lt;td&gt;Intel Skylake&lt;/td&gt;
&lt;td&gt;Intel Broadwell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Cores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;td&gt;8 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk space&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;160 GB&lt;/td&gt;
&lt;td&gt;160 GB&lt;/td&gt;
&lt;td&gt;160 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traffic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20 TB&lt;/td&gt;
&lt;td&gt;5 TB&lt;/td&gt;
&lt;td&gt;5 TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$13.37&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geekbench(single core)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;608&lt;/td&gt;
&lt;td&gt;639&lt;/td&gt;
&lt;td&gt;605&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geekbench(multi core)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1822&lt;/td&gt;
&lt;td&gt;2215&lt;/td&gt;
&lt;td&gt;2077&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Block storage
&lt;/h2&gt;

&lt;p&gt;Hetzner now also has block storage. They do not offer object storage however. The difference between block and object storage is that block storage is mountable as volumes while object storage is more like &lt;a href="https://aws.amazon.com/s3/"&gt;Amazon S3&lt;/a&gt;. Let's compare Hetzner to DigitalOcean once again. I also ran a couple of naive read/write tests to gauge performance. Please don't take these as exact numbers but as a rough estimate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Heztner&lt;/th&gt;
&lt;th&gt;DigitalOcean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage class&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SSD&lt;/td&gt;
&lt;td&gt;SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Replication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Minimum volume size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maximum volume size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10 TB&lt;/td&gt;
&lt;td&gt;16 TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maximum volumes per server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;309 MB/s&lt;/td&gt;
&lt;td&gt;310 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Write performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;284 MB/s&lt;/td&gt;
&lt;td&gt;168 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price/GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.04&lt;/td&gt;
&lt;td&gt;$0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hetzner is about half the price of DigitalOcean while performance remains excellent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unfair comparison
&lt;/h2&gt;

&lt;p&gt;So we have established that the price/performance ratio of the Hetzner VPS offering is superior to popular platforms such as DigitalOcean, but that is not an entirely fair comparison. DigitalOcean has a lot more to offer than just "bare metal" virtual servers. If you bear with me here we are going to do one final table to compare features:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Hetzner&lt;/th&gt;
&lt;th&gt;DigitalOcean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Block storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private networking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snapshots&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Floating IPs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server graphs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Object storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CDN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Load balancers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Managed databases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Managed Kubernetes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DNS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker registry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data centers on multiple continents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DigitalOcean also offers flexible server plans with better CPU or more RAM and such. They also have managed databases as well as Kubernetes. In my opinion the most important part is that DigitalOcean offers deployment on other continents than Europe. If you choose Hetzner you can either deploy your servers in Germany or Finland. On DigitalOcean you have the option of New York, Amsterdam, San Francisco, Singapore, London, Frankfurt, Toronto and Bangalore.&lt;/p&gt;

&lt;p&gt;I don't know what Hetzner is planning on doing in the future, but I don't think it is reasonable to expect them to introduce features like managed databases. Hetzner does have some &lt;a href="https://www.hetzner.com/managed-server"&gt;managed servers&lt;/a&gt; but the vast majority of their services are "bare metal" servers. I wouldn't hold out for any kind of expansion into other continents either, especially not these days.&lt;/p&gt;

&lt;p&gt;There are some others offering competitive pricing like &lt;a href="https://www.ovhcloud.com/en/vps/"&gt;OVH&lt;/a&gt; and &lt;a href="https://genesishosting.com/genesis_public_cloud_vm_pricing_table.html"&gt;Genesis&lt;/a&gt;. I might take a look at these in a future post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Hetzner Cloud remains a very competitive service when you need a "bare metal" VPS at the lowest possible price and you still get amazing performance. If you're ok with hosting your applications in Europe and you don't need anything fancy like managed Kubernetes then you simply cannot go wrong with Hetzner. I remain a happy customer myself and I see no reason at all to look elsewhere at this time.&lt;/p&gt;

</description>
      <category>hosting</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Self-hosted tools for web development</title>
      <dc:creator>Magnus Skog</dc:creator>
      <pubDate>Mon, 18 May 2020 14:20:00 +0000</pubDate>
      <link>https://dev.to/mskog/self-hosted-tools-for-web-development-9kh</link>
      <guid>https://dev.to/mskog/self-hosted-tools-for-web-development-9kh</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gYw8f9eH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/devn-JmmXKlJ8MKQ-unsplash.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gYw8f9eH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/devn-JmmXKlJ8MKQ-unsplash.jpg" alt="Self-hosted tools for web development"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having some self-hosted services and tools can make your life as a developer, and your life in general, much easier. I will share some of my favorites in this post. I use these for just about every project I make and they really make my life easier.&lt;/p&gt;

&lt;p&gt;All of this, except OpenFaaS, is hosted on a single VPS with 2 CPU cores, 8GB of RAM and 80GB SSD with plenty of capacity to spare.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you need a cheap and solid server to run all this on then feel free to use my &lt;a href="https://hetzner.cloud/?ref=GjnpYTNYtgRU"&gt;referral code&lt;/a&gt; to get €20 in credit at Hetzner. Read my &lt;a href="https://dev.to/mskog/hetzner-cloud-review-revisited-in-2020-2jii"&gt;review&lt;/a&gt; for more information. I use the CX31 model for all of this but you can probably get away with something cheaper.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/huginn/huginn"&gt;Huginn&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Huginn is an application for building automated agents. You can think of it like a self hosted version of &lt;a href="https://zapier.com/home"&gt;Zapier&lt;/a&gt;. To understand Huginn you have to understand two concepts: Agens and Events. An Agent is a thing that will do something. Some Agents will scrape a website while others post a message to Slack. The second concept is an Event. Agents emit Events and Agents can also receive Events.&lt;/p&gt;

&lt;p&gt;As an example you can have a Huginn agent check the local weather, then pass that along as an event to another agent which checks if it is going to rain. If it is going to rain the rain checker agent will pass the event along, otherwise it will be discarded. A third agent will receive an event from the second agent and then it will send a text message to your phone telling you that it is going to rain.&lt;/p&gt;

&lt;p&gt;This is barely scratching the surface of what Huginn can do though. It has agents for everything: Sending email, posting to slack, IoT support with MQTT, website APIs, scrapers, and much more. You can have agents which receive inputs from custom web hooks and cron-like agents which schedules other agents and so on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qOYxirTv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/BGyg19qU.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qOYxirTv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/BGyg19qU.png" alt="Self-hosted tools for web development"&gt;&lt;/a&gt;The Huginn interface&lt;/p&gt;

&lt;p&gt;Huginn is a Ruby on Rails application and can be hosted in Docker. I host mine on &lt;a href="https://www.mskog.com/posts/heroku-vs-self-hosted-paas/"&gt;Dokku&lt;/a&gt;. I use it for so many things and it is truly the base of all my automation needs. Highly recommended! If you are looking for alternatives then you can take a look at &lt;a href="https://nodered.org/"&gt;Node-RED&lt;/a&gt; and &lt;a href="https://github.com/muesli/beehive"&gt;Beehive&lt;/a&gt;. I don't have personal experience with either though.&lt;/p&gt;

&lt;p&gt;Huginn uses about 350MB of RAM on my server, including the database and the background workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/thumbor/thumbor"&gt;Thumbor&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Thumbor is a self-hosted image proxy like &lt;a href="https://www.imgix.com/"&gt;Imgix&lt;/a&gt;. It can do all sorts of things with a single image URL. Some examples:&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple caching proxy
&lt;/h3&gt;

&lt;p&gt;Take the URL and put your Thumbor URL in front like so:&lt;br&gt;&lt;br&gt;
&lt;a href="https://thumbs.mskog.com/https://images.pexels.com/photos/4048182/pexels-photo-4048182.jpeg"&gt;https://thumbs.mskog.com/https://images.pexels.com/photos/4048182/pexels-photo-4048182.jpeg&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Simple enough. Now you have a version of the image hosted on your proxy. This is handy for example when you don't want to hammer the origin servers with requests when linking to the image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resizing
&lt;/h3&gt;

&lt;p&gt;That image is much too large. Lets make it smaller! &lt;a href="https://thumbs.mskog.com/https://images.pexels.com/photos/4048182/pexels-photo-4048182.jpeg"&gt;https://thumbs.mskog.com/800x600/https://images.pexels.com/photos/4048182/pexels-photo-4048182.jpeg&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DWQT9Kfw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/pexels-photo-4048182--1-.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DWQT9Kfw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/pexels-photo-4048182--1-.jpeg" alt="Self-hosted tools for web development"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Much smaller. Note that all we had to do is add the desired format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resizing to specific height or width
&lt;/h3&gt;

&lt;p&gt;What about a specific width while keeping the aspect ratio? No problem!&lt;br&gt;&lt;br&gt;
 &lt;a href="https://thumbs.mskog.com/300x0/https://images.pexels.com/photos/4048182/pexels-photo-4048182.jpeg"&gt;https://thumbs.mskog.com/300x/https://images.pexels.com/photos/4048182/pexels-photo-4048182.jpeg&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Q3oelrjn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/pexels-photo-4048182--2-.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Q3oelrjn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/pexels-photo-4048182--2-.jpeg" alt="Self-hosted tools for web development"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Quality
&lt;/h3&gt;

&lt;p&gt;Smaller file size?&lt;br&gt;&lt;br&gt;
&lt;a href="https://thumbs.mskog.com/1920x/filters:quality(10)/https://images.pexels.com/photos/4048182/pexels-photo-4048182.jpeg"&gt;https://thumbs.mskog.com/1920x/filters:quality(10)/https://images.pexels.com/photos/4048182/pexels-photo-4048182.jpeg&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0njvdoQ2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/pexels-photo-4048182--3-.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0njvdoQ2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/pexels-photo-4048182--3-.jpeg" alt="Self-hosted tools for web development"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You get the idea! Thumbor also has a &lt;a href="https://thumbor.readthedocs.io/en/latest/filters.html?highlight=filters#available-filters"&gt;bunch of other filters&lt;/a&gt; like making the image black and white, changing the format and so on. It is very versatile and is useful in more scenarios then I can count. I use it for all my images in all my applications. Thumbor also has client libraries for a lot of languages such as &lt;a href="https://www.npmjs.com/package/thumbor"&gt;Node&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thumbor is a Python application and is most easily hosted using Docker. There are a number of great projects on Github that have Docker compose setups for Docker. I use &lt;a href="https://github.com/MinimalCompact/thumbor/blob/master/recipes/docker-compose/remotecv/docker-compose.yml"&gt;this one&lt;/a&gt;. It comes with a built-in Nginx proxy for caching. All the images will be served through an Nginx cache, both on disk and in memory by default. This means that only the first request for an image will hit Thumbor itself. Any requests after that will only hit the Nginx cache and will thus be very fast.&lt;/p&gt;

&lt;p&gt;To make it even faster you can deploy a CDN in front of your Thumbor server. If your site is on &lt;a href="https://www.cloudflare.com/"&gt;Cloudflare&lt;/a&gt; you can use theirs for free. Just keep in mind that Cloudflare will not be happy if you just use their CDN to cache a very large number of big images. You can of course use &lt;a href="http://cdncomparison.com/"&gt;any other CDN like Cloudfront&lt;/a&gt;. My entire Thumbor stack takes up about 200MB of RAM.&lt;/p&gt;

&lt;p&gt;In conclusion I think that Thumbor is a vital part of my self hosted stack and I use it every single time I need to show images on any website or app. Once you have this working properly you never have to worry about image formatting ever again since the Thumbor is always there.&lt;/p&gt;

&lt;p&gt;Hosted alternatives to Thumbor: &lt;a href="https://www.imgix.com/"&gt;Imgix&lt;/a&gt;, &lt;a href="https://cloudinary.com/"&gt;Cloudinary&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Self hosted alternatives: &lt;a href="https://github.com/h2non/imaginary"&gt;Imaginary&lt;/a&gt;, &lt;a href="https://github.com/imazen/imageflow"&gt;Imageflow&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Searx
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://searx.me/"&gt;Searx&lt;/a&gt; is a self hosted metasearch engine. It will strip any identifying headers and such from your searches and then it will use one or many search engines to run your query. It can search on for example Google, Bing and DuckDuckGo. What makes Searx great as a self hosted service is that it has a simple JSON api. Simply tell it to use JSON and your query will be returned as JSON. This will enable some pretty neat combinations, but more of that later. It can also search for images, music, news and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HGJw5xXn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/6J9MbA2A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HGJw5xXn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/6J9MbA2A.png" alt="Self-hosted tools for web development"&gt;&lt;/a&gt;Searx in action&lt;/p&gt;

&lt;p&gt;This is another killer service. The JSON formatting is what really sells it for me since it can be combined with other services in lots of different ways.&lt;/p&gt;

&lt;p&gt;Searx is another Python app and is easily hosted through the use of the &lt;a href="https://hub.docker.com/r/searx/searx"&gt;official Docker image&lt;/a&gt;. It uses about 230MB of RAM on my server.&lt;/p&gt;

&lt;h2&gt;
  
  
  InfluxDB + Telegraf + Grafana
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.influxdata.com/"&gt;InfluxDB&lt;/a&gt; is a time series database. It is built to receive time based event data from sensors, servers and so on. For example it is very good at things like storing CPU load data every 5 seconds. It also has built-in ways to makes sure it doesn't fill the disk with all this data and much more. There are client libraries for most languages as well as a very simple &lt;a href="https://docs.influxdata.com/influxdb/v1.8/tools/api/"&gt;HTTP API&lt;/a&gt; for adding data. It goes very well together with Huginn where you can create agents to poll data from somewhere and then use the HTTP API in InfluxDB and a Post Agent to store it . There will be examples of this later on!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.influxdata.com/time-series-platform/telegraf/"&gt;Telegraf&lt;/a&gt; is a service that will collect data about your server and send it to InfluxDB. It can also send the data to other databases and such but for this we use InfluxDB. It can collect just about any data about a server that you want, include statistics from Docker containers. It has a very simple out of the box configuration that you can tweak if you wish. I install it on all my machines to send data to InfluxDB, including my at-home NAS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://grafana.com/"&gt;Grafana&lt;/a&gt; is a graphing, analytics, and monitoring tool. It can graph the data from many different sources including InfluxDB, AWS Cloudwatch and PostgreSQL. It also has alerting capabilities for Slack for example. It is a delight to use and you will quickly be able to create some very nice looking graphs of your data. You need to be careful though because I find that it is very addicting to graph all your things.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vfsG858P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/kjc8zTZo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vfsG858P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/kjc8zTZo.png" alt="Self-hosted tools for web development"&gt;&lt;/a&gt;A Grafana dashboard for server monitoring&lt;/p&gt;

&lt;p&gt;There are many Docker setups for this stack that you can find on Github, so hosting this is easy.&lt;/p&gt;

&lt;p&gt;Grafana is truly delightful to work with and it is probably the slickest graphing tool I've ever used and it can easily be compared to commercial projects like &lt;a href="https://datadoghq.eu/"&gt;Datadog&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Registry
&lt;/h2&gt;

&lt;p&gt;This is a simple one but oh so useful. It is good to have a place to store your own Docker images and this is what you need. You can use &lt;a href="https://hub.docker.com/pricing"&gt;Docker Hub&lt;/a&gt; for this but private images cost about $7 a month. It is however &lt;a href="https://docs.docker.com/registry/deploying/"&gt;very easy to host your own registry&lt;/a&gt;. A Docker registry is a requirement to be able to use OpenFaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ghost CMS
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ghost.org/"&gt;Ghost&lt;/a&gt; is an open source publishing and blogging platform. It was originally kind of a replacement for Wordpress but it has since grown to be something more. I use it as a &lt;a href="https://www.storyblok.com/tp/headless-cms-explained"&gt;headless CMS&lt;/a&gt; for this blog as well as other websites. It has a great GraphQL and REST API that you can use to pull your articles and pages out to use in a static site or show on another website. I have &lt;a href="https://www.mskog.com/posts/changing-my-blog-from-hugo-to-gatsby/"&gt;another article&lt;/a&gt; about how my blog works with this if you want to know more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w72Nq1Ud--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/dLmzxWs0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w72Nq1Ud--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3-eu-central-1.amazonaws.com/mskog-cms/2020/05/dLmzxWs0.png" alt="Self-hosted tools for web development"&gt;&lt;/a&gt;The Ghost editor while typing this&lt;/p&gt;

&lt;p&gt;Ghost has a &lt;a href="https://ghost.org/faq/using-the-editor/"&gt;great editor&lt;/a&gt; that makes it very easy to include Twitter posts, images, Spotify links, and so on. It is also hosted on your server so you can write from anywhere. You don't have to deal with markdown files if you don't want to and I find it to be a delight to use and write in.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenFaaS
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.openfaas.com/"&gt;OpenFaaS&lt;/a&gt; is self-hosted functions-as-a-service aka serverless. I have &lt;a href="https://www.mskog.com/posts/self-hosting-serverless-with-openfaas/"&gt;another article about OpenFaaS&lt;/a&gt; so I won't go into too much detail here. You can use OpenFaaS to easily deploy functions in any programming language without having to setup a microservice. Also, I understand the irony of self-hosting a &lt;strong&gt;serverless&lt;/strong&gt; setup, but it is a strange world we live in so just go with it.&lt;/p&gt;

&lt;p&gt;It is very useful for a number of tools and combinations. I have a number of these functions and here are some examples:&lt;/p&gt;

&lt;h3&gt;
  
  
  Readability
&lt;/h3&gt;

&lt;p&gt;Python function that uses the &lt;a href="https://pypi.org/project/newspaper3k/"&gt;newspaper3k&lt;/a&gt; library to pull out metadata and the article content from any URL. I use it to render snippets from articles, prepare for sentiment analysis, and things like that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Puppeteer renderer
&lt;/h3&gt;

&lt;p&gt;Sometimes websites will not work at all without Javascript or they have systems in place to prevent scraping and interacting with the sites automatically. &lt;a href="https://www.rottentomatoes.com/"&gt;Rotten Tomatoes&lt;/a&gt; is such a site that will fight back against any automation attempts. Enter &lt;a href="https://github.com/puppeteer/puppeteer"&gt;Puppeteer&lt;/a&gt;, the headless Chrome API. This function simply takes a URL, renders the page with Javascript and returns the resulting body. This is then ready for processing in a scraper for example using Huginn. There will be examples of how to use this later with Huginn later so stick around if you're interested in that.&lt;/p&gt;

&lt;p&gt;OpenFaaS is hosted on its own server because that made sense to me. It is a tiny little thing though and doesn't use much resources at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combinations
&lt;/h2&gt;

&lt;p&gt;This is where we unlock the real magic of having all these things. You can combine these in clever ways to create something really neat. Here are some examples to get you started:&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenFaaS+Searx = First image
&lt;/h3&gt;

&lt;p&gt;This is a combination that I really like. Create a function in OpenFaaS in any language of your choice, I used Javascript, that will search for the given query in Searx, making sure to return the result as JSON. Then parse the results in the OpenFaaS function and return the URL for the first image result.&lt;/p&gt;

&lt;p&gt;You now have a function that you can call with any query and it will return the first image result. This is useful in a number of different ways. You can for example search for &lt;code&gt;bryan cranston site:wikipedia.org&lt;/code&gt; to get a good image of actor Bryan Cranston. Now you can use some cool Thumbor filters and such to process the image if you want!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggestions for improvements:&lt;/strong&gt; Add more functionality to the OpenFaaS function. For example you can add &lt;a href="https://github.com/nodeca/probe-image-size"&gt;probe-image-size&lt;/a&gt; to your function. You can now reject images which are too small for example.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenFaaS+Huginn+Trello = Movie recommendations
&lt;/h3&gt;

&lt;p&gt;This is a simple one which adds movie recommendations to my &lt;a href="https://www.trello.com"&gt;Trello&lt;/a&gt; inbox daily.&lt;br&gt;&lt;br&gt;
Steps to create:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a Website Agent to Huginn. Use the URL for the Rotten Tomatoes front page and add the OpenFaaS Puppeteer function to render that URL with Javascript enabled. Scrape the section with new movies.
&lt;/li&gt;
&lt;li&gt;(Optional) Create a Trigger Agent in Huginn to select movies with a minimum score. Perhaps you want only the movies which have a score of 80 or better in your inbox.
&lt;/li&gt;
&lt;li&gt;Add a Post Agent to Huginn that will post the movie names to your Trello inbox using the &lt;a href="https://developer.atlassian.com/cloud/trello/"&gt;Trello API&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Suggestions for improvements:&lt;/strong&gt; Add another step that will also link to the &lt;a href="https://www.imdb.com/"&gt;IMDB&lt;/a&gt; page for the movie. You can use Searx for this. Simple search for the movie like so: &lt;code&gt;"fried green tomatoes" site:imdb.com&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  RescueTime+Huginn+InfluxDB+Grafana = Productivity graph
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.rescuetime.com"&gt;RescueTime&lt;/a&gt; is automatic time tracking software. It keeps track of what you do on your computer and will tell you when you are being productive and when you are slacking off on Reddit. You can use a Web Site Agent in Huginn to access your productivity data on RescueTime. You can then use a Post Agent to add this data to InfluxDB. Finally you can graph it using Grafana. I use something similar to get data about our hot water bill and such. Once you have Huginn and InfluxDB you can graph almost anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Huginn+Slack = Notifications center
&lt;/h3&gt;

&lt;p&gt;If you're like me and you have a lot of notifications then you might want to use Huginn to sort these out. Instead of interfacing directly with Slack or whatever notification system you use, you can instead use a Webhook Agent in Huginn to create an API endpoint. Post your notifications to this endpoint. You can then use for example a Slack Agent to post the notifications to Slack.&lt;/p&gt;

&lt;p&gt;What is the point of this then? Well, you can very easily change to using something else than Slack for your notifications without changing it on every site that creates them. Perhaps you want to delay some notifications? You can do that with a Delay Agent in Huginn. Perhaps some notifications should go to Trello instead of Slack? No problem using Huginn. You can even use a Digest Agent to group low level notifications and send them all at once by email or something. Don't forget that you can also graph all of this using Grafana.&lt;/p&gt;

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

&lt;p&gt;This is by no means an exhaustive list of things you can self host to make your life easier as a developer. Do you have any favorites that I've missed? Please reply in the comments below or hit me up on Twitter!&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
