<?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: Christian</title>
    <description>The latest articles on DEV Community by Christian (@christianfei).</description>
    <link>https://dev.to/christianfei</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%2F16677%2F239ffab3-b4d4-4717-950c-9e2f75cbfd34.jpeg</url>
      <title>DEV Community: Christian</title>
      <link>https://dev.to/christianfei</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/christianfei"/>
    <language>en</language>
    <item>
      <title>ZenGPT: a simple ChapGPT alternative frontend</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Wed, 18 Oct 2023 09:21:47 +0000</pubDate>
      <link>https://dev.to/christianfei/zengpt-a-simple-chapgpt-alternative-frontend-2kln</link>
      <guid>https://dev.to/christianfei/zengpt-a-simple-chapgpt-alternative-frontend-2kln</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2023-10-17-zengpt-chapgpt-alternative-frontend-opensource-self-hosting/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've been playing around with a home-made, super simple ChatGPT UI clone, mainly with the excuse to try out &lt;a href="https://cri.dev/posts/2023-10-16-functions-as-views-javascript-node-javascript-template-htmx-alpine/"&gt;htmx and Alpine.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a fun little project that I've been working on for a few days.&lt;/p&gt;

&lt;p&gt;I've been programming it on an &lt;a href="https://cri.dev/posts/2023-03-22-ipad-programming-github-codespaces-raspberry-pi-vscode/"&gt;iPad Pro (as my main device)&lt;/a&gt;, and it's been a fun experience.&lt;/p&gt;

&lt;p&gt;In this post I want to get deeper into the technical details of the project, and share some of the things I've learned while working on it.&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--o_yQ14lj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cri.dev/assets/images/posts/zengpt.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--o_yQ14lj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cri.dev/assets/images/posts/zengpt.jpeg" alt="zengpt" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  node.js server and bare functions as views
&lt;/h2&gt;

&lt;p&gt;The other day I wrote about &lt;a href="https://cri.dev/posts/2023-10-16-functions-as-views-javascript-node-javascript-template-htmx-alpine/"&gt;functions as views&lt;/a&gt;, and how I've been using them in this project.&lt;/p&gt;

&lt;p&gt;Namely, I'm using the native &lt;code&gt;http&lt;/code&gt; node.js module, a simple home made router with if statements and a few functions that return HTML strings.&lt;/p&gt;

&lt;p&gt;The functions are called with optional additional data and they return a string that is sent back to the client.&lt;/p&gt;

&lt;p&gt;E.g.&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mainView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listing&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;h2&gt;
  
  
  Integrating with OpenAI's API
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://platform.openai.com/docs/api-reference/completions"&gt;Completions API&lt;/a&gt; is pretty straightforward too:&lt;/p&gt;

&lt;p&gt;You can use the &lt;code&gt;chat.completions.create&lt;/code&gt; method to send the conversation to the API, and get back a text completion (llm response/message).&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;completion&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;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newUserMessage&lt;/span&gt; &lt;span class="p"&gt;}]),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;llmText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  htmx
&lt;/h2&gt;

&lt;p&gt;As mentioned before, the main reason I started this project was to try out &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt; (and Alpine.js)&lt;/p&gt;

&lt;p&gt;The coolest thing I refreshed during this excursus was the concept of rethinking what we consider RESTful APIs (spoiler: they are actually HTTP JSON RPC APIs), HATEOAS, hypertext, and much more honestly. The htmx website is a goldmine of resources.&lt;/p&gt;

&lt;p&gt;In short: our websites and "RESTful" APIs should be way more discoverable (for humans, not machines) and self-contained.&lt;/p&gt;

&lt;p&gt;By making use of existing powerful technology like HTML and HTTP, with sprinkles of JavaScript, to make the web more accessible, lightweight, more reliable and easier to use.&lt;/p&gt;

&lt;p&gt;But let's got back to &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;build modern user interfaces with the simplicity and power of hypertext&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The emphasis here is leveraging the power of HTML.&lt;/p&gt;

&lt;p&gt;E.g. in this small project, the client is loaded with a simple HTML page, preloaded with messages from the server.&lt;/p&gt;

&lt;p&gt;The rest (sorry for the poor choice of words) is done by the client, that makes requests to load small snippets of HTML, and updates the DOM with the response.&lt;/p&gt;

&lt;p&gt;This is an oversimplification, but it's the gist of it.&lt;/p&gt;

&lt;p&gt;By using a declarative approach on the HTML, you can get a quite robust and powerful UI, with very little code.&lt;/p&gt;

&lt;p&gt;The main focus of ZenGPT is the UI, this is the input and conversation part:&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;id=&lt;/span&gt;&lt;span class="s"&gt;"messages"&lt;/span&gt; &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"scroll:bottom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ${renderMessages(messages)}
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;
  &lt;span class="na"&gt;hx-post=&lt;/span&gt;&lt;span class="s"&gt;"https://cri.dev/chat"&lt;/span&gt;
  &lt;span class="na"&gt;hx-trigger=&lt;/span&gt;&lt;span class="s"&gt;"keyup[keyCode==13]"&lt;/span&gt;
  &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#messages"&lt;/span&gt;
  &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"beforeend scroll:bottom"&lt;/span&gt;
  &lt;span class="na"&gt;hx-indicator=&lt;/span&gt;&lt;span class="s"&gt;"#loading-message"&lt;/span&gt;
  &lt;span class="na"&gt;hx-on:htmx:before-request=&lt;/span&gt;&lt;span class="s"&gt;"this.disabled=true"&lt;/span&gt;
  &lt;span class="na"&gt;hx-on:htmx:after-request=&lt;/span&gt;&lt;span class="s"&gt;"this.disabled=false;setTimeout(() =&amp;gt; this.focus(), 20)"&lt;/span&gt;
  &lt;span class="na"&gt;x-bind:disabled=&lt;/span&gt;&lt;span class="s"&gt;"messageDisabled"&lt;/span&gt;
  &lt;span class="na"&gt;x-ref=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;
  &lt;span class="na"&gt;x-model=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;
  &lt;span class="na"&gt;x-on:keyup.enter=&lt;/span&gt;&lt;span class="s"&gt;"setTimeout(() =&amp;gt; {message = '';pristineChat = false}, 10)"&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-message"&lt;/span&gt; &lt;span class="na"&gt;autofocus&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"your message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&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;"position:fixed;bottom:3em;right:2em;"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"htmx-indicator"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"loading-message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.1"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"64"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"64"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"#000"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M12 2 L12 6 M12 18 L12 22 M4.93 4.93 L7.76 7.76 M16.24 16.24 L19.07 19.07 M2 12 L6 12 M18 12 L22 12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&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;This is so-called "no-javascript", where the JS is simply hidden (remember you need to include a &lt;code&gt;&amp;lt;script src="https://cri.dev//unpkg.com/htmx.org"&amp;gt;&lt;/code&gt;). I think it's a refreshing approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alpine.js
&lt;/h2&gt;

&lt;p&gt;In ZenGPT, Alpine.js is used to manage the state of the UI, and to make client-side only UI changes and updates.&lt;/p&gt;

&lt;p&gt;It is used to add interactivity to the UI.&lt;/p&gt;

