<?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: Alec Brunelle</title>
    <description>The latest articles on DEV Community by Alec Brunelle (@aleccool213).</description>
    <link>https://dev.to/aleccool213</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%2F27218%2F95ff9c73-a8b8-4ca3-bb60-4c53cd93bf50.png</url>
      <title>DEV Community: Alec Brunelle</title>
      <link>https://dev.to/aleccool213</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleccool213"/>
    <language>en</language>
    <item>
      <title>Running SQL Migrations Before Booting Docker Compose Services</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Tue, 29 Sep 2020 18:45:55 +0000</pubDate>
      <link>https://dev.to/aleccool213/running-sql-migrations-before-booting-docker-compose-services-3d3d</link>
      <guid>https://dev.to/aleccool213/running-sql-migrations-before-booting-docker-compose-services-3d3d</guid>
      <description>&lt;p&gt;Having a great local development experience is critical to happy engineers. Developers are happy to come into a codebase and start hacking away at problems, as opposed to dreading picking up their laptops. As services become more and more separated into separate "micro-services", new problems arise. When I look back on my short career, one problem that has induced me a lot of pain are SQL Migrations and how they should work in Docker Compose. Ideally, they should run before your web apps boot so they have access to a well-structured database. Your strategy to make this happen may be different for each environment, in this post I'll cover your local experience which easily extends to CI and I'll also touch how to do this in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Bit of Background
&lt;/h2&gt;

&lt;p&gt;I ran into this problem when we started to adopt &lt;a href="https://www.apollographql.com/blog/apollo-federation-f260cf525d21/" rel="noopener noreferrer"&gt;Apollo Federation&lt;/a&gt; into our stack. One gateway service lives in front of our other GraphQL services (which have their own databases). This gateway is what the client-side apps ("Web Browser" in diagram) queries to get its data. For clients to start using the gateway API every dependent service must be up and healthy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1601385465%2Fdocker-sql-post%2Fentire_service_diagram.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1601385465%2Fdocker-sql-post%2Fentire_service_diagram.png" alt="Service diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My goal is to start the entire set of services in a deterministic manner, have each service wait until its dependent service is running and healthy. I'll focus on the database and migrations in this post but the concepts here extend to any service having dependencies on another service.&lt;/p&gt;

&lt;p&gt;A typical Docker Compose file which runs the products service with it's migrations will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.7"&lt;/span&gt;

&lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yourorg/postgres&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres -c 'max_connections=1024'&lt;/span&gt;
  &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432"&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DATABASE_USERNAME}&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DATABASE_PASSWORD}&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${YOUR_ORG_INSTALL_PATH}/volumes/services/postgres-data:/var/lib/postgresql/data:delegated&lt;/span&gt;

&lt;span class="na"&gt;products-run-migrations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/products/migrations/.&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yourorg/products-run-migrations:${IMAGE_TAG:-latest}&lt;/span&gt;
  &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./apps/products/products.env&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;

&lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/products/.&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yourorg/products:${IMAGE_TAG:-latest}&lt;/span&gt;
  &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./apps/products/products.env&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;products-run-migrations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few issues with this setup, focusing on &lt;code&gt;depends_on&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;products-run-migrations&lt;/code&gt; script will run right when &lt;code&gt;postgres&lt;/code&gt; starts to run, not when it's healthy and ready to accept connections. There is a 99% chance that the &lt;code&gt;postgres&lt;/code&gt; container will not be healthy when the migrations begin to run, causing them to fail.&lt;/li&gt;
&lt;li&gt;Similar situation with the &lt;code&gt;products&lt;/code&gt; service, it will run right when &lt;code&gt;products-run-migrations&lt;/code&gt; starts to run. If you query the &lt;code&gt;products&lt;/code&gt; service when it's healthy, there is a good chance the migrations have not yet completed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What We Want
&lt;/h2&gt;

&lt;p&gt;This is currently not what we want, ideally developers should be able to start the products service and when it's healthy, know that the migrations ran successfully and they can query its API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1601385467%2Fdocker-sql-post%2Fexternal-content.duckduckgo.com.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1601385467%2Fdocker-sql-post%2Fexternal-content.duckduckgo.com.gif"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This required me to do some research.&lt;/p&gt;

&lt;p&gt;I found out there is a &lt;code&gt;conditions&lt;/code&gt; argument to &lt;code&gt;depends_on&lt;/code&gt; I could use to achieve the implementation we wanted. But the next thing I found out is where things went haywire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We needed to downgrade our docker-compose version.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/peter-evans/docker-compose-healthcheck/issues/3" rel="noopener noreferrer"&gt;Turns out, Docker Compose 3.x versions are meant to be used for Docker Swarm and Kubernetes environments where services are not strictly dependent on each other.&lt;/a&gt; This promotes a more fault-tolerant environment where services run independently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://peterevans.dev/posts/how-to-wait-for-container-x-before-starting-y/" rel="noopener noreferrer"&gt;What I saw people suggesting&lt;/a&gt; was to switch to Docker Compose 2.4 and use a port waiting script like &lt;a href="https://github.com/vishnubob/wait-for-it" rel="noopener noreferrer"&gt;wait-for-it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our new Docker Compose 2.4 file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.4'&lt;/span&gt;

&lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yourorg/postgres&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres -c 'max_connections=1024'&lt;/span&gt;
  &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432'&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DATABASE_USERNAME}&lt;/span&gt;
    &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DATABASE_PASSWORD}&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${YOUR_ORG_INSTALL_PATH}/volumes/services/postgres-data:/var/lib/postgresql/data:delegated&lt;/span&gt;
  &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;root'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;

&lt;span class="na"&gt;products-run-migrations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/products/migrations/.&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yourorg/products-run-migrations:${IMAGE_TAG:-latest}&lt;/span&gt;
  &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-c&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wait-for-it&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$$DATABASE_HOSTNAME:5432&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;60&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;db-migrate:products&lt;/span&gt;
  &lt;span class="s"&gt;restart:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on-failure:5&lt;/span&gt;
  &lt;span class="s"&gt;env_file:&lt;/span&gt;
    &lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;./apps/products/products.env&lt;/span&gt;
  &lt;span class="s"&gt;depends_on:&lt;/span&gt;
      &lt;span class="s"&gt;postgres:&lt;/span&gt;
        &lt;span class="s"&gt;condition:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;service_healthy&lt;/span&gt;

&lt;span class="s"&gt;products:&lt;/span&gt;
  &lt;span class="s"&gt;build:&lt;/span&gt;
    &lt;span class="s"&gt;context:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;./apps/products/.&lt;/span&gt;
  &lt;span class="s"&gt;image:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;yourorg/products:${IMAGE_TAG:-latest}&lt;/span&gt;
  &lt;span class="s"&gt;env_file:&lt;/span&gt;
    &lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;./apps/products/products.env&lt;/span&gt;
  &lt;span class="s"&gt;restart:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on-failure:5&lt;/span&gt;
  &lt;span class="s"&gt;depends_on:&lt;/span&gt;
      &lt;span class="s"&gt;postgres:&lt;/span&gt;
        &lt;span class="s"&gt;condition:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="s"&gt;products-run-migrations:&lt;/span&gt;
        &lt;span class="s"&gt;condition:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;service_started&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few new additions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Added a &lt;code&gt;healthcheck&lt;/code&gt; to the &lt;code&gt;postgres&lt;/code&gt; container. This made it so migration scripts like &lt;code&gt;products-run-migrations&lt;/code&gt; can start to run only when the database is ready to accept connections.&lt;/li&gt;
&lt;li&gt;We added an &lt;code&gt;entrypoint&lt;/code&gt; along with the &lt;code&gt;condition&lt;/code&gt; arg to &lt;code&gt;depends_on&lt;/code&gt; to our migration images. This made sure that not only the database is running but that the port is returning a 200 response code.&lt;/li&gt;
&lt;li&gt;Added &lt;code&gt;condition&lt;/code&gt; args to both &lt;code&gt;depends_on&lt;/code&gt; in the &lt;code&gt;products&lt;/code&gt; service definition.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's not perfect but works much better than before. The issue still remains of the app not waiting for the migrations to be fully finished before booting up. If they run relatively quickly, local developers may not run into problems. In CI, we have full control over the environment so we can add an extra command to skirt around this issue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .circleci/config.yml&lt;/span&gt;
&lt;span class="na"&gt;test-products&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-docker&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;test-project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;project-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;products&lt;/span&gt;
        &lt;span class="na"&gt;pre-test-command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker-compose run -T products-run-migrations&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run-project-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;project-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;products&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Issues That Still Remain
&lt;/h2&gt;

&lt;p&gt;What I didn't touch on is what our production environment looks like in terms of Docker Compose. We decided to maintain two versions of our Docker Compose files, one in 2.4 and one in 3.7. This is because we want to adopt Kubernetes easily in the future. You may stick with one or the other but for a great local development experience, we decided to always have a 2.4 file going.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>sql</category>
      <category>devops</category>
    </item>
    <item>
      <title>Build a Next.js Blog with Cosmic’s GraphQL API</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Wed, 23 Sep 2020 14:06:06 +0000</pubDate>
      <link>https://dev.to/aleccool213/build-a-next-js-blog-with-cosmic-s-graphql-api-44hd</link>
      <guid>https://dev.to/aleccool213/build-a-next-js-blog-with-cosmic-s-graphql-api-44hd</guid>
      <description>&lt;p&gt;&lt;strong&gt;Want to follow along with the build? &lt;a href="https://www.cosmicjs.com/apps/nextjs-static-blog"&gt;Click here to grab the app or fork the project.&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;With so many choices for which technology to use when building a website, it can get overwhelming. You need to consider who is going to use it, what content to display and who will maintain it. A static website is a great choice when creating a blog, band website or e-commerce store. Static websites are an ode to the past when websites were just plain-ol files on a server you accessed via URL. They provide benefits like being fast, having great SEO and not being dependent on a certain runtime like PHP. This is in comparison to a server-rendered website like what you would have with Wordpress, Drupal or Ruby on Rails.&lt;/p&gt;

&lt;p&gt;Static websites are built using static assets. The next question becomes where to store (and manage) this content. If you are a solo webmaster, the content can be files in a Git repo. If you have clients or other developers who will want to manage the content, a CMS (Content Management System) is what you need. A CMS is a service which stores your website content, for example blog posts and concert dates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZVEYhnna--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443356/next-js-cosmic-post/CleanShot_2020-09-18_at_10.15.26_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZVEYhnna--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443356/next-js-cosmic-post/CleanShot_2020-09-18_at_10.15.26_2x.png" alt="Screenshot of the Cosmic CMS Dashboard" width="880" height="534"&gt;&lt;/a&gt;&lt;br&gt;Cosmic CMS!
  &lt;/p&gt;

&lt;p&gt;With &lt;a href="https://nextjs.org/docs/basic-features/pages#static-generation-recommended"&gt;Next.js SSG&lt;/a&gt;, we are using the CMS in a &lt;a href="https://www.cosmicjs.com/headless-cms"&gt;"headless" fashion&lt;/a&gt;. After trying a bunch of Headless CMS offerings, one I've stuck with is Cosmic. &lt;a href="https://www.cosmicjs.com"&gt;Cosmic&lt;/a&gt; is an intuitive, powerful, and simple-to-use service which lets us get up and running quickly. They provide &lt;a href="https://www.cosmicjs.com/apps"&gt;many starter apps&lt;/a&gt; that you can preview or fork. For example, I chose the Next.js Static Blog and had a production version of the website running in under &lt;strong&gt;5 minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing the Tech
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://nextjs.org/"&gt;Next.js&lt;/a&gt; with GraphQL is my personal choice when it comes to Static site development. Next.js is a hybrid React framework which supports building static websites. It also lets you build &lt;a href="https://nextjs.org/docs/basic-features/pages#server-side-rendering"&gt;server-side rendered pages&lt;/a&gt; when the need arises. It handles routing, has a large community supporting it making it one of the best ways to build a React app in 2020. The other tech you may have heard also does this is &lt;a href="https://www.gatsbyjs.com/"&gt;Gatsby.js&lt;/a&gt;. Gatsby is more user-friendly but is more opinionated with its technology choices (forced use of GraphQL versus it being a choice).&lt;/p&gt;

&lt;p&gt;We are choosing to use GraphQL over &lt;a href="https://www.npmjs.com/package/cosmicjs"&gt;the Cosmic NPM module&lt;/a&gt;. &lt;a href="https://www.cosmicjs.com/blog/what-is-graphql"&gt;GraphQL&lt;/a&gt; is a standardized way to get data from services and is a great choice when needing to get data from a CMS. As you create custom data types in Cosmic, you are able to query for it in the GraphQL API. One of the benefits of using GraphQL is that you are less dependent on a specific SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tutorial
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;For reference, I forked the example Cosmic Next.js project &lt;a href="https://github.com/vercel/next.js/tree/canary/examples/cms-cosmic"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Creating the Cosmic Project
&lt;/h3&gt;

&lt;p&gt;After creating an account on Cosmic and going through the product tour, you will be shown the “Create New Bucket” screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MPvxkBf8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443361/next-js-cosmic-post/CleanShot_2020-09-18_at_08.34.47_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MPvxkBf8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443361/next-js-cosmic-post/CleanShot_2020-09-18_at_08.34.47_2x.png" alt="Screenshot of the Cosmic CMS App Search Page" width="880" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click "Start with an App" then search and select "&lt;a href="https://www.cosmicjs.com/apps/nextjs-static-blog"&gt;Next.js Static Blog&lt;/a&gt;" from the list of apps presented. This will do many of things.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Cosmic bucket&lt;/li&gt;
&lt;li&gt;Create sane data-types inside the bucket for use with a blog&lt;/li&gt;
&lt;li&gt;Fill the bucket with demo content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--87utLqQ5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1599827352/next-js-cosmic-post/CleanShot_2020-09-11_at_08.28.20_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--87utLqQ5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1599827352/next-js-cosmic-post/CleanShot_2020-09-11_at_08.28.20_2x.png" alt="Screenshot of the Cosmic CMS Dashboard after creating a bucket" width="880" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is what the created bucket looks like on your Cosmic dashboard&lt;/p&gt;

&lt;h3&gt;
  
  
  Next.js local development
&lt;/h3&gt;

&lt;p&gt;The next thing we need to do is clone the Next.js code to our local environments. This will enable us to run the Next.js locally and pull content from the demo Cosmic bucket we created.&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 git@github.com:aleccool213/nextjs-cosmic-graphql-app.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also choose to create a GitHub repository for yourself using &lt;a href="https://github.com/aleccool213/nextjs-cosmic-graphql-app/generate"&gt;the template&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once inside the new directory, make sure you are using the correct Node.js version by using &lt;a href="https://github.com/nvm-sh/nvm"&gt;NVM&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm use v12.18.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install Yarn and install the project dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;yarn &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; yarn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the app!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bVBVq7yO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1599828409/next-js-cosmic-post/CleanShot_2020-09-11_at_08.46.14_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bVBVq7yO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1599828409/next-js-cosmic-post/CleanShot_2020-09-11_at_08.46.14_2x.png" alt="Screenshot of the app running locally but encountering an error due to no environment variables being set" width="880" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Almost there!&lt;/p&gt;

&lt;h3&gt;
  
  
  Cosmic Environment Variables
&lt;/h3&gt;

&lt;p&gt;Before we are able to query the Cosmic GraphQL API, our app needs to know where it lives. Environment Variables are deployment specific values which contain sensitive things like API keys.&lt;/p&gt;

&lt;p&gt;There are three env vars we need to define to have the app work locally. Create a file named &lt;code&gt;.env.local&lt;/code&gt; (don't worry it's ignored by Git), it should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;COSMIC_BUCKET_SLUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-nextjs-static-blog
&lt;span class="nv"&gt;COSMIC_READ_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;77H1zN7bTktdsgekxyB9FTpOrlVNE3KUP0UTptn5EqA7T0J8Qt
&lt;span class="c"&gt;# Preview secret can be anything you choose&lt;/span&gt;
&lt;span class="nv"&gt;COSMIC_PREVIEW_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;iwvrzpakhaavqbihwlrv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get these values, head over to the Settings sidebar menu in your bucket, and click "Basic Settings".&lt;/p&gt;

&lt;p&gt;Run the app again with &lt;code&gt;yarn dev&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ct_s3VZX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1599829500/next-js-cosmic-post/CleanShot_2020-09-11_at_09.04.40_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ct_s3VZX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1599829500/next-js-cosmic-post/CleanShot_2020-09-11_at_09.04.40_2x.png" alt="Screenshot of the example blog running on a local machine" width="880" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And we are up!&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking inside the box
&lt;/h3&gt;