&lt;p&gt;E.g. it handles the display state of the action buttons in the header&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;"display:flex"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&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;"flex:1"&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;h1&amp;gt;&lt;/span&gt;zengpt&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"!pristineChat"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"flex:1;"&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:block;padding:1rem;font-size:1.5rem;"&lt;/span&gt; &lt;span class="na"&gt;hx-delete=&lt;/span&gt;&lt;span class="s"&gt;"https://cri.dev/chat"&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#messages"&lt;/span&gt; &lt;span class="na"&gt;x-on:click=&lt;/span&gt;&lt;span class="s"&gt;"$refs.message.focus();messageDisabled=false;pristineChat=true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      new chat
    &lt;span class="nt"&gt;&amp;lt;/button&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&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"!pristineChat"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"flex:1;"&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:block;padding:1rem;font-size:1.5rem;"&lt;/span&gt; &lt;span class="na"&gt;hx-post=&lt;/span&gt;&lt;span class="s"&gt;"https://cri.dev/chats"&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#messages"&lt;/span&gt; &lt;span class="na"&gt;x-on:click=&lt;/span&gt;&lt;span class="s"&gt;"$refs.message.value = '';messageDisabled=false;pristineChat=true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      save chat
    &lt;span class="nt"&gt;&amp;lt;/button&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&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"viewingPreviousChat"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"flex:1;"&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:block;padding:1rem;font-size:1.5rem;"&lt;/span&gt; &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"https://cri.dev/chat"&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#messages"&lt;/span&gt; &lt;span class="na"&gt;x-on:click=&lt;/span&gt;&lt;span class="s"&gt;"$refs.message.value = '';messageDisabled=false;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      go back
    &lt;span class="nt"&gt;&amp;lt;/button&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;h2&gt;
  
  
  open source
&lt;/h2&gt;

&lt;p&gt;You can find the project on &lt;a href="https://github.com/christian-fei/zengpt"&gt;github.com/christian-fei/zengpt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2023-10-17-zengpt-chapgpt-alternative-frontend-opensource-self-hosting/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>javascript</category>
      <category>htmx</category>
      <category>alpine</category>
    </item>
    <item>
      <title>Simple Static site/blog search in 14 Lines of JavaScript</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Sun, 11 Apr 2021 10:11:08 +0000</pubDate>
      <link>https://dev.to/christianfei/simple-static-site-blog-search-in-14-lines-of-javascript-1nhl</link>
      <guid>https://dev.to/christianfei/simple-static-site-blog-search-in-14-lines-of-javascript-1nhl</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-04-06-simple-static-site-blog-search-javascript-client/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is my approach to a purely client side search feature for static blogs and sites.&lt;/p&gt;

&lt;p&gt;I am currently using this under &lt;a href="https://cri.dev/posts"&gt;/posts&lt;/a&gt; to let readers search through my blog posts.&lt;/p&gt;

&lt;p&gt;Read more below about how to integrate it in your site.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backstory
&lt;/h2&gt;

&lt;p&gt;In 2015 I did a similar thing for Jekyll sites, namely &lt;a href="https://github.com/christian-fei/Simple-Jekyll-Search"&gt;Simple-Jekyll-Search&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's gotten quite a bit of attention and merged various Pull-Requests (64!) from others.&lt;/p&gt;

&lt;p&gt;I ditched Jekyll a few years ago, to use a JavaScript based static site generator:&lt;/p&gt;

&lt;p&gt;My home-made static site generator &lt;a href="https://github.com/christian-fei/devblog"&gt;devblog&lt;/a&gt;, and settled with &lt;a href="https://www.11ty.dev/"&gt;Eleventy&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Under &lt;a href="https://cri.dev/posts"&gt;/posts&lt;/a&gt; I render the full list of blog posts.&lt;/p&gt;

&lt;p&gt;(Not with the full content but with an &lt;em&gt;excerpt/description&lt;/em&gt;.)&lt;/p&gt;

&lt;p&gt;In my Nunjucks template I render the posts like 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;class=&lt;/span&gt;&lt;span class="s"&gt;"searchable"&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;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;autofocus&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"🔍 Search posts"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  {% set postslist = collections.post | reverse %}
  {% for post in postslist %}
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"searchable-item"&lt;/span&gt; &lt;span class="na"&gt;data-search=&lt;/span&gt;&lt;span class="s"&gt;"{{ post.data.title | escape }} {{ post.data.tags | json | escape }}"&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;
  {% endfor %}
&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;The idea is the following:&lt;/p&gt;

&lt;p&gt;Within a &lt;code&gt;.searchable&lt;/code&gt; element&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;look for a &lt;code&gt;input&lt;/code&gt; element and attach an event listener to it (&lt;code&gt;keyup&lt;/code&gt; event)&lt;/li&gt;
&lt;li&gt;when searching, parse all &lt;code&gt;.searchable-item&lt;/code&gt;s &lt;code&gt;data-search&lt;/code&gt; attribute and test a RegExp on it&lt;/li&gt;
&lt;li&gt;make items that match visible, and make others disappear&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;This is the full code for the search functionality on this blog:&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="p"&gt;;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;search&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.searchable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;makeSearchable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;makeSearchable&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$searchable&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;$searchableItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;$searchable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.searchable-item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$searchable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&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;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keyup&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;e&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;$searchableItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&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;$el&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;$el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;show&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;test&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;$el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;show&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to integrate it on your site
&lt;/h2&gt;

&lt;p&gt;Grabbed the JavaScript above and put it in a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;Define a minimal markup to enable the search functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"searchable"&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;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;autofocus&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"🔍 Search posts"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Loop through your blog posts --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"searchable-item"&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;Make sure you have the wrapper element &lt;code&gt;.searchable&lt;/code&gt;, containing an input field and your post items with the &lt;code&gt;.searchable-item&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;By default the search function will match the blog posts to filter with the element's &lt;code&gt;innerText&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Optionally, apply a &lt;code&gt;data-search&lt;/code&gt; attribute to each &lt;code&gt;.searchable-item&lt;/code&gt; and set the value of your blog post title, tags, short description etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full example
&lt;/h2&gt;

&lt;p&gt;Check out a &lt;a href="https://codepen.io/christian-fei/pen/abpyERy"&gt;full example on CodePen&lt;/a&gt; if you need to better understand how to structure your markup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-04-06-simple-static-site-blog-search-javascript-client/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>blogging</category>
      <category>search</category>
    </item>
    <item>
      <title>5$/month Self Hosted VPN with WireGuard</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Thu, 01 Apr 2021 11:07:34 +0000</pubDate>
      <link>https://dev.to/christianfei/5-month-self-hosted-vpn-with-wireguard-49o5</link>
      <guid>https://dev.to/christianfei/5-month-self-hosted-vpn-with-wireguard-49o5</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-02-01-5-dollar-easy-self-hosted-vpn-setup/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This will take you less than 5 minutes to get a private VPN on your own Ubuntu server.&lt;/p&gt;




&lt;p&gt;What you need to get started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a VPS (Ubuntu 20.04 LTS preferrably)&lt;/li&gt;
&lt;li&gt;docker and docker-compose installed&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;On the VPS, I suggest to do the following:&lt;/p&gt;

&lt;p&gt;Create a folder &lt;code&gt;wireguard/config&lt;/code&gt; in your $HOME:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p wireguard/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the &lt;code&gt;wireguard&lt;/code&gt; folder (next to the &lt;code&gt;config&lt;/code&gt; folder), create the docker-compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vim docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: "2.1"
services:
  wireguard:
    image: linuxserver/wireguard
    container_name: wireguard
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Rome
      - SERVERURL=YOUR_IP_OR_DNS_NAME_OF_YOUR_SERVER #optional
      - SERVERPORT=51820
      - PEERS=5
    volumes:
      - /home/YOUR_USERNAME/wireguard/config:/config
      - /lib/modules:/lib/modules
    ports:
      - 51820:51820/udp
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    restart: always
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simply change the &lt;code&gt;SERVERURL&lt;/code&gt; variable, or delete that line if you want to use the server IP.&lt;/p&gt;