&lt;p&gt;There are two things that we need to understand when it comes to Statically-Generated Next.js apps, pages and routes. &lt;a href="https://nextjs.org/docs/basic-features/pages#scenario-1-your-page-content-depends-on-external-data"&gt;Pages are content which depend on external data&lt;/a&gt;, and &lt;a href="https://nextjs.org/docs/basic-features/pages#scenario-1-your-page-content-depends-on-external-data"&gt;routes are URL routes which depend on external data&lt;/a&gt;. Both have you defining special Next.js specific functions, &lt;code&gt;getStaticProps&lt;/code&gt; and &lt;code&gt;getStaticPaths&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The file which contains the logic for generating page content based on the Cosmic GraphQL API is located at &lt;a href="https://github.com/aleccool213/nextjs-cosmic-graphql-app/blob/661144a8eddebff19c709ec18ad8e1765f7600ec/pages/posts/%5Bslug%5D.js#L57"&gt;pages/posts/[slug].js&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="k"&gt;export&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;getStaticProps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Get the data from the API&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getPostAndMorePosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Convert markdown content to HTML content&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;markdownToHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;morePosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;morePosts&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="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 javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&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;getPostAndMorePosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Query for the data through the Cosmic GraphQL API using Apollo Client&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;moreObjectsResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
      query getPostQuery(
        $bucketSlug: String!
        $readKey: String!
        $status: status
      ) {
        getObjects(
          bucket_slug: $bucketSlug
          input: {
            read_key: $readKey
            type: "posts"
            status: $status
            limit: 3
          }
        ) {
          objects {
            _id
            slug
            title
            metadata
            created_at
          }
        }
      }
    `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;bucketSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BUCKET_SLUG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;readKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;READ_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one example of a page using &lt;code&gt;getStaticProps&lt;/code&gt;. It is &lt;a href="https://github.com/aleccool213/nextjs-cosmic-graphql-app/blob/661144a8eddebff19c709ec18ad8e1765f7600ec/pages/index.js#L40"&gt;also used in the Index page&lt;/a&gt; for getting all the blog post titles and excerpts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pages/posts/[slug].js&lt;/code&gt; &lt;a href="https://github.com/aleccool213/nextjs-cosmic-graphql-app/blob/661144a8eddebff19c709ec18ad8e1765f7600ec/pages/posts/%5Bslug%5D.js#L73"&gt;also contains &lt;code&gt;getStaticPaths&lt;/code&gt;&lt;/a&gt; which tells our Next.js app which routes to generate.&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;export&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;getStaticPaths&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Get all post data (including content)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allPosts&lt;/span&gt; &lt;span class="o"&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;getAllPostsWithSlug&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Tell Next.js all of the potential URL routes based on slugs&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;allPosts&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;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After understanding these two aspects, the blog is just a regular React app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying
&lt;/h2&gt;

&lt;p&gt;Now that we have our website working locally, let's deploy it to &lt;a href="https://vercel.com/"&gt;Vercel&lt;/a&gt;. The first step is making sure you have the code in a Git repository.&lt;/p&gt;

&lt;p&gt;I recommend you have the code in GitHub. You can use the &lt;a href="https://cli.github.com/"&gt;GitHub CLI&lt;/a&gt; to create a repo in your current directory with &lt;code&gt;gh repo create&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We now need to sign up for Vercel and have it use the code from the GitHub repo. You can sign up for Vercel with your GitHub account &lt;a href="https://vercel.com/signup"&gt;here&lt;/a&gt;. You can import the code from GitHub using the "Import Project" feature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qMWgmaq2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443380/next-js-cosmic-post/CleanShot_2020-09-16_at_08.34.54_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qMWgmaq2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443380/next-js-cosmic-post/CleanShot_2020-09-16_at_08.34.54_2x.png" alt="Screenshot of the Vercel project view with the Import Project button highlighted" width="880" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When importing the project, make sure you define the environment variables, &lt;code&gt;COSMIC_BUCKET_SLUG&lt;/code&gt;, &lt;code&gt;COSMIC_READ_KEY&lt;/code&gt;, and &lt;code&gt;COSMIC_PREVIEW_SECRET&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When deployed, all pushes to your default Git branch will have Vercel deploy a new version of your website!&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Previewing
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;The Next.js docs on preview mode are right &lt;a href="https://nextjs.org/docs/advanced-features/preview-mode"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Local development and deploying the website to production will cover most of your use-cases. Another common workflow is saving a draft of changes on your CMS and then previewing those changes on your local machine. To do so, we will enable "Preview" mode both on Cosmic and our Next.js app.&lt;/p&gt;

&lt;p&gt;First thing we will need to do is have Cosmic know that the Posts object type will be preview-able. On the Posts setting page, add the preview link.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;http://localhost:3000/api/preview?secret&lt;span class="o"&gt;=&lt;/span&gt;iwvrzpakhaavqbihwlrv&amp;amp;slug&lt;span class="o"&gt;=[&lt;/span&gt;object_slug]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When finished, click "Save Object Type".&lt;/p&gt;

&lt;p&gt;Let's try editing a post and see it show up on our local machine. Try changing the title of "Learn How to Pre-render Pages Using Static Generation with Next.js" and click "Save Draft" instead of "Publish".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XwGKjqcy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443385/next-js-cosmic-post/CleanShot_2020-09-16_at_08.45.27_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XwGKjqcy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443385/next-js-cosmic-post/CleanShot_2020-09-16_at_08.45.27_2x.png" alt="Screenshot of the Cosmic CMS Post data type editing page with Save Draft button highlighted" width="880" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Save Draft&lt;/code&gt; button&lt;/p&gt;

&lt;p&gt;We now have unpublished changes. Run the app locally with &lt;code&gt;yarn dev&lt;/code&gt; and then click "Preview" right under "Save Draft".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3Nikviio--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443400/next-js-cosmic-post/CleanShot_2020-09-16_at_09.20.58_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3Nikviio--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443400/next-js-cosmic-post/CleanShot_2020-09-16_at_09.20.58_2x.png" alt="Screenshot of the example blog running on a local machine with the preview edits being shown" width="880" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our preview mode!&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhooks
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Note this feature requires a Cosmic paid plan&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The only way to deploy new content to our blog is to have a developer push to the default git branch. This action will trigger Vercel take the new code and push a new version of our website. We ideally want our content creators to have the same control. Webhooks are a way we can do this.&lt;/p&gt;

&lt;p&gt;Let's set up a webhook which lets Vercel know when our posts in Cosmic have new updates. This will let us deploy new versions of the website without developers needing to do anything.&lt;/p&gt;

&lt;p&gt;Go to the Git integration settings page (&lt;a href="https://vercel.com/%5Bproject%5D/settings/git-integration"&gt;https://vercel.com/[project]/settings/git-integration&lt;/a&gt;) in your Vercel project and create a new Deploy Hook named "Cosmic Hook".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--a9w-yZeh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443413/next-js-cosmic-post/CleanShot_2020-09-18_at_08.02.26_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--a9w-yZeh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443413/next-js-cosmic-post/CleanShot_2020-09-18_at_08.02.26_2x.png" alt="Screenshot of the Vercel webhook settings" width="880" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What your settings should look like when the webhook is created&lt;/p&gt;

&lt;p&gt;Now over in Cosmic settings, we can add this webhook. Let's add Cosmic notify Vercel when changes get published. You can see how we can do this for previews as well if we wanted to in the future.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Q-MOHVLm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443424/next-js-cosmic-post/CleanShot_2020-09-18_at_08.05.34_2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Q-MOHVLm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1600443424/next-js-cosmic-post/CleanShot_2020-09-18_at_08.05.34_2x.png" alt="Screenshot of the Cosmic CMS webhooks settings" width="880" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Edited/Created and Published!&lt;/p&gt;

&lt;p&gt;To test this go to the same post we tested Previews with and add some content to the end of the article and publish. You should see a deploy happen on Vercel with the new content deployed to the live version of your website!&lt;/p&gt;

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

&lt;p&gt;Want to see what the final website looks like? &lt;a href="https://nextjs-cosmic-graphql-app.vercel.app/"&gt;Click here to check it out.&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Like this post? &lt;a href="https://mailchi.mp/f91826b80eb3/alecbrunelleemailsignup"&gt;Subscribe to my email newsletter&lt;/a&gt; for more like it in the future.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>beginners</category>
      <category>react</category>
      <category>graphql</category>
      <category>cms</category>
    </item>
    <item>
      <title>Calm, Private and Healthy, A HEY Email Review</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Wed, 29 Jul 2020 12:41:57 +0000</pubDate>
      <link>https://dev.to/aleccool213/calm-private-and-healthy-a-hey-email-review-j18</link>
      <guid>https://dev.to/aleccool213/calm-private-and-healthy-a-hey-email-review-j18</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Want more great content like this? Sign up for my newsletter, visit: &lt;a href="//alec.coffee/newsletter"&gt;alec.coffee/signup&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You might have seen Basecamp making headlines when &lt;a href="https://www.theverge.com/2020/6/18/21296180/apple-hey-email-app-basecamp-rejection-response-controversy-antitrust-regulation"&gt;they took on Apple to fight for fair app-store monetization policies&lt;/a&gt;. HEY is a new email service which, you guessed it, sends and receives email. Going up against competitors with huge head starts, HEY sets out to change the flavour of email as opposed to adding a little spice. After trying it for two weeks, HEY made my email experience a more calm, private and healthy experience with little to complain about.&lt;/p&gt;

&lt;p&gt;HEY targets those who are tired with existing solutions, the users who see email as a chore and not what it should be, a delightful, curated experience. HEY gives you complete control over where email should go, who is allowed to give you a push notification and so on. Shifting away from automated filters and into a more controlling experience is tough at first but is worth it in the end.&lt;/p&gt;

&lt;p&gt;I switched from Gmail to &lt;a href="https://www.fastmail.com"&gt;Fastmail&lt;/a&gt; years ago for simplicity and enhanced privacy. There is something to be said about spending money on a small business versus one which &lt;a href="https://techcrunch.com/2020/01/23/squint-and-youll-click-it/"&gt;cares about its ad revenue more than it's users&lt;/a&gt; that lures me to a service. HEY is a big step forward in the same direction as Fastmail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email Hygiene
&lt;/h2&gt;

&lt;p&gt;Basecamp is looking at email from the ground up. Jason Fried, founder and CEO of Basecamp, says it well &lt;a href="https://youtu.be/UCeYTysLyGI?t=44"&gt;in his walk-through of the product:&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One of the problems with email is that everybody can email you, which is also one of the great things about email&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I see myself as a hygienic email user, using filters and labels as much as I can, and try my best to combat the fact that anyone has access to my inbox. A few custom rules to put emails to certain folders, rules to make my inbox feel a more clear and less cluttered. For example, I have a newsletter rule where emails I don't care about go (sorry to the marketers who are reading this) and a rule for receipts. This happened to map 1-1 with &lt;a href="https://hey.com/how-it-works"&gt;HEY's "The Feed" and "The Paper Trail"&lt;/a&gt;. This meant that using HEY was already familiar to me. It made my workflow for creating rules much easier with &lt;a href="https://hey.com/how-it-works"&gt;"The Screener"&lt;/a&gt; which buckets sender's into these categories with unmatched speed. I can say that these buckets were enough. Non-categorized senders turned out to be important and kept going into my &lt;a href="https://hey.com/features/the-imbox/"&gt;"Imbox"&lt;/a&gt;. If they abused the fact they had this access, I would screen them out, banished to the shadow-realm, my spam folder.&lt;/p&gt;

&lt;p&gt;When all the filters are at full steam, checking email is a calming and less stressful experience. This is something I never thought email would make me feel. Another reason for this is that no notifications are sent to you for new emails by default, you need to choose who is able to notify you. This falls in line with how much control HEY gives you, it lets you decide what is important.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0x3PoRrg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1595858961/hey-email-post/preview-imbox-508814e250e89a00b534371089a2310ff7d89796fbaa17d199bf8ae1f44ab114.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0x3PoRrg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1595858961/hey-email-post/preview-imbox-508814e250e89a00b534371089a2310ff7d89796fbaa17d199bf8ae1f44ab114.jpg" alt="HEY email app screenshot" width="880" height="442"&gt;&lt;/a&gt;&lt;br&gt;The Imbox
  &lt;/p&gt;

&lt;h2&gt;
  
  
  Ultimate privacy
&lt;/h2&gt;

&lt;p&gt;Okay, get this, every email that you receive has a tracker inside it, it can tell &lt;strong&gt;when you opened it, where you opened it and how many times you opened it&lt;/strong&gt;. Well, not every single one, but every email you get might have it because you wouldn't be able to tell the difference. Senders put a 1x1 HTML image element inside an email (hidden from view), when you open it this image gets loaded from the sender's server. This is how a bunch of information like I described earlier is then sent back to the email sender. Blocking this behaviour is as easy as not loading images when you open an email, which no email client does by default, it needs to be set by the user. The other problem is that if you turn that on, you can hardly read emails because usually, the content depends on images loading. &lt;a href="https://hey.com/features/spy-pixel-blocker/"&gt;HEY brings a feature&lt;/a&gt; that surprises me no one else is doing yet, filtering out those pixel trackers.&lt;/p&gt;

&lt;p&gt;Filtering out pixel trackers is akin to a DNS server that has ad-blocking enabled. I already use one of these called &lt;a href="https://nextdns.io/"&gt;NextDNS&lt;/a&gt;. It works well and makes it so I don't have to do ad-blocking client-side, for example with a browser extension. How HEY does this is that it loads images within your email on a server within their network. This makes the sender think the email has been opened by you (but it was opened by HEY). HEY then shows you these pre-loaded images from their server, along with the email it was placed in. Being the privacy-oriented person I am the price tag for HEY may be worth it just for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The horror, vendor lock-in
&lt;/h2&gt;

&lt;p&gt;To do this magic, Basecamp decided to not let users integrate using email protocols such as &lt;a href="https://www.emailaddressmanager.com/tips/protocol.html"&gt;IMAP, POP3 and SMTP&lt;/a&gt;. This means you can't use HEY with any other client than their own. Mail for MacOS, Thunderbird, yup, all out of the question. This is a con of the service, other email clients have been in development for eons and come with useful features, too many to mention here. HEY comes with native apps for every device you use but it stills feels wrong to not talk about it. I get it why they don't want to support it, most of HEY's features wouldn't make sense on a traditional email client. For example, screening senders, merging/renaming threads, and others.&lt;/p&gt;

&lt;p&gt;If you decide to switch off of HEY, exporting all your emails is possible so you don't have to worry about your history being lost. HEY promises to forward your emails sent to your old HEY address to a new address if you do decide to switch (that is if you paid for HEY in the first place).&lt;/p&gt;

&lt;h2&gt;
  
  
  Yet another thing to "learn"
&lt;/h2&gt;

&lt;p&gt;The problem with new software products is that the learning curve usually is steep. New product fatigue is hard to deal with and this is why new products that come out try to steal features from existing solutions for easy adoption. HEY suffers from this, you will need to learn how to use it or you will get lost and won't feel comfortable switching. An on-boarding experience and tutorials covering HEY's features help but I felt like these need improvement. I felt sad when the tutorials stopped coming, I wasn't comfortable to navigate my email alone yet. Nuances like moving emails around to different folders and not knowing if future emails will go there. Little things to criticize but shouldn't stop anyone from enjoying what Basecamp has built.&lt;/p&gt;

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

&lt;p&gt;Getting to this point took some strong-willed people willing to take a risk, going up against established players in the game. HEY isn't a perfect email app but it pushes the boundaries of how email &lt;strong&gt;should&lt;/strong&gt; work. This alone is commendable, most companies don't have the gusto to do this. I decided not to pay for a full-year sub as they don't support custom domains and switching email addresses was painful the last time I did. I only have the energy, time and motivation to do this once a decade. &lt;a href="https://hey.com/custom-domains/"&gt;They have a form to stay up to date&lt;/a&gt; and get notified when this feature ships as they know it's the reason a lot of people are waiting to pay.&lt;/p&gt;

</description>
      <category>email</category>
      <category>privacy</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A Better Way to use GraphQL Fragments in React</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Tue, 12 May 2020 13:27:39 +0000</pubDate>
      <link>https://dev.to/aleccool213/a-better-way-to-use-graphql-fragments-in-react-33oo</link>
      <guid>https://dev.to/aleccool213/a-better-way-to-use-graphql-fragments-in-react-33oo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Want more great content like this? Sign up for my newsletter, visit: &lt;a href="//alec.coffee/newsletter"&gt;alec.coffee/signup&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One of the great reasons to use a component-based framework (React, Vue) is that it allows for more isolated component design, which helps with decoupling and unit-testing. Another benefit is using showcase apps such as &lt;a href="https://storybook.js.org/"&gt;Storybook&lt;/a&gt;, these continue the philosophy of isolation and allow for design and prototyping outside the main application. When component count starts to grow and we start to fetch data, we need a new pattern, &lt;a href="https://learn.co/lessons/react-container-components"&gt;the Container Component pattern&lt;/a&gt;. If using GraphQL for your data transport, we want to keep using this pattern but with a new twist. When creating isolated components, they should define the data they need to render. This can be better achieved by each component, even presentational ones, defining the data they need to render with their own GraphQL fragment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show Time
&lt;/h2&gt;

&lt;p&gt;Let's say we have a component which renders a list of Github issues showing their title. In the Container Component pattern, we would have a "container" component, &lt;code&gt;GithubIssueListContainer&lt;/code&gt;, which handles running the query. After this, it passes down the data to its presentational components which need it to render, &lt;code&gt;GithubIssueInfoCard&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_ISSUES_LIST_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
  query GithubIssuesListContainerQuery {
    organization {
      id
      name
    }
    issues {
    totalCount
    pageInfo {
      endCursor
      hasNextPage
    }
    edges {
      node {
        id
        title
        description
      }
    }
  }
`&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;GithubIssueListContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loading&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;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_ISSUES_LIST_QUERY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&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;edge&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubIssueInfoCard&lt;/span&gt; &lt;span class="nx"&gt;issueDetails&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GithubIssueInfoCardProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;issueDetails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="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;GithubIssueInfoCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;issueDetails&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;issueDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;issueDetails&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="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;issueDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issue here is that &lt;code&gt;GithubIssueInfoCard&lt;/code&gt; is dependent on its parent component in its knowledge of where data comes from in the GraphQL graph.&lt;/p&gt;

&lt;p&gt;If we want to render a new field from the graph, e.g. &lt;code&gt;labels&lt;/code&gt;, we will need to add that to the query in &lt;code&gt;GithubIssueListContainer&lt;/code&gt; and pass that down to &lt;code&gt;GithubIssueInfoCard&lt;/code&gt; via props. This requires changes to the both the query in &lt;code&gt;GithubIssueListContainer&lt;/code&gt; and the props in &lt;code&gt;GithubIssueInfoCard&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is the Way
&lt;/h2&gt;

&lt;p&gt;Following along our mantra of isolation, how about if &lt;code&gt;GithubIssueInfoCard&lt;/code&gt; defined what data it needs to render from the GraphQL graph. That way, when we make changes to what data this component, only this component needs to change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_ISSUES_LIST_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_ISSUE_INFO_CARD_FRAGMENT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  query GithubIssuesListContainerQuery {
    organization {
      id
      name
    }
    issues {
      totalCount
      pageInfo {
        endCursor
        hasNextPage
      }
      edges {
        node {
          ...GithubIssueInfoCardFragment
        }
      }
    }
  }
`&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;GithubIssueListContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_ISSUES_LIST_QUERY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&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;edge&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubIssueInfoCard&lt;/span&gt; &lt;span class="nx"&gt;issueDetails&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_ISSUE_INFO_CARD_FRAGMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
  fragment GithubIssueInfoCardFragment on Issue {
    id
    title
    description
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GithubIssueInfoCardProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;issueDetails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="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;GithubIssueInfoCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;issueDetails&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;issueDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;issueDetails&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="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;issueDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This might seem odd at first, but the benefits are worth it. As with anything in programming it doesn't come without tradeoffs.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Less parent component coupling
&lt;/h3&gt;

&lt;p&gt;When components define the data it needs to render, it de-couples the component from its parent. If for example you wanted to show &lt;code&gt;GithubIssueInfoCard&lt;/code&gt; on another page, import the fragment into that container component to get the right data fetched. e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;GITHUB_ISSUE_INFO_CARD_FRAGMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;GithubIssueInfoCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./GithubIssueInfoCard&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;NOTIFICATIONS_LIST_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_ISSUE_INFO_CARD_FRAGMENT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  query NotificationsContainerQuery {
    notifications {
      totalCount
      pageInfo {
        endCursor
        hasNextPage
      }
      edges {
        node {
          id
          eventText
          eventAssignee {
            id
            avatar
            username
          }
          relatedIssue {
            ...GithubIssueInfoCardFragment
          }
        }
      }
    }
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Types become easier to maintain
&lt;/h3&gt;

&lt;p&gt;If using a TypeScript, you likely are generating types from your GraphQL queries. A large benefit of our new pattern comes with defining props in components. You can define the data it needs to render as a type from our generated type file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GithubIssueInfoCardFragment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../graphql-types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GithubIssueInfoCardProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;issueDetails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GithubIssueInfoCardFragment&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;When the fragment changes, after you generate types, no prop changes needed!&lt;/p&gt;

&lt;h3&gt;
  
  
  Less chance of changes when developing component first
&lt;/h3&gt;

&lt;p&gt;With Storybook becoming popular, many developers are starting to develop components in Storybook first and the integrating them into the app at a later time. What may happen is that in app integration, props are defined incorrectly.&lt;/p&gt;

&lt;p&gt;Defining the fragment of the GraphQL graph this component needs to render, there are less chances of code changes when integration happens due to forcing the developer to know the exact shape of the data it needs to render. This of course is only possible defining the api in advance which sometimes isn't always the case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;Of course, like everything in programming, there are trade-offs in this approach. It's up to you to see if it's worth it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Presentational components are not generic
&lt;/h3&gt;

&lt;p&gt;The crummy thing is that our presentational components become more coupled to the application and API data model. If we want to migrate over to a component library for others to use, these components will need to be refactored to have their fragments removed. It's not too much work, but it is more work than the alternative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fragments sometimes become difficult to manage
&lt;/h3&gt;

&lt;p&gt;Importing many fragments into a single GraphQL query isn't the best experience. If we have many presentational components within a container component, importing them all can be hairy. Sometimes you may forget to import the fragment and Apollo can return some unhelpful messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GITHUB_ISSUES_LIST_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gql&lt;/span&gt;&lt;span class="s2"&gt;`
  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_ORG_INFO_CARD_FRAGMENT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_ISSUE_COUNT_CARD_FRAGMENT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_ISSUE_INFO_CARD_FRAGMENT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  query GithubIssuesListContainerQuery {
    ...GithubOrgInfoCardFragment
    issues {
      ...GithubIssueCountCardFragment
      pageInfo {
        endCursor
        hasNextPage
      }
      edges {
        node {
          ...GithubIssueInfoCardFragment
        }
      }
    }
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;We have been using this pattern at Yolk for a while now and it has grown on everyone. We develop our components first in Storybook and it forces the developer to understand where the data is coming from and ask questions about the data model and it's usage.&lt;/p&gt;

</description>
      <category>react</category>
      <category>graphql</category>
      <category>javascript</category>
      <category>storybook</category>
    </item>
    <item>
      <title>How does your company handle research and investigation work?</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Sat, 25 Apr 2020 14:45:42 +0000</pubDate>
      <link>https://dev.to/aleccool213/how-does-your-company-handle-research-and-investigation-work-1h2h</link>
      <guid>https://dev.to/aleccool213/how-does-your-company-handle-research-and-investigation-work-1h2h</guid>
      <description>&lt;p&gt;Software companies which follow agile usually do some sort of planning and estimation. It lets stakeholders know how long things will take to get done and can help with how a project plays out. To get better estimations, developers are usually tasked with doing some initial research to find out how difficult things will be, what unknowns exist. &lt;/p&gt;

&lt;p&gt;How does your component do this from a process standpoint? Tickets? Spikes (a form of research ticket familiar to some)? Nothing at all?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>agile</category>
      <category>estimation</category>
      <category>planning</category>
    </item>
    <item>
      <title>Publishing a JavaScript Package to NPM automatically with Github Actions</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Wed, 25 Mar 2020 15:03:03 +0000</pubDate>
      <link>https://dev.to/aleccool213/publishing-a-javascript-package-automatically-with-github-actions-42lk</link>
      <guid>https://dev.to/aleccool213/publishing-a-javascript-package-automatically-with-github-actions-42lk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Want more great content like this? Sign up for my newsletter, visit: &lt;a href="//alec.coffee/newsletter"&gt;alec.coffee/signup&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maintaining an open-source package can be a time-consuming task. Issues to be triaged, pull requests to be reviewed and changelogs to write. Publishing new versions of the code is usually done manually and making it automated is often on the back-burner of the maintainers' to-do list. There are a couple of key features of a rock-solid release process, the &lt;a href="https://www.techopedia.com/definition/13934/changelog"&gt;changelog&lt;/a&gt;, &lt;a href="https://git-scm.com/book/en/v2/Git-Basics-Tagging"&gt;Git tags&lt;/a&gt;, &lt;a href="https://stackoverflow.com/questions/10972176/find-the-version-of-an-installed-npm-package"&gt;NPM versions&lt;/a&gt;, and enforcing &lt;a href="https://semver.org/"&gt;Semantic Versioning&lt;/a&gt;. Keeping all these in sync makes it so users understand changes in a release and understand how to keep up-to-date. Maintainers who fail to perform all of these steps will have a hard time triaging issues, which leads to more time debugging and less time spent coding. I recently came across a combo of tools, &lt;a href="https://github.com/semantic-release/semantic-release"&gt;semantic-release&lt;/a&gt; and &lt;a href="https://github.com/features/actions"&gt;Github Actions&lt;/a&gt;, which made the entire release process automated, transparent, and simple to understand.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/semantic-release"&gt;
        semantic-release
      &lt;/a&gt; / &lt;a href="https://github.com/semantic-release/semantic-release"&gt;
        semantic-release
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      📦🚀 Fully automated version management and package publishing
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;h1&gt;
📦🚀 semantic-release&lt;/h1&gt;
&lt;h3&gt;
Fully automated version management and package publishing&lt;/h3&gt;
&lt;p&gt;
  &lt;a href="https://github.com/semantic-release/semantic-release/discussions"&gt;
    &lt;img alt="Join the community on GitHub Discussions" src="https://camo.githubusercontent.com/bb930414e2c6183bd51e40ddb477c3b78a0ef73fa5d65c077f6d8635ec79e408/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a6f696e253230746865253230636f6d6d756e6974792d6f6e25323047697448756225323044697363757373696f6e732d626c7565"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/semantic-release/semantic-release/actions?query=workflow%3ATest+branch%3Amaster"&gt;
    &lt;img alt="Build states" src="https://res.cloudinary.com/practicaldev/image/fetch/s--l4fD4728--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/semantic-release/semantic-release/workflows/Test/badge.svg"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/semantic-release/semantic-release#badge"&gt;
    &lt;img alt="semantic-release: angular" src="https://camo.githubusercontent.com/b6a92c3a7964f89a4239acf06e58206b927884803eece79cf1392a6a4dc52096/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73656d616e7469632d2d72656c656173652d616e67756c61722d6531303037393f6c6f676f3d73656d616e7469632d72656c65617365"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://www.npmjs.com/package/semantic-release" rel="nofollow"&gt;
    &lt;img alt="npm latest version" src="https://camo.githubusercontent.com/77116d1992240aaa287643aef7f66b959680678afa11f8c0068a2d847219c04e/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f73656d616e7469632d72656c656173652f6c61746573742e737667"&gt;
  &lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/semantic-release" rel="nofollow"&gt;
    &lt;img alt="npm next version" src="https://camo.githubusercontent.com/09d9c48c4f09b04d5a043c4ec4a85a129aa9daa21fa7dd2e0c1d20deaeddfc33/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f73656d616e7469632d72656c656173652f6e6578742e737667"&gt;
  &lt;/a&gt;
  &lt;a href="https://www.npmjs.com/package/semantic-release" rel="nofollow"&gt;
    &lt;img alt="npm beta version" src="https://camo.githubusercontent.com/e0773a785f0b210c8fd2f451dfd87c41683fdcdd440a69326cdc98dbfb989fbb/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f73656d616e7469632d72656c656173652f626574612e737667"&gt;
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;semantic-release&lt;/strong&gt; automates the whole package release workflow including: determining the next version number, generating the release notes, and publishing the package.&lt;/p&gt;

&lt;p&gt;This removes the immediate connection between human emotions and version numbers, strictly following the &lt;a href="http://semver.org" rel="nofollow"&gt;Semantic Versioning&lt;/a&gt; specification and communicating the &lt;strong&gt;impact&lt;/strong&gt; of changes to consumers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Trust us, this will change your workflow for the better. – &lt;a href="https://egghead.io/lessons/javascript-how-to-write-a-javascript-library-automating-releases-with-semantic-release" rel="nofollow"&gt;egghead.io&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
Highlights&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Fully automated release&lt;/li&gt;
&lt;li&gt;Enforce &lt;a href="https://semver.org" rel="nofollow"&gt;Semantic Versioning&lt;/a&gt; specification&lt;/li&gt;
&lt;li&gt;New features and fixes are immediately available to users&lt;/li&gt;
&lt;li&gt;Notify maintainers and users of new releases&lt;/li&gt;
&lt;li&gt;Use formalized commit message convention to document changes in the codebase&lt;/li&gt;
&lt;li&gt;Publish on different distribution channels (such as &lt;a href="https://docs.npmjs.com/cli/dist-tag" rel="nofollow"&gt;npm dist-tags&lt;/a&gt;) based on git merges&lt;/li&gt;
&lt;li&gt;Integrate with your &lt;a href="https://github.com/semantic-release/semantic-releasedocs/recipes/release-workflow/README.md#ci-configurations"&gt;continuous integration workflow&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Avoid potential errors associated with manual releases&lt;/li&gt;
&lt;li&gt;Support any &lt;a href="https://github.com/semantic-release/semantic-releasedocs/recipes/release-workflow/README.md#package-managers-and-languages"&gt;package managers and languages&lt;/a&gt; via &lt;a href="https://github.com/semantic-release/semantic-releasedocs/usage/plugins.md"&gt;plugins&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Simple and reusable configuration via &lt;a href="https://github.com/semantic-release/semantic-releasedocs/usage/shareable-configurations.md"&gt;shareable configurations&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
How does it work?&lt;/h2&gt;
&lt;h3&gt;
Commit&lt;/h3&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/semantic-release/semantic-release"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Before we talk about implementation, it's important to understand what work our tools will perform. That way, if there are problems or modifications, we can fix them. semantic-release is going to do the majority of the work here, they say it best on their README.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It automates the whole package release workflow including determining the next version number, generating the release notes and publishing the package.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Next Version Number
&lt;/h3&gt;

&lt;p&gt;During a release, to determine the next version number, the tool reads commits since the last release. It knows your last release by looking at your Git tags. Based on the type of commit, it can determine how to bump up the version of the package. For this to work, commits need to be written in a certain way. By default, semantic-release uses the &lt;a href="https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines"&gt;Angular Commit Message Conventions&lt;/a&gt;. This is critical because consumers of the package need to know if a new version releases a new feature, introduces breaking changes or both. For example, if someone commits &lt;code&gt;fix(pencil): stop graphite breaking when too much pressure applied&lt;/code&gt;, semantic-release knows this contains a fix and to create a patch release. This will increase the version in the minor version range (0.0.x).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never seen this type of versioning before? &lt;a href="https://semver.org/"&gt;Check out Semantic Versioning&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After analyzing all the commits, it takes the highest priority type of change and makes sure that is the one that is applied. For example, if two commits were introduced since the last release, one breaking (x.0.0) and one minor (0.0.x), it would know to just up the version by breaking range.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating Release Notes
&lt;/h3&gt;

&lt;p&gt;Once it has done finding out what type of release the next version is, changelog notes are generated based on the commits. semantic-release doesn't use conventional CHANGELOG.md file to notify users of what has changed, it does so with a &lt;a href="https://help.github.com/en/github/administering-a-repository/about-releases"&gt;Github Release&lt;/a&gt; which is attached to a Git tag.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils/releases/tag/v1.0.3"&gt;An example of a Github Release that semantic-release generates and pushes on builds.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Automating With Github Actions
&lt;/h2&gt;

&lt;p&gt;So semantic-release will be the tool to perform most of the work, but we still need a service to run the tool on. That is where &lt;a href="https://github.com/features/actions"&gt;Github Actions&lt;/a&gt; comes into play. When pull-requests are merged into master (or any base branch you configure), Github Actions will run a job that simply runs semantic-release with your configuration. All of the work we described previously will be performed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils/runs/463521573?check_suite_focus=true"&gt;An example of a Github Actions run using semantic-release to publish a new release.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Steps to Take
&lt;/h2&gt;

&lt;p&gt;We will be using as many defaults as possible to make configuration dead simple. semantic-release uses a plugins system to enhance functionality. &lt;a href="https://github.com/semantic-release/semantic-release/blob/master/docs/usage/plugins.md#default-plugins"&gt;Here are the default plugins semantic-release uses.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's go over the steps which will make this all run smoothly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Add a dummy version property to the package.json of package. Released code will have the proper version written to this file by semantic-release.
&lt;/li&gt;
&lt;/ol&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="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.0.0-development"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt; Add a new property to the package.json, &lt;code&gt;publishConfig&lt;/code&gt;. This will be the home of our semantic-release configuration.
&lt;/li&gt;
&lt;/ol&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="nl"&gt;"publishConfig"&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;"access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"branches"&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="err"&gt;'master'&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;ol&gt;
&lt;li&gt; The last step is to create a Github Action YAML file. This will tell Github Actions what to do when a commit is made to the repository.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;        &lt;span class="c1"&gt;# .github/workflows/test-and-release.yaml&lt;/span&gt;

        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test and Release&lt;/span&gt;
        &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

        &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;test-and-release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests and release&lt;/span&gt;
            &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-18.04&lt;/span&gt;
            &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
                &lt;span class="s"&gt;uses&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
                &lt;span class="s"&gt;uses&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v1&lt;/span&gt;
                &lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
                &lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
                &lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release&lt;/span&gt;
                &lt;span class="s"&gt;env&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
                &lt;span class="na"&gt;NPM_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NPM_TOKEN }}&lt;/span&gt;
                &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run semantic-release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Add &lt;code&gt;NPM_TOKEN&lt;/code&gt; to the secrets in the Github repos settings page. You can generate one of these from your NPM account at &lt;a href="https://www.npmjs.com/settings/"&gt;https://www.npmjs.com/settings/&lt;/a&gt;/tokens&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TMJ1GpE1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1585055788/npm-publish-post/Screenshot_at_Mar_24_08-47-49.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TMJ1GpE1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1585055788/npm-publish-post/Screenshot_at_Mar_24_08-47-49.png" alt="screenshot of github repo settings screen" width="880" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And that's it! You have a fully automated package release process 🎉&lt;/p&gt;