&lt;p&gt;Additionally, change the location of your wireguard config path in the &lt;code&gt;volumes&lt;/code&gt; section. (use &lt;code&gt;pwd&lt;/code&gt; to get the current path you are in).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For more info and environment variables, check out the official &lt;a href="https://hub.docker.com/r/linuxserver/wireguard"&gt;linuxserver/wireguard doc&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've used &lt;code&gt;restart: always&lt;/code&gt; so that WireGuard comes up after a system restart.&lt;/p&gt;

&lt;p&gt;Changed &lt;code&gt;PEERS&lt;/code&gt; to 5, so that I have 5 configurations available for my devices. &lt;/p&gt;

&lt;h2&gt;
  
  
  Launching the container
&lt;/h2&gt;

&lt;p&gt;Start the container in the background with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create the needed configurations in the &lt;code&gt;wireguard/config&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;The structure of your &lt;code&gt;wireguard&lt;/code&gt; folder looks something like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── config
│   ├── coredns
│   │   └── Corefile
│   ├── peer1
│   │   ├── peer1.conf
│   │   ├── peer1.png
│   │   ├── privatekey-peer1
│   │   └── publickey-peer1
.............................
│   ├── server
│   │   ├── privatekey-server
│   │   └── publickey-server
│   ├── templates
│   │   ├── peer.conf
│   │   └── server.conf
│   └── wg0.conf
└── docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Firewall configuration
&lt;/h2&gt;

&lt;p&gt;If you're using &lt;code&gt;ufw&lt;/code&gt;, simply enable the port &lt;code&gt;51820&lt;/code&gt; so that you can connect to your server from outside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ufw enable 51820
ufw reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run &lt;code&gt;sudo ufw status&lt;/code&gt; you should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status: active

To                         Action      From
--                         ------      ----
....................................................
51820                      ALLOW       Anywhere
51820 (v6)                 ALLOW       Anywhere (v6)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connecting devices
&lt;/h2&gt;

&lt;p&gt;Now you can &lt;code&gt;cat&lt;/code&gt; or &lt;code&gt;scp&lt;/code&gt; the configurations individually to your devices.&lt;/p&gt;

&lt;p&gt;The configurations are located in the &lt;code&gt;config/peerX&lt;/code&gt; folder, where &lt;code&gt;X&lt;/code&gt; represents the peer number.&lt;/p&gt;

&lt;p&gt;E.g. I could &lt;code&gt;cat/scp&lt;/code&gt; the configuration in &lt;code&gt;wireguard/config/peer1/peer1.conf&lt;/code&gt; and put it in &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt; on my host machine.&lt;/p&gt;

&lt;p&gt;View the configuration for peer 1 on your server with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat /home/YOUR_USERNAME/wireguard/config/peer1/peer1.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and place it in &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt; on your host machine.&lt;/p&gt;

&lt;p&gt;If you want to use &lt;code&gt;scp&lt;/code&gt;, you could run the following on your local machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scp USER@SERVER:/home/YOUR_USERNAME/wireguard/config/peer1/peer1.conf /etc/wireguard/wg0.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To connect to your newly created WireGuard VPN from one of your devices, you'll need to install &lt;code&gt;wireguard-tools&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;E.g. &lt;code&gt;apt install wireguard-tools&lt;/code&gt; , &lt;code&gt;pacman -S wireguad-tools&lt;/code&gt; based on your distro&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.wireguard.com/install/"&gt;Here you can find a more detailed explanation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can simply run &lt;strong&gt;&lt;code&gt;wg-quick up wg0&lt;/code&gt;&lt;/strong&gt; and you're connected to your VPN.&lt;/p&gt;

&lt;p&gt;Test it out by running &lt;code&gt;curl ipinfo.io&lt;/code&gt; and inspect the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting mobile device with QR code
&lt;/h2&gt;

&lt;p&gt;On your mobile device, install the WireGuard client.&lt;/p&gt;

&lt;p&gt;Then add a new WireGuard tunnel by creating a new configuration scanning a QR code.&lt;/p&gt;

&lt;p&gt;On your VPS run the following to output a QR code on the terminal that you can scan on your mobile device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec -it wireguard app/show-peer 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Inspecting connections
&lt;/h2&gt;

&lt;p&gt;If you want to understand who is connected and which profiles are in use, simply run the following on your VPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec -it wireguard wg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will give you more information about your connections with the following output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface: wg0
  public key: (redacted)
  private key: (hidden)
  listening port: 51820

peer: (redacted)
  endpoint: (redacted):51820
  allowed ips: (redacted)/32
  latest handshake: 27 seconds ago
  transfer: 5.04 MiB received, 172.64 MiB sent

...

peer: (redacted)
  allowed ips: (redacted)/32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This was most definitely the easiest way I found to connect computers and mobile devices to your own WireGuard VPN.&lt;/p&gt;

&lt;p&gt;Let me know if you had troubles setting it up yourself!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-02-01-5-dollar-easy-self-hosted-vpn-setup/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Self-hosted Miniflux + Wallabag</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Thu, 01 Apr 2021 11:06:58 +0000</pubDate>
      <link>https://dev.to/christianfei/self-hosted-miniflux-wallabag-4j81</link>
      <guid>https://dev.to/christianfei/self-hosted-miniflux-wallabag-4j81</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-03-23-private-self-hosted-reading-rss-miniflux-wallabag/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Found the perfect combination for self-hosting my reading stack: Miniflux and Wallabag, you may now kiss!&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Note: This is an update to &lt;a href="https://cri.dev/posts/2021-02-09-my-reading-stack-miniflux-rss-pocket/"&gt;"My reading stack"&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;Primarily for keeping the stuff I read private by self-hosting alternative services.&lt;/p&gt;

&lt;p&gt;Additionally, to do all my reading from a web browser, without third party services and apps involved.&lt;/p&gt;

&lt;p&gt;Another thing I wanted to experiment with was the interoperability between Miniflux and Wallabag.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;Miniflux acts as my RSS feed reader, which integrates with the Wallabag API.&lt;/p&gt;

&lt;p&gt;Wallabag is the alternative I use to Pocket, especially for longer reads.&lt;/p&gt;