&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;I implemented this on a repo we recently open-sourced at Yolk AI. It's named next-utils and everything described here can be found there.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Yolk-HQ"&gt;
        Yolk-HQ
      &lt;/a&gt; / &lt;a href="https://github.com/Yolk-HQ/next-utils"&gt;
        next-utils
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🥩 🍳 A set of Next.js HoC utilities to make your life easier
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;h1&gt;
next-utils&lt;/h1&gt;
&lt;p&gt;A set of Next.js utilities to make your life easier.&lt;/p&gt;
&lt;p&gt;ATTENTION: This project is no longer maintained.&lt;/p&gt;
&lt;/div&gt;




&lt;p&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils/actions"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qtpO7TB1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/Yolk-HQ/next-utils/workflows/Test/badge.svg" alt="Actions Status"&gt;&lt;/a&gt;
&lt;a href="https://www.npmjs.com/package/@yolkai/next-utils" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/7a2d0d6cc2c299e2b168bb2965dd87bbd0e37fe06c8ca081077d8ecef6443564/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f6e6578742d7574696c732e7376673f7374796c653d666c61742d737175617265" alt="version"&gt;&lt;/a&gt;
&lt;a href="http://www.npmtrends.com/@yolkai/next-utils" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/484dae580b0c855cb52f28d3870b1a8f56cfe6c12a65b2db3ccfe43c3c685188/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f646d2f40796f6c6b61692f6e6578742d7574696c732e7376673f7374796c653d666c61742d737175617265" alt="downloads"&gt;&lt;/a&gt;
&lt;a href="https://github.com/yolk-hq/next-utils/blob/master/LICENSE"&gt;&lt;img src="https://camo.githubusercontent.com/84e48fc6f97a55c0f608c36bcb4aec01fb2a71d9ce29f1cb67249825c5cd48f1/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f6c2f40796f6c6b61692f6e6578742d7574696c732e7376673f7374796c653d666c61742d737175617265" alt="MIT License"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#contributors"&gt;&lt;img src="https://camo.githubusercontent.com/60f4e6a9ba8683a8303d97cab7693a1d022cc822a729ebeb434e801ce870eb67/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f616c6c5f636f6e7472696275746f72732d312d6f72616e67652e7376673f7374796c653d666c61742d737175617265" alt="All Contributors"&gt;&lt;/a&gt;
&lt;a href="http://makeapullrequest.com" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/0ff11ed110cfa69f703ef0dcca3cee6141c0a8ef465e8237221ae245de3deb3d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt; &lt;a href="https://github.com/yolk-hq/next-utils/blob/master/other/CODE_OF_CONDUCT.md"&gt;&lt;img src="https://camo.githubusercontent.com/7cd7c9431068f145e0a61cde3bbe8abd0be87567023c8ffde96818d69f82c907/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f64652532306f662d636f6e647563742d6666363962342e7376673f7374796c653d666c61742d737175617265" alt="Code of Conduct"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://twitter.com/intent/tweet?text=Check%20out%20next-utils%20by%20%40yolkai%20https%3A%2F%2Fgithub.com%2Fyolk-hq%2Fnext-utils%20%F0%9F%91%8D" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/fe76e4a31a95a6832a2a7912edb887e262ce420e8acc9bb733a30c0e63bc8f67/68747470733a2f2f696d672e736869656c64732e696f2f747769747465722f75726c2f68747470732f6769746875622e636f6d2f74657374696e672d6c6962726172792f637970726573732d74657374696e672d6c6962726172792e7376673f7374796c653d736f6369616c" alt="Tweet"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
Overview&lt;/h2&gt;
&lt;p&gt;React &lt;a href="https://reactjs.org/docs/higher-order-components.html" rel="nofollow"&gt;Higher-Order Components&lt;/a&gt; for use with &lt;a href="https://nextjs.org/" rel="nofollow"&gt;Next.js&lt;/a&gt;, enabling simple, server-side-render-compatible configuration of functionality such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/apollographql/apollo-client"&gt;Apollo Client&lt;/a&gt; + &lt;a href="https://github.com/apollographql/react-apollo"&gt;react-apollo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sentry.io/for/javascript/" rel="nofollow"&gt;Sentry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lingui/js-lingui"&gt;LinguiJS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bukinoshita/react-cookies"&gt;react-cookies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#installation"&gt;Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Yolk-HQ/next-utils#usage"&gt;Usage&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#appwithapolloclient"&gt;appWithApolloClient&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#appWithSentry"&gt;appWithSentry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#appWithLingui"&gt;appWithLingui&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#appwithcookies"&gt;appWithCookies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#withauthentication"&gt;withAuthentication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#checkauthenticated"&gt;checkAuthenticated&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#redirect"&gt;redirect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#RouterContext"&gt;RouterContext&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#other-solutions"&gt;Other Solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils#license"&gt;LICENSE&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
Installation&lt;/h2&gt;
&lt;p&gt;This module is distributed via &lt;a href="https://www.npmjs.com/" rel="nofollow"&gt;npm&lt;/a&gt; which is bundled with &lt;a href="https://nodejs.org" rel="nofollow"&gt;node&lt;/a&gt; and
should be installed as one of your project's &lt;code&gt;dependencies&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm install @yolkai/next-utils&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
Note&lt;/h2&gt;
&lt;p&gt;NOTE: Using any of these Higher-Order-Components will disable &lt;a href="https://nextjs.org/docs/old#automatic-static-optimization" rel="nofollow"&gt;Automatic Static Optimization&lt;/a&gt; (statically built pages), since the Higher-Order-Component forces every page to implement &lt;code&gt;getInitialProps&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
🔮 &lt;strong&gt;Apollo Client&lt;/strong&gt;
&lt;/h3&gt;
&lt;h4&gt;
appWithApolloClient&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils/tree/master/examples/appWithApolloClient.example.tsx"&gt;Example Usage&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils/tree/master/src/internal/appWithApolloClient.tsx"&gt;Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;React higher-order component (HoC) which wraps the App component and:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Performs the page's initial GraphQL request on the server, and serializes the result to be used as the initial Apollo state once the client…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Yolk-HQ/next-utils"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Another great thing about using semantic-release with Github Actions is that it has out-of-the-box support for bot comments. It will go into every issue and pull-request closed since the last release and comment to make sure everyone is aware. Here is an example:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/Yolk-HQ/next-utils/issues/12#issuecomment-581484992"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Comment for
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#12&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/apps/github-actions"&gt;
        &lt;img class="github-liquid-tag-img" src="https://res.cloudinary.com/practicaldev/image/fetch/s--e06Ix6ab--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://avatars2.githubusercontent.com/in/15368%3Fv%3D4" alt="github-actions[bot] avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/apps/github-actions"&gt;github-actions[bot]&lt;/a&gt;
        &lt;/strong&gt; commented on &lt;a href="https://github.com/Yolk-HQ/next-utils/issues/12#issuecomment-581484992"&gt;&lt;time&gt;Feb 03, 2020&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;🎉 This issue has been resolved in version 1.0.0 🎉&lt;/p&gt;
&lt;p&gt;The release is available on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@yolkai/next-utils/v/1.0.0" rel="nofollow"&gt;npm package (@latest dist-tag)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yolk-HQ/next-utils/releases/tag/v1.0.0"&gt;GitHub release&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your &lt;strong&gt;&lt;a href="https://github.com/semantic-release/semantic-release"&gt;semantic-release&lt;/a&gt;&lt;/strong&gt; bot 📦🚀&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Yolk-HQ/next-utils/issues/12#issuecomment-581484992"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;If you liked this post, check out more at &lt;a href="https://blog.alec.coffee"&gt;https://blog.alec.coffee&lt;/a&gt; and &lt;a href="https://mailchi.mp/f91826b80eb3/alecbrunelleemailsignup"&gt;signup for my newsletter&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>javascript</category>
      <category>npm</category>
      <category>opensource</category>
      <category>github</category>
    </item>
    <item>
      <title>Quit Google Analytics, Self-hosted Gatsby Statistics with Ackee</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Wed, 12 Feb 2020 20:26:27 +0000</pubDate>
      <link>https://dev.to/aleccool213/quit-google-analytics-self-hosted-gatsby-statistics-with-ackee-4011</link>
      <guid>https://dev.to/aleccool213/quit-google-analytics-self-hosted-gatsby-statistics-with-ackee-4011</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Want more great content like this? Sign up for my newsletter, visit: &lt;a href="//alec.coffee/newsletter"&gt;alec.coffee/signup&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are many different goals one can have when it comes to hosting your own website or blog. For myself, it means just having a place where I own the content of my words and can customize it to my liking. When it comes to analytics, my needs aren’t many, as most of my audience reads my content via platforms like &lt;a href="http://dev.to"&gt;dev.to&lt;/a&gt; or &lt;a href="http://medium.com"&gt;Medium&lt;/a&gt;. All I need to know is how many people visit my site, which posts are doing well and where users come from (referral links). Given my recent obsessive elimination of all things tracking and advertising in my life, I chose to stop supporting Google and move from Google Analytics to something self-hosted. It wasn't an easy product to use and most of the features were useless to me as I don't sell anything on my blog. This way I own the data and am not contributing it to a company that could use it in potentially malicious ways.&lt;/p&gt;