&lt;p&gt;In Miniflux I set up the integration to my Wallabag instance, so that when I hit "Save article", it sends it to Wallabag so that I can read it later (if it's a long read).&lt;/p&gt;

&lt;p&gt;I access both services (under practical dns names) on my own domain (both from phone and PC).&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;p&gt;Both Miniflux and Wallabag can be "installed" as a PWA on your mobile device and accessed through a web browser on your PC.&lt;/p&gt;

&lt;p&gt;No profiling, no tracking involved.&lt;/p&gt;

&lt;p&gt;Extremely lightweight stack.&lt;/p&gt;

&lt;p&gt;You use FOSS software, to which you can contribute to (or fork it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Downsides
&lt;/h2&gt;

&lt;p&gt;Minimum cost of 5$/month for hosting it on &lt;a href="https://www.linode.com/?r=a248b42e63f90809eedddafd6bb07f1522e09993"&gt;Linode&lt;/a&gt; or &lt;a href="https://m.do.co/c/880e8f681b50"&gt;DigitalOcean&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You need to make sure your VPS is secured, duh.&lt;/p&gt;

&lt;p&gt;So far I didn't see any other downsides.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to self-host
&lt;/h2&gt;

&lt;p&gt;Both Miniflux and Wallabag have docker-compose files in their repositories, so the installation is quite trivial.&lt;/p&gt;

&lt;p&gt;You'll need to figure out the desired environment variables depending on your needs.&lt;/p&gt;

&lt;p&gt;I host mine through caprover, through a Web UI it's a few clicks away.&lt;/p&gt;

&lt;p&gt;I plan on writing a detailed how to in the future.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-03-23-private-self-hosted-reading-rss-miniflux-wallabag/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rss</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Resources to learn Elixir - A study path</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Thu, 01 Apr 2021 11:05:05 +0000</pubDate>
      <link>https://dev.to/christianfei/resources-to-learn-elixir-a-study-path-47pa</link>
      <guid>https://dev.to/christianfei/resources-to-learn-elixir-a-study-path-47pa</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-03-03-learn-elixir-study-path/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Below you can find resources for getting started with Elixir.&lt;/p&gt;




&lt;p&gt;It's what I am following and it could be helpful for new programmers that want to learn Elixir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Official guide
&lt;/h3&gt;

&lt;p&gt;The official "Getting started" guide by Elixir-lang.org&lt;/p&gt;

&lt;p&gt;&lt;a href="https://elixir-lang.org/getting-started/introduction.html"&gt;elixir-lang.org official Getting started&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find &lt;a href="https://cri.dev/posts/2021-02-04-Learning-Elixir-Notes/"&gt;my notes here&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Books
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://pragprog.com/titles/elixir16/programming-elixir-1-6/"&gt;Programming Elixir&lt;/a&gt; by Dave Thomas is the go-to book if you want to learn all about Elixir.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.manning.com/books/elixir-in-action-second-edition"&gt;Elixir in Action&lt;/a&gt; by Saša Jurić is also highly recommended.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crash course about syntax
&lt;/h3&gt;

&lt;p&gt;This is another official resource I can highly recommend for learning about Elixir's syntax&lt;/p&gt;

&lt;p&gt;&lt;a href="https://elixir-lang.org/crash-course.html"&gt;elixir-lang.org syntax crash course&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Elixir school
&lt;/h3&gt;

&lt;p&gt;This course will get you through the basic concepts (similar to the guide on elixir-lang) and more advances topics about Elixir.&lt;/p&gt;

&lt;p&gt;It also goes briefly into Ecto and other specific module often used in Elixir applications.&lt;/p&gt;

&lt;p&gt;Visit &lt;a href="https://elixirschool.com/en/"&gt;elixirschool.com&lt;/a&gt; to learn more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Elixir Videos
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Alchemist Camp
&lt;/h3&gt;

&lt;p&gt;If you want code tutorials in form of video, &lt;a href="https://alchemist.camp/"&gt;Alchemist Camp&lt;/a&gt; can be a great place to start. &lt;/p&gt;

&lt;p&gt;You can also find the videos on YouTube.&lt;/p&gt;

&lt;h3&gt;
  
  
  Talks
&lt;/h3&gt;

&lt;p&gt;Saša Jurić's talk at GOTO 2019 about &lt;a href="https://www.youtube.com/watch?v=JvBT4XBdoUE"&gt;"The Soul of Erlang and Elixir"&lt;/a&gt; is highly recommended&lt;/p&gt;

&lt;h3&gt;
  
  
  Paid video courses
&lt;/h3&gt;

&lt;p&gt;To learn about the basics of the language, you could check out &lt;a href="https://www.learnelixir.tv/"&gt;learnelixir.tv&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For 18$ you can watch 30 episodes, as of March 2021.&lt;/p&gt;

&lt;p&gt;A more complete bundle of video courses can be &lt;a href="https://elixircasts.io/"&gt;elixircasts.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At the time of writing there are around 130 episodes that guide you through various scenarios and examples on using Elixir.&lt;/p&gt;

&lt;p&gt;This video library can be watched for 19$ / month.&lt;/p&gt;

&lt;h2&gt;
  
  
  More
&lt;/h2&gt;

&lt;h3&gt;
  
  
  awesome-elixir
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/h4cc/awesome-elixir"&gt;github.com/h4cc/awesome-elixir&lt;/a&gt; is an infinite list of Elixir libraries &amp;amp; resources&lt;/p&gt;

&lt;h3&gt;
  
  
  Books &amp;amp; resources
&lt;/h3&gt;

&lt;p&gt;A more exhaustive list of other resources to learning Elixir can be found on &lt;a href="https://elixir-lang.org/learning.html"&gt;elixir-lang.org&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Erlang
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Learn you some Erlang
&lt;/h3&gt;

&lt;p&gt;If you want to get your hands dirty understand concepts in Erlang, &lt;a href="https://learnyousomeerlang.com/"&gt;Learn you some Erlang&lt;/a&gt; can be a highly valuable resource. &lt;/p&gt;

&lt;h2&gt;
  
  
  In-depth
&lt;/h2&gt;

&lt;p&gt;There is also an official &lt;a href="https://www.erlang.org/course"&gt;Erlang course&lt;/a&gt; which can be used for learning more about Erlang and important concepts of the language and VM.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It will usually take four days to complete.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You'll learn more about the History of Erlang, Sequental / Concurrent Programming, Error handling and other advanced topics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-03-03-learn-elixir-study-path/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>learn</category>
      <category>study</category>
    </item>
    <item>
      <title>Bitcoin is trademarked. Is it really?</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Sat, 20 Feb 2021 10:08:56 +0000</pubDate>
      <link>https://dev.to/christianfei/bitcoin-is-trademarked-is-it-really-24ch</link>
      <guid>https://dev.to/christianfei/bitcoin-is-trademarked-is-it-really-24ch</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-02-17-Bitcoin-trademark/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Useless to say, but here we go &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm not a lawyer.&lt;br&gt;
This is not legal advice.&lt;br&gt;
This is for informational and educational purposes only. &lt;br&gt;
This is my opinion.&lt;br&gt;
May contain incorrect interpretations of the law.&lt;br&gt;
May contain traces of peanut.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Bitcoin, the cryptocurrency invented in 2008 by an unknown person or group of people using the name Satoshi Nakamoto, is trademarked? Is it really?&lt;/p&gt;

&lt;p&gt;I did some "research" and this is what I found.&lt;/p&gt;




&lt;h2&gt;
  
  
  United Kingdom
&lt;/h2&gt;

&lt;p&gt;First of all, the term "bitcoin" for clothing/food/drinks, seems to be registered in the &lt;strong&gt;United Kingdom&lt;/strong&gt; through the &lt;em&gt;Intellectual Property Office (IPO)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The registration of the trademark dates back to &lt;strong&gt;December 22, 2017&lt;/strong&gt; (&lt;a href="https://trademarks.ipo.gov.uk/ipo-tmcase/page/Results/1/UK00003279106"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Apparently &lt;strong&gt;a merchant on etsy.co.uk&lt;/strong&gt; was selling t-shirts with the bitcoin logo on it.&lt;/p&gt;

&lt;p&gt;This is the original &lt;a href="https://www.reddit.com/r/Bitcoin/comments/8m35ox/a_uk_company_has_trademarked_the_word_bitcoin/"&gt;Reddit post under /r/Bitcoin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;They received an adorable &lt;strong&gt;cease-and-desist&lt;/strong&gt; letter from a company "holding the trademark", namely "A.B.C. IPHOLDINGS SOUTH WEST LLC". (&lt;a href="https://news.bitcoin.com/a-london-based-company-successfully-trademarks-the-name-bitcoin/"&gt;source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://imgur.com/a/JTBZKui"&gt;The lovely cease-and-desist letter&lt;/a&gt; is also still available. With a heartwarming "Yours sincerely," at the end of the letter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sttfBaZ7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/btc-cease-and-desist-letter.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sttfBaZ7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/btc-cease-and-desist-letter.png" alt="btc-cease-and-desist-letter.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do a quick search for the term &lt;a href="https://www.google.com/search?q=A.B.C.+IPHOLDINGS+SOUTH+WEST+LLC"&gt;"A.B.C. IPHOLDINGS SOUTH WEST LLC"&lt;/a&gt; and look through the 30k+ results for this company.&lt;/p&gt;




&lt;p&gt;The goods and services covered by this trademark are from the &lt;strong&gt;classes 25, 32 and 33&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here some examples (just for giggles):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adhesive bras&lt;/li&gt;
&lt;li&gt;Ballroom dancing shoes&lt;/li&gt;
&lt;li&gt;Bowties&lt;/li&gt;
&lt;li&gt;Camouflage jackets&lt;/li&gt;
&lt;li&gt;Flip-flops&lt;/li&gt;
&lt;li&gt;Kimonos&lt;/li&gt;
&lt;li&gt;Judo suits&lt;/li&gt;
&lt;li&gt;Mittens&lt;/li&gt;
&lt;li&gt;Silk scarves&lt;/li&gt;
&lt;li&gt;Taekwondo suits&lt;/li&gt;
&lt;li&gt;Waterskiing suits&lt;/li&gt;
&lt;li&gt;Wooden bodies for Japanese style clogs&lt;/li&gt;
&lt;li&gt;Yoga pants&lt;/li&gt;
&lt;li&gt;Zoot suits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And this was only class 25, representing &lt;strong&gt;Clothing, footwear, headgear&lt;/strong&gt;. (&lt;a href="https://trademark.eu/list-of-classes-with-explanatory-notes/"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Class 32 represents &lt;strong&gt;non-alcoholic beverages including beers&lt;/strong&gt;. (&lt;a href="https://trademark.eu/list-of-classes-with-explanatory-notes/"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Class 33 represents &lt;strong&gt;Alcoholic beverages excluding beers&lt;/strong&gt;. (&lt;a href="https://trademark.eu/list-of-classes-with-explanatory-notes/"&gt;source&lt;/a&gt;).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;I'm not a lawyer. But this seems fishy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What's the point of registering a trademark for which you don't have any business to?&lt;/p&gt;

&lt;p&gt;Could the seller have gotten away with it? Or even sue them for false litigation?&lt;/p&gt;

&lt;p&gt;What is this? Getting a trademark registration for the sole purpose of following legal action to who "abuses" it?&lt;/p&gt;

&lt;p&gt;This seems to be pure "trademark trolling".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bitcoinfoundation.org/the-bitcoin-foundations-view-and-policy-on-trademarking-bitcoin/"&gt;Here is the opinion of "The Bitcoin foundation" on the matter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It seems the &lt;a href="https://www.theblockcrypto.com/post/21131/bitcoin-tm-application-abandoned"&gt;TM application has been dropped&lt;/a&gt;, although it seems to still be registered and active on &lt;a href="https://trademarks.ipo.gov.uk/ipo-tmcase/page/Results/1/UK00003279106"&gt;UK IPO official website&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other parts of the world
&lt;/h2&gt;

&lt;p&gt;Looking through &lt;a href="https://trademarks.justia.com/search?q=Bitcoin"&gt;trademarks.justia.com&lt;/a&gt; you can find more registration (I think at an international level).&lt;/p&gt;

&lt;p&gt;There are 14 registrations for the word "Bitcoin" in various categories.&lt;/p&gt;

&lt;p&gt;Two of which are currently marked with the status "Registered".&lt;/p&gt;

&lt;p&gt;The other 12 were either express abandoned or suspended due to inactivity by the party.&lt;/p&gt;

&lt;p&gt;The 2 ones that are currently registered are regarding&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://trademarks.justia.com/792/67/bitcoin-79267862.html"&gt;alcoholic beverages (except beers) + Tobacco/smokers' articles&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://trademarks.justia.com/792/44/bitcoin-79244996.html"&gt;coffee, tea, sugar, flour, rice, salt mustard etc.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No but seriously, WTF?&lt;/p&gt;

&lt;p&gt;The latter one is registered by a german company that produces confectionery products.&lt;/p&gt;

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

&lt;p&gt;The Bitcoin logo itself is released with the public domain license.&lt;/p&gt;

&lt;p&gt;Therefore it is free to use for both commercial and non-commercial use.&lt;/p&gt;

&lt;p&gt;I don't know what these companies were thinking, and if they are in good or bad faith.&lt;/p&gt;

&lt;p&gt;I mean, how ridiculous do you have to be to trademark "bitcoin" for coffee, tea, salt mustard, or alcoholic beverages?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are you serious?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Additionally this is what I found when looking for the bitcoin logo:&lt;/p&gt;

&lt;p&gt;On Wikipedia it is explicitly stated&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This work is ineligible for copyright and therefore in the public domain because it consists entirely of information that is common property and contains no original authorship.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But at the same time regarding Trademark&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This work includes material that may be protected as a trademark in some jurisdictions. If you want to use it, you have to ensure that you have the legal right to do so and that you do not infringe any trademark rights&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QYnh2ZqP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/btc-logo-trademark.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QYnh2ZqP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/btc-logo-trademark.png" alt="btc-logo-trademark.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful resources
&lt;/h2&gt;

&lt;p&gt;Resources I found useful about this topic&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://trademarks.justia.com/search?q=Bitcoin"&gt;"Bitcoin" on Justia.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://trademarks.ipo.gov.uk/ipo-tmcase/page/Results/1/UK00003279106"&gt;Currently registered Trademark for BITCOIN in UK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.inta.org/fact-sheets/international-trademark-rights/"&gt;International Trademark Rights&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.upcounsel.com/what-can-be-trademarked#:~:text=A%20phrase%2C%20word%2C%20symbol%2C%20device%2C%20or%20even%20a,obtain%20protection%20from%20the%20law."&gt;What can be trademarked&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.citma.org.uk/resources/battle-for-bitcoin-and-other-cryptocurrency-brands-trademark.html"&gt;The battle for Bitcoin and other cryptocurrency brands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.bitcoin.com/a-london-based-company-successfully-trademarks-the-name-bitcoin/"&gt;A London-Based Company Successfully Trademarks the Name 'Bitcoin'&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bitcoinfoundation.org/the-bitcoin-foundations-view-and-policy-on-trademarking-bitcoin/"&gt;The Bitcoin Foundation’s View and Policy on Trademarking “Bitcoin”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theblockcrypto.com/post/21131/bitcoin-tm-application-abandoned"&gt;Bitcoin TM application abandoned&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.cryptonomist.ch/2018/11/18/are-cryptocurrency-logos-protected-by-copyright/"&gt;are-cryptocurrency-logos-protected-by-copyright&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bitcoin.org/en/legal"&gt;Bitcoin.org Legal disclaimer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Bitcoin_Foundation"&gt;Bitcoin Foundation Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://depot-de-marque.com/en/bookmark-2/"&gt;10 years of Bitcoin and 400 trademark applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.google.com/search?q=A.B.C.+IPHOLDINGS+SOUTH+WEST+LLC"&gt;Search for "A.B.C. IPHOLDINGS SOUTH WEST LLC"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://coingeek.com/bitcoin-gets-trademarked-uk-firm-threatens-etsy-store/"&gt;‘Bitcoin’ gets trademarked in UK, firm threatens Etsy store&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-02-17-Bitcoin-trademark/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>bitcoin</category>
    </item>
    <item>
      <title>A retrospective of 100k yearly pageviews</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Sat, 20 Feb 2021 10:08:08 +0000</pubDate>
      <link>https://dev.to/christianfei/a-retrospective-of-100k-yearly-pageviews-1nn7</link>
      <guid>https://dev.to/christianfei/a-retrospective-of-100k-yearly-pageviews-1nn7</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-01-22-A-retrospective-of-100k-yearly-pageviews/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Incredible! &lt;/p&gt;

&lt;p&gt;70k visitors / 100k pageviews (excluding those with analytics blockers installed) reached this very website in the last year.&lt;/p&gt;

&lt;p&gt;Around July 2020 I switched to &lt;a href="https://plausible.io/cri.dev"&gt;Plausible Analytics&lt;/a&gt; and made my dashboard public.&lt;/p&gt;

&lt;p&gt;In 2020 a lot happened: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SEO improvements and mistakes&lt;/li&gt;
&lt;li&gt;Domain name change from christianfei.com to &lt;a href="https://cri.dev"&gt;cri.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HackerNews "virality" and backlinks&lt;/li&gt;
&lt;li&gt;Reddit experiments&lt;/li&gt;
&lt;li&gt;93 blog posts in 2020&lt;/li&gt;
&lt;li&gt;Numerous design iterations&lt;/li&gt;
&lt;li&gt;Ethical ads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me explain in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO improvements
&lt;/h2&gt;

&lt;p&gt;Learned quite a bit in the last year about effective SEO. &lt;/p&gt;

&lt;p&gt;Even wrote a short ebook about how to &lt;a href="https://gum.co/yUhsLz"&gt;improve SEO for your site&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes syndicating content to &lt;a href="https://dev.to/christianfei"&gt;dev.to&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Most people coming from search engines land by looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an Ansible error (&lt;code&gt;ansible undefined variable&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Node.js 14 optional chaining + nullish coalescing&lt;/li&gt;
&lt;li&gt;Puppeteer issue (&lt;code&gt;err_invalid_argument&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;HTTP error 431 in Node.js (&lt;code&gt;431 request header fields too large nodejs&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means a lot to me, since I started this blog to document my findings and learnings, and how to solve programming related issues.&lt;/p&gt;

&lt;p&gt;Still, I need to learn a lot about effective SEO and keyword planning/strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO mistakes
&lt;/h2&gt;

&lt;p&gt;As mentioned above, around February 2020 (I think) I switched domain from christianfei.com to &lt;a href="https://cri.dev"&gt;cri.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During this procedure, I made the mistake of not properly configuring the Canonical URL!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--duT0XtDB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/100k/facepalm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--duT0XtDB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/100k/facepalm.png" alt="facepalm"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This affected the discovery quite a bit, for a month or so.&lt;/p&gt;

&lt;p&gt;Then after figuring out the issue and implementing a fix, things got back to normal.&lt;/p&gt;

&lt;p&gt;Google Search Console was vital to get this right.&lt;/p&gt;

&lt;h2&gt;
  
  
  HackerNews and Reddit
&lt;/h2&gt;

&lt;p&gt;HackerNews and Reddit are both a blessing and a curse sometimes..&lt;/p&gt;

&lt;p&gt;You post something you think could be interesting, and nobody checks it out.&lt;/p&gt;

&lt;p&gt;Other times, you submit your post, and it blows up.&lt;/p&gt;

&lt;p&gt;Don't let me get started on the discussion on these boards.&lt;/p&gt;

&lt;p&gt;People on there might comment with kind words and appreciation, and in the same thread find your greatest enemy and hater.&lt;/p&gt;

&lt;p&gt;I guess it's like in real life ^^&lt;/p&gt;

&lt;p&gt;Anyways, here is &lt;strong&gt;the blog post&lt;/strong&gt; that brought most visitors on my site through HackerNews.&lt;/p&gt;

&lt;p&gt;Namely &lt;a href="https://cri.dev/posts/2020-09-12-Raspberry-Pi-as-a-local-server-for-self-hosting-applications/"&gt;Raspberry Pi as a local server for self hosting applications&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This seriously blew up: at the peak I had around 250+ realtime visitors, and the wave lasted for days.&lt;/p&gt;

&lt;p&gt;To this day, this blog post brought over 24k visitors and 35k pageviews, and loads of backlinks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fHLlFOrd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/100k/rpi-hn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fHLlFOrd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/100k/rpi-hn.png" alt="rpi-hn"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No Hug-of-death fortunately, since this site is purely static, hosted on AWS with CloudFlare in front..&lt;/p&gt;

&lt;p&gt;Here are some other posts that got people interested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cri.dev/posts/2020-05-20-Testing-in-Nodejs-by-example-using-the-SOLID-principles/"&gt;Testing in Node.js by example using the SOLID principles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cri.dev/posts/2020-04-24-Resuming-Elixir-by-self-hosting-plausible-analytics/"&gt;Resuming Elixir by self-hosting plausible analytics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  93 blog posts
&lt;/h2&gt;

&lt;p&gt;I sat down and wrote about whatever I learned or felt like was useful to share, almost every 4 days on average.&lt;/p&gt;

&lt;p&gt;Shared some in &lt;a href="https://cri.dev/subscribe/"&gt;my newsletter&lt;/a&gt; over the year.&lt;/p&gt;

&lt;p&gt;Writing these blog posts gives me the ability to share what I learned, and to declutter my head.&lt;/p&gt;

&lt;p&gt;This is a very useful practice that I definitely want to continue throughout 2021.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design iterations
&lt;/h2&gt;

&lt;p&gt;Tried different designs, various CSS frameworks, bare handwritten CSS at some point, changed static site generator etc.&lt;/p&gt;

&lt;p&gt;I think this is quite common for personal blogs.&lt;/p&gt;

&lt;p&gt;At some point I had a sidebar in the post view. Now I removed it because it hurt the reading experience.&lt;/p&gt;

&lt;p&gt;I tried to improve the navigation header over time, and now it has a proper mobile view with all the relevant links.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethical ads
&lt;/h2&gt;

&lt;p&gt;Switched from carbonads to ethicalads a month ago.&lt;/p&gt;

&lt;p&gt;It has pros and cons.&lt;/p&gt;

&lt;p&gt;Pros: there is no targeting, option to show textual ads, no cookies being placed. GDPR hooray!&lt;/p&gt;

&lt;p&gt;Cons: payout is once 50$ are reached (instead of monthly payout).&lt;/p&gt;

&lt;p&gt;Currently I'm making around 5$ a month, so it's going to take some time to see the first payout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;This year I want to get my SEO game to the next level.&lt;/p&gt;

&lt;p&gt;More quality content, less quick fixes and short how tos.&lt;/p&gt;

&lt;p&gt;Consistency in writing blog posts regularly, but with more focus on quality.&lt;/p&gt;

&lt;p&gt;If you want to stay in touch, do so by &lt;a href="https://cri.dev/subscribe/"&gt;subscribing to my newsletter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-01-22-A-retrospective-of-100k-yearly-pageviews/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>seo</category>
    </item>
    <item>
      <title>From Mac to Linux</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Sun, 03 Jan 2021 02:03:45 +0000</pubDate>
      <link>https://dev.to/christianfei/from-mac-to-linux-4ki3</link>
      <guid>https://dev.to/christianfei/from-mac-to-linux-4ki3</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-01-03-mac-linux/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I am switching from Mac to Linux.&lt;/p&gt;

&lt;p&gt;This is how the adventure started to find the best linux distro, with a seemless user experience and minimum maintenance.&lt;/p&gt;

&lt;p&gt;In the past months I experimented with various distros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bare debian&lt;/li&gt;
&lt;li&gt;Pop!_OS&lt;/li&gt;
&lt;li&gt;Ubuntu&lt;/li&gt;
&lt;li&gt;Elementary OS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read below to read my experience and to which distro I settled using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experience recap
&lt;/h2&gt;

&lt;h3&gt;
  
  
  with bare debian
&lt;/h3&gt;

&lt;p&gt;Driver support for WiFi and Bluetooth were absent out of the box.&lt;/p&gt;

&lt;p&gt;Still too much to adjust and tweak to get a decent user experience.&lt;/p&gt;

&lt;p&gt;Not for me.&lt;/p&gt;

&lt;h3&gt;
  
  
  with Pop!_OS
&lt;/h3&gt;

&lt;p&gt;Seemed like a fresh approach to the linux distro landscape, but I had major hiccups when it comes to display drivers. &lt;/p&gt;

&lt;p&gt;Weird issues like blank screen after switching to HDMI external display. Troublesome boots when display was attached.&lt;/p&gt;

&lt;p&gt;Weird glitches and graphical artifacts when resizing windows.&lt;/p&gt;

&lt;p&gt;No issues with drivers.&lt;/p&gt;

&lt;p&gt;I bet it works great on System76 machines though.&lt;/p&gt;

&lt;h3&gt;
  
  
  with Ubuntu
&lt;/h3&gt;

&lt;p&gt;Glitches when working with external monitors.&lt;/p&gt;

&lt;p&gt;Blank screen after booting connected via HDMI.&lt;/p&gt;

&lt;p&gt;Weird behaviour when putting laptop in clamshell mode. Either not turning on, or shortly showing the login screen and then going to sleep.&lt;/p&gt;

&lt;p&gt;No issues with drivers.&lt;/p&gt;

&lt;p&gt;Recurrent issues with the confusing Software center versions not compatible with the applications on it. &lt;/p&gt;

&lt;p&gt;Amazon EKS keeped popping up as a "promoted" application, without the possibility to remove it.&lt;/p&gt;

&lt;p&gt;Bye Ubuntu.&lt;/p&gt;

&lt;h3&gt;
  
  
  Elementary OS
&lt;/h3&gt;

&lt;p&gt;No issues with drivers.&lt;/p&gt;

&lt;p&gt;No issues with external monitor. Wake and sleep with HDMI works like expected.&lt;/p&gt;

&lt;p&gt;No blank screen when booting from external monitor.&lt;/p&gt;

&lt;p&gt;Decent software center (with donations built in), also using snap was great.&lt;/p&gt;

&lt;p&gt;Slight issues when disconnecting the external monitor and using the laptop, works after a few tries.&lt;/p&gt;

&lt;p&gt;So far so good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Elementary OS is good enough for me
&lt;/h2&gt;

&lt;p&gt;The user experience is stunningly similar to what I was used to with a Mac.&lt;/p&gt;

&lt;p&gt;Switching windows is intuitive.&lt;/p&gt;

&lt;p&gt;If you forget a shortcut, just press &lt;code&gt;&amp;lt;SUPER&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;snap&lt;/code&gt; it almost feels like using &lt;code&gt;brew&lt;/code&gt; and &lt;code&gt;brew cask&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using it in clamshell mode works like a charm.&lt;/p&gt;

&lt;p&gt;The system even wakes up from sleep using a Bluetooh logitech keyboard with mouse (namely the Logitech k400 I think).&lt;/p&gt;

&lt;p&gt;Still need to find a better Terminal emulator, because I'm not really fond of the one built-in. Perhaps time to try Alacritty?&lt;/p&gt;

&lt;p&gt;This is how I am writing this very blog post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YMbrphA---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/elementary-os-preview.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YMbrphA---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/elementary-os-preview.png" alt="elementary os preview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2021-01-03-mac-linux/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>mac</category>
    </item>
    <item>
      <title>Mapping historic market data</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Sat, 02 Jan 2021 13:33:57 +0000</pubDate>
      <link>https://dev.to/christianfei/mapping-historic-market-data-2cbd</link>
      <guid>https://dev.to/christianfei/mapping-historic-market-data-2cbd</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2017-12-11-Mapping-historic-market-data/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Market data is generally represented as an array of arrays, and each entry is a data point that represents a "snapshot" of the market in a given period of time.&lt;/p&gt;

&lt;p&gt;Each data point contains information like the timestamp, open/close price, high/low price and the volume of transactions in this period.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--WhUZWKF_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/market-data.full.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WhUZWKF_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cri.dev/assets/images/posts/market-data.full.png" alt="market data"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a recent sample of a few data points for the currency pair &lt;code&gt;LTC-EUR&lt;/code&gt; on the market &lt;a href="https://www.gdax.com/"&gt;&lt;code&gt;gdax&lt;/code&gt;&lt;/a&gt;:&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;marketData&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="mi"&gt;1513025761&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// time&lt;/span&gt;
    &lt;span class="mi"&gt;161&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// low&lt;/span&gt;
    &lt;span class="mf"&gt;163.11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// high&lt;/span&gt;
    &lt;span class="mf"&gt;162.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// open&lt;/span&gt;
    &lt;span class="mf"&gt;162.67&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// close&lt;/span&gt;
    &lt;span class="mf"&gt;823.99626973&lt;/span&gt;    &lt;span class="c1"&gt;// volume&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="mi"&gt;1513024062&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;157.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;164.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;157.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;163&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;5343.671050650007&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;h2&gt;
  
  
  map to array of objects
&lt;/h2&gt;

&lt;p&gt;To get a more pleasant array of objects to work with, you can map the array structure to an Object with named property 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="nx"&gt;marketData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&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;And the result would be the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1513025761&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;161&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;163.11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;162.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"close"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;162.67&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"volume"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;823.99626973&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="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1513024062&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;157.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;164.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;157.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"close"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;163&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"volume"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5343.671050650007&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;h2&gt;
  
  
  Map to array of objects grouped by key
&lt;/h2&gt;

&lt;p&gt;It can also be interesting to have the data grouped by key.&lt;/p&gt;

&lt;p&gt;You can achieve this, reducing each key to an array of the corresponding values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marketData&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="c1"&gt;// ['time','low','high','open','close','volume']&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;marketData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="c1"&gt;// returns an array of values for each key&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&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="mi"&gt;1513024062&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1513025761&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"low"&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="mf"&gt;157.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;161&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"high"&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="mf"&gt;164.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;163.11&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"open"&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="mf"&gt;157.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;162.98&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"close"&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="mi"&gt;163&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;162.67&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"volume"&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="mf"&gt;5343.671050650007&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;823.99626973&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;Discuss these thoughts with me on &lt;a href="https://twitter.com/christian_fei"&gt;twitter&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2017-12-11-Mapping-historic-market-data/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
➜  christian-fei.github.io git:(master) &lt;/p&gt;

</description>
      <category>crypto</category>
      <category>blockchain</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Functional JavaScript: Write your own negate/not function</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Sat, 02 Jan 2021 13:32:36 +0000</pubDate>
      <link>https://dev.to/christianfei/functional-javascript-write-your-own-negate-not-function-3jfp</link>
      <guid>https://dev.to/christianfei/functional-javascript-write-your-own-negate-not-function-3jfp</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2015-05-09-Functional-JavaScript-write-your-negate-not-function/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's say you want to filter odd numbers from a list of numbers, like &lt;code&gt;[1,2,3,4,5,6]&lt;/code&gt; should become &lt;code&gt;[1,3,5]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One way to solve this problem in a functional fashion is to provide an &lt;code&gt;odd&lt;/code&gt; filter function, that is internally composed by a negation of an &lt;code&gt;even&lt;/code&gt; filter on those numbers.&lt;/p&gt;

&lt;p&gt;So, let's define these functions step by step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function odd(number){
  return not(even)
}

function not(predicate){
  return function(){
    return !predicate.apply(this, arguments)
  }
}

function even(number){
  return number % 2 == 0
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;even&lt;/code&gt; function needs no explaination, where the &lt;code&gt;functional magic ®&lt;/code&gt; happens is in the &lt;code&gt;odd&lt;/code&gt; and &lt;code&gt;not&lt;/code&gt; functions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;odd&lt;/code&gt; takes a number and uses the &lt;code&gt;even&lt;/code&gt; filter, by negating it with &lt;code&gt;not&lt;/code&gt;, to determine whether a number is odd or not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;not&lt;/code&gt; just wraps the &lt;code&gt;predicate&lt;/code&gt; function passed in and negates its application. Note that this application makes sense with functions that return boolean values, thus the name &lt;code&gt;predicate&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;One use case could be in combination with &lt;code&gt;Array.prototype.filter&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1,2,3,4,5,6].filter(not(even))

// or

[1,2,3,4,5,6].filter(odd)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2015-05-09-Functional-JavaScript-write-your-negate-not-function/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
➜  christian-fei.github.io git:(master) &lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What is a retrospective?</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Thu, 31 Dec 2020 05:25:44 +0000</pubDate>
      <link>https://dev.to/christianfei/what-is-a-retrospective-41c5</link>
      <guid>https://dev.to/christianfei/what-is-a-retrospective-41c5</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2020-09-24-What-is-a-retrospective/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  What is it?
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Comes from the necessity to regularly reflect about how project / team is going&lt;/li&gt;
&lt;li&gt;It’s an interactive way to focus on past things and see if there is a way we can improve them through a team effort and collaboration&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  The Prime Directive
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Regardless of what we discover, we understand and truly believe that everyone did the best job they could, given what they knew at the time, their skills and abilities, the resources available, and the situation at hand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  What do I get from it?
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Discussion about important topics 

&lt;ul&gt;
&lt;li&gt;already a step in the right direction&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Actions to improve in the future&lt;/li&gt;
&lt;li&gt;Grow, connect as a team&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  4 questions retrospective
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;What went well?&lt;/li&gt;
&lt;li&gt;What didn’t go so well?&lt;/li&gt;
&lt;li&gt;What have I learned?&lt;/li&gt;
&lt;li&gt;What still puzzles me?&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;We look at the past to improve in the future &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. What went well?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;we start with stating positive things to ensure we continue on doing those positive things in the future&lt;/li&gt;
&lt;li&gt;State a fact&lt;/li&gt;
&lt;li&gt;Saying thx is cool&lt;/li&gt;
&lt;li&gt;You can go into details&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. What didn’t go so well?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Difficulties, issues, dissatisfactions&lt;/li&gt;
&lt;li&gt;“Where better to look to make improvements than where things are not going so well?”&lt;/li&gt;
&lt;li&gt;Something less than ideal&lt;/li&gt;
&lt;li&gt;Give observations, not judgement&lt;/li&gt;
&lt;li&gt;“We spent a long time making the decision on the ordering process”, rather than “it shouldn’t take us long to decide the ordering process”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. What have I learned?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Positive + negative things&lt;/li&gt;
&lt;li&gt;&amp;gt; Observations on the past Create Opportunities for the Future&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. What still puzzles me?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Questions!&lt;/li&gt;
&lt;li&gt;Concerns&lt;/li&gt;
&lt;li&gt;Doubts&lt;/li&gt;
&lt;li&gt;Worries&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Turning 4 questions into improvements
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;We have some insights now

&lt;ul&gt;
&lt;li&gt;Identify actions to improve in the future&lt;/li&gt;
&lt;li&gt;Volunteer for action&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Remember that the volunteer is not necessarily the person doing the actions, they should remind the team about the action and be mindful about respecting the action&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agile</category>
      <category>feedback</category>
    </item>
    <item>
      <title>Ultimate web scraping with browserless, puppeteer and Node.js</title>
      <dc:creator>Christian</dc:creator>
      <pubDate>Mon, 28 Dec 2020 16:36:21 +0000</pubDate>
      <link>https://dev.to/christianfei/ultimate-web-scraping-with-browserless-puppeteer-and-node-js-ead</link>
      <guid>https://dev.to/christianfei/ultimate-web-scraping-with-browserless-puppeteer-and-node-js-ead</guid>
      <description>&lt;p&gt;&lt;strong&gt;Originally posted on &lt;a href="https://cri.dev/posts/2020-03-08-Ultimate-web-scraping-with-browserless,-puppeteer-and-Node.js/"&gt;cri.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Browser automation built for enterprises, loved by developers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://www.browserless.io/"&gt;browserless.io&lt;/a&gt; is a neat service for hosted puppeteer scraping, but there is also &lt;a href="https://docs.browserless.io/docs/docker.html"&gt;the official Docker image&lt;/a&gt; for running it locally.&lt;/p&gt;

&lt;p&gt;I was amazed when I found out about it 🤯!&lt;/p&gt;

&lt;p&gt;Find the whole source code on &lt;a href="https://github.com/christian-fei/browserless-example"&gt;Github christian-fei/browserless-example&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Running browserless in docker
&lt;/h2&gt;

&lt;p&gt;A one-liner is enough to have a full puppeteer backend, with configured concurrency etc., to leverage using &lt;strong&gt;puppeteer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can connect to a browserless backend by passing the option &lt;code&gt;browserWSEndpoint&lt;/code&gt; 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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;createBrowser&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;puppeteer&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="na"&gt;browserWSEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws://localhost:3000&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;To start the backend you can use the following command, using the docker image &lt;code&gt;browserless/chrome&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"MAX_CONCURRENT_SESSIONS=15"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"MAX_QUEUE_LENGTH=0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"PREBOOT_CHROME=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"DEFAULT_BLOCK_ADS=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"DEFAULT_IGNORE_HTTPS_ERRORS=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"CONNECTION_TIMEOUT=600000"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; browserless/chrome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Source code
&lt;/h2&gt;

&lt;p&gt;Find the whole source code on &lt;a href="https://github.com/christian-fei/browserless-example"&gt;Github christian-fei/browserless-example&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;You'll find a web crawler with puppeteer!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/christian-fei/browserless-example.git
&lt;span class="nb"&gt;cd &lt;/span&gt;browserless-example
npm i

npm run start-browserless
node crawl-with-api.js https://christianfei.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Puppeteer using browserless docker backend
&lt;/h2&gt;

&lt;p&gt;You simply connect to the &lt;strong&gt;Browser WebSocket Endpoint&lt;/strong&gt; &lt;code&gt;ws://localhost:3000&lt;/code&gt; and you're connected to the &lt;em&gt;browserless&lt;/em&gt; backend!&lt;/p&gt;

&lt;p&gt;Here is a short example of getting all links &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; on &lt;code&gt;christianfei.com&lt;/code&gt;:&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;puppeteer&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="s1"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="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;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finished, exiting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exit&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="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://christianfei.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&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;createBrowser&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;page&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;goto&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;links&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;links.length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;createBrowser&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;puppeteer&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="na"&gt;browserWSEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws://localhost:3000&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;An example video:&lt;/p&gt;


  


</description>
      <category>node</category>
      <category>puppeteer</category>
      <category>browserless</category>
      <category>scraping</category>
    </item>
  </channel>
</rss>