&lt;p&gt;I set out to search for a new tracking tool for my blog. My criteria for choosing a new product were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Be simple&lt;/li&gt;
&lt;li&gt;Have features I will use&lt;/li&gt;
&lt;li&gt;Put a focus on privacy&lt;/li&gt;
&lt;li&gt;Built with a programming language I know so making changes is easy&lt;/li&gt;
&lt;li&gt;Be able to easily host on a Platform-as-a-Service like Heroku&lt;/li&gt;
&lt;li&gt;Have the ability to be easily added to a Gatsby blog&lt;/li&gt;
&lt;li&gt;Have an option to not collect unique user data such as OS, Browser Info, Device &amp;amp; ScreenSize&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Meet Ackee
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tQ6mX2oA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581282052/ackee-post/Screenshot_at_Feb_09_16-00-43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tQ6mX2oA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581282052/ackee-post/Screenshot_at_Feb_09_16-00-43.png" alt="ackee website homepage" width="880" height="449"&gt;&lt;/a&gt;&lt;br&gt;Beautiful, isn't it
  &lt;/p&gt;

&lt;p&gt;I came across &lt;a href="https://ackee.electerious.com/"&gt;Ackee 🔮&lt;/a&gt;, a self-hosted analytics tool. This tool fit my requirements almost perfectly. It is built using Node.js which I have experience in and it focuses on anonymizing data that it collects. More information on how Ackee anonymizes data &lt;a href="https://github.com/electerious/Ackee/blob/master/docs/Anonymization.md"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The steps you need to take to start collecting statistics with Ackee are to start running it on a server, Heroku in my case, add the Javascript tracker to your Gatsby site and test to see if the data is flowing correctly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This a detailed guide on how I went about deploying it to Heroku. Afterwards, &lt;a href="https://github.com/electerious/Ackee/pull/77"&gt;I contributed back a Deploy-to-Heroku&lt;/a&gt; button which deploys it in one-click. &lt;a href="https://github.com/electerious/Ackee/blob/master/docs/Get%20started.md#with-heroku"&gt;Find the button here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Up and running on Heroku
&lt;/h2&gt;

&lt;p&gt;First thing is to start running the server which is going to receive the tracking data from your website.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create a new Heroku app instance&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kdX8YI4_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581282566/ackee-post/Screenshot_at_Feb_09_16-09-18.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kdX8YI4_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581282566/ackee-post/Screenshot_at_Feb_09_16-09-18.png" alt="https://res.cloudinary.com/dscgr6mcw/image/upload/v1581282566/ackee-post/Screenshot_at_Feb_09_16-09-18.png" width="880" height="519"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use the &lt;a href="https://devcenter.heroku.com/articles/heroku-cli"&gt;heroku-cli&lt;/a&gt; to upload the code&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# clone the code
git clone git@github.com:electerious/Ackee.git

# login to heroku
heroku login

# add the heroku remote
heroku git:remote -a ackee-server

# push the code
git push heroku master
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Configure a MongoDB add-on, this is where the data will be stored&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IN62GaVs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581282745/ackee-post/Screenshot_at_Feb_09_16-12-18.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IN62GaVs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581282745/ackee-post/Screenshot_at_Feb_09_16-12-18.png" alt="https://res.cloudinary.com/dscgr6mcw/image/upload/v1581282745/ackee-post/Screenshot_at_Feb_09_16-12-18.png" width="880" height="558"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://devcenter.heroku.com/articles/config-vars#using-the-heroku-cli"&gt;Configure the environment variables&lt;/a&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heroku config:set ACKEE_PASSWORD=&amp;lt;your password&amp;gt;
heroku config:set ACKEE_USERNAME=&amp;lt;your username&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And voila! You are finished, that was easy, wasn't it? Open the webpage Heroku automatically configures for you, it should be &lt;a href="https://ackee-instance.herokuapp.com/"&gt;&lt;code&gt;https://ackee-server.herokuapp.com/&lt;/code&gt;&lt;/a&gt;, you should see this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lBp0NljY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581283089/ackee-post/Screenshot_at_Feb_09_16-18-00.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lBp0NljY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581283089/ackee-post/Screenshot_at_Feb_09_16-18-00.png" alt="ackee login page" width="880" height="494"&gt;&lt;/a&gt;&lt;br&gt;The log in page!
    &lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the tracker
&lt;/h2&gt;

&lt;p&gt;Now we need to send data over from the website to the server we now have running on Heroku. If you are using Gatsby, this is incredibly easy with the plugin.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install the tracker&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install gatsby-plugin-ackee-tracker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a domain on Ackee and get the domain id. Find this option in the settings tab of your Ackee instance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add it to your Gatsby config&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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="nl"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gatsby-plugin-ackee-tracker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Domain ID found when adding a domain in the admin panel.&lt;/span&gt;
        &lt;span class="nl"&gt;domainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;your domain id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// URL to Server eg: "https://analytics.test.com".&lt;/span&gt;
        &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://ackee-server.herokuapp.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// Disabled analytic tracking when running locally&lt;/span&gt;
        &lt;span class="c1"&gt;// IMPORTANT: Set this back to false when you are done testing&lt;/span&gt;
        &lt;span class="nx"&gt;ignoreLocalhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// If enabled it will collect info on OS, BrowserInfo, Device  &amp;amp; ScreenSize&lt;/span&gt;
        &lt;span class="c1"&gt;// False due to detailed information being personalized:&lt;/span&gt;
        &lt;span class="c1"&gt;// https://github.com/electerious/Ackee/blob/master/docs/Anonymization.md#personal-data&lt;/span&gt;
        &lt;span class="nx"&gt;detailed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Run the site locally&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gatsby develop
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Testing to make sure it worked
&lt;/h2&gt;

&lt;p&gt;Open up your site at &lt;code&gt;http://localhost:8000&lt;/code&gt; and go to a new url.&lt;/p&gt;

&lt;p&gt;Observe the network requests your site is sending. You will notice it now sends requests to your Heroku instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AEZem4FC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581283787/ackee-post/Screenshot_at_Feb_09_16-29-09.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AEZem4FC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581283787/ackee-post/Screenshot_at_Feb_09_16-29-09.png" alt="using the brave browser dev tools" width="880" height="469"&gt;&lt;/a&gt;&lt;br&gt;Using the dev tools
    &lt;/p&gt;

&lt;p&gt;And with that, we now have the server running Ackee and our Gatsby sending analytics!&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;Let’s explore Ackee, shall we.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--12EDUVCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581518650/ackee-post/Screenshot_at_Feb_12_09-32-59.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--12EDUVCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581518650/ackee-post/Screenshot_at_Feb_12_09-32-59.png" alt="ackee home page screenshot" width="880" height="534"&gt;&lt;/a&gt;&lt;br&gt;Home page with total site views
    &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--olS21J7c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581518650/ackee-post/Screenshot_at_Feb_12_09-33-47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--olS21J7c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581518650/ackee-post/Screenshot_at_Feb_12_09-33-47.png" alt="ackee list of referrers screenshot" width="880" height="531"&gt;&lt;/a&gt;&lt;br&gt;List of referrers
    &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7asW-VfU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581518650/ackee-post/Screenshot_at_Feb_12_09-31-43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7asW-VfU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/dscgr6mcw/image/upload/v1581518650/ackee-post/Screenshot_at_Feb_12_09-31-43.png" alt="ackee per page view count screenshot" width="880" height="529"&gt;&lt;/a&gt;&lt;br&gt;Per page view count
    &lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives
&lt;/h2&gt;

&lt;p&gt;Here are some alternative methods I considered when thinking about analytics for my blog.&lt;/p&gt;

&lt;h3&gt;
  
  
  No tracking
&lt;/h3&gt;

&lt;p&gt;Combined with the fact more and more people are blocking trackers all-together (Firefox, Brave and Chrome ad blocking extensions), JavaScript-based tracking is becoming less and less valuable over-time. Most analytics can easily become a way to be vain about your blog and you can start a bad habit of always checking them (wasted time compared to producing actual content). Deciding not to track any analytics at all is not a bad decision these days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-side analytics
&lt;/h3&gt;

&lt;p&gt;The most private and fast way of collecting analytics on your website may be to collect analytics at the server level. What this means is instead of using a JavaScript tracker (which may be blocked by the browser), stats are collected when the HTML is sent from the server. Integration with your static host provider or DNS provider is needed here. The main con about this method is that data is collected by a third party service and also is usually not free. &lt;a href="https://www.cloudflare.com/en-ca/analytics/"&gt;Cloudflare&lt;/a&gt; offers these types of analytics alongside &lt;a href="https://www.netlify.com/products/analytics/"&gt;Netlify&lt;/a&gt;. A huge benefit is the ease of setup, usually the provider just turns it on with a switch on their side, no setup needed from you.&lt;/p&gt;

</description>
      <category>gatsby</category>
      <category>analytics</category>
      <category>node</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Is knowing how to use Docker a prerequisite to becoming a Senior Software Developer?</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Wed, 22 Jan 2020 21:12:52 +0000</pubDate>
      <link>https://dev.to/aleccool213/is-knowing-how-to-use-docker-a-prerequisite-to-becoming-a-senior-software-developer-6n9</link>
      <guid>https://dev.to/aleccool213/is-knowing-how-to-use-docker-a-prerequisite-to-becoming-a-senior-software-developer-6n9</guid>
      <description>&lt;p&gt;Just a question I think needs some discussion. I know titles in software dev are pretty much out the window at this point, but was curious on the communities thoughts.&lt;/p&gt;

&lt;p&gt;Is knowing what containers are and how to use them necessary in 2020? Docker specifically? &lt;/p&gt;

</description>
      <category>discuss</category>
    </item>
    <item>
      <title>1 year with Cypress: The Guide to End-To-End Testing 🚀</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Fri, 20 Dec 2019 13:35:11 +0000</pubDate>
      <link>https://dev.to/aleccool213/the-hitchhikers-guide-to-cypress-end-to-end-testing-dii</link>
      <guid>https://dev.to/aleccool213/the-hitchhikers-guide-to-cypress-end-to-end-testing-dii</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Want more great content like this? Sign up for my newsletter, visit: &lt;a href="//alec.coffee/newsletter"&gt;alec.coffee/signup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Like this post? Checkout my &lt;a href="https://dev.to/aleccool213"&gt;other posts on dev.to&lt;/a&gt;, or consider &lt;a href="https://www.buymeacoffee.com/yourboybigal" rel="noopener noreferrer"&gt;buying me a coffee&lt;/a&gt; to support me writing more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In software development, the faster you move, the more things break. As a codebase grows larger and larger, its pieces become more and more complex, every line adding a potential bug. The best organizations keep a handle on this through rigorous amounts of testing. Manual testing requires a lot of effort, that's where automated testing comes in. One of the hot frameworks on the scene is &lt;a href="https://www.cypress.io/" rel="noopener noreferrer"&gt;Cypress&lt;/a&gt;, a complete end-to-end testing solution.&lt;/p&gt;

&lt;p&gt;In the past, web-app end-to-end testing has been a tricky beast. &lt;a href="https://www.npmjs.com/package/selenium-webdriver" rel="noopener noreferrer"&gt;Selenium&lt;/a&gt; has been the main solution for quite some time and has a huge history. It has great browser compatibility but having your tests be consistent is difficult because it wasn't designed for app testing. That's why I got so excited when I heard about Cypress, promising to fix all of the old and broken ways of past frameworks. After writing and reviewing close to 200 test scenarios in the past year (that's with a small team), I wanted to write about what I wish I knew when I started and share my thoughts on my journey with Cypress thus far.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fcypress_overview.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fcypress_overview.png" alt="cypress features from website"&gt;&lt;/a&gt;&lt;/p&gt;
So many features packed in 😃



&lt;p&gt;End-to-end testing has always been a fragmented experience. You need to bring a lot of your own tools, for example, a test runner, an assertion library, and maybe other things like mocks. With Cypress, it packages all of those things together, this makes set up and configuration, dead simple. Not only that, the documentation is some of the best I have ever read in my career, with &lt;a href="https://docs.cypress.io/guides/guides/command-line.html#Installation" rel="noopener noreferrer"&gt;guides on everything&lt;/a&gt; you are likely to encounter. Not only do they do a great job telling you how to use the product, but have in-depth explanations on the &lt;a href="https://docs.cypress.io/guides/overview/key-differences.html#Architecture" rel="noopener noreferrer"&gt;architecture&lt;/a&gt;, &lt;a href="https://docs.cypress.io/guides/overview/key-differences.html#Flake-resistant" rel="noopener noreferrer"&gt;flakey tests&lt;/a&gt; and &lt;a href="https://docs.cypress.io/guides/references/best-practices.html" rel="noopener noreferrer"&gt;best practices&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prototyping
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/379033499" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you have the chance, before adopting anything of this scale, I always think it's a good idea to test it on a small project first, just to get a feel. Before advocating for it, I added it to my personal blog, just to see how the experience was.&lt;/p&gt;

&lt;p&gt;A very simple scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load up the app&lt;/li&gt;
&lt;li&gt;Go to the index page&lt;/li&gt;
&lt;li&gt;Click the first blog post link&lt;/li&gt;
&lt;li&gt;Assert content shows up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was blown away with how fast it took me, under an hour. This was really as simple as &lt;a href="https://github.com/aleccool213/blog/blob/eb20d81a1bd10ba6037d5ac26ee5142ce951d7df/cypress/integration/home_page_spec.js#L8" rel="noopener noreferrer"&gt;writing a few lines of Javascript for the test itself&lt;/a&gt;, &lt;a href="https://github.com/aleccool213/blog/blob/60aa908e27aa83a539e563498b34f71a93167ec6/package.json#L70" rel="noopener noreferrer"&gt;the npm script in the package.json&lt;/a&gt;, and &lt;a href="https://github.com/aleccool213/blog/blob/8fa71b2486660e0175ee49cea1559112a224f146/circle.yml#L39" rel="noopener noreferrer"&gt;running it in CircleCI&lt;/a&gt;. Not only did Cypress perform the assertions but also it was recording videos! This could have been an even faster setup if I used the &lt;a href="https://github.com/cypress-io/circleci-orb" rel="noopener noreferrer"&gt;CircleCi Cypress Orb&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This got me a huge amount of test coverage in very little time. This proof of concept was more than enough to convince my team that Cypress was the right choice when it came time to start writing end-to-end automated tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decisions and Tradeoffs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_decisions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_decisions.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser-based products we have at &lt;a href="https://www.yolk.ai/" rel="noopener noreferrer"&gt;Yolk&lt;/a&gt; are completely separated from the server-side API's they fetch data from, they build and are served separately. This presents a few ways forward when deciding to write end-to-end tests. You can either deploy your backend with your frontend and test as if the app is in production or completely mock out API responses. Using a real backend means spinning up potentially memory-intensive processes when running on CI but you get the assurance that apps are near-production. With mocking your API responses, you test less of your stack, risk stubbing out non-realistic responses, and have to worry about the extra maintenance of keeping them up-to-date.&lt;/p&gt;

&lt;p&gt;We decided on deploying live instances of the backends related to the app we were testing. This decision was easy for us to make due to already having a CLI tool to do much of the hard work. This tool (aptly named yolk-cli) downloads the latest docker images for apps and knows how to spin up products with minimal configuration. This made getting the real APIs working on CI not too huge of a task.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Turns out, running two or three large python apps and a few &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; servers on CircleCI does crap out the memory limit pretty fast. We reached out to CircleCI and they gave us access to their large resource classes (up to 16gb of RAM), score!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Seeding Data
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_seeding_data.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_seeding_data.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next challenge we faced was seeding data. Your test scenarios must share as little state as possible with each other. This is a testing fundamental and &lt;a href="https://docs.cypress.io/guides/references/best-practices.html#Having-tests-rely-on-the-state-of-previous-tests" rel="noopener noreferrer"&gt;Cypress addresses it&lt;/a&gt; in their guides. Having test scenarios data-independent goes a long way when debugging why things are going wrong. On the flip side, having all of your data be created through the UI will make for slow tests, there is a balance. This will be highly customized to how your app works but I will go into what worked for us.&lt;/p&gt;

&lt;p&gt;Going back to our cli tool once again, it had a few commands which seeded some basic data. The commands looked like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yolk seed-articles&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yolk seed-bots&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Getting your feet off the ground with data that is basic to your app, static data or very high-level entities, for example, will speed up this process and will be easy to run on each CI build.&lt;/p&gt;

&lt;p&gt;The next part will be seeding data for entities that may be more specific to individual tests. This is where things get contested, there is no silver bullet for this. We decided to call the APIs directly for these situations and use &lt;a href="https://docs.cypress.io/api/cypress-api/custom-commands.html#Syntax" rel="noopener noreferrer"&gt;Cypress custom commands&lt;/a&gt; to initiate these requests. This was a decent choice because we are using GraphQL; the custom commands that use the API were easy to write and document.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Writing custom commands for actions which your tests will be performing over and over are a great way to consolidate all code, not just data seeders!&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Scenarios with Gherkin
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_google_doc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_google_doc.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have written end-to-end tests before, you may be familiar with Gherkin syntax, used by Cucumber. This is an expressive, English-like way to write test scenarios. It can help with documenting your features and non-developers can contribute to writing test cases. We found &lt;a href="https://github.com/TheBrainFamily/cypress-cucumber-preprocessor" rel="noopener noreferrer"&gt;a way to integrate this file syntax into Cypress using a plugin&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;After writing these commands, the plugin will then go to Cypress to actually run the implementations:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h2&gt;
  
  
  Asserting Elements and Best Practices
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_asserting_elements.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_asserting_elements.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it comes down to it, end-to-end testing is just making sure elements on the page have the correct content. When writing Cypress tests, 90% of the time you will need to be selecting elements and peering inside them. Cypress has a standard &lt;a href="https://docs.cypress.io/api/commands/get.html#Syntax" rel="noopener noreferrer"&gt;get()&lt;/a&gt; command which exposes a JQuery-like selector to you, this should be familiar to those who have worked with Selenium. The problem with this selector is that it can be used incorrectly and you can't enforce (with code) it's usage. Welcome &lt;a href="https://github.com/testing-library/cypress-testing-library" rel="noopener noreferrer"&gt;cypress-testing-library&lt;/a&gt;, a wonderful tool maintained by a great testing advocate in the community, &lt;a href="https://kentcdodds.com/" rel="noopener noreferrer"&gt;Kent C. Dodds&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This plugin exposes a myriad of commands prefixed with &lt;code&gt;find&lt;/code&gt; which work similarly to how &lt;code&gt;get()&lt;/code&gt; does in native Cypress. All of these commands make for selectors &lt;a href="https://kentcdodds.com/blog/making-your-ui-tests-resilient-to-change" rel="noopener noreferrer"&gt;that are resilient to change&lt;/a&gt;. This can have a dramatic effect on how your tests stay consistent as your application progresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848055%2Fcypress-post%2Fundraw_debugging.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848055%2Fcypress-post%2Fundraw_debugging.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have ever worked with Selenium before, you know that debugging end-to-end tests can be somewhat of a nightmare. With Cypress, this pain is at an all-time low. Being a focus of the core product, being able to debug is one of the more pleasant experiences in your Cypress journey. Like for most things, &lt;a href="https://on.cypress.io/plugins-guide" rel="noopener noreferrer"&gt;they have a great guide to get you started&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Most of the things they have mentioned are great but the case which you will likely run into the most is a selector being incorrect. For this type of scenario, the GUI is a great way to find out what is going wrong. &lt;a href="https://vimeo.com/237115455" rel="noopener noreferrer"&gt;There is a nice video explaining how to write your first test&lt;/a&gt; and it shows the GUI in action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual Testing and Catching Regressions
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fpercy_example_screenshot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fpercy_example_screenshot.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another critical part of end-to-end testing will be how things look. HTML and CSS play a huge part in how your application will look like in different scenarios. Cypress can give you a lot of coverage in terms of how your app works but starts to break down when you want to assert its looks. Especially when it comes to browser compatibility and the different screen sizes your application is used in, visual regressions are hard to catch without proper implementation of &lt;a href="https://blog.hichroma.com/visual-testing-the-pragmatic-way-to-test-uis-18c8da617ecf" rel="noopener noreferrer"&gt;Visual Snapshot Testing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The solution we ended up with was &lt;a href="https://percy.io/" rel="noopener noreferrer"&gt;Percy&lt;/a&gt; as it integrates nicely with Cypress and &lt;a href="https://storybook.js.org/" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt;. What it can do is take the current HTML and CSS which is being rendered in your Cypress test scenario and send it over to Percy's servers. Percy then renders the markup on its own internal browsers, with Chrome and Firefox being options. Percy knows which feature branch your Cypress test is being run in and compares this with your configured base branch. This can give you great confidence in pull requests when you don't know if code is changing the look of a certain component in your application. This can be a big time-saver if you have a lot of code in your Cypress tests that assert css values or how things should look.&lt;/p&gt;

&lt;p&gt;Hot Tip: You can have Cypress take snapshots locally and then with Percy only when it's enabled by creating a new &lt;code&gt;takeSnapshot&lt;/code&gt; custom command:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h2&gt;
  
  
  Parallel Builds and the Cypress Dashboard
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_parallel_builds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_parallel_builds.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once test runs start to become long enough, you will start looking for other strategies to speed them up. Parallelization is something that can be performed due to Cypress running feature scenario files with a clean state each time they are run. You can decide on your own balance strategy, how your tests can be broken up, but the hosted version of &lt;a href="https://docs.cypress.io/faq/questions/dashboard-faq.html#What-is-the-Dashboard" rel="noopener noreferrer"&gt;Cypress Dashboard&lt;/a&gt; provides a way to do this automatically.&lt;/p&gt;

&lt;p&gt;Let's say I can afford to have three CircleCI containers to run my Cypress tests. First, I define the &lt;code&gt;parallelism: 3&lt;/code&gt; in &lt;a href="https://circleci.com/docs/2.0/parallelism-faster-jobs/" rel="noopener noreferrer"&gt;my CircleCI job step config&lt;/a&gt;. What this will do is spin up three instances of your job, all with different job ids. Pass those ids off to Cypress, and you are off to the races. If you have Cypress Dashboard set up correctly, that service will tell your container which tests it should run. Here is an example of the config:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;The super neat thing about this is that Cypress Dashboard knows your past test history and their speeds. It will use this knowledge to optimize your parallel builds by making sure the containers get a balanced load of tests to run!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't worry if this doesn't make much sense to you, &lt;a href="https://docs.cypress.io/faq/questions/dashboard-faq.html#My-CI-setup-is-based-on-Docker-but-is-very-custom-How-can-I-load-balance-my-test-runs" rel="noopener noreferrer"&gt;Cypress has answered how to do this&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser Support
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_browser_support.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_browser_support.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, if your organization needs to have support for IE11, you are out of luck. &lt;a href="https://github.com/cypress-io/cypress/issues/310#issuecomment-337349727" rel="noopener noreferrer"&gt;The Cypress team has explicitly said they won't be supporting it&lt;/a&gt;. &lt;a href="https://github.com/cypress-io/cypress/issues/310" rel="noopener noreferrer"&gt;There is an incredible thread on Github&lt;/a&gt; that I really hope you read through. It goes into why they are rolling this out slowly and didn't choose WebDriver from the beginning and wrote their own custom driver.&lt;/p&gt;

&lt;p&gt;For us at Yolk, we needed IE11 support for a couple of our applications. We kept getting regressions within IE11 and needed more comprehensive test coverage. We decided to use &lt;a href="https://www.browserstack.com/automate" rel="noopener noreferrer"&gt;Browserstack Automate&lt;/a&gt; and Selenium to cover these apps. For CI, we already had the app built and running in Cypress, we just needed to add a new build step that ran these tests using the &lt;a href="https://github.com/browserstack/browserstack-local-nodejs" rel="noopener noreferrer"&gt;Browserstack Local Proxy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the tests themselves, we decided to integrate Selenium with &lt;a href="https://github.com/cucumber/cucumber-js" rel="noopener noreferrer"&gt;Cucumber&lt;/a&gt;, a common pairing. To make this process easier, we copied our Gherkin &lt;code&gt;.feature&lt;/code&gt; files over to a new folder and wrote specific Selenium-based step implementations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A cool concept I had an idea for was to re-use the same &lt;code&gt;.feature&lt;/code&gt; files across both Cypress and Selenium. If anyone has ideas on this, please comment below with your suggestion 😃&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It depends on how far you take this strategy and to decide if having duplicate test coverage is worth it to you. For us, having at least happy-path end-to-end test coverage in I.E.11 gave us a huge amount of confidence when deploying so the cost was worth it. In my opinion, it isn't as bad as it seems, our Cypress tests cover Chromium-based browsers (with Firefox support coming soon) and our Selenium tests cover I.E.11. With I.E.11 being phased out more and more, even in the enterprise, the need for Selenium will go away and the need for Cypress will get even larger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Typescript Support and Code Coverage
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_bonus.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1576848054%2Fcypress-post%2Fundraw_bonus.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All of the libraries and modules I have mentioned previously come with Typescript support. Getting Typescript to work with Cypress doesn't require many configs and is totally worth it in the long run. All you will need are Webpack, TS config, plugin files which integrate with Cypress. A good guide provided by Cypress is &lt;a href="https://docs.cypress.io/guides/tooling/typescript-support.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I know a lot of people wonder about code coverage and generating reports, Cypress can do that as well! Again, there &lt;a href="https://github.com/cypress-io/code-coverage" rel="noopener noreferrer"&gt;is a nice plugin&lt;/a&gt; that lets you do it. The one caveat is that it will attach coverage counters to your code so running your tests will be slower and may not mimic production. A good strategy here is to just generate them locally once in a while to see how you are doing.&lt;/p&gt;

&lt;p&gt;If your backend and frontend are in Typescript, a cool idea is having code coverage running in both apps when Cypress runs. You can then see the coverage across your entire app!&lt;/p&gt;

</description>
      <category>testing</category>
      <category>cypress</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My first meetup talk @ GraphQL Toronto ⚛️</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Thu, 05 Dec 2019 23:23:39 +0000</pubDate>
      <link>https://dev.to/aleccool213/my-first-meetup-talk-graphql-toronto-5259</link>
      <guid>https://dev.to/aleccool213/my-first-meetup-talk-graphql-toronto-5259</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Not me pictured lol&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The one skill I think every developer should practice is public speaking. Unless you are a remote-developer who doesn't do audio OR video calls (idk if this exists), every developer needs to communicate ideas, thoughts, and plans with other co-workers.&lt;/p&gt;

&lt;p&gt;I am pretty committed to this idea so I did a couple of talks last year just to my coworkers. This got me much needed practice and gave the chance for people to give me feedback.&lt;/p&gt;

&lt;p&gt;Anyways, I decided that once a year is a good cadence for giving talks and meetups are the next stepping stone for me. I reached out to &lt;a href="https://twitter.com/pauldowman" rel="noopener noreferrer"&gt;Paul Dowman&lt;/a&gt; at Shopify who was running the next GraphQL Toronto meetup. He accepted my offer to do a talk on Apollo Local State and the rest is history 😇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apollotalk.alec.coffee" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fspkq9jkfh78kq201xkwa.png" alt="Alt Text"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apollotalk.alec.coffee" rel="noopener noreferrer"&gt;The slides&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.alec.coffee/apollo-local-state-pains/" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fb0lel9a14ywb8e0xxhoc.png" alt="Alt Text"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.alec.coffee/apollo-local-state-pains/" rel="noopener noreferrer"&gt;Blog post I wrote on the subject&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lot of people gave me positive feedback which was awesome. I practiced a lot in front of co-workers (and my wife &amp;lt;3) which all gave me super good feedback.&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>apollo</category>
      <category>localstate</category>
      <category>talks</category>
    </item>
    <item>
      <title>How To Make Money From Your Software Blog Without Hosting Ads 💸</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Mon, 21 Oct 2019 12:59:47 +0000</pubDate>
      <link>https://dev.to/aleccool213/how-to-make-money-from-your-software-blog-without-hosting-ads-cjn</link>
      <guid>https://dev.to/aleccool213/how-to-make-money-from-your-software-blog-without-hosting-ads-cjn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Want more great content like this? Sign up for my newsletter, visit: &lt;a href="//alec.coffee/newsletter"&gt;alec.coffee/signup&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We all know advertising sucks, but when it comes to writing blog articles and getting paid for it, a necessary evil. If you are a conscious online user, you care about leaving a minimal online footprint. You also know that advertising companies track every move a user makes online so that their banners are clicked through. You shouldn't have to subject your blog readers to ads so that you can make money. Leaving you with two options, either succumb to the ad overlords and add them to your blog or gate your content behind a paywall where little people get to read what you write. If you are starting, good luck making any return on blog ads as your views need to be in the thousands to return any profit. Hosting ads on your blog is also tough because most people block them, especially software developers, making them worthless. The good news is that there are great alternatives for people who want to get paid to write words, just like how people are paid to generate other sorts of online content, like videos on Youtube and stock photography.&lt;/p&gt;

&lt;p&gt;The solutions I offer supplement posting on your personal blog, compared to another hosted platform. This way, you aren't subject to changes in a companies direction; examples being a platform doesn't have ads today but adds them tomorrow.&lt;/p&gt;

&lt;p&gt;A little disclaimer, I have no disrespect for people who want to write and share knowledge online for free. Like open-source software, writing contributes positively to a vast amount of people. If you look at the other side of the spectrum, getting paid to write for a blog is like getting paid to write software, the people who value it highly and have the means to do so will donate funds. There are examples of how this works in open-source sponsorship programs, examples like &lt;a href="https://tidelift.com/" rel="noopener noreferrer"&gt;Tidelift&lt;/a&gt;, &lt;a href="https://github.com/sponsors" rel="noopener noreferrer"&gt;Github Sponsors&lt;/a&gt; and &lt;a href="https://issuehunt.io/" rel="noopener noreferrer"&gt;Issuehunt&lt;/a&gt;, to name a few.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571407976%2Fads-post%2Fdownload_1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571407976%2Fads-post%2Fdownload_1.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This platform is a place where writers of all disciplines can post their content, and when it's read paying you in the process. Their revenue model is simple, readers pay \$5 a month, and while interacting with content, funds get distributed to the pieces they interact with. So far, I haven't seen a platform that does the same with much success. Usually, writers have a tough time making money online; it's the sad truth. You may have a great story on your hands, but if you aren't out pitching to publishers every day, your story may never be read by that many people. Think of Medium as a publisher who lets most pieces in on the platform, but lets their algorithm do most of the work to get the user base to read it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-posting
&lt;/h3&gt;

&lt;p&gt;If your writing only lives in Medium, you are potentially leaving out readers who don't use the platform. The group of people who want to read your blog content isn't the same exact group that uses Medium. While I'm advocating to put your content behind Medium's paywall (something I mentioned before was terrible), I'm not saying this is the only place your content should live. If you are like me, I want my content to be read by the most amount of people that it can. Medium's SEO is fantastic and most likely better than your blogs unless you are an SEO master. Not only that, but while on Medium itself, there is a feed that hands out recommendations. This feed is how you will get the majority of your views, at least I have from experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I Use It
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571414450%2Fads-post%2FScreenshot_at_Oct_18_12-00-42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571414450%2Fads-post%2FScreenshot_at_Oct_18_12-00-42.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I do, is first post content on &lt;a href="https://blog.alec.coffee" rel="noopener noreferrer"&gt;my blog&lt;/a&gt; and &lt;a href="https://dev.to"&gt;dev.to&lt;/a&gt;, which gets some early feedback and gives me signals as to what is sticking and what isn't. I post a final edit to Medium and point the personal blog &lt;code&gt;canonical_url&lt;/code&gt; to it, when search engines pick it, I want to use this link as it gives me money. To provide more traction for the post, I often submit them to Medium publications. To find a couple of top publications related to the content of the post, I use &lt;a href="https://toppubs.smedian.com/" rel="noopener noreferrer"&gt;this website&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://yoast.com/rel-canonical/" rel="noopener noreferrer"&gt;Read more about canonical URLs here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.patreon.com/" rel="noopener noreferrer"&gt;Patreon&lt;/a&gt; / &lt;a href="https://ko-fi.com" rel="noopener noreferrer"&gt;Ko-Fi&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571408282%2Fads-post%2F421651-patreon.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571408282%2Fads-post%2F421651-patreon.jpg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For blogs, you usually don't see a Patreon link or a 'donate here' button. For me, I see anyone who produces content online as someone who should be using this. Just like streaming on Twitch or creating Youtube videos, writing takes time, and if someone has the funds, they will see donating to you via these platforms as viable, especially if they see you don't use advertising as a source of revenue. Offering subscriptions can go as far as you like to take it; you could offer monthly subscriptions, give higher tiers of subs earlier access to your writing or give them the ability to write you personally for Q/A. When you write more, your communication skills improving, people will want access to this.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I Use It
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571414648%2Fads-post%2FScreenshot_at_Oct_18_12-04-02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571414648%2Fads-post%2FScreenshot_at_Oct_18_12-04-02.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For ko-fi, I signed up for an account and added a link to it in the bio on my blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://brave.com/brave-rewards/" rel="noopener noreferrer"&gt;Brave Rewards&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571408429%2Fads-post%2Fbrave-rewards.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571408429%2Fads-post%2Fbrave-rewards.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the &lt;a href="https://www.wired.com/story/cambridge-analytica-facebook-privacy-awakening/" rel="noopener noreferrer"&gt;Great Cambridge Analytical Scandal&lt;/a&gt; and many other events like it, I started to re-evaluate my online usage. I began to ween myself off apps from companies that use advertising as a source of revenue. One of the best alternatives to Google Chrome right now is the &lt;a href="https://brave.com/ale477" rel="noopener noreferrer"&gt;Brave Browser&lt;/a&gt;. It blocks ads while browsing, it does so at the browser networking level and not the Javascript level like most browser extensions do, giving performance benefits over a traditional browser extension. They knew that without ads, some websites couldn't exist, so they built in their own donation platform, which distributes funds to sites you visit and give attention to. Another option for users is to tip the website directly using the icon in the address bar.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I Use It
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571415342%2Fads-post%2FScreenshot_at_Oct_18_12-09-51.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1571415342%2Fads-post%2FScreenshot_at_Oct_18_12-09-51.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After &lt;a href="https://creators.brave.com/" rel="noopener noreferrer"&gt;signing up to be a creator&lt;/a&gt; on Brave, that's it! When users visit your blog, if they have auto-contribute on, you will receive funds based on the amount of time they spent there.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Like this post? Consider &lt;a href="https://www.buymeacoffee.com/yourboybigal" rel="noopener noreferrer"&gt;buying me a coffee&lt;/a&gt; to support me writing more. &lt;/p&gt;

&lt;p&gt;Want to receive quarterly emails with new posts? &lt;a href="https://mailchi.mp/f91826b80eb3/alecbrunelleemailsignup" rel="noopener noreferrer"&gt;Signup for my newsletter&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>blogging</category>
      <category>money</category>
      <category>webdev</category>
      <category>career</category>
    </item>
    <item>
      <title>The Brave Browser just released a new way to support contributors on Github 🤑</title>
      <dc:creator>Alec Brunelle</dc:creator>
      <pubDate>Fri, 04 Oct 2019 16:04:00 +0000</pubDate>
      <link>https://dev.to/aleccool213/the-brave-browser-just-released-a-new-way-to-support-contributors-on-github-3fo6</link>
      <guid>https://dev.to/aleccool213/the-brave-browser-just-released-a-new-way-to-support-contributors-on-github-3fo6</guid>
      <description>&lt;p&gt;With the recent release of Brave (&lt;a href="https://twitter.com/brave/status/1179789944451022854?s=20" rel="noopener noreferrer"&gt;0.69&lt;/a&gt;), they have added a new way to share your &lt;a href="https://brave.com/brave-rewards/" rel="noopener noreferrer"&gt;Basic Attention Tokens&lt;/a&gt; on Github, through comments and issues.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1570204698%2Fbrave-github-post%2FScreenshot_at_Oct_04_11-58-13.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdscgr6mcw%2Fimage%2Fupload%2Fv1570204698%2Fbrave-github-post%2FScreenshot_at_Oct_04_11-58-13.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Brave already had Github accounts setup to be channels for which users could send their tokens, but you had to visit their Github profile page. With this feature, it should be much easier for Brave users to tip.&lt;/p&gt;

&lt;p&gt;Anyone think this is a good idea? Too pushy? It would be interesting to hear what people think.&lt;/p&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>money</category>
      <category>brave</category>
    </item>
  </channel>
</rss>
